from parallels.plesk.source.plesk import messages
import logging
import posixpath
from collections import defaultdict
import os
from xml.etree import ElementTree
from parallels.core.actions.utils.logging_properties import LoggingProperties

from parallels.core.utils.windows_utils import path_join as windows_path_join
from parallels.core.logging_context import log_context
from parallels.core.actions.base.common_action import CommonAction
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.core.utils import migrator_utils
from parallels.core.utils.common import format_list, cached
import parallels.plesk.source.plesk

logger = logging.getLogger(__name__)


class TransferPackages(CommonAction):
	def get_description(self):
		return messages.ACTION_TRANSFER_APS_PACKAGES

	def get_failure_message(self, global_context):
		error_message = messages.ACTION_TRANSFER_APS_PACKAGES_FAILURE
		solution = messages.ACTION_TRANSFER_APS_PACKAGES_FAILURE_SOLUTION
		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.core.actions.utils.logging_properties.LoggingProperties
		"""
		return LoggingProperties(compound=False)

	def run(self, global_context):
		logger.debug(messages.LOG_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, messages.FAILED_TO_READ_INFORMATION_ABOUT_APS_PACKAGES,
				is_critical=False
			):
				for application in subscription.converted_dump.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:
			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):
					for package in packages:
						num += 1
						name, version, release = package
						logger.debug(
							messages.LOG_COPY_PACKAGE,
							name, version, release, format_list(packages_subscriptions[package]),
							num, len(packages_subscriptions)
						)

						error_message = messages.FAILED_TO_INSTALL_APS_PACKAGE % (name, version, release)
						with safe.try_subscriptions(
							subscriptions={
								subscription: error_message
								for subscription in packages_subscriptions[package]
							},
							is_critical=False
						):
							local_package_path = self._download_package(
								global_context.migrator_server, source_server, package
							)
							target_package_path = target_plesk_server.get_session_file_path(
								self._basename(local_package_path)
							)

							runner_target.upload_file(local_package_path, target_package_path)

							runner_target.run(
								aps_util,
								["--import-package", target_package_path]
							)
							runner_target.remove_file(target_package_path)

	def _download_package(self, migrator_server, source_server, package):
		aps_packages_local_dir = self._create_local_packages_directory(migrator_server)

		if source_server.is_windows():
			if source_server.plesk_major_version in (8, 9):
				name, version, release = package
				package_name = '%s-%s-%s' % (name, version, release)
				remote_package_dir = windows_path_join(
					source_server.data_dir,
					'var', 'cgitory', u'%s' % package_name
				)
				aps_zip_file = os.path.join(aps_packages_local_dir, '%s.zip' % package_name)

				with source_server.runner() as runner:
					runner.download_directory_as_zip(remote_package_dir, aps_zip_file)
				return aps_zip_file
			else:
				packages_paths = self._get_aps_packages_locations_plesk_10(source_server)
				return self._download_package_from_file(aps_packages_local_dir, source_server, packages_paths, package)
		else:
			if source_server.plesk_major_version in (8, 9):
				packages_paths = self._get_aps_packages_locations_pfu9(source_server)
				return self._download_package_from_file(aps_packages_local_dir, source_server, packages_paths, package)
			else:
				# Plesk >= 10.3
				packages_paths = self._get_aps_packages_locations_plesk_10(source_server)
				return self._download_package_from_file(aps_packages_local_dir, source_server, packages_paths, package)

	def _download_package_from_file(self, aps_packages_local_dir, source_server, remote_package_path_list, package):
		remote_package_path = remote_package_path_list.get(package)

		if remote_package_path is None:
			raise Exception(messages.APS_PACKAGE_WAS_NOT_FOUND_SOURCE)

		local_package_path = os.path.join(aps_packages_local_dir, self._basename(remote_package_path))

		with source_server.runner() as runner:
			runner.get_file(remote_package_path, local_package_path)

		return local_package_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.plesk.source.plesk, 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(
					messages.UNABLE_TO_FIND_FILENAME_FOR_APS_PACKAGE,
					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')
			}

	@cached
	def _create_local_packages_directory(self, migrator_server):
		aps_packages_local_dir = migrator_server.get_session_file_path('aps-packages')
		with migrator_server.runner() as runner:
			runner.mkdir(aps_packages_local_dir)
		return aps_packages_local_dir

	@staticmethod
	def _basename(path):
		"""Get base name of a file by full path for both Unix and Windows

		For example, for:
		- '/root/test/a.zip' it will return 'a.zip'
		- 'C:\example\test.zip' it will return 'test.zip'
		"""
		last_delim = max(
			path.rfind('/'),
			path.rfind('\\')
		)
		result = path[last_delim+1:]
		assert result != ''
		return result
