import os
import posixpath
import threading
from xml.etree import ElementTree

from parallels.core.logging import get_logger
from parallels.core import messages, MigrationError
from parallels.core.migrator_config import DEFAULT_RESTORE_HOSTING_SETTINGS_POLL_INTERVAL, \
    DEFAULT_RESTORE_HOSTING_SETTINGS_POLL_LIMIT
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import default, is_empty, none_str
from parallels.core.utils.common.threading_utils import synchronized_by_lock
from parallels.core.utils.migrator_utils import normalize_domain_name
from parallels.core.utils.pmm.pmmcli import PMMCLICommandUnix
from parallels.core.utils.pmm.pmmcli import PMMCLICommandWindows
from parallels.core.utils.pmm.restore_result import RestoreResult
from parallels.core.utils.steps_profiler import sleep
from parallels.plesk.utils.xml_rpc.plesk.core import PleskError

logger = get_logger(__name__)


# There should be only one PMM restoration process running, otherwise:
# 1) PMM will explicitly fail to start the second restoration process
# if some is already running.
# 2) Plesk itself is not completely thread safe, and it is better to avoid
# running multiple operations in Plesk at the same time.
restore_hosting_settings_lock = threading.Lock()


def get_restore_hosting_utils(plesk_server):
    if plesk_server.is_windows():
        return RestoreHostingUtilsWindows(plesk_server)
    else:
        return RestoreHostingUtilsUnix(plesk_server)


