import ntpath
import re
from xml.etree import ElementTree

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.common import default, cached
from parallels.core import messages
from parallels.core.utils.migrator_utils import split_unix_path
from parallels.core.utils.plesk_utils import get_windows_util_path

logger = get_logger(__name__)


class PMMCLICommandBase(object):
    """Wrapper around "pmmcli" command"""

    def __init__(self, server):
        """
        :param parallels.core.connections.plesk_server.PleskServer server:
        """
        self._server = server

    @property
    def _pmmcli_bin(self):
        """Get full path to pmmcli.exe binary"""
        raise NotImplementedError()

    def import_dump(self, backup_path, additional_env_vars=None, target_dir=None):
        """Call pmmcli --import-file-as-dump for specified backup

        :param str|unicode backup_path: path to backup archive
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode target_dir:
        :return: PmmCliResult
        """
        raise NotImplementedError()

    def export_dump(self, specification_file_path, exported_dump_path, additional_env_vars=None, source_dir=None):
        """Call pmmcli --export-dump-as-file for specified dump specification file

        :param str|unicode specification_file_path: path to dump specification file
        :param str|unicode exported_dump_path: path to file where to export the dump
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode source_dir:
        :return: PmmCliResult
        """
        raise NotImplementedError()

    def restore(self, admin_guid, dump_xml_path, env=None):
        """Call pmmcli --restore for specified dump XML

        Arguments:
        :param admin_guid: GUID of administrator (required by Plesk to restore dump)
        :param dump_xml_path: path to domain backup XML
        :param env: additional environment variables as dictionary

        Returns: RestoreResult
        """
        raise NotImplementedError()

    def get_task_status(self, task_id):
        """Request restoration task status.

        Args:
        task_id: ID of a started restore task.

        Returns:
        RestoreTaskStatus object representing current restore status
        """
        raise NotImplementedError()

    def delete_dump(self, backup_path, env=None):
        """Call pmmcli --delete-dump for specified backup

        :param backup_path: path to backup archive
        :param env: additional environment variables as dictionary
        :return: None
        """
        raise NotImplementedError()

    def _get_import_dump_request(self, source_dir, source_file, target_dir):
        return get_import_dump_request(source_dir, source_file, target_dir)

    def _get_export_dump_request(self, source_dir, xml_file, target_dir, target_file):
        return get_export_dump_request(source_dir, xml_file, target_dir, target_file)

    def _get_restore_dump_request(self, admin_guid, xml_root_dir, xml_filename):
        # By default file names are returned in:
        # - UTF-8 for Unix
        # - CP1252 (which is commonly called ANSI) for Windows
        # If path to domain XML contains binary symbols (not-UTF-8 for Unix and non-CP1251 for Windows),
        # this code will fail when decoding string.
        # But probability to get that is very very low - you should have Plesk installed to directory with
        # binary symbols, or have migrator directories with binary symbols, which is a pretty unlikely situation.
        filename_encoding = 'cp1252' if self._server.is_windows() else 'utf-8'
        xml_root_dir = xml_root_dir.decode(filename_encoding)
        xml_filename = xml_filename.decode(filename_encoding)

        return get_restore_dump_request(xml_root_dir, xml_filename, admin_guid)

    def _get_delete_dump_request(self, dump_dir, relative_path_to_xml_filename):
        return get_delete_dump_request(dump_dir, relative_path_to_xml_filename)


