import ntpath
import shlex

from parallels.core import messages
from parallels.core.content.mail.base import CopyMailBase
from parallels.core.content.mail.mail_migrator_utils import get_mailbox_temp_directory, write_account_description_file
from parallels.core.logging import get_logger
from parallels.core.reports.model.issue import Issue
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.utils import pipe_commands, transfer_file, TRUNCATED_STDERR_LENGTH
from parallels.core.utils.clean_logging import clean_dict_args_for_log
from parallels.core.utils.common import is_empty, safe_format
from parallels.core.utils.mail_migrator import get_mail_migrator_executable
from parallels.core.utils.windows_utils import format_command

logger = get_logger(__name__)


class AdditionalMailServerOptions(object):
    """Mail server object"""

    def __init__(self, server_type, username, password):
        self._server_type = server_type
        self._username = username
        self._password = password

    @property
    def server_type(self):
        """Type of mail server

        :rtype: str | unicode
        """
        return self._server_type

    @property
    def username(self):
        """Mail server administrator's login

        :rtype: str | unicode
        """
        return self._username

    @property
    def password(self):
        """Mail server administrator's password

        :rtype: str | unicode
        """
        return self._password


class CopyMailMailMigrator(CopyMailBase):
    """Copy mail content with MailMigrator.exe utility

    The utility could dump mail messages for specified mailbox and restore them on another server.
    It dumps messages into universal format, that does not depend on mail server software. For example,
    it allows to transfer messages from MailEnable to SmarterMail and vice verso. The format is binary,
    for better performance.

    Here we copy mail messages by streaming data from the utility on source server
    to the utility on target server, with no intermediate files with messages. That provides:
    - less disk space requirements: we don't create archive with messages
    - (in theory) better performance, as there is no disk I/O
    - potential ability to track progress: how many messages were completely transferred.
    """

    def copy_mail(self, global_context, subscription, issues):
        for domain in subscription.mail_converted_dump.iter_domains():
            if domain.mailsystem is None:
                logger.debug(messages.SKIP_COPYING_MAIL_CONTENT_FOR_DOMAIN, domain.name)
                continue

            self._copy_domain_mail(subscription, domain, issues)

    def _copy_domain_mail(self, subscription, domain, issues):
        for mailbox in domain.iter_mailboxes():
            if not mailbox.enabled:
                logger.fdebug(messages.SKIP_DISABLED_MAILBOX, mailbox=mailbox.full_name)
                continue
            logger.finfo(messages.COPY_MESSAGES_OF_MAILBOX, mailbox=mailbox.full_name)
            self.copy_mailbox(subscription, domain, mailbox, issues)

    def copy_mailbox(
        self, subscription, domain, mailbox, issues,
        source_mailname=None, source_domain=None, source_password=None,
        source_mail_server_options=None
    ):
        source_server = subscription.mail_source_server
        target_server = subscription.mail_target_server

        target_mailbox_temp_dir = get_mailbox_temp_directory(target_server, domain, mailbox)
        source_mailbox_temp_dir = get_mailbox_temp_directory(source_server, domain, mailbox)
        target_mailbox_description_file = ntpath.join(target_mailbox_temp_dir, 'mailbox-description')
        source_mailbox_description_file = ntpath.join(source_mailbox_temp_dir, 'mailbox-description')
        target_exclude_message_ids_file = ntpath.join(target_mailbox_temp_dir, 'exclude-message-ids')
        source_exclude_message_ids_file = ntpath.join(source_mailbox_temp_dir, 'exclude-message-ids')

        target_mail_migrator_exe = get_mail_migrator_executable(target_server)
        source_mail_migrator_exe = get_mail_migrator_executable(source_server)

        with source_server.runner() as runner_source, target_server.runner() as runner_target:
            assert isinstance(runner_source, BaseRunner)
            assert isinstance(runner_target, BaseRunner)

            runner_target.mkdir(target_mailbox_temp_dir)
            runner_source.mkdir(source_mailbox_temp_dir)

            # Create files describing mailbox to transfer
            write_account_description_file(target_server, domain, mailbox, target_mailbox_description_file)
            write_account_description_file(
                source_server, domain, mailbox, source_mailbox_description_file,
                source_mailname, source_domain, source_password
            )

            list_message_ids_command_str, list_message_ids_command_options = self._create_list_message_ids_command(
                source_server, target_exclude_message_ids_file, target_mail_migrator_exe,
                target_mailbox_description_file, target_mailbox_temp_dir
            )

            runner_target.execute_command(list_message_ids_command_str, list_message_ids_command_options)

            transfer_file(
                runner_target, target_exclude_message_ids_file, runner_source, source_exclude_message_ids_file
            )

            backup_command_str, backup_command_options = self._create_backup_command(
                source_server, source_exclude_message_ids_file,
                source_mail_migrator_exe, source_mailbox_description_file, source_mailbox_temp_dir,
                source_mail_server_options
            )
            restore_command_str, restore_command_options = self._create_restore_command(
                source_server, target_mail_migrator_exe, target_mailbox_description_file, target_mailbox_temp_dir
            )

            # Start utility on the source server, in backup mode. Start utility on the target server in restore mode.
            # Attach stdout of utility on the source server (which writes message dumps to stdout) to stdin
            # of utility on the target server (which accepts message dumps to stdin)
            execution_results = pipe_commands(
                runner_source, backup_command_str, backup_command_options,
                runner_target, restore_command_str, restore_command_options
            )

            # remove files with mailbox description, as they could contain password of mailbox
            runner_target.remove_file(target_mailbox_description_file)
            runner_source.remove_file(source_mailbox_description_file)

            self._check_results(
                execution_results, issues,
                mailbox,
                backup_command_options, backup_command_str,
                restore_command_options, restore_command_str
            )

    @classmethod
    def _create_restore_command(
        cls,
        source_server, target_mail_migrator_exe, target_mailbox_description_file, target_mailbox_temp_dir
    ):
        restore_command_str = (
            "{mail_migrator_exe} restore --account-description-file={description}"
        )
        restore_command_args = dict(
            description=target_mailbox_description_file,
            mail_migrator_exe=target_mail_migrator_exe,
        )
        if source_server.mail_settings.target_log_enabled:
            restore_command_str += ' --log-file={log_file}'
            restore_command_args['log_file'] = ntpath.join(target_mailbox_temp_dir, 'mail-restore.log')
        if not is_empty(source_server.mail_settings.target_provider):
            restore_command_str += ' --provider={provider}'
            restore_command_args['provider'] = source_server.mail_settings.target_provider
        if not is_empty(source_server.mail_settings.target_additional_options):
            additional_cmd_str, additional_args = cls._compose_additional_options(
                source_server.mail_settings.target_additional_options
            )
            restore_command_str += additional_cmd_str
            restore_command_args.update(additional_args)

        return restore_command_str, restore_command_args

    @classmethod
    def _create_backup_command(
        cls,
        source_server, source_exclude_message_ids_file, source_mail_migrator_exe,
        source_mailbox_description_file, source_mailbox_temp_dir,
        source_mail_server_options
    ):
        """Create backup command for mail agent

        :type: source_mail_server_options: parallels.core.content.mail.mail_migrator.MailServer | None
        :rtype: tuple
        """
        backup_command_str = (
            "{mail_migrator_exe} backup --account-description-file={description} "
            "--exclude-message-ids-file={exclude_ids_file}"
        )
        backup_command_args = dict(
            description=source_mailbox_description_file,
            mail_migrator_exe=source_mail_migrator_exe,
            exclude_ids_file=source_exclude_message_ids_file,
        )
        if source_server.mail_settings.source_log_enabled:
            backup_command_str += ' --log-file={log_file}'
            backup_command_args['log_file'] = ntpath.join(source_mailbox_temp_dir, 'mail-backup.log')
        if not is_empty(source_server.mail_settings.source_provider):
            backup_command_str += ' --provider={provider}'
            backup_command_args['provider'] = source_server.mail_settings.source_provider
        elif source_mail_server_options is not None:
            backup_command_str += ' --provider={provider}'
            backup_command_args['provider'] = source_mail_server_options.server_type
            if source_mail_server_options.server_type == 'hmail4':
                if source_mail_server_options.username is not None:
                    backup_command_str += ' --hmail-login={login}'
                    backup_command_args['login'] = source_mail_server_options.username
                if source_mail_server_options.password is not None:
                    backup_command_str += ' --hmail-password={password}'
                    backup_command_args['password'] = source_mail_server_options.password

        if not is_empty(source_server.mail_settings.source_additional_options):
            additional_cmd_str, additional_args = cls._compose_additional_options(
                source_server.mail_settings.source_additional_options
            )
            backup_command_str += additional_cmd_str
            backup_command_args.update(additional_args)
        return backup_command_str, backup_command_args

    @classmethod
    def _create_list_message_ids_command(
        cls,
        source_server, target_exclude_message_ids_file, target_mail_migrator_exe,
        target_mailbox_description_file, target_mailbox_temp_dir
    ):
        list_message_ids_command_str = (
            "{mail_migrator_exe} list-message-ids "
            "--account-description-file={description} "
            "--output-file={exclude_ids_file}"
        )
        list_message_ids_command_args = dict(
            mail_migrator_exe=target_mail_migrator_exe,
            description=target_mailbox_description_file,
            exclude_ids_file=target_exclude_message_ids_file,
        )
        if source_server.mail_settings.target_log_enabled:
            list_message_ids_command_str += ' --log-file={log_file}'
            list_message_ids_command_args['log_file'] = ntpath.join(
                target_mailbox_temp_dir, 'mail-list-messages.log'
            )
        if not is_empty(source_server.mail_settings.target_provider):
            list_message_ids_command_str += ' --provider={provider}'
            list_message_ids_command_args['provider'] = source_server.mail_settings.target_provider
        if not is_empty(source_server.mail_settings.target_additional_options):
            additional_cmd_str, additional_args = cls._compose_additional_options(
                source_server.mail_settings.target_additional_options
            )
            list_message_ids_command_str += additional_cmd_str
            list_message_ids_command_args.update(additional_args)
        return list_message_ids_command_str, list_message_ids_command_args

    @staticmethod
    def _compose_additional_options(additional_options_str):
        """Compose additional options for mail migrator command by string specified in configuration file

        It takes string, parses it by Unix rules, and composes command part and arguments for Windows runner.
        Main purpose of that operation is to avoid logging of passwords passed as additional arguments.

        Returns tuple with:
        - first element - string that should be added to command line
        - second element - arguments dictionary

        :type additional_options_str: str | unicode
        :rtype: tuple
        """
        arguments = shlex.split(additional_options_str)

        cmd_str = ''
        cmd_args = {}

        for argument in arguments:
            if '=' in argument:
                name, value = argument.split('=', 1)

                if name.startswith('--'):
                    short_name = name[2:]
                elif name.startswith('-'):
                    short_name = name[1:]
                else:
                    short_name = name

                cmd_str += ' %s={%s}' % (name, short_name)
                cmd_args[short_name] = value
            else:
                cmd_str += ' ' + argument

        return cmd_str, cmd_args

    @staticmethod
    def _check_results(
            execution_results, issues,
            mailbox,
            backup_command_options, backup_command_str,
            restore_command_options, restore_command_str
    ):
        if execution_results.source_error is not None:
            issues.append(
                Issue(
                    'mail-migrator-transfer-source-failed', Issue.SEVERITY_ERROR,
                    safe_format(
                        messages.WINDOWS_MAIL_TRANSFER_SOURCE_FAILED,
                        mailbox=mailbox.full_name,
                        reason=execution_results.source_error
                    )
                )
            )
        elif (
            (
                execution_results.source_exit_code is not None and
                execution_results.source_exit_code != 0
            ) or (
                execution_results.source_stderr is not None and
                execution_results.source_stderr != ''
            )
        ):
            try:
                issues.append(
                    Issue(
                        'mail-migrator-backup-failed', Issue.SEVERITY_ERROR,
                        safe_format(
                            messages.WINDOWS_MAIL_BACKUP_FAILED,
                            mailbox=mailbox.full_name,
                            command=format_command(
                                backup_command_str, **clean_dict_args_for_log(backup_command_options)
                            ),
                            exit_code=execution_results.source_exit_code,
                            stderr=execution_results.source_stderr,
                        )
                    )
                )
            except MemoryError:
                logger.fdebug(messages.WINDOWS_MAIL_FAILED_TO_APPEND_ISSUE)
                logger.exception()
                issues.append(
                    Issue(
                        'mail-migrator-backup-failed', Issue.SEVERITY_ERROR,
                        safe_format(
                            messages.WINDOWS_MAIL_BACKUP_FAILED_TRUNCATED,
                            mailbox=mailbox.full_name,
                            command=format_command(
                                backup_command_str, **clean_dict_args_for_log(backup_command_options)
                            ),
                            exit_code=execution_results.source_exit_code,
                            stderr=execution_results.source_stderr[-TRUNCATED_STDERR_LENGTH:],
                        )
                    )
                )

        if execution_results.target_error is not None:
            issues.append(
                Issue(
                    'mail-migrator-transfer-target-failed', Issue.SEVERITY_ERROR,
                    safe_format(
                        messages.WINDOWS_MAIL_TRANSFER_TARGET_FAILED,
                        mailbox=mailbox.full_name,
                        reason=execution_results.target_error
                    )
                )
            )
        elif (
            (
                execution_results.target_exit_code is not None and
                execution_results.target_exit_code != 0
            ) or (
                execution_results.target_stderr is not None and
                execution_results.target_stderr != ''
            )
        ):
            try:
                issues.append(
                    Issue(
                        'mail-migrator-restore-failed', Issue.SEVERITY_ERROR,
                        safe_format(
                            messages.WINDOWS_MAIL_RESTORE_FAILED,
                            mailbox=mailbox.full_name,
                            command=format_command(
                                restore_command_str, **clean_dict_args_for_log(restore_command_options)
                            ),
                            exit_code=execution_results.target_exit_code,
                            stderr=execution_results.target_stderr,
                        )
                    )
                )
            except MemoryError:
                logger.fdebug(messages.WINDOWS_MAIL_FAILED_TO_APPEND_ISSUE)
                logger.exception()
                issues.append(
                    Issue(
                        'mail-migrator-restore-failed', Issue.SEVERITY_ERROR,
                        safe_format(
                            messages.WINDOWS_MAIL_RESTORE_FAILED_TRUNCATED,
                            mailbox=mailbox.full_name,
                            command=format_command(
                                restore_command_str, **clean_dict_args_for_log(restore_command_options)
                            ),
                            exit_code=execution_results.target_exit_code,
                            stderr=execution_results.target_stderr[-TRUNCATED_STDERR_LENGTH:],
                        )
                    )
                )
