from contextlib import contextmanager

from parallels.core.logging import get_logger
from parallels.core import migrator_config, MigrationError
from parallels.core.utils import migrator_utils
from parallels.core.utils.cache import CacheOwner
from parallels.core.utils.common import is_run_on_windows, safe_format
from parallels.core.utils.common_constants import CACHE_OWNER_EXTENSION_DEPLOYER
from parallels.core.utils.file_utils import get_relative_path
from parallels.core.utils.paths import web_paths
from parallels.core.utils.paths.converters.source import SourceWebPathConverter
from parallels.core.utils.paths.converters.target import TargetWebPathConverter
from parallels.core.utils.paths.copy_web_content import CopyContentItem
from parallels.core.utils.tracer import Tracer, TracerMeta
from parallels.plesk.hosting_repository.model import PleskHostingRepositoryModel
from parallels.plesk.source.plesk import messages

logger = get_logger(__name__)


class ExtensionDeployer(CacheOwner):
    def __init__(self, context, cache_state_controller, extension, extension_data_type, entity_name=None):
        """
        :type extension: parallels.core.hosting_repository.extension.ExtensionEntity
        :type cache_state_controller: parallels.core.utils.cache.CacheStateController
        :type extension_data_type: str
        :type entity_name: str | None
        """
        self._context = context
        self._cache_state_controller = cache_state_controller
        self._extension = extension
        self._extension_data_type = extension_data_type
        self._entity_name = entity_name

    @property
    def cache_owner_name(self):
        return safe_format(
            CACHE_OWNER_EXTENSION_DEPLOYER,
            extension_name=self._extension.name,
            extension_data_type=self._extension_data_type,
            entity_name=self._entity_name if self._entity_name is not None else u'server'
        )

    def deploy(
        self, deploy_messages, source, target, is_remove_backup_temp_data,
        source_entity_id=None, target_entity_id=None
    ):
        """Backup extensions data of given type on source Plesk servers and restore it
        on target Plesk server; return list of errors if any

        :type deploy_messages: parallels.plesk.source.plesk.actions.deploy.extensions.utils.DeployMessages
        :type source: parallels.plesk.source.plesk.server.PleskSourceServer
        :type target: parallels.plesk.connections.target_server.PleskTargetServer
        :type is_remove_backup_temp_data: bool
        :type source_entity_id: int | None
        :type target_entity_id: int | None
        :rtype: list
        """
        errors = []

        cache_state = self._cache_state_controller.get_cache_state(
            self._entity_name if self._entity_name is not None else 'server',
            self
        )
        if cache_state.is_valid():
            return errors

        with self._get_tracer(target_entity_id) as tracer:
            # set flag that deployment of extension was started
            tracer.meta.set_in_progress()

            # backup extension
            logger.info(deploy_messages.backup_message.format(
                extension_name=self._extension.name,
                entity_name=self._entity_name,
                server_description=source.description()
            ))
            try:
                source_hosting_repository = PleskHostingRepositoryModel(source)
                extension_backup_node = source_hosting_repository.extension.trace(tracer).make_backup(
                    self._extension.extension_id,
                    self._extension_data_type,
                    entity_id=source_entity_id
                )
                if extension_backup_node is None:
                    raise MigrationError(messages.ACTION_DEPLOY_EXTENSIONS_BACKUP_EMPTY.format(
                        extension_id=self._extension.extension_id,
                        object_type=self._extension_data_type,
                        object_id=source_entity_id,
                        server_description=source.description()
                    ))
            except Exception:
                logger.exception()
                error_message = deploy_messages.backup_failed_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    server_description=source.description(),
                )
                solution = deploy_messages.backup_failed_solution_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    server_description=source.description(),
                )
                errors.append((error_message, solution))
                logger.error(error_message)
                # this is critical error, no need to proceed
                return errors

            # copy extension content
            logger.info(deploy_messages.copy_content_message.format(
                extension_name=self._extension.name,
                entity_name=self._entity_name,
                source_description=source.description(),
                target_description=target.description()
            ))
            target_temp_content_path = None
            try:
                target_temp_content_path = self._copy_content(
                    source,
                    target,
                    [n.text for n in extension_backup_node.findall('content/include')],
                    [n.text for n in extension_backup_node.findall('content/exclude')],
                    self._extension.name,
                    self._extension_data_type,
                    tracer=tracer
                )
            except Exception:
                logger.exception()
                # content could not be copied but it should not block transferring of other data,
                # so just place error into log and report and go ahead
                error_message = deploy_messages.copy_content_failed_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    source_description=source.description(),
                    target_description=target.description()
                )
                solution = deploy_messages.copy_content_failed_solution_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    server_description=target.description()
                )
                errors.append((error_message, solution))
                logger.error(error_message)

            if is_remove_backup_temp_data:
                # clean up temporary data on source
                try:
                    source_hosting_repository.extension.trace(tracer).clear_backup_temp_data(
                        self._extension.extension_id,
                        self._extension_data_type,
                        entity_id=source_entity_id
                    )
                except Exception:
                    logger.exception()
                    # temporary data could not be removed for some reason, just put warning into log
                    logger.warning(deploy_messages.clear_backup_temp_data_failed_message.format(
                        extension_name=self._extension.name,
                        entity_name=self._entity_name,
                        server_description=source.description()
                    ))

            # restore extension
            logger.info(deploy_messages.restore_message.format(
                extension_name=self._extension.name,
                entity_name=self._entity_name,
                server_description=target.description()
            ))
            try:
                self._context.hosting_repository.extension.trace(tracer).restore_backup(
                    extension_backup_node,
                    self._extension_data_type,
                    self._entity_name,
                    target_temp_content_path
                )
            except Exception:
                logger.exception()
                error_message = deploy_messages.restore_failed_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    server_description=target.description()
                )
                solution = deploy_messages.restore_failed_solution_message.format(
                    extension_name=self._extension.name,
                    entity_name=self._entity_name,
                    server_description=target.description()
                )
                errors.append((error_message, solution))
                logger.error(error_message)
                # this is critical error, no need to proceed
                return errors

            # restoration of extension completed successfully, so remove temporary files
            try:
                with target.runner() as runner:
                    runner.remove_directory(target_temp_content_path)
            except Exception:
                # temporary data could not be removed for some reason,
                # just put exception into debug log
                logger.exception()

            # set flag that deployment of extension was completed successfully
            tracer.meta.set_completed()

        cache_state.set_valid()
        return errors

    def _copy_content(self, source, target, include_paths, exclude_paths, extension_name, content_type, tracer=None):
        """Copy listed files of given extension

        :type source: parallels.plesk.source.plesk.server.PleskSourceServer
        :type target: parallels.plesk.connections.target_server.PleskTargetServer
        :type include_paths: list[str]
        :type exclude_paths: list[str]
        :type extension_name: str
        :type content_type: str
        :type tracer: parallels.core.utils.tracer.Tracer
        """
        temp_content_prefix = 'plesk-migrator-temp-%s' % content_type
        if self._entity_name is not None:
            temp_content_prefix += '-%s' % self._entity_name
        # create temporary storage for extensions content on target server
        target_temp_content_path = target.join_path(target.get_extension_var_dir(extension_name), temp_content_prefix)
        with target.runner() as runner:
            runner.remove_directory(target_temp_content_path)
            runner.mkdir(target_temp_content_path)
        # list items to copy
        tocopy = []
        for include_path in include_paths:
            # find exclude paths related to current include path
            tocopy.append(CopyContentItem(
                source_path=web_paths.AbsolutePath(source.join_path(
                    source.get_extension_var_dir(extension_name), include_path
                )),
                target_path=web_paths.AbsolutePath(target.join_path(
                    target.get_extension_var_dir(extension_name), temp_content_prefix, include_path
                )),
                skip_if_source_not_exists=True,
                exclude=self._get_related_exclude_paths(include_path, exclude_paths, target)
            ))
        # copy content
        source_path_converter = SourceWebPathConverter()
        target_path_converter = TargetWebPathConverter()
        rsync_source_path_converter = SourceWebPathConverter(is_rsync=True)
        rsync_target_path_converter = TargetWebPathConverter(is_rsync=True)
        rsync_additional_args = migrator_config.read_rsync_additional_args(self._context.config)
        with source.runner() as runner_source, target.runner() as runner_target:
            for item in tocopy:
                # expand paths; obtain physical paths on file system for creation of parent directories and logging
                # and special paths which should be used with rsync tool (it could differ slightly)
                source_path = source_path_converter.expand(item.source_path, source)
                target_path = target_path_converter.expand(item.target_path, target)
                rsync_source_path = rsync_source_path_converter.expand(item.source_path, source)
                rsync_target_path = rsync_target_path_converter.expand(item.target_path, target)
                is_directory = runner_source.is_dir(source_path)
                # create target directories on target
                if is_directory:
                    runner_target.mkdir(target_path)
                else:
                    runner_target.mkdir(target.get_base_path(target_path))
                # trace copy content operation
                if tracer is not None:
                    tracer.trace_copy_content(source.description(), source_path, target.description(), target_path)
                # run rsync
                if is_run_on_windows():
                    rsync = self._context.get_rsync(source, target)
                    rsync.sync(
                        source_path=rsync_source_path,
                        target_path=rsync_target_path,
                        exclude=item.exclude,
                        rsync_additional_args=rsync_additional_args,
                        is_directory=is_directory
                    )
                else:
                    migrator_utils.copy_directory_content_unix(
                        source.ip(),
                        source.user(),
                        source,
                        target,
                        rsync_source_path,
                        rsync_target_path,
                        self._context.ssh_key_pool.get(source, target).key_pathname,
                        item.exclude,
                        item.skip_if_source_not_exists,
                        rsync_additional_args=rsync_additional_args,
                        source_rsync_bin=source.rsync_bin,
                        source_port=source.config.ssh_auth.port,
                        preserve_owner=True,
                        preserve_group=True,
                        preserve_perms=True
                    )

        return target_temp_content_path

    @contextmanager
    def _get_tracer(self, target_entity_id=None):
        """Retrieve tracer of the Plesk extension deployment process

        :type target_entity_id: int
        :rtype: parallels.plesk.source.plesk.actions.deploy.extensions.utils.DeployExtensionTracer
        """
        extension_traces_file = self._context.session_files.get_extension_traces_file(
            self._extension.name, self._extension_data_type, target_entity_id
        )
        tracer = DeployExtensionTracer(extension_traces_file)
        yield tracer
        tracer.dump()

    @staticmethod
    def _get_related_exclude_paths(path, all_exclude_paths, server):
        related_exclude_paths = []
        for exclude_path in all_exclude_paths:
            related_exclude_path = get_relative_path(exclude_path, path, server.is_windows())
            if related_exclude_path is None:
                continue
            related_exclude_paths.append(related_exclude_path)
        return related_exclude_paths