class PMMCLICommandUnix(PMMCLICommandBase):
    """Wrapper around "pmmcli" command for Unix"""

    @property
    def _pmmcli_bin(self):
        """Get full path to pmmcli binary"""
        return self._server.plesk_dir + u"/admin/bin/pmmcli"

    def import_dump(self, backup_path, additional_env_vars=None, target_dir=None):
        """Call pmmcli --import-file-as-dump for specified backup

        :param str|unicode backup_path: path to backup archive
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode target_dir:
        :return: PmmCliResult
        """
        logger.debug(messages.IMPORT_BACKUP_TARGET_PANEL_PMM_REPOSITORY)
        additional_env_vars = default(additional_env_vars, {})
        request_root_dir, request_file_name = split_unix_path(backup_path)
        target_dir = default(target_dir, self._server.dump_dir)
        import_dump_request_xml = self._get_import_dump_request(
            source_dir=request_root_dir,
            source_file=request_file_name,
            target_dir=target_dir
        )
        logger.debug(messages.DEBUG_IMPORT_DUMP_REQUEST_XML, import_dump_request_xml)
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --import-file-as-dump',
                dict(
                    pmmcli_bin=self._pmmcli_bin,
                ),
                ExecutionOptions(
                    stdin_content=import_dump_request_xml,
                    environment_variables=additional_env_vars,
                    output_encoding='utf-8'
                )
            ).stdout

        return PmmCliResult(stdout)

    def export_dump(self, specification_file_path, exported_dump_path, additional_env_vars=None, source_dir=None):
        """Call pmmcli --export-dump-as-file for specified dump specification file

        :param str|unicode specification_file_path: path to dump specification file
        :param str|unicode exported_dump_path: path to file where to export the dump
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode source_dir:
        :return: ExportResult
        """
        logger.debug(messages.EXPORT_BACKUP_TARGET_PANEL_PMM_REPOSITORY)
        additional_env_vars = default(additional_env_vars, {})
        exported_dump_root_dir, exported_dumpfile_name = split_unix_path(exported_dump_path)
        backup_dir = default(source_dir, self._server.dump_dir)
        export_dump_request_xml = self._get_export_dump_request(
            source_dir=backup_dir,
            xml_file=specification_file_path,
            target_dir=exported_dump_root_dir,
            target_file=exported_dumpfile_name
        )
        logger.debug(messages.DEBUG_EXPORT_DUMP_REQUEST_XML, export_dump_request_xml)
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --export-dump-as-file',
                dict(
                    pmmcli_bin=self._pmmcli_bin
                ),
                ExecutionOptions(
                    stdin_content=export_dump_request_xml,
                    environment_variables=additional_env_vars,
                    output_encoding='utf-8'
                )
            ).stdout

        return PmmCliResult(stdout)

    def restore(self, admin_guid, dump_xml_path, env=None):
        """Call pmmcli --restore for specified dump XML

        Arguments:
        :param admin_guid: GUID of administrator (required by Plesk to restore dump)
        :param dump_xml_path: path to domain backup XML
        :param env: additional environment variables as dictionary

        Returns: RestoreResult
        """
        backup_xml_directory, backup_xml_filename = split_unix_path(
            dump_xml_path
        )
        with logger.debug_level_context(messages.DEBUG_PREPARE_RESTORE_REQUEST_XML):
            restore_request_xml = self._get_restore_dump_request(
                admin_guid,
                backup_xml_directory,
                backup_xml_filename,
            )
            logger.debug(messages.DEBUG_RESTORE_REQUEST_XML, restore_request_xml)

        with logger.debug_level_context(messages.DEBUG_START_RESTORATION):
            with self._server.runner() as runner:
                assert isinstance(runner, BaseRunner)
                stdout = runner.execute_command(
                    u'{pmmcli_bin} --restore',
                    dict(pmmcli_bin=self._pmmcli_bin),
                    ExecutionOptions(
                        stdin_content=restore_request_xml.encode('utf-8'),
                        environment_variables=env,
                        output_encoding='utf-8'
                    )
                ).stdout

        return RestoreResult(stdout)

    def get_task_status(self, task_id):
        """Request restoration task status.

        Args:
        task_id: ID of a started restore task.

        Returns:
        RestoreTaskStatus object representing current restore status
        """
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --get-task-status {task_id}',
                dict(
                    pmmcli_bin=self._pmmcli_bin,
                    task_id=task_id
                ),
                ExecutionOptions(
                    output_encoding='utf-8'
                )
            ).stdout
        return RestoreTaskStatus(stdout)

    def delete_dump(self, backup_path, env=None):
        """Call pmmcli --delete-dump for specified backup

        :param backup_path: path to backup archive
        :param env: additional environment variables as dictionary
        :return: None
        """

        logger.debug(messages.REMOVE_BACKUP_FILE_FROM_TARGET % backup_path)
        remove_dump_xml = self._get_delete_dump_request(self._server.dump_dir, backup_path)
        logger.debug(messages.DEBUG_DELETE_DUMP_REQUEST_XML % remove_dump_xml)
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.execute_command(
                u'{pmmcli_bin} --delete-dump',
                dict(pmmcli_bin=self._pmmcli_bin),
                ExecutionOptions(
                    stdin_content=remove_dump_xml,
                    environment_variables=env,
                    output_encoding='utf-8'
                )
            )


