import posixpath
import ntpath
from collections import defaultdict
import os
from xml.etree import ElementTree

from parallels.core.logging import get_logger
from parallels.core.logging import log_context
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.plesk.source.plesk import messages
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.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 = get_logger(__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.items():
            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.items():
            packages_by_source[plesk_id].add(package)

        num = 0
        with target_plesk_server.runner() as runner_target:
            assert isinstance(runner_target, BaseRunner)
            aps_util = u"%s/bin/aps" % target_plesk_server.plesk_dir
            for plesk_id, packages in packages_by_source.items():
                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.execute_command(
                                "{aps_util} --import-package {target_package_path}",
                                dict(
                                    aps_util=aps_util, target_package_path=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:
            assert isinstance(runner_source, BaseRunner)
            if source_server.is_windows():
                source_plesk_dir = source_server.plesk_dir
                packages_xml_string = runner_source.execute_command(
                    "{php} -dauto_prepend_file="" {aps} --get-packages-list-xml",
                    dict(
                        php=ntpath.join(source_plesk_dir, u'admin\\bin\\php'),
                        aps=ntpath.join(source_plesk_dir, u'admin\\plib\\cu\\aps.php'),
                    )
                ).stdout
            else:
                source_aps_util = posixpath.join(source_server.plesk_dir, u"bin/aps")
                packages_xml_string = runner_source.execute_command(
                    "{aps_util} --get-packages-list-xml", dict(aps_util=source_aps_util)
                ).stdout

            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.execute_command(
                    r'cmd.exe /c "{php} -dauto_prepend_file="" {aps} --get-packages-global-settings"',
                    dict(
                        php=ntpath.join(source_server.plesk_dir, u'admin\\bin\\php'),
                        aps=ntpath.join(source_server.plesk_dir, u'admin\\plib\\cu\\aps.php')
                    )
                ).stdout
                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:
                source_aps_util = posixpath.join(source_server.plesk_dir, u"bin/aps")
                packages_global_settings = runner_source.execute_command(
                    "{aps_util} --get-packages-global-settings", dict(aps_util=source_aps_util)
                ).stdout

                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
                )
                package_filenames_output = runner_source.execute_command(
                    '/usr/bin/perl {source_script_path}', dict(source_script_path=source_script_path),
                    ExecutionOptions(
                        stdin_content=packages_global_settings
                    )
                ).stdout
                registry_uid_to_filename = dict([
                    line.split(" ", 1)
                    for line in package_filenames_output.split("\n")
                    if line.strip() != ''
                ])

        packages_filenames = {} 
        for package, registry_uid in package_to_registry_uid.items():
            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 = posixpath.join(source_server.plesk_dir, "var/apspkgarc")
            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