class RestoreHostingUtilsBase(object):
    """Base class for hosting restoration utility functions"""

    def __init__(self, server):
        self._server = server

    def import_backup(self, target_backup_path, additional_env_vars=None, backup_info_file=None):
        """Import backup to PMM repository. Returns backup ID."""
        pmmcli_cmd = self._create_pmmcli()
        additional_env_vars = default(additional_env_vars, {})
        dump_validation_string = '<archive dump-header-offset="0" dump-xml-offset="0"/>'
        logger.debug(messages.DEBUG_FIX_DUMP)
        with open(target_backup_path, "ab") as file:
            file.write(dump_validation_string.encode("ascii"))
        import_result = pmmcli_cmd.import_dump(target_backup_path, additional_env_vars)
        if import_result.error_code not in {'0', '116', '152'}:
            raise Exception(messages.ERROR_FAILED_TO_IMPORT_DUMP % import_result.raw_stdout)
        if import_result.error_code == '152':
            logger.warning(messages.ERROR_FAILED_TO_IMPORT_DUMP_INVALID_FORMAT)
            if backup_info_file is not None:
                pass

        backup_id = import_result.backup_id
        if backup_id is not None:
            logger.debug(messages.DEBUG_IMPORTED_DUMP_ID, backup_id)
        return import_result.backup_prefix, backup_id

    def remove_backup(self, backup_info_file):
        """Remove backup from PMM repository. Silently ignore all errors."""
        try:
            self._create_pmmcli().delete_dump(backup_info_file)
        except PleskError:
            # silently skip if we failed to remove backup
            logger.debug(messages.FAILED_REMOVE_BACKUP_FILE_FROM_TARGET, exc_info=True)

    @synchronized_by_lock(lock=restore_hosting_settings_lock)
    def restore_hosting_settings(
        self, admin_guid, domain_name, domain_dump_xml, safe,
        restore_siteapps_only=False, additional_env_vars=None,
        restore_task_poll_limit=DEFAULT_RESTORE_HOSTING_SETTINGS_POLL_LIMIT,
        restore_task_poll_interval=DEFAULT_RESTORE_HOSTING_SETTINGS_POLL_INTERVAL
    ):
        """Restore hosting settings of specified subscription
        Domain dump XML should be already in PMM repository, so use import_backup
        function before using that function.
        """
        pmmcli_cmd = self._create_pmmcli()
        additional_env_vars = default(additional_env_vars, {})
        env = self._get_restore_env_variables(
            restore_siteapps_only, additional_env_vars
        )
        restore_result = pmmcli_cmd.restore(admin_guid, domain_dump_xml, env)

        if restore_result.error_code is not None and restore_result.error_code != "0":
            raise MigrationError(
                messages.RESTORATION_SUBSCRIPTION_FAILED_ERROR_CODE.format(
                    code=restore_result.error_code,
                    message=none_str(restore_result.error_message)
                )
            )
        if restore_result.task_id is None:
            raise Exception(
                messages.RESTORATION_SUBSCRIPTION_FAILED_NO_TASK_ID.format(
                    output=restore_result.raw_stdout
                )
            )

        # Workaround for race condition in PMM:
        # when we start restoration, it starts task, and in that task it starts
        # deployer. If deployer was never running, PMM will report "failed"
        # status for restoration task to handle situation with absent/broken
        # deployer binary. So if we request task status *before*
        # deployer was started, we get "failed" task status, which is not correct.
        # The most simple way to fix - add 2 second wait to ensure that deployer
        # was started before our first task status request. Considering that
        # restoration practically never takes less than 5 seconds, that should be ok.
        sleep(2, messages.INITIALLY_WAIT_FOR_PMM_DEPLOYER_START)

        logger.debug(
            messages.WAITING_FOR_RESTORE_TASK_FINISH_CHECK,
            restore_task_poll_interval, restore_task_poll_limit
        )

        status = None

        for attempt in xrange(restore_task_poll_limit):
            with logger.debug_level_context(
                messages.POLL_PLESK_FOR_RESTORATION_TASK_STATUS, attempt=attempt
            ):
                status = pmmcli_cmd.get_task_status(restore_result.task_id)
                if status.is_running:
                    sleep(restore_task_poll_interval, messages.WAITING_FOR_PLESK_RESTORE_TASK_FINISH)
                else:
                    break

        assert status is not None  # loop executed at least once

        if status.is_running:
            # if still running - raise exception
            raise Exception(messages.RESTORATION_SUBSCRIPTION_FAILED_TIMED_OUT_WAITING)

        if status.is_interrupted:
            # if interrupted by /usr/local/psa/admin/bin/pmmcli --stop-task
            raise Exception(messages.RESTORATION_SUBSCRIPTION_FAILED_RESTORATION_TASK_STOPPED)

        result_log = self._download_result_xml(status.log_location)
        restore_result = RestoreResult(domain_name, result_log)

        if not self._server.is_windows() and not is_empty(status.log_location):
            logger.debug(messages.RESTORE_HOSTING_SETTINGS_INFO.format(
                domain=domain_name,
                session_dir=posixpath.dirname(status.log_location),
                log_filename=posixpath.join(posixpath.dirname(status.log_location), 'migration.log'),
                xml_filename=posixpath.join(posixpath.dirname(status.log_location), 'dump/info.xml')
            ))

        for error_message in restore_result.get_error_messages():
            if not self._is_error_message_exist_in_subscription_issues(safe, domain_name, error_message):
                safe.add_issue_subscription(
                    domain_name,
                    Issue(
                        'pmm_warning', Issue.SEVERITY_WARNING, messages.PLESK_RESTORE_REPORT_PROBLEM,
                        error_message
                    )
                )

    @staticmethod
    def _is_error_message_exist_in_failed_subscription(safe, domain_name, error_message):
        for issue in safe.failed_objects.subscriptions[domain_name]:
            if error_message in issue.error_message:
                return True

        return False

    @staticmethod
    def _is_error_message_exist_in_subscription_issues(safe, domain_name, error_message):
        for issue in safe.issues.subscriptions[domain_name]:
            if error_message in issue.solution_text:
                return True

        return False

    def index_imported_domain_backup_files(self, backup_prefix, backup_id):
        """Create mapping {domain: backup file path} for backup imported to repository

        We find all XML files with corresponding backup_id and then check and
        if it is a domain backup XML, then we parse it and take domain name from <domain> node
        ATM it is impossible to reliably get file name where domain information is
        stored by domain name in case of long domain names, so we index files in such way.
        """
        domain_to_backup_file = {}
        with self._server.runner() as runner:
            for filename in runner.iter_files_list_nested(self._server.dump_dir):
                basename = os.path.basename(filename)
                if not (basename.startswith(backup_prefix) and basename.endswith('_%s.xml' % backup_id)):
                    continue
                domain_name = self._get_backup_xml_domain(filename)
                if domain_name is None:
                    continue

                domain_to_backup_file[domain_name] = filename

        return domain_to_backup_file

    def _create_pmmcli(self):
        """Create PMMCLICommand object able to import backup, restore backup, etc
        :rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandBase
        """
        raise NotImplementedError()

    def _get_restore_env_variables(self, restore_siteapps_only, additional_env_vars):
        """Get environment variables necessary for restoration as dictionary

        Arguments:
        - restore_siteapps_only - if true, only APS application settings will be restored
        - additional_env_vars - dictionary with additional environment variables

        Notes on environment variables:

        PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL is set to 'true' as we
        control Apache restart interval ourselves. Reasons:

        - we need to set it once at the beginning of restore hosting step and
        revert it back at the end of the step, no need to pull it for each
        subscription
        - in case of critical failure (which sometimes happens) of PMM it
        leaves Apache restart interval equal to 999999, and there is no
        idea how to fix it now in a reasonable time
        - we need to leave an ability to customize the interval to customer

        PLESK_MIGRATION_MODE is used to specify PMM that database assimilation
        scenario is possible, and if database with such name already exists on the
        server PMM will try to just register it in Plesk (security is considered
        too - check PMM code for more details)
        """
        raise NotImplementedError()

    def _get_backup_xml_domain(self, filename):
        with self._server.runner() as runner:
            file_contents = runner.get_file_contents(filename)
        try:
            file_xml = ElementTree.fromstring(file_contents)
            domain_node = file_xml.find('domain')
            if domain_node is not None:
                domain_name = normalize_domain_name(
                    domain_node.attrib.get('name')
                )
                return domain_name
        except Exception:
            # Ignore all parse errors
            logger.debug(messages.EXCEPTION_WHILE_PARSING_XML, exc_info=True)
            return None

    def _download_result_xml(self, log_location):
        """Download restoration result XML contents."""
        if log_location is None:
            raise Exception(
                messages.RESTORATION_SUBSCRIPTION_FAILED_NO_RESTORATION_STATUS)
        logger.debug(messages.RETRIEVING_RESTORATION_LOG_S, log_location)
        with self._server.runner() as runner:
            return runner.get_file_contents(log_location)