class PMMCLICommandWindows(PMMCLICommandBase):
    def import_dump(self, backup_path, additional_env_vars=None, target_dir=None):
        """Call pmmcli --import-file-as-dump for specified backup

        :param str|unicode backup_path: path to backup archive
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode target_dir:
        :return: PmmCliResult
        """
        logger.debug(messages.IMPORT_BACKUP_TARGET_PANEL_PMM_REPOSITORY)
        additional_env_vars = default(additional_env_vars, {})
        backup_directory = ntpath.dirname(backup_path)
        backup_filename = ntpath.basename(backup_path)
        target_dir = default(target_dir, self._server.dump_dir)
        import_dump_request_xml = self._get_import_dump_request(
            source_dir=backup_directory,
            source_file=backup_filename,
            target_dir=target_dir
        )
        logger.fdebug(messages.PREPARED_XML_FOR_PMMCLI_IMPORT, xml_content=import_dump_request_xml)
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            full_env = dict(PLESK_DIR=self._server.plesk_dir.encode('utf-8'))
            full_env.update(additional_env_vars)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --import-file-as-dump',
                dict(
                    pmmcli_bin=self._pmmcli_bin,
                ),
                ExecutionOptions(
                    environment_variables=full_env,
                    stdin_content=import_dump_request_xml,
                    output_encoding='utf-8'
                )
            ).stdout

        return PmmCliResult(stdout)

    def export_dump(self, specification_file_path, exported_dump_path, additional_env_vars=None, source_dir=None):
        """Call pmmcli --export-dump-as-file for specified dump specification file

        :param str|unicode specification_file_path: path to dump specification file
        :param str|unicode exported_dump_path: path to file where to export the dump
        :param dict[str|unicode, str|unicode] additional_env_vars:
        :param str|unicode source_dir:
        :return: ExportResult
        """
        logger.debug(messages.EXPORT_BACKUP_TARGET_PANEL_PMM_REPOSITORY)
        additional_env_vars = default(additional_env_vars, {})
        exported_dump_root_dir = ntpath.dirname(exported_dump_path)
        exported_dumpfile_name = ntpath.basename(exported_dump_path)
        backup_dir = default(source_dir, self._server.dump_dir)
        export_dump_request_xml = self._get_export_dump_request(
            source_dir=backup_dir,
            xml_file=specification_file_path,
            target_dir=exported_dump_root_dir,
            target_file=exported_dumpfile_name
        )
        logger.fdebug(messages.PREPARED_XML_FOR_PMMCLI_EXPORT, xml_content=export_dump_request_xml)
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            full_env = dict(PLESK_DIR=self._server.plesk_dir.encode('utf-8'))
            full_env.update(additional_env_vars)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --export-dump-as-file',
                dict(
                    pmmcli_bin=self._pmmcli_bin,
                ),
                ExecutionOptions(
                    environment_variables=full_env,
                    stdin_content=export_dump_request_xml,
                    output_encoding='utf-8'
                )
            ).stdout

        return PmmCliResult(stdout)

    def restore(self, admin_guid, dump_xml_path, env=None):
        """Call pmmcli --restore for specified dump XML

        Arguments:
        :param admin_guid: GUID of administrator (required by Plesk to restore dump)
        :param dump_xml_path: path to domain backup XML
        :param env: additional environment variables as dictionary

        Returns: RestoreResult
        """
        backup_xml_directory = ntpath.dirname(dump_xml_path)
        backup_xml_filename = ntpath.basename(dump_xml_path)
        restore_request_xml = self._get_restore_dump_request(
            admin_guid, backup_xml_directory, backup_xml_filename
        )

        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            full_env = dict(PLESK_DIR=self._server.plesk_dir.encode('utf-8'))
            if env:
                full_env.update(env)
            stdout = runner.execute_command(
                u'{pmmcli_bin} --restore',
                dict(
                    pmmcli_bin=self._pmmcli_bin,
                ),
                ExecutionOptions(
                    environment_variables=full_env,
                    stdin_content=restore_request_xml.encode('utf-8'),
                    output_encoding='utf-8'
                )
            ).stdout

        return RestoreResult(stdout)

    def get_task_status(self, task_id):
        """Request restoration task status.

        Args:
        task_id: ID of a started restore task.

        Returns:
        RestoreTaskStatus object representing current restore status
        """
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            full_env = dict(PLESK_DIR=self._server.plesk_dir.encode('utf-8'))
            stdout = runner.execute_command(
                u'{pmmcli_bin} --get-task-status {task_id}', dict(
                    pmmcli_bin=self._pmmcli_bin,
                    task_id=task_id,
                ),
                ExecutionOptions(
                    environment_variables=full_env,
                    output_encoding='utf-8'
                )
            ).stdout
        return RestoreTaskStatus(stdout)

    def delete_dump(self, backup_path, env=None):
        """Call pmmcli --delete-dump for specified backup
        :param backup_path: path to backup archive
        :param env: additional environment variables as dictionary
        :return: None
        """

        logger.debug(messages.REMOVE_BACKUP_FILE_FROM_TARGET % backup_path)
        remove_dump_xml = self._get_delete_dump_request(self._server.dump_dir, backup_path)
        logger.debug(messages.DEBUG_DELETE_DUMP_REQUEST_XML % remove_dump_xml)

        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            full_env = dict(PLESK_DIR=self._server.plesk_dir.encode('utf-8'))
            if env:
                full_env.update(env)
            runner.execute_command(
                u'{pmmcli} --delete-dump', dict(
                    pmmcli=self._pmmcli_bin,
                ),
                ExecutionOptions(
                    stdin_content=remove_dump_xml,
                    environment_variables=full_env,
                    output_encoding='utf-8'
                )
            )

    @property
    def _pmmcli_bin(self):
        """Get full path to pmmcli.exe binary"""
        with self._server.runner() as runner:
            return get_windows_util_path(runner, 'pmmcli.exe')


