import pipes
import posixpath

from parallels.core import MigrationError
from parallels.core import messages, CommandExecutionError
from parallels.core import migrator_config
from parallels.core.content.mail.base import CopyMailBase
from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.utils.common import ilen, append_once, default, safe_string_repr
from parallels.core.utils.hosting_analyser_utils import apply_hosting_analyser_strategy
from parallels.core.utils.paths.mail_paths import (
    MailboxDirectory, MailDomainDirectory, MailboxMaildirDirectory, MailboxTemplatePath, MailAbsolutePath
)
from parallels.core.utils.plesk_utils import recalculate_mailbox_maildir_size_linux, update_mail_usage_stats
from parallels.core.utils.unix_utils import format_command

logger = get_logger(__name__)


class CopyMailRsync(CopyMailBase):
    """Copy mail messages with rsync to target Unix server"""

    DEFAULT_MAIL_USER_DIR_EXCLUDES = [u"maildirsize", u".qmail*", u"@attachments/"]

    def __init__(self, source_mail_directory):
        """Object constructor

        Arguments:
            source_mail_directory - constructor of class which provides
                paths to directories with mail messages on source server,
                subclass of SourceMailDirectory
        """
        self.source_mail_directory = source_mail_directory

    def copy_mail(self, global_context, subscription, issues):
        """
        Copy mail content of given subscription by rsync

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type issues: list[parallels.core.reports.model.issue.Issue]
        """
        if subscription.mail_converted_dump is None:
            logger.debug(messages.SUBSCRIPTION_NOT_PRESENTED_ON_MAIL_SERVER, subscription.name)
            return

        if subscription.mail_source_server is None:
            logger.debug(messages.SOURCE_MAIL_SERVER_IS_NOT_DEFINED, subscription.name)
            return

        for domain in subscription.mail_converted_dump.iter_domains():
            if domain.mailsystem is None:
                logger.debug(messages.SKIP_COPYING_MAIL_CONTENT_FOR_DOMAIN_1, domain.name)
                continue
            if ilen(domain.iter_mailboxes()) == 0:
                logger.debug(messages.SKIP_COPYING_MAIL_CONTENT_FOR_DOMAIN_2, domain.name)
                continue

            logger.finfo(messages.COPY_MESSAGES_OF_DOMAIN, domain=domain.name)
            self._copy_domain_mail(global_context, subscription, domain)
            self._repair_domain_file_permissions(subscription, domain)
            self._recalculate_maildirsize(subscription, domain)

        self.update_panel_usage_stats(subscription)

    def copy_mailbox_directories(self, global_context, subscription, mailbox, copy_content_items):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type mailbox: parallels.core.dump.data_model.Mailbox
        :type copy_content_items: list[parallels.core.utils.paths.copy_web_content.CopyContentItem]
        """
        source_server = subscription.mail_source_server
        target_server = subscription.mail_target_server
        rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)

        ssh_key_info = global_context.ssh_key_pool.get(source_server, target_server)

        for copy_content_item in copy_content_items:
            source_path = copy_content_item.source_path
            assert isinstance(source_path, MailAbsolutePath)
            target_path = self._get_target_path(target_server, copy_content_item.target_path, mailbox.domain_name)
            rsync_command, rsync_args = self._get_rsync_args(
                source_server, source_path.path, target_path, ssh_key_info.key_pathname,
                rsync_additional_args=rsync_additional_args, excludes=copy_content_item.exclude
            )
            self._run_rsync(source_server, target_server, rsync_command, rsync_args)

        self._repair_mailbox_file_permissions(subscription, mailbox)
        self._recalculate_mailbox_maildir_size(subscription, mailbox)

    def _copy_domain_mail(self, global_context, subscription, domain):
        """Transfer mail content of the specified domain to the target server.
        
        When copying mail from Plesk to Plesk, mail content is transferred per
        domain, because mail directory structure is the same.

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        source_server = subscription.mail_source_server
        target_server = subscription.mail_target_server
        rsync_additional_args = migrator_config.read_rsync_additional_args(global_context.config)
        apply_hosting_analyser_strategy(global_context, subscription, rsync_additional_args)

        ssh_key_info = global_context.ssh_key_pool.get(source_server, target_server)

        source_dir = self.source_mail_directory(subscription, domain)
        for source_path, target_location in source_dir.prepare():
            target_path = self._get_target_path(target_server, target_location, domain.name)

            rsync_command, rsync_args = self._get_rsync_args(
                source_server, source_path, target_path, ssh_key_info.key_pathname,
                rsync_additional_args=rsync_additional_args, source_dir=source_dir,
                excludes=self.DEFAULT_MAIL_USER_DIR_EXCLUDES
            )
            self._run_rsync(source_server, target_server, rsync_command, rsync_args)

        source_dir.cleanup()

    @staticmethod
    def _run_rsync(source_server, target_server, rsync_command, rsync_args):
        try:
            with target_server.runner() as runner_target:
                assert isinstance(runner_target, BaseRunner)
                runner_target.execute_command(rsync_command, rsync_args)
        except CommandExecutionError as e:
            logger.exception()
            raise MigrationError(
                messages.RSYNC_FAILED_COPY_MAIL_CONTENT % (
                    source_server.description(), target_server.description(), e.stderr
                )
            )
        except Exception as e:
            logger.exception()
            raise MigrationError(
                messages.RSYNC_FAILED_COPY_MAIL_CONTENT % (
                    source_server.description(), target_server.description(), safe_string_repr(e)
                )
            )

    @staticmethod
    def _get_rsync_args(
        source_server, source_path, target_path, key_pathname,
        rsync_additional_args, source_dir=None, excludes=None
    ):
        """Generate rsync parameters for transferring domain mail content."""
        excludes = default(excludes, [])

        rsync_command = u'/usr/bin/rsync -e {ssh_command} --archive --timeout=30'
        rsync_args = dict(
            ssh_command=format_command(
                u'ssh -i {key} -p {port} '
                u'-o PasswordAuthentication=no -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no',
                key=key_pathname,
                port=source_server.settings().ssh_auth.port
            )
        )

        # Add additional arguments
        for num, rsync_additional_arg in enumerate(rsync_additional_args):
            key = 'rsync_additional_arg_%s' % num
            rsync_command += u' {%s}' % key
            rsync_args[key] = rsync_additional_arg

        # Add excludes
        if source_dir is not None:
            mail_dir_exclude = source_dir.get_excluded_files()
        else:
            mail_dir_exclude = None

        if mail_dir_exclude is None:
            mail_dir_exclude = []

        for num, exclude_value in enumerate(excludes + mail_dir_exclude):
            key = 'exclude_%s' % num
            rsync_command += u' --exclude={%s}' % key
            rsync_args[key] = exclude_value

        # Add source and destination paths
        rsync_command += u' {source_path} {target_path}'
        rsync_args.update(dict(
            source_path=u'{source_user}@{source_ip}:{source_filepath}'.format(
                source_user=source_server.user(),
                source_ip=source_server.ip(),
                source_filepath=pipes.quote(append_once(source_path, '/'))
            ),
            target_path=append_once(target_path, '/')
        ))

        return rsync_command, rsync_args

    def _get_target_path(self, target_server, target_location, domain_name):
        domain_dir = domain_name.encode('idna').lower()
        if isinstance(target_location, MailboxDirectory):
            return posixpath.join(target_server.mail_dir, domain_dir, target_location.mailbox_name.lower())
        elif isinstance(target_location, MailboxMaildirDirectory):
            return posixpath.join(target_server.mail_dir, domain_dir, target_location.mailbox_name.lower(), 'Maildir')
        elif isinstance(target_location, MailDomainDirectory):
            return posixpath.join(target_server.mail_dir, domain_dir)
        elif isinstance(target_location, MailboxTemplatePath):
            variable_paths = {
                'mailbox_directory': MailboxDirectory(target_location.mailbox_name),
                'mailbox_maildir_directory': MailboxMaildirDirectory(target_location.mailbox_name),
                'mail_domain_directory': MailDomainDirectory()
            }
            variables = {}
            for var_name, path in variable_paths.items():
                variables[var_name] = self._get_target_path(target_server, path, domain_name)
            variables['domain'] = domain_name
            variables['domain_lower_case'] = domain_name.lower()
            variables['domain_idn'] = domain_name.encode('idna')
            variables['mailbox_short_name'] = target_location.mailbox_name
            variables['mailbox_short_name_lower_case'] = target_location.mailbox_name.lower()

            return target_location.template.format(**variables)
        else:
            assert False

    @staticmethod
    def _repair_domain_file_permissions(subscription, domain):
        """
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :rtype: None
        """
        target_server = subscription.mail_target_server

        with target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            target_path = posixpath.join(
                target_server.mail_dir,
                domain.name.encode('idna').lower()
            )
            path_args = dict(path=target_path)
            runner.execute_command('chown popuser:popuser -R {path}', path_args)
            runner.execute_command('chmod 750 {path}', path_args)

    @staticmethod
    def _repair_mailbox_file_permissions(subscription, mailbox):
        """
        :type mailbox: parallels.core.dump.data_model.Mailbox
        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :rtype: None
        """
        target_server = subscription.mail_target_server

        with target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            target_path = posixpath.join(
                target_server.mail_dir,
                mailbox.domain_name.encode('idna').lower(),
                mailbox.name.lower()
            )
            path_args = dict(path=target_path)
            runner.execute_command('chown popuser:popuser -R {path}', path_args)
            runner.execute_command('chmod 750 {path}', path_args)

    @staticmethod
    def _recalculate_maildirsize(subscription, domain):
        """
        After migration of mail content we have to recalculate mail usage in
        "maildirsize" file of mail directory. Otherwise, we could get
        incorrect mail user in the control panel, and mail quota limitations
        could work incorrectly.

        For example, we have a mailbox. It was initially migrated to target server.
        During DNS switch:
        - several messages were received by the source mail server
        - several other messages were received by the target mail server

        Then customer runs mail content resync, which merges messages from
        source and target server. "maildirsize" file on the target server
        becomes invalid, as new messages from the source server were put into
        the mailbox (and mail server does not know that). But we can't just
        take the file from the source server, because there are new messages
        that were received by the target server, but not the source server, and
        they are not included into "maildirsize" file calculation on the source
        server. So neither source, nor target "maildirsize" file is correct.
        We need Plesk backend to recalculate it. To do that, we call mailbox
        quota update.

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        try:
            for mailbox in domain.iter_mailboxes():
                CopyMailRsync._recalculate_mailbox_maildir_size(subscription, mailbox)
        except Exception:
            logger.exception()
            raise MigrationError(
                messages.FAILED_RECALCULATE_MAIL_DIRECTORY_SIZE_MAIL)

    @staticmethod
    def _recalculate_mailbox_maildir_size(subscription, mailbox):
        """Recalculate maildir size on single mailbox

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        target_mail_server = subscription.mail_target_server
        with target_mail_server.runner() as runner_target:
            assert isinstance(runner_target, BaseRunner)
            recalculate_mailbox_maildir_size_linux(runner_target, mailbox.domain_name, mailbox.name, mailbox.quota)
            
    @staticmethod
    def update_panel_usage_stats(subscription):
        """Update mail usage stats in control panel

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        """
        with subscription.panel_target_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            update_mail_usage_stats(runner, subscription.name)


class SourceMailDirectory(object):
    """Provide paths to directories with mail messages on source server"""

    def __init__(self, subscription, domain):
        self.subscription = subscription
        self.domain = domain

    def prepare(self):
        """Prepare mail directory on source server to copy with rsync
        
        Returns tuple with two items:
        - path to a directory on source server to copy for
        domain specified in constructor of that class.
        - relative path to a directory on target server

        This function may perform some conversion on files on the 
        source server, and return path to some temporary converted 
        directory which should be removed in cleanup function

        :rtype: list[tuple(str, parallels.core.content.mail.rsync.MailHostingPath]]
        """
        raise NotImplementedError()

    def cleanup(self):
        """Clean up temporary files when copy is finished"""
        pass

    def get_excluded_files(self):
        """Return a list of files that should not be transferred

        :rtype: list[str | unicode]
        """
        return []
