import ntpath
import posixpath
from xml.etree import ElementTree
from collections import namedtuple
from contextlib import closing
import os

import parallels.core
from parallels.core import MigrationError
from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core.dump.dump_archive_writer import DumpArchiveWriter
from parallels.core.runners.base import BaseRunner
from parallels.core.utils import migrator_utils
from parallels.core.utils.common.xml import xml_to_string_pretty
from parallels.core.utils.tar_utils import tar_directory, untar
from parallels.core.utils.yaml_utils import pretty_yaml
from parallels.core.utils.common import cached, open_no_inherit, safe_format, remove_if_exists
from parallels.core.utils.zip_utils import unzip, zip_directory

logger = get_logger(__name__)


class DumpAll(namedtuple('DumpAll', [])):
    # noinspection PyUnusedLocal
    @staticmethod
    def covers(selection):
        """Check if this selection covers another selection

        If true, that means that backup dump created by current
        selection contains all objects from another selection, and so
        could be used.
        :type selection: parallels.core.utils.pmm.agent.DumpAll|parallels.core.utils.pmm.agent.DumpSelected
        :rtype: bool
        """
        return True

    @staticmethod
    def get_filter_xml():
        return None

    # noinspection PyUnusedLocal
    @staticmethod
    def is_reseller_selected(name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return True

    # noinspection PyUnusedLocal
    @staticmethod
    def is_client_selected(name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return True

    # noinspection PyUnusedLocal
    @staticmethod
    def is_domain_selected(name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return True

    def __repr__(self):
        return 'DumpAll()'


DumpAll = pretty_yaml(DumpAll)


class DumpSelected(namedtuple('DumpSelected', ('resellers', 'clients', 'domains'))):
    def covers(self, selection):
        """Check if this selection covers another selection

        If true, that means that backup dump created by current
        selection contains all objects from another selection, and so
        could be used.
        :type selection: parallels.core.utils.pmm.agent.DumpAll|parallels.core.utils.pmm.agent.DumpSelected
        :rtype: bool
        """
        if isinstance(selection, DumpSelected):
            return (
                set(self.resellers) >= set(selection.resellers) and
                set(self.clients) >= set(selection.clients) and
                set(self.domains) >= set(selection.domains)
            )
        else:
            return False

    def get_filter_xml(self):
        migration_objects_list = ElementTree.Element("migration-objects-list")

        resellers = ElementTree.SubElement(migration_objects_list, "resellers")
        for reseller in self.resellers:
            ElementTree.SubElement(resellers, "reseller").text = reseller
        clients = ElementTree.SubElement(migration_objects_list, "clients")
        for client in self.clients:
            ElementTree.SubElement(clients, "client").text = client
        domains = ElementTree.SubElement(migration_objects_list, "domains")
        for domain in self.domains:
            ElementTree.SubElement(domains, "domain").text = domain

        return xml_to_string_pretty(ElementTree.ElementTree(migration_objects_list))

    def is_reseller_selected(self, name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return name in self.resellers

    def is_client_selected(self, name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return name in self.clients

    def is_domain_selected(self, name):
        """
        :type name: str|unicode
        :rtype: bool
        """
        return name in self.domains

    def __repr__(self):
        return "DumpSelected(%r, %r, %r)" % (
            self.resellers, self.clients, self.domains
        )


DumpSelected = pretty_yaml(DumpSelected)


class PmmMigrationAgentBase(object):
    def __init__(self, global_context, source_server):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type source_server: parallels.plesk.source.plesk.server.PleskSourceServer
        """
        self.global_context = global_context
        self._source_server = source_server
        self.agent_dir = self._deploy()

    def source_server(self):
        """
        :rtype: parallels.plesk.source.plesk.server.PleskSourceServer
        """
        return self._source_server

    def create_dump(self, filename, selection=DumpAll()):
        remove_if_exists(filename)

        source_server = self._source_server
        target_server = self.global_context.conn.target.plesk_server

        target_plesk_version = target_server.plesk_version
        source_server_id = source_server.id

        session_files = self.global_context.session_files

        downloaded_dump_file_path = session_files.get_downloaded_dump_filename(source_server_id)

        remove_if_exists(downloaded_dump_file_path)

        if self._is_dump_agent_creates_archive():
            with logger.debug_level_context(messages.RUN_PMM_AGENT_CREATE_DUMP_ARCHIVE):
                self._run(downloaded_dump_file_path, self.configuration_dump_log, selection)
        else:
            with logger.debug_level_context(messages.RUN_PMM_AGENT_CREATE_DUMP_XML):
                self._run(self._dump_xml_file, self.configuration_dump_log, selection)

            with logger.debug_level_context(messages.PACK_DUMP_XML_INTO_DUMP_ARCHIVE):
                with open_no_inherit(self._dump_xml_file) as dump_xml_fp:
                    with closing(DumpArchiveWriter(downloaded_dump_file_path)) as archive_writer:
                        archive_writer.add_dump(dump_xml_fp.read())

        if target_plesk_version < (17, 8) or not self._is_dump_version_upgrade_required():
            # Plesk 12.5 to 17.5 allows to import old versions of dumps - so use dump as is
            os.rename(downloaded_dump_file_path, filename)
        else:
            # Plesk 18.0 and later allows to import recent dump versions only
            # (starting from 17.0 format - see "MIN_DUMP_VERSION" in Plesk code).
            # Perform dump version upgrade here.
            try:
                with logger.debug_level_context(messages.UPGRADE_DUMP_VERSION):
                    upgraded_dump_file_path = session_files.get_upgraded_dump_filename(source_server_id)
                    self._upgrade_dump_version(downloaded_dump_file_path, upgraded_dump_file_path)
            except Exception as e:
                logger.exception()
                # Try to continue without conversion, but that will fail with high probability
                logger.ferror(messages.FAILED_TO_UPGRADE_DUMP_VERSION, reason=e)
                os.rename(downloaded_dump_file_path, filename)
            else:
                os.rename(upgraded_dump_file_path, filename)

    def _is_dump_agent_creates_archive(self):
        """Whether dump agent creates ready to use dump archive (ZIP or TAR). If False then agent creates plain XML.

        :rtype: bool
        """
        return True

    def _is_dump_version_upgrade_required(self):
        """Whether dump agent creates dump in old format (< 12.5), and upgrade is required to use with target Plesk 17.8

        :rtype: bool
        """
        return False

    def _upgrade_dump_version(self, original_dump_path, upgraded_dump_path):
        """
        :type original_dump_path: str | unicode 
        :type upgraded_dump_path: str | unicode 
        :rtype: None 
        """
        source_server = self._source_server
        target_server = self.global_context.conn.target.plesk_server

        source_server_id = source_server.id

        session_files = self.global_context.session_files

        original_dump_dir = session_files.get_dump_format_update_original_directory(source_server_id)
        converted_dump_dir = session_files.get_dump_format_update_converted_directory(source_server_id)

        with target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.remove_directory(original_dump_dir, is_remove_root=True)

        with logger.debug_level_context(
                messages.UNPACK_UPGRADED_DUMP, dump=original_dump_path, directory=original_dump_dir
        ):
            if target_server.is_windows():
                unzip(original_dump_path, original_dump_dir)
            else:
                untar(original_dump_path, original_dump_dir)

        info_files = [
            os.path.join(dp, filename) for dp, dn, fn in os.walk(original_dump_dir) for filename in fn
            # File may have name:
            # - "converted_info_*.xml" (when source dump created on PfW 8) or
            # - "info_*.xml" (all other cases)
            if 'info_' in filename and filename.endswith('.xml')
        ]
        if len(info_files) == 0:
            raise MigrationError(
                safe_format(messages.FAILED_TO_FIND_INFO_FILES, directory=original_dump_dir)
            )

        dump_version_upgrade_php = migrator_utils.get_package_extras_file_path(
            parallels.core,
            os.path.join('dump-version-upgrade', 'DumpVersionUpgrade.php')
        )

        with target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.remove_directory(converted_dump_dir, is_remove_root=True)
            for main_dump_info_file in info_files:
                logger.fdebug(messages.ORIGINAL_MAIN_DUMP_FILENAME, filename=main_dump_info_file)
                runner.mkdir(os.path.join(converted_dump_dir,
                                          os.sep.join(main_dump_info_file.split(original_dump_dir)[1].split(os.sep)[1:-1])))
                with logger.debug_level_context(messages.PERFORM_DUMP_UPGRADE, directory=converted_dump_dir):
                    if target_server.is_windows():
                        php_bin = ntpath.join(target_server.plesk_dir, u'admin\\bin\\php.exe')
                    else:
                        php_bin = posixpath.join(target_server.plesk_dir, u'bin/sw-engine-pleskrun')

                    runner.execute_command(
                        '{php_bin} -dauto_prepend_file="" -d display_errors="On" {dump_version_upgrade_php} '
                        '{main_dump_info_file} {converted_dump_dir}',
                        dict(
                            php_bin=php_bin,
                            dump_version_upgrade_php=dump_version_upgrade_php,
                            main_dump_info_file=main_dump_info_file,
                            converted_dump_dir=os.path.join(converted_dump_dir,
                                                            os.sep.join(main_dump_info_file.split(original_dump_dir)[1].split(os.sep)[1:-1]))
                        )
                    )
            runner.remove_file(upgraded_dump_path)

        with logger.debug_level_context(messages.PACK_CONVERTED_DUMP, filename=upgraded_dump_path):
            if target_server.is_windows():
                zip_directory(converted_dump_dir, upgraded_dump_path)
            else:
                tar_directory(converted_dump_dir, upgraded_dump_path)

    def create_shallow_dump(self, filename):
        logger.debug(messages.RUN_PMM_AGENT_CREATE_SHALLOW_DUMP)
        self._run_shallow(filename)

    def create_capability_dump(self, filename, selection=DumpAll()):
        logger.debug(messages.RUN_PMM_AGENT_CREATE_CAPABILITY_DUMP)
        self._run_capability(filename, self.capability_dump_log, selection)

    def check(self):
        raise NotImplementedError()

    def execute_sql(self, query, query_args=None, log_output=True):
        """Execute SQL query on panel database
        Returns list of rows, each row is a dictionary.
        Query and query arguments:
        * execute_sql("SELECT * FROM clients WHERE type=%s AND country=%s", ['client', 'US'])

        :type query: str|unicode
        :type query_args: list|dict
        :type log_output: bool
        :rtype: list[dict]
        """
        raise NotImplementedError()

    def _run(self, dump_xml_filename, dump_log_filename, selection=DumpAll()):
        raise NotImplementedError()

    def _run_shallow(self, dump_xml_filename):
        raise NotImplementedError()

    def _run_capability(self, dump_xml_filename, dump_log_filename, selection=DumpAll()):
        raise NotImplementedError()

    def _deploy(self):
        raise NotImplementedError()

    def uninstall(self):
        raise NotImplementedError()

    @property
    def _dump_xml_file(self):
        return os.path.join(self._get_pmm_session_dir(), 'dump.xml')

    @property
    def configuration_dump_log(self):
        return os.path.join(self._get_pmm_session_dir(), 'configuration-dump.log')

    @property
    def capability_dump_log(self):
        return os.path.join(self._get_pmm_session_dir(), 'capability-dump.log')

    @property
    def check_report_file(self):
        return os.path.join(self._get_pmm_session_dir(), 'pre-migration.xml')

    @property
    def check_log_file(self):
        return os.path.join(self._get_pmm_session_dir(), 'pre-migration.log')

    @property
    @cached
    def _perl_bin(self):
        """Perl binary used to run agent

        :rtype: str | unicode
        """
        with self._source_server.runner() as runner:
            # System Perl binary has priority over the other ones,
            # even if they are resolved from "perl" name before by PATH.
            # That is necessary because other Perl binaries could be broken,
            # or do not contain all necessary modules required to run agent.
            if runner.file_exists('/usr/bin/perl'):
                return '/usr/bin/perl'

        return 'perl'

    @cached
    def _get_pmm_session_dir(self):
        directory = self.global_context.session_files.get_path_to_pmm_agent_dir()
        with self.global_context.migrator_server.runner() as runner:
            runner.mkdir(directory)
        return directory
