import os
import ntpath
from xml.etree import ElementTree

from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.runners.exceptions.directory_remove_exception import DirectoryRemoveException
from parallels.core.utils.download_utils import download
from parallels.core import MigrationNoContextError
from parallels.core.utils import get_agents_base_path
from parallels.core.utils.plesk_utils import get_windows_psa_database_name, get_windows_psa_database_login, \
    get_windows_psa_database_port, get_windows_psa_database_host
from parallels.core.utils.yaml_utils import read_yaml, write_yaml
from parallels.core.utils.pmm.agent import PmmMigrationAgentBase, DumpAll, DumpSelected
from parallels.core.utils import windows_utils
from parallels.core.utils.windows_utils import path_join as windows_path_join, get_from_registry, find_in_registry
from parallels.core.utils.common import safe_string_repr, safe_format, cached
from parallels.core.runners.exceptions.non_zero_exit_code import NonZeroExitCodeException
from parallels.plesk.source.plesk.capability_dump.model.factory import create_plesk_capability_model
from parallels.plesk.source.plesk.capability_dump.xml_converter import CapabilityXMLConverter
from parallels.plesk.source.plesk.pmm_agent.hosting_description import HostingDescriptionAgent
from parallels.plesk.source.plesk.shallow_dump.model.factory import create_plesk_shallow_model
from parallels.plesk.source.plesk.shallow_dump.xml_converter import ShallowXMLConverter
from parallels.core.utils import migrator_utils
import parallels.plesk.source.plesk

logger = get_logger(__name__)