class PmmCliResult(object):
    def __init__(self, response_str):
        """
        :type response_str: str 
        """
        self._response_str = response_str
        self._xml = ElementTree.fromstring(response_str)

    @property
    def raw_stdout(self):
        return self._response_str

    @property
    def error_code(self):
        return self._xml.findtext('errcode')

    @property
    def error_message(self):
        return self._xml.findtext('errmsg')

    @property
    def backup_xml_file_name(self):
        dump_info = self._xml.find('data/dump')
        if dump_info is not None:
            return dump_info.attrib['name']
        else:
            return None

    @property
    def backup_xml_file_path(self):
        dump_info = self._xml.find('data/dump')
        if dump_info is not None:
            return dump_info.attrib['fullname']
        else:
            return None

    @property
    def backup_id(self):
        if self.backup_xml_file_name is None:
            return None

        match = re.search(r'(\d+)\.xml$', self.backup_xml_file_name)
        if match is None:
            return None
        else:
            backup_id = match.group(1)
            return backup_id

    @property
    def backup_prefix(self):
        if self.backup_xml_file_name is not None:
            return self.backup_xml_file_name[:self.backup_xml_file_name.find('_')]
        else:
            return None


class RestoreResult(object):
    def __init__(self, response_str):
        """
        :type response_str: str 
        """
        self._response_str = response_str
        self._restore_result_xml = ElementTree.fromstring(response_str)

    @property
    def raw_stdout(self):
        return self._response_str

    @property
    def task_id(self):
        return self._restore_result_xml.findtext('./data/task-id')

    @property
    def error_code(self):
        return self._restore_result_xml.findtext('./errcode')

    @property
    def error_message(self):
        return self._restore_result_xml.findtext('./errmsg')


class RestoreTaskStatus(object):
    def __init__(self, response_str):
        """
        :type response_str: str 
        """
        self._response_str = response_str
        xml = ElementTree.fromstring(response_str)
        self._status_xml = xml.find('data/task-status/mixed')
        self._stopped_xml = xml.find('data/task-status/stopped')

    @property
    def raw_stdout(self):
        return self._response_str

    @property
    @cached
    def is_running(self):
        """Whether restoration process is running or finished."""
        if self.is_interrupted:
            return False

        if self._status_xml is not None and 'status' in self._status_xml.attrib:
            # 'status' node is '<mixed status="success" ...>'
            logger.debug(messages.DEBUG_RESTORE_TASK_FINISHED)
            return False
        else:
            # 'status' node is '<mixed>'
            logger.debug(messages.DEBUG_RESTORE_TASK_RUNNING)
            return True

    @property
    def is_interrupted(self):
        return self._stopped_xml is not None

    @property
    def log_location(self):
        """Path to restoration log"""
        if self._status_xml is None:
            return None
        else:
            return self._status_xml.attrib.get('log-location')


