import json
import os
import posixpath
import textwrap

import parallels
from parallels.core import MigrationError
from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils import migrator_utils
from parallels.core.utils.common import is_empty_list, safe_format
from parallels.core.utils.json_utils import encode_json
from parallels.core.utils.common.sql_utils import format_sql_results_for_log
from parallels.core.utils.pmm.agent import DumpAll, DumpSelected
from parallels.core.utils.pmm.utils import convert_sql_query_for_perl
from parallels.plesk.source.legacy.pmm_agent import UnixPmmMigrationAgent
from parallels.plesk.source.plesk import messages
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

logger = get_logger(__name__)


class PleskXPmmMigrationAgent(UnixPmmMigrationAgent):
    """Migration agent for Plesk panel and Expand.

    'self.max_plesk_version' specifies maximum version of source Plesk panel
    that is supported by the agent.
    """
    def __init__(self, global_context, server, migrator_pmm_dir, settings):
        source_plesk_version = server.plesk_version
        if source_plesk_version < (17, 0):
            self.max_plesk_version = {'MAJOR': 12, 'MINOR': 5, 'PATCH': 30}
        else:
            self.max_plesk_version = {'MAJOR': 17, 'MINOR': 0, 'PATCH': 17}
        self.remote_log_filename = 'dump.log'
        super(PleskXPmmMigrationAgent, self).__init__(global_context, server, migrator_pmm_dir, settings)

    @property
    def common_pmm_dir(self):
        source_plesk_version = self._source_server.plesk_version
        if source_plesk_version < (17, 0):
            shared_dir = 'plesk_12_5_pmm_shared'
        else:
            shared_dir = 'plesk_17_0_pmm_shared'

        return migrator_utils.get_package_extras_file_path(parallels.plesk.source.legacy, shared_dir)

    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.

        :type query: str|unicode
        :type query_args: list|dict
        :rtype: list[dict]
        :type log_output: bool
        """
        with logger.debug_level_context(messages.EXECUTE_SQL_QUERY):
            logger.fdebug(messages.EXECUTE_SQL_QUERY_QUERY, query=query)
            command = u"""cd {path}; {perl} ExecutePleskSQL.pl"""

            query_template, query_template_params = convert_sql_query_for_perl(query, query_args)
            stdin_content = encode_json(dict(
                query=query_template,
                query_args=query_template_params
            ))

            with self._source_server.runner() as runner:
                assert isinstance(runner, BaseRunner)

                arguments = {
                    'perl': self._perl_bin,
                    'path': self.agent_dir,
                }
                execution_result = runner.execute_command(
                    command, arguments,
                    ExecutionOptions(
                        stdin_content=stdin_content, log_output=False,
                        ignore_exit_code=True
                    )
                )

                try:
                    result = json.loads(execution_result.stdout.decode('utf-8'))
                except Exception:
                    logger.exception()
                    raise MigrationError(
                        safe_format(
                            messages.FAILED_TO_EXECUTE_SQL_QUERY_PARSE_JSON,
                            exit_code=execution_result.exit_code,
                            stdout=execution_result.stdout, stderr=execution_result.stderr,
                            query=query
                        )
                    )

                if result.get('status') != 'ok':
                    raise MigrationError(safe_format(
                        messages.FAILED_TO_EXECUTE_SQL_QUERY,
                        reason=result['message']
                    ))
                else:
                    rowset = self._convert_rowset_int_values_to_str(result['rowset'])
                    logger.fdebug(messages.EXECUTE_SQL_QUERY_RESULTS, result=format_sql_results_for_log(rowset))
                    return rowset

    def _deploy(self):
        agent_dir = super(PleskXPmmMigrationAgent, self)._deploy()
        self._patch_template(agent_dir, self.max_plesk_version)
        return agent_dir

    def _run(self, local_data_filename, local_log_filename, selection=DumpAll()):
        """Run migration agent on a source panel and download the results.

        Execute the agent which puts results into XML dump file.
        Download dump XML file and log file to specified locations:
        - dump_xml_filename - local filename to put dump XML to
        - dump_log_filename - local filename to put dump log to

        Raise MigrationError in case of any failure.
        """
        logger.info(messages.LOG_CREATE_SOURCE_PANEL_MIGRATION_DUMP)
        self.local_log_filename = local_log_filename
        source_plesk_version = self._source_server.plesk_version
        if source_plesk_version < (17, 0):
            logger.debug(messages.LOG_RUN_MIGRATION_AGENT)
            self._make_dump(selection)
            self._download_dump(local_data_filename)
        else:
            HostingDescriptionAgent(self).create_dump(local_data_filename, selection)

    def _run_shallow(self, local_data_filename):
        logger.info(messages.LOG_CREATE_SOURCE_PANEL_MIGRATION_SHALLOW_DUMP)
        shallow_model = create_plesk_shallow_model(
            self, self._source_server.plesk_version
        )
        ShallowXMLConverter(shallow_model).write_xml(local_data_filename)

    def _run_capability(self, local_data_filename, local_log_filename, selection=DumpAll()):
        logger.info(messages.LOG_CREATE_SOURCE_PANEL_MIGRATION_CAPABILITY_DUMP)
        self.local_log_filename = local_log_filename
        source_plesk_version = self._source_server.plesk_version
        if source_plesk_version < (12, 0):
            logger.debug(messages.LOG_RUN_MIGRATION_AGENT)
            self._make_capability_dump(selection)
            self._download_agent_file('dump.xml', local_data_filename)
        else:
            capability_model = create_plesk_capability_model(
                self, self._source_server.plesk_version, selection
            )
            CapabilityXMLConverter(capability_model).write_xml(local_data_filename)

    def _patch_template(self, agent_dir, max_plesk_version):
        """Fill in maximum supported Plesk version."""
        command = (
            u"""
            sed "s/@@MAJOR_VERSION@@/{MAJOR}/;
            s/@@MINOR_VERSION@@/{MINOR}/;
            s/@@PATCH_VERSION@@/{PATCH}/;" \\
            ./PleskMaxVersion.pm.in > ./PleskMaxVersion.pm""".format(
                **max_plesk_version
            )
        )
        self._run_agent_command(
            u"cd {path}; %s" % textwrap.dedent(command),
            {'path': agent_dir})

    def _make_dump(self, selection=DumpAll()):
        """Run the agent and create migration dump.

        Download agent log file even in case, if the agent has failed.
        """
        logger.debug(messages.LOG_RUN_MIGRATION_AGENT)
        command = u"""cd {path}; {perl} {script} %s > {log} 2>&1"""

        selection_options, selection_args = self._get_selection_cli_options(selection)
        cli_options = [
            '--configuration-only',
            # dump admin info to get admin descriptions of resellers, clients and subscriptions
            '--admin-info',
            '--server', '-v', '5'
        ] + selection_options

        target_plesk_version = self.global_context.conn.target.plesk_server.get_plesk_version()
        if target_plesk_version >= (12, 5):
            cli_options.append('--skip-extra-server-settings')

        cli_args = {
            'perl': self._perl_bin,
            'path': self.agent_dir,
            'script': self.settings.agent_script_name,
            'log': self.remote_log_filename,
        }
        cli_args.update(selection_args)

        try:
            self._run_agent_command(command % (" ".join(cli_options)), cli_args)
        finally:
            self._download_agent_file(
                self.remote_log_filename, self.local_log_filename
            )
            logger.fdebug(messages.LOG_CONFIGURATION_DUMP_LOG_FILE, filename=self.local_log_filename)

    def _make_capability_dump(self, selection=DumpAll()):
        """Run the agent and create capability dump.

        Download agent log file even in case, if the agent has failed.
        """
        logger.debug(messages.LOG_RUN_MIGRATION_AGENT)
        command = u"""cd {path}; {perl} {script} %s > {log} 2>&1"""

        selection_options, selection_args = self._get_selection_cli_options(selection)
        cli_options = ['--capability-only', '-v', '5'] + selection_options
        cli_args = {
            'perl': self._perl_bin,
            'path': self.agent_dir,
            'script': self.settings.agent_script_name,
            'log': self.remote_log_filename,
        }
        cli_args.update(selection_args)

        try:
            self._run_agent_command(command % (" ".join(cli_options)), cli_args)
        finally:
            self._download_agent_file(
                self.remote_log_filename, self.local_log_filename
            )
            logger.fdebug(messages.LOG_CAPABILITY_DUMP_LOG_FILE, filename=self.local_log_filename)

    @staticmethod
    def _get_selection_cli_options(selection):
        selection_cli_options = []
        selection_cli_args = {}
        if isinstance(selection, DumpAll):
            selection_cli_options.append('--dump-all')
        elif isinstance(selection, DumpSelected):
            if not is_empty_list(selection.domains):
                selection_cli_options.append('--dump-domains={domains}')
                selection_cli_args['domains'] = ",".join(selection.domains)
            if not is_empty_list(selection.clients):
                selection_cli_options.append('--dump-clients={clients}')
                selection_cli_args['clients'] = ",".join(selection.clients)
            if not is_empty_list(selection.resellers):
                selection_cli_options.append('--dump-resellers={resellers}')
                selection_cli_args['resellers'] = ",".join(selection.resellers)
        return selection_cli_options, selection_cli_args

    def _download_dump(self, local_data_filename):
        logger.debug(messages.LOG_DOWNLOAD_DUMP_XML, local_data_filename)
        remote_dump_file = 'migration_dump.tar'
        dump_directories = ['clients', 'domains', 'resellers', '.discovered']
        with self._source_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.remove_file(posixpath.join(self.agent_dir, remote_dump_file))
            command = u"tar rf {dumpfile} backup_info*"
            args = dict(
                agent_dir=self.agent_dir,
                dumpfile=remote_dump_file
            )
            i = 0
            for directory in dump_directories:
                if runner.file_exists(posixpath.join(self.agent_dir, directory)):
                    command += ' {dir%i}' % i
                    args['dir%i' % i] = directory
                    i += 1
            runner.execute_command('cd {agent_dir}; ' + command, args)

        self._download_agent_file(remote_dump_file, local_data_filename)

    def _download_agent_file(self, remote_file, local_file):
        """Download a file from agent directory on source server."""
        try:
            with self._source_server.runner() as runner:
                logger.debug(messages.LOG_DOWNLOAD_FILE_TO, local_file)
                runner.get_file(os.path.join(self.agent_dir, remote_file), local_file)
        except Exception as e:
            logger.exception()
            logger.error(messages.ERROR_DOWNLOADING_FILE, e)

    def _run_agent_command(self, command, arguments):
        """Run a command on the source server."""
        try:
            with self._source_server.runner() as runner:
                assert isinstance(runner, BaseRunner)
                runner.execute_command(textwrap.dedent(command), arguments)
        except Exception as e:
            logger.exception()
            raise MigrationError(
                safe_format(messages.ERROR_RUNNING_COMMAND, reason=e)
            )

    @staticmethod
    def _convert_rowset_int_values_to_str(rowset):
        """Convert integer values in rowset to strings.

        Old DBI::mysql libraries return MySQL integer values as strings, new DBI::mysql libraries return
        integer MySQL values as integers. We don't want to keep this difference in mind for code which uses
        execute MySQL query feature of PMM agent. So convert all integers to strings.

        :type rowset: list[dict]
        :rtype: list[dict]
        """
        rowset_fixed = []

        for row in rowset:
            row_fixed = {}

            for column_name, value in row.items():
                if isinstance(value, int):
                    value = str(value)

                row_fixed[column_name] = value

            rowset_fixed.append(row_fixed)

        return rowset_fixed