class WindowsPleskPmmMigrationAgent(PmmMigrationAgentBase):
    """Plesk migration agent for Windows."""

    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]
        """
        with self._source_server.runner() as runner:
            return runner.execute_sql(query, query_args, self._get_panel_db_connection_settings(), log_output)

    @cached
    def _get_panel_db_connection_settings(self):
        """Get connections settings to connect to panel database ('psa') as dictionary

        :rtype: dict
        """
        with self._source_server.runner() as runner:
            return {
                'host': get_windows_psa_database_host(runner),
                'port': get_windows_psa_database_port(runner),
                'user': get_windows_psa_database_login(runner),
                'password': self._source_server.panel_admin_password,
                'database': get_windows_psa_database_name(runner),
            }

    def _run(self, local_dump_path, local_log_filename, selection=DumpAll()):
        """Create migration dump and download it to session directory."""
        logger.finfo(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_MIGRATION_DUMP, dump_path=local_dump_path)

        if os.path.exists(local_dump_path):
            os.remove(local_dump_path)

        source_server = self._source_server
        source_plesk_version = source_server.plesk_version

        # For Plesk < 17.0 we create dump using backup agent
        # For Plesk >= 17.0 we create dump ourselves using hosting description format,
        # convert it to Plesk backup format and merge it with dump created with Plesk Backup
        if source_plesk_version < (17, 0):
            self._clean_source()
            source_server_dump_file_path = self._make_dump(selection)
            self._download_dump(source_server_dump_file_path, local_dump_path)
            self._clean_source()
        else:
            HostingDescriptionAgent(self).create_dump(local_dump_path, selection)

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

        :rtype: bool
        """
        source_server = self._source_server
        source_plesk_version = source_server.plesk_version
        return source_plesk_version < (17, 0)

    def _run_shallow(self, local_dump_path):
        """Create a dump with simplified structure, useful for creation of migration list."""
        logger.finfo(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_SHALLOW_DUMP, dump_path=local_dump_path)
        source_plesk_version = self._source_server.plesk_version
        # The new schema needs support of SQL queries to MS SQL/Jet for Plesks < 12.5
        if source_plesk_version < (12, 5):
            self._clean_source()
            self._make_shallow_dump()
            with self._source_server.runner() as runner:
                runner.get_file(self._source_shallow_dump_xml_file, local_dump_path)
            self._clean_source()
        else:
            shallow_model = create_plesk_shallow_model(
                self, self._source_server.plesk_version
            )
            ShallowXMLConverter(shallow_model).write_xml(local_dump_path)

    def _run_capability(self, local_dump_path, local_log_filename, selection=DumpAll()):
        """Create capability dump and download it to session directory."""
        logger.finfo(messages.UTILS_PMM_WINDOWS_AGENT_CREATE_CAPABILITY_DUMP, dump_path=local_dump_path)
        source_plesk_version = self._source_server.plesk_version
        # The new schema needs support of SQL queries to MS SQL/Jet for Plesks < 12.5
        if source_plesk_version < (12, 5):
            self._clean_source()
            self._make_capability_dump(selection)
            with self._source_server.runner() as runner:
                runner.get_file(self._source_capability_dump_xml_file, local_dump_path)
            self._clean_source()
        else:
            capability_model = create_plesk_capability_model(
                self, self._source_server.plesk_version, selection
            )
            CapabilityXMLConverter(capability_model).write_xml(local_dump_path)

    def _make_dump(self, selection=DumpAll()):
        """Run agent on source server, generate migration dump."""
        with self._source_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            command, options = self._get_create_dump_command(runner, selection)
            runner.execute_command(command, options)

        if not self._source_server.plesk_major_version >= 9:
            self._convert_dump_to_plesk9()
            return self._source_converted_dump_path
        else:
            return self._source_configuration_dump_dir

    def _make_shallow_dump(self):
        """Run agent on source server, generate shallow migration dump."""
        command, options = self._get_create_shallow_dump_command()
        with self._source_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.execute_command(command, options)

    def _make_capability_dump(self, selection):
        """Run agent on source server, generate capability dump."""
        with self._source_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            command, options = self._get_create_capability_dump_command(runner, selection)
            runner.execute_command(command, options)

    def _create_filter_file(self, runner, selection):
        migration_objects_list = ElementTree.Element("migration-objects-list")

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

        filter_filename = self._source_server.get_session_file_path("filter.xml")
        runner.upload_file_content(
            filter_filename,
            ElementTree.tostring(migration_objects_list, 'utf-8', 'xml')
        )
        return filter_filename

    def _get_create_dump_command(self, runner, selection=DumpAll()):
        if isinstance(selection, DumpSelected):
            filter_file = self._create_filter_file(runner, selection)
        else:
            filter_file = ""

        options = {
            'agent_script': self._get_agent_path(),
            'dump_command': '--create-full-migration-dump',
            'filter_file': filter_file,
            'pmm_temp_dir': self._source_pmm_temp_dir,
            'configuration_dump_dir': self._source_configuration_dump_dir,
        }
        command = '{agent_script} {dump_command} {filter_file} {pmm_temp_dir} {configuration_dump_dir}'

        return command, options

    def _get_create_shallow_dump_command(self):
        options = {
            'agent_script': self._get_agent_path(),
            'dump_command': '--create-shallow-migration-dump',
            'pmm_temp_dir': self._source_pmm_temp_dir,
            'shallow_dump_xml_file': self._source_shallow_dump_xml_file
        }
        command = "{agent_script} {dump_command} {pmm_temp_dir} {shallow_dump_xml_file}"
        return command, options

    def _get_create_capability_dump_command(self, runner, selection):
        if isinstance(selection, DumpSelected):
            filter_file = self._create_filter_file(runner, selection)
        else:
            filter_file = ""

        options = {
            'agent_script': self._get_agent_path(),
            'dump_command': '--create-capability-info-dump',
            'pmm_temp_dir': self._source_pmm_temp_dir,
            'capability_dump_xml_file': self._source_capability_dump_xml_file,
            'filter_file': filter_file
        }
        command = "{agent_script} {dump_command} {filter_file} {pmm_temp_dir} {capability_dump_xml_file}"

        return command, options

    def _download_dump(self, remote_dump_path, local_dump_path):
        """Download migration dump files as a ZIP archive with specified name.
        """
        logger.info(messages.DOWNLOADING_MIGRATION_DUMP_S % (remote_dump_path,))
        is_zipfile = remote_dump_path[-4:] == '.zip'
        if is_zipfile:
            zip_filename = remote_dump_path
            with self._source_server.runner() as runner:
                runner.get_file(zip_filename, local_dump_path)
        else:
            with self._source_server.runner() as runner:
                runner.download_directory_as_zip(remote_dump_path, local_dump_path)

    def _get_agent_path(self):
        return ntpath.join(self._get_base_path(), 'WINAgentMng.exe')

    def _deploy(self):
        if self._source_server.plesk_version >= (17, 0):
            return
        if self._get_base_path() is None:
            with self._source_server.runner() as runner:
                assert isinstance(runner, BaseRunner)

                storage_path = self._get_installer_path_in_storage()

                if os.path.exists(storage_path) is False:
                    # download installer and save it into storage
                    storage_url = 'http://autoinstall.plesk.com/panel-migrator/agents/%s' % (
                        self._agent_installer_filename
                    )
                    logger.info(messages.DOWNLOAD_DUMP_AGENT.format(url=storage_url))
                    download(storage_url, storage_path)
                # upload installer
                installer_path = self._source_server.get_session_file_path(self._agent_installer_filename)
                logger.info(messages.UPLOAD_DUMP_AGENT.format(
                    path=installer_path,
                    server=self._source_server.description())
                )
                runner.upload_file(storage_path, installer_path)

                self._check_requirements(runner)

                possible_registry_keys = [
                    'HKLM\SOFTWARE\Wow6432Node\PleskMigrator\Agent',
                    'HKLM\SOFTWARE\Wow6432Node\PanelMigrator\Agent',
                    'HKLM\SOFTWARE\PleskMigrator\Agent',
                    'HKLM\SOFTWARE\PanelMigrator\Agent'
                ]

                # check if agent was already installed and remove it
                base_path = get_from_registry(runner, possible_registry_keys, 'INSTALLDIR')
                if base_path is not None:
                    runner.execute_command(
                        'msiexec /x {installer_path} /qn', dict(installer_path=installer_path),
                        ExecutionOptions(ignore_exit_code=True, output_encoding=runner.native_output_encoding)
                    )

                # install agent and store path in session
                logger.info(messages.INSTALL_DUMP_AGENT.format(server=self._source_server.description()))

                cmd = 'msiexec /i {installer_path} /qn'
                execution_results = runner.execute_command(
                    cmd, dict(installer_path=installer_path),
                    ExecutionOptions(ignore_exit_code=True, output_encoding=runner.native_output_encoding)
                )
                if execution_results.exit_code != 0:
                    if execution_results.exit_code == 1618:
                        raise NonZeroExitCodeException(
                            messages.UNABLE_TO_INSTALL_DUMP_AGENT_ANOTHER_INSTALLER % (
                                self._source_server.description()
                            ),
                            stdout=execution_results.stdout, stderr=execution_results.stderr,
                            exit_code=execution_results.exit_code
                        )
                    else:
                        raise NonZeroExitCodeException(
                            messages.INSTALL_COMMAND_FAILED_WITH_EXIT_CODE %
                            (
                                cmd,
                                execution_results.exit_code,
                                safe_string_repr(execution_results.stdout.replace('\x00', '')),
                                safe_string_repr(execution_results.stderr)
                            ),
                            stdout=execution_results.stdout, stderr=execution_results.stderr,
                            exit_code=execution_results.exit_code
                        )

                base_path = get_from_registry(runner, possible_registry_keys, 'INSTALLDIR')
                if base_path is not None:
                    write_yaml(
                        self.global_context.session_files.get_path_to_migration_agent(self._source_server.ip()),
                        base_path
                    )
                return base_path

    def _check_requirements(self, runner):
        """
        Check that source server meet requirements to deploy and run Panel Migrator Agent,
        raise exception otherwise
        :type runner: parallels.core.run_command.BaseRunner
        """
        def _raise_exception():
            raise MigrationNoContextError(
                messages.UNABLE_USE_PANEL_MIGRATOR_AGENT_SERVER.format(
                    server=self._source_server.description()
                )
            )
        if not find_in_registry(runner, ['HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework'], 'v4.*'):
            _raise_exception()

    def _get_base_path(self):
        path = read_yaml(
            self.global_context.session_files.get_path_to_migration_agent(self._source_server.ip()),
            True
        )
        if path is not None:
            # If agent was removed, but we have information in session directory that it was installed,
            # then try to install it again
            with self._source_server.runner() as runner:
                if not runner.file_exists(path):
                    path = None
        return path

    def _get_installer_path_in_storage(self):
        return os.path.join(get_agents_base_path(), self._agent_installer_filename)

    @property
    def _agent_installer_filename(self):
        return 'panel-migrator-agent-12.5.msi'

    def _convert_dump_to_plesk9(self):
        """Convert Windows backup format remotely (on the source server)."""
        logger.info(messages.CONVERT_PLESK_DUMP_PLESK_FORMAT)

        with self._source_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            dump_archive = ntpath.join(
                self._source_configuration_dump_dir,
                'psaDump.zip'  # this name is hardcoded at agent side
            )
            if not runner.file_exists(dump_archive):
                raise MigrationNoContextError(safe_format(
                    messages.PLESK_8_DUMP_FILE_NOT_FOUND, dump_archive=dump_archive
                ))

            converter_path = self.upload_plesk9_converter(runner)
            runner.execute_command(
                u'{converter_path} --source={source_dump_archive} --destination={converted_dump_path}',
                dict(
                    converter_path=converter_path,
                    source_dump_archive=dump_archive,
                    converted_dump_path=self._source_converted_dump_path
                )
            )

    def check(self):
        raise NotImplementedError()

    def uninstall(self):
        """Windows agent uninstallation is not supported."""
        pass

    def upload_plesk9_converter(self, runner):
        """Upload Plesk dump format converter to the source server.

        :type runner: parallels.core.runners.base.BaseRunner
        """
        source_plesk_dir = self._source_server.plesk_dir
        runner.mkdir(u'%s\\PMM' % (source_plesk_dir,))
        converter = u'admin/bin/pre9-backup-convert.exe'
        converter_xsl = u'PMM/migration-dump-convert.xsl'
        for path in [converter, converter_xsl]:
            local_path = migrator_utils.get_package_extras_file_path(
                parallels.plesk.source.plesk,
                os.path.basename(path)
            )
            remote_path = os.path.join(source_plesk_dir.rstrip('\\'), path)
            runner.upload_file(local_path, remote_path)

        upload_path = windows_path_join(source_plesk_dir, windows_utils.to_cmd(converter))
        logger.debug(messages.DEBUG_UPLOAD_CONVERTER, upload_path)
        return upload_path

    def _clean_source(self):
        """Remove all temporary files from the source server"""
        with self._source_server.runner() as runner:
            try:
                runner.remove_directory(self._source_pmm_temp_dir)
            except DirectoryRemoveException as e:
                # print warning, try to continue anyway
                logger.warning(safe_string_repr(e))
            try:
                runner.remove_directory(self._source_converted_dump_path)
            except DirectoryRemoveException as e:
                # print warning, try to continue anyway
                logger.warning(safe_string_repr(e))

    @property
    def _source_pmm_temp_dir(self):
        return self._source_server.get_session_file_path('pmm-temp')

    @property
    def _source_capability_dump_xml_file(self):
        return self._source_server.get_session_file_path(r'pmm-temp\capability.xml')

    @property
    def _source_shallow_dump_xml_file(self):
        return self._source_server.get_session_file_path(r'pmm-temp\shallow.xml')

    @property
    def _source_configuration_dump_dir(self):
        return self._source_server.get_session_file_path(r'pmm-temp\dump')

    @property
    def _source_converted_dump_path(self):
        # Workaround for issue PMT-436:
        # make path to backup file as short as possible, but root cause, as we suppose,
        # should be fixed in plesk 8 backup converter
        return self._source_server.get_session_file_path('c')