class DeployMessages(object):
    def __init__(
        self,
        backup_message, backup_failed_message, backup_failed_solution_message,
        copy_content_message, copy_content_failed_message, copy_content_failed_solution_message,
        clear_backup_temp_data_failed_message,
        restore_message, restore_failed_message, restore_failed_solution_message
    ):
        self.backup_message = backup_message
        self.backup_failed_message = backup_failed_message
        self.backup_failed_solution_message = backup_failed_solution_message
        self.copy_content_message = copy_content_message
        self.copy_content_failed_message = copy_content_failed_message
        self.copy_content_failed_solution_message = copy_content_failed_solution_message
        self.clear_backup_temp_data_failed_message = clear_backup_temp_data_failed_message
        self.restore_message = restore_message
        self.restore_failed_message = restore_failed_message
        self.restore_failed_solution_message = restore_failed_solution_message


class DeployExtensionTracer(Tracer):
    def _init_meta(self, data=None):
        status = data.get('status') if isinstance(data, dict) else None
        return DeployExtensionTracerMeta(status)


class DeployExtensionTracerMeta(TracerMeta):
    def __init__(self, status):
        self._status = status

    @property
    def is_completed(self):
        return self._status == 'completed'

    def set_in_progress(self):
        self._status = 'in progress'

    def set_completed(self):
        self._status = 'completed'

    def get_data(self):
        return dict(status=self._status)
