from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core.actions.base.subscription_action import SubscriptionAction
from parallels.core.actions.utils.multithreading_properties import MultithreadingProperties
from parallels.core.utils.common import group_by, safe_format
from parallels.core.utils.hosting_analyser_utils import apply_hosting_analyser_strategy
from parallels.core import MigrationError
from parallels.core import migrator_config
from parallels.core.utils.paths import web_paths
from parallels.core.utils.paths.converters.windows.source import WindowsSourceWebPathConverter
from parallels.core.utils.paths.converters.windows.target import WindowsTargetWebPathConverter

logger = get_logger(__name__)


class CopyWindowsWebContent(SubscriptionAction):
    """Copy web files for Windows servers"""
    def __init__(self):
        # Whether to check if copied file exists on source server before
        # running rsync command for better error reporting and ability to skip
        # files/directories if they don't exist on source server
        self._check_source_file_exists = False

    def get_description(self):
        return messages.COPY_WEB_FILES_FROM_WINDOWS_SERVERS

    def get_failure_message(self, global_context, subscription):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        return messages.FAILED_COPY_WEB_FILES_FOR_SUBSCRIPTION % subscription.name

    def is_critical(self):
        """If action is critical or not

        If action is critical and it failed for a subscription, migration tool
        won't run the next operations for the subscription.

        :rtype: bool
        """
        return False

    def get_multithreading_properties(self):
        """Get how multithreading should be applied for that action

        :rtype: parallels.core.actions.utils.multithreading_properties.MultithreadingProperties
        """
        return MultithreadingProperties(can_use_threads=True, use_threads_by_default=True)

    def filter_subscription(self, global_context, subscription):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        # skip subscriptions w/o physical server and unix subscriptions
        source_info = subscription.get_source_info()
        if not source_info.is_server:
            return False
        if not source_info.is_windows:
            return False

        return global_context.migrator.web_files.need_to_copy_files(global_context, subscription)

    def run(self, global_context, subscription):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        files = global_context.migrator.web_files.list_files_to_copy(global_context, subscription)
        logger.debug(messages.DEBUG_FILES_TO_COPY.format(subscription_name=subscription.name, files=repr(files)))

        files_by_domain_name = group_by(files, lambda x: x.domain_name)
        # On some source panels each domain within subscription could be located on
        # different web node, so we detect source server object and source server IP address
        # on per-domain basis, not on per-subscription basis. In case of default implementation
        # subscription's source web server is used.
        for domain_name, files in files_by_domain_name.items():
            web_source_ip = global_context.migrator.get_domain_source_web_ip(subscription, domain_name)
            web_source_server = global_context.migrator.get_domain_source_web_server(subscription, domain_name)
            self._copy_files_windows(
                global_context,
                subscription,
                files,
                web_source_ip,
                subscription.web_target_server,
                web_source_server
            )

    def _copy_files_windows(self, global_context, subscription, files, source_ip, target_node, source_server):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type files: list[parallels.core.utils.paths.copy_web_content.CopyWebContentItem]
        """

        rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)
        apply_hosting_analyser_strategy(global_context, subscription, rsync_additional_args)
        # We should update existent files instead of replace them to keep permissions
        # assigned in CreateSecuredWebContent and ResetPermissions actions.
        rsync_additional_args.append('--inplace')

        rsync = global_context.get_rsync(source_server, target_node, source_ip)
        for item in files:
            logger.debug(
                messages.DEBUG_START_COPY_FILES.format(subscription_name=subscription.name, files_item=repr(item))
            )
            if not self._check_item_exists_on_source(
                global_context, item, source_server, subscription
            ):
                continue
            if self._is_absent_by_default(item.target_path):
                with target_node.runner() as runner:
                    runner.mkdir(WindowsTargetWebPathConverter().expand(item.target_path, target_node))
            try:
                rsync.sync(
                    source_path=self._get_source_rsync_converter().expand(item.source_path, source_server),
                    target_path=WindowsTargetWebPathConverter(is_rsync=True).expand(
                        item.target_path, target_node
                    ),
                    exclude=item.exclude,
                    rsync_additional_args=rsync_additional_args
                )
            except Exception as e:
                logger.exception()
                raise MigrationError(safe_format(
                    messages.RSYNC_FAILED_COPY_FILES_FROM_SOURCE,
                    source=source_server.description(),
                    target=target_node.description(),
                    reason=e
                ))

    def _check_item_exists_on_source(self, global_context, item, source_server, subscription):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        path_converter = self._get_source_converter(global_context, source_server)

        source_path = path_converter.expand(item.source_path, source_server)

        with source_server.runner() as source_runner:
            if not source_runner.file_exists(source_path):
                if item.skip_if_source_not_exists:
                    logger.debug(
                        messages.FILE_DIRECTORY_S_SUBSCRIPTION_S_DOES % (
                            source_path, subscription.name,
                            source_server.description()
                        )
                    )
                    return False
                else:
                    global_context.safe.fail_subscription(
                        subscription.name,
                        safe_format(
                            messages.SOURCE_DIRECTORY_DOES_NOT_EXIST,
                            path=source_path, server_ip=source_server.ip()
                        ),
                        is_critical=False
                    )
                    return False
            else:
                return True

    @staticmethod
    def _get_source_rsync_converter():
        """Get converter from abstract source path to concrete path for rsync

        :rtype: parallels.core.utils.paths.copy_web_content.BaseWebPathConverter
        """
        return WindowsSourceWebPathConverter(is_rsync=True)

    @staticmethod
    def _get_source_converter(global_context, source_server):
        """Get converter from abstract source path to concrete path

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: parallels.core.utils.paths.copy_web_content.BaseWebPathConverter
        """
        source_vhosts_dir = global_context.migrator.web_files.get_source_vhosts_dir(global_context, source_server)
        return WindowsSourceWebPathConverter(vhost_dir=source_vhosts_dir)

    @staticmethod
    def _is_absent_by_default(path):
        """Check whether specified web path is absent by default on target server.

        If so - we need to create directory for content before running rsync.

        :type path: parallels.core.utils.web_paths.WebHostingPath
        """
        return isinstance(
            path, (
                web_paths.WebspaceSSLDocumentRoot,
                web_paths.WebspaceMainDomainPrivate,
                web_paths.SitePrivate,
                web_paths.WebspacePathTemplate,
                web_paths.SitePathTemplate
            )
        )