def get_delete_dump_request(dump_dir, relative_path_to_xml_filename):
    """Create XML that should be passed to pmmcli.exe --delete-dump

    Return XML as a string"""

    return u"""<?xml version="1.0"?>
<delete-dump-query>
    <dump-specification>
        <dumps-storage-credentials storage-type="local">
            <root-dir>{dump_dir}</root-dir>
            <file-name/>
        </dumps-storage-credentials>
        <name-of-info-xml-file>{file_name}</name-of-info-xml-file>
    </dump-specification>
    <object-specification type="server" guid="00000000-0000-0000-0000-000000000000"/>
</delete-dump-query>
    """.format(
        dump_dir=dump_dir,
        file_name=relative_path_to_xml_filename
    )


def get_restore_dump_request(xml_root_dir, xml_filename, admin_guid=""):
    """Create XML that should be passed to pmmcli.exe --restore

    Return XML as a string"""

    return u"""<?xml version="1.0"?>
<restore-task-description owner-guid="{admin_guid}" owner-type="server">
    <source>
        <dump-specification>
            <dumps-storage-credentials storage-type="local">
                <root-dir>{xml_root_dir}</root-dir>
                <file-name/>
            </dumps-storage-credentials>
            <name-of-info-xml-file>{xml_filename}</name-of-info-xml-file>
        </dump-specification>
    </source>
    <objects>
        <selected>
            <node children-processing-type="copy" name="domain"/>
            <node children-processing-type="copy" name="dump-info"/>
        </selected>
    </objects>
    <ignore-errors>
        <ignore-error type="sign"/>
    </ignore-errors>
    <misc delete-downloaded-dumps="false" suspend="false" verbose-level="5"/>
    <conflict-resolution-rules>
        <policy>
            <timing>
                <resolution>
                    <overwrite/>
                </resolution>
            </timing>
            <resource-usage>
                <resolution>
                    <overuse/>
                </resolution>
            </resource-usage>
            <configuration>
                <resolution>
                    <automatic/>
                </resolution>
            </configuration>
        </policy>
    </conflict-resolution-rules>
</restore-task-description>""".format(
        xml_root_dir=xml_root_dir,
        xml_filename=xml_filename,
        admin_guid=admin_guid,
    )


def get_import_dump_request(source_dir, source_file, target_dir):
    """Create XML that should be passed to pmmcli.exe --import-file-as-dump

    Return XML as a string"""

    return u"""<?xml version="1.0"?>
<src-dst-files-specification guid="00000000-0000-0000-0000-000000000000" type="server">
    <src>
        <dumps-storage-credentials storage-type="file">
            <root-dir>{source_dir}</root-dir>
            <file-name>{source_file}</file-name>
        </dumps-storage-credentials>
    </src>
    <dst>
        <dumps-storage-credentials storage-type="local">
            <root-dir>{target_dir}</root-dir>
            <ignore-backup-sign>true</ignore-backup-sign>
        </dumps-storage-credentials>
    </dst>
</src-dst-files-specification>
    """.format(
        source_dir=source_dir, 
        source_file=source_file,
        target_dir=target_dir
    )


def get_export_dump_request(source_dir, xml_file, target_dir, target_file):
    """Create XML that should be passed to pmmcli.exe --export-dump-as-file

    Return XML as a string"""

    return u"""<?xml version="1.0"?>
<src-dst-files-specification guid="00000000-0000-0000-0000-000000000000" type="server">
    <src>
        <dumps-storage-credentials storage-type="local">
            <root-dir>{source_dir}</root-dir>
        </dumps-storage-credentials>
        <name-of-info-xml-file>{xml_file}</name-of-info-xml-file>
    </src>
    <dst>
        <dumps-storage-credentials storage-type="file">
            <root-dir>{target_dir}</root-dir>
            <file-name>{target_file}</file-name>
        </dumps-storage-credentials>
    </dst>
</src-dst-files-specification>
    """.format(
        source_dir=source_dir,
        xml_file=xml_file,
        target_dir=target_dir,
        target_file=target_file
    )
