import logging
import posixpath
from collections import defaultdict
from contextlib import contextmanager
import os
from xml.etree import ElementTree
from parallels.common.actions.utils.logging_properties import LoggingProperties

from parallels.common.utils.windows_utils import path_join as windows_path_join
from parallels.common.logging_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 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 is_critical(self):
		"""If action is critical or not

		If action is critical and it failed for a subscription, migration tool
		won't run the next operations for the subscription.

		:rtype: bool
		"""
		return False

	def get_logging_properties(self):
		"""Get how action should be logged to migration tools end-user

		:rtype: parallels.common.actions.utils.logging_properties.LoggingProperties
		"""
		return LoggingProperties(compound=False)

	def run(self, global_context):
		logger.debug(u"Looking for APS packages to transfer")

		subscriptions_by_target_plesk = defaultdict(list)
		for subscription in global_context.iter_all_subscriptions():
			subscriptions_by_target_plesk[subscription.panel_target_server].append(
				subscription
			)

		for target_plesk_server, subscriptions in subscriptions_by_target_plesk.iteritems():
			self._transfer_aps_packages(global_context, target_plesk_server, subscriptions)

	def _transfer_aps_packages(self, global_context, target_plesk_server, subscriptions):
		target_plesk_aps_packages = set([
			(result.data.name, result.data.version, result.data.release)	
			for result in target_plesk_server.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 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 target_plesk_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 target_plesk_server.runner() as runner_target:
			ppa_aps_util = u"%s/bin/aps" % target_plesk_server.plesk_dir
			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):
								with source_server.runner() as runner_source:
									aps_dir = windows_path_join(
										source_server.data_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.debug(
								u"Copy package '%s-%s-%s' required to restore "
								u"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'. "
								u"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 = target_plesk_server.get_session_file_path(package_filename)

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

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

	@staticmethod
	def _source_windows_zip_folder(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)

	@staticmethod
	def _get_aps_packages_locations_plesk_10(source_server):
		with source_server.runner() as runner_source:
			if source_server.is_windows():
				source_plesk_dir = source_server.plesk_dir
				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" % (source_server.plesk_dir,)
				packages_xml_string = runner_source.run(source_aps_util, ['--get-packages-list-xml'])

			packages_xml = ElementTree.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 = ElementTree.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'. "
					u"This can cause errors while restoring APS applications.",
					name, version, release
				)

		return packages_filenames

	@staticmethod
	def _get_aps_packages_locations_pfu9(source_server):
		with source_server.runner() as runner_source:
			aps_dir = u"%s/var/apspkgarc" % source_server.plesk_dir
			packages_xml = ElementTree.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')
			}