class RestoreHostingUtilsUnix(RestoreHostingUtilsBase):
    """Unix-specific hosting restoration functions"""

    def _create_pmmcli(self):
        """Create PMMCLICommand object able to import backup, restore backup, etc
        :rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandUnix
        """
        return PMMCLICommandUnix(self._server)

    def _get_restore_env_variables(self, restore_siteapps_only, additional_env_vars):
        """Get environment variables necessary for restoration

        """
        env = dict(
            PLESK_MIGRATION_MODE=u'1',
            PLESK_RESTORE_DO_NOT_CHANGE_APACHE_RESTART_INTERVAL=u'true'
        )
        if restore_siteapps_only:
            env['PLESK_RESTORE_SITE_APPS_ONLY'] = u'true'
        env.update(additional_env_vars)

        return env


class RestoreHostingUtilsWindows(RestoreHostingUtilsBase):
    """Windows-specific hosting restoration commands"""

    def _create_pmmcli(self):
        """Create PMMCLICommand object able to import backup, restore backup, etc
        :rtype parallels.core.utils.pmm.pmmcli.PMMCLICommandWindows
        """
        return PMMCLICommandWindows(self._server)

    def _get_restore_env_variables(self, restore_siteapps_only, additional_env_vars):
        """Get environment variables necessary for restoration"""
        env = dict(
            PLESK_MIGRATION_MODE='1',
            PLESK_DISABLE_PROVISIONING='false',
        )
        if restore_siteapps_only:
            env['PLESK_RESTORE_SITE_APPS_ONLY'] = 'true'
        env.update(additional_env_vars)

        return env
