import logging
import posixpath
from collections import defaultdict
from contextlib import contextmanager
import os
from xml.etree import ElementTree as et

from parallels.common.utils.windows_utils import path_join as windows_path_join
from parallels.common.context import log_context
from parallels.common.actions.base.common_action import CommonAction
from parallels.plesk_api import operator as plesk_ops
from parallels.common.utils import plesk_utils
from parallels.common.utils import windows_utils
from parallels.common.utils import migrator_utils
from parallels.utils import format_list
import parallels.plesks_migrator

logger = logging.getLogger(__name__)

class TransferPackages(CommonAction):
	def get_description(self):
		return "Transfer APS packages"

	def get_failure_message(self, global_context):
		error_message = "Failed to transfer APS packages. "
		solution = (
			"Old and custom APS applications will be likely not registered "
			"in target panel as APS applications (still migration tools. "
			"will try to transfer their files and databases). "
			"To fix the issue manually, you could try to register "
			"APS packages manually in the target panel, then restart "
			"copy content with 'copy-content' command."
		)
		return error_message + solution

	def run(self, global_context):
		logger.info(u"Looking for APS packages to transfer")
		ppa_aps_packages = set([
			(result.data.name, result.data.version, result.data.release)	
			for result in global_context.conn.target.plesk_api().send(
				plesk_ops.ApsOperator.GetPackagesList(
					filter=plesk_ops.ApsOperator.GetPackagesList.FilterAll()
				)
			)
		])

		safe = global_context.safe
		packages_subscriptions = defaultdict(set) 
		packages_source = {}

		for subscription in global_context.iter_all_subscriptions():
			with safe.try_subscription(subscription.name, u"Failed to read information about APS packages for subscription.", is_critical=False):
				for application in subscription.converted_backup.get_aps_applications():
					if (application.name, application.version, application.release) not in ppa_aps_packages:
						package = (application.name, application.version, application.release)
						packages_subscriptions[package].add(subscription.name)
						if package not in packages_source: # the first source has more priority
							packages_source[package] = subscription.model.source

		packages_by_source = defaultdict(set)
		for package, plesk_id in packages_source.iteritems():
			packages_by_source[plesk_id].add(package)

		num = 0
		with global_context.conn.target.main_node_runner() as runner_ppa:
			ppa_aps_util = u"%s/bin/aps" % (plesk_utils.get_unix_product_root_dir(runner_ppa) if not global_context.conn.target.is_windows else plesk_utils.get_windows_plesk_dir(runner_ppa),)
			for plesk_id, packages in packages_by_source.iteritems():
				source_server = global_context.conn.get_source_node(plesk_id)
				with log_context(plesk_id):
					if source_server.is_windows():
						if source_server.plesk_major_version in (8, 9):
							@contextmanager
							def get_package_path(package):
								source_server = global_context.conn.get_source_node(plesk_id)
								with source_server.runner() as runner_source:
									plesk_dir = windows_utils.detect_plesk_dir(runner_source.sh)
									aps_dir = windows_path_join(plesk_dir, 'var', 'cgitory', u'%s-%s-%s' % (name, version, release))
									aps_zip_file = source_server.get_session_file_path(u'%s-%s-%s.zip' % (name, version, release))
									self._source_windows_zip_folder(source_server, aps_dir, aps_zip_file)
									yield aps_zip_file
									runner_source.remove_file(aps_zip_file)
						else:
							packages_paths = self._get_aps_packages_locations_plesk_10(source_server)
							@contextmanager
							def get_package_path(package):
								yield windows_utils.fix_path(
									packages_paths.get(package)
								)
					else:
						if source_server.plesk_major_version in (8, 9):
							packages_paths = self._get_aps_packages_locations_pfu9(source_server)
							@contextmanager
							def get_package_path(package):
								yield packages_paths.get(package)
						else: # Plesk >= 10.3
							packages_paths = self._get_aps_packages_locations_plesk_10(source_server)
							@contextmanager
							def get_package_path(package):
								yield packages_paths.get(package)

					with source_server.runner() as runner_source:
						for package in packages:
							num += 1
							name, version, release = package
							logger.info(
								u"Copy package '%s-%s-%s' required to restore the following subscription(s): %s (#%d out of #%d)", 
								name, version, release, format_list(packages_subscriptions[package]), num, len(packages_subscriptions)
							)

							error_message = (
								u"Failed to install APS package '%s-%s-%s'. PPA will not consider this application as installed. " + \
								u"However all application-related content should be restored correctly."
							) % (name, version, release)
							with safe.try_subscriptions(subscriptions={subscription: error_message for subscription in packages_subscriptions[package]}, is_critical=False):
								with get_package_path(package) as source_package_path:
									logger.debug(
										"APS package archive path on source server: %s", 
										source_package_path
									)
									if source_package_path is not None:
										last_delim = max(
											source_package_path.rfind('/'), 
											source_package_path.rfind('\\')
										)
										package_filename = source_package_path[last_delim+1:]
										assert(len(package_filename) > 0)
										local_package_path = global_context.migrator_server.get_session_file_path(package_filename)
										ppa_package_path = global_context.conn.target.main_node_session_file_path(package_filename)

										runner_source.get_file(source_package_path, local_package_path)
										runner_ppa.upload_file(local_package_path, ppa_package_path)
										os.remove(local_package_path)

										runner_ppa.run(
											ppa_aps_util, 
											["--import-package", ppa_package_path]
										)
										runner_ppa.remove_file(ppa_package_path)
									else:
										raise Exception(u"APS package was not found on the source Plesk server")


	def _source_windows_zip_folder(self, source_server, folder, zipfile):
		logger.debug(
			u"ZIP folder '%s' into '%s' zipfile on %s", folder, zipfile, source_server.description()
		)
		zipfolder_vbs = 'zipfolder.vbs'
		zipfolder_vbs_path = source_server.get_session_file_path(zipfolder_vbs)
		with source_server.runner() as runner:
			runner.upload_file(
				migrator_utils.get_package_extras_file_path(
					parallels.plesks_migrator, zipfolder_vbs
				),
				zipfolder_vbs_path
			)
			runner.sh(
				ur'cmd.exe /C "cscript {zipfolder_vbs_path} {folder} {zipfile}"', 
				dict(zipfolder_vbs_path=zipfolder_vbs_path, folder=folder, zipfile=zipfile)
			)
			runner.remove_file(zipfolder_vbs_path)

	def _get_aps_packages_locations_plesk_10(self, source_server):
		with source_server.runner() as runner_source:
			if source_server.is_windows():
				source_plesk_dir = windows_utils.detect_plesk_dir(runner_source.sh)
				packages_xml_string = runner_source.sh(
					ur'cmd.exe /c "{php} -dauto_prepend_file="" {aps} --get-packages-list-xml"',
					dict(
						php=ur"%s\admin\bin\php" % (source_plesk_dir,),
						aps=ur"%s\admin\plib\cu\aps.php" % (source_plesk_dir,),
						source_plesk_dir=source_plesk_dir, 
					)
				)
			else:
				source_aps_util = u"%s/bin/aps" % (plesk_utils.get_unix_product_root_dir(runner_source),)
				packages_xml_string = runner_source.run(source_aps_util, ['--get-packages-list-xml'])

			packages_xml = et.fromstring(packages_xml_string)
			package_to_registry_uid = {
				(pkg.findtext('name'), pkg.findtext('version'), pkg.findtext('release')): pkg.findtext('registryUid') 
				for pkg in packages_xml.findall('package')
			}

			if source_server.is_windows():
				packages_global_settings = runner_source.sh(
					r'cmd.exe /c "{php} -dauto_prepend_file="" {aps} --get-packages-global-settings"',
					dict(
						php=ur"%s\admin\bin\php" % (source_plesk_dir,),
						aps=u"%s\\admin\\plib\\cu\\aps.php" % (source_plesk_dir,),
						source_plesk_dir=source_plesk_dir, 
					)
				)
				global_settings_xml = et.fromstring(packages_global_settings)
				registry_uid_to_filename = {
					pkg.findtext('registryUid'): pkg.findtext('package-archive') 
					for pkg in global_settings_xml.findall('package')
				}
			else:
				packages_global_settings = runner_source.run(source_aps_util, ['--get-packages-global-settings'])

				script_name = 'get_pfu10_package_filenames.pl'
				source_script_path = source_server.get_session_file_path(script_name)
				runner_source.upload_file(
					migrator_utils.get_package_scripts_file_path(
						parallels.plesks_migrator, script_name
					),
					source_script_path
				)
				registry_uid_to_filename = dict([
					line.split(" ", 1)
					for line in
					runner_source.run('/usr/bin/perl', [source_script_path], stdin_content=packages_global_settings).split("\n")
					if line.strip() != ''
				])

		packages_filenames = {} 
		for package, registry_uid in package_to_registry_uid.iteritems():
			if registry_uid in registry_uid_to_filename:
				packages_filenames[package] = registry_uid_to_filename[registry_uid]
			else:
				name, version, release = package
				logger.warning(u"Unable to find filename for APS package '%s-%s-%s'. This can cause errors while restoring APS applications.", name, version, release)

		return packages_filenames

	def _get_aps_packages_locations_pfu9(self, source_server):
		with source_server.runner() as runner_source:
			product_root_d = plesk_utils.get_unix_product_root_dir(runner_source)
			aps_dir = u"%s/var/apspkgarc" % product_root_d
			packages_xml = et.fromstring(
				runner_source.get_file_contents(posixpath.join(aps_dir, 'archive-index.xml'))
			)
			return {
				(elem.findtext('app-name'), elem.findtext('app-version'), elem.findtext('app-release')): posixpath.join(aps_dir, elem.attrib.get('name'))
				for elem in packages_xml.findall('file')
			}
