import ntpath
import re
import time
from threading import Thread

import parallels
from parallels.core import safe_format
from parallels.core.content.mail.mail_migrator_utils import write_account_description_file_plain
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_local_to_local
from parallels.core.utils import windows_utils
from parallels.core.utils.clean_logging import clean_dict_args_for_log
from parallels.core.utils.common import is_empty, default
from parallels.core.utils.config_utils import global_section
from parallels.core.utils.json_utils import read_json
from parallels.core.utils.mail_migrator import get_local_mail_migrator_executable
from parallels.core.utils.migrator_utils import get_package_extras_file_path
from parallels.core.utils.plesk_utils import get_remote_smarter_mail_ip
from parallels.plesk.source.mail import messages
from parallels.plesk.source.mail.actions.base.mail_migration import MailMigrationAction
from parallels.plesk.source.mail.imap_encryption import ImapEncryption
from parallels.plesk.source.mail.exceptions import MailMigratorError
from parallels.plesk.source.mail.status_storage import StatusStorage

logger = get_logger(__name__)


class CopyMailMessages(MailMigrationAction):
    """Copy mail messages"""

    def get_description(self):
        """Get short description of action as string

        :rtype: str | unicode
        """
        return messages.ACTION_COPY_MAIL_MESSAGES_DESCRIPTION

    def get_failure_message(self, global_context):
        """Get message for situation when action failed

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: str | unicode
        """
        return messages.ACTION_COPY_MAIL_MESSAGES_FAILURE

    def run(self, global_context):
        """Run action

        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :rtype: None
        """
        stat = global_context.statistics_transfer_mail
        try:
            stat.start_copy_time = int(time.time())
            stat.write()
        except Exception:
            logger.fwarn(messages.FAILED_ACTION_START_COPY_MAIL_STATISTICS)

        try:
            self._copy_mail_messages(global_context, global_context.migrated_mail_address_pair)
        finally:
            try:
                stat.end_copy_time = int(time.time())
                stat.write()
            except Exception:
                logger.fwarn(messages.FAILED_ACTION_FINISH_COPY_MAIL_STATISTICS)

    def _copy_mail_messages(self, global_context, mail_address_pair):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :rtype: None
        """
        global_context.status_storage.set_sync_task_step(StatusStorage.STEP_PREPARE_CONNECT_SOURCE)

        session_files = global_context.session_files
        migrator_server = global_context.migrator_server
        source_account_description_file = session_files.get_new_random_source_account_description_file().path
        target_account_description_file = session_files.get_new_random_target_account_description_file().path
        write_account_description_file_plain(
            migrator_server, source_account_description_file,
            mail_address_pair.source_email, "", mail_address_pair.source_password
        )
        write_account_description_file_plain(
            migrator_server, target_account_description_file,
            mail_address_pair.target_mailname, mail_address_pair.target_domain,
            default(mail_address_pair.target_password, "")
        )
        message_ids_cache_file = global_context.session_files.get_message_ids_cache_file(
            mail_address_pair.source_email, mail_address_pair.target_email
        ).path
        try:
            session_files = global_context.session_files
            backup_progress_file = session_files.get_new_random_mail_migrator_backup_progress_file().path
            restore_progress_file = session_files.get_new_random_mail_migrator_restore_progress_file().path

            command_running = True

            def poll_status_thread_main():
                while command_running:
                    try:
                        self._update_step_from_backup_progress_file(
                            global_context, mail_address_pair, backup_progress_file
                        )
                    except Exception:
                        # Ignore all errors when reading JSON, as it could be incomplete/locked
                        # when mail migrator writes status to it; just try to read it in another second
                        pass

                    try:
                        self._update_progress_from_restore_progress_file(
                            global_context, mail_address_pair, restore_progress_file
                        )
                    except Exception:
                        # Ignore all errors when reading JSON, as it could be incomplete/locked
                        # when mail migrator writes status to it; just try to read it in another second
                        pass

                    time.sleep(1)

                self._update_step_from_backup_progress_file(
                    global_context, mail_address_pair, backup_progress_file
                )
                self._update_imap_vendor_from_backup_progress_file(
                    global_context, backup_progress_file, mail_address_pair.source_email
                )
                self._update_imap_host_from_backup_progress_file(
                    global_context, mail_address_pair, backup_progress_file
                )
                self._update_task_status_from_backup_progress_file(
                    global_context, backup_progress_file
                )
                self._update_progress_from_restore_progress_file(
                    global_context, mail_address_pair, restore_progress_file
                )

            poll_status_thread = Thread(target=poll_status_thread_main)
            poll_status_thread.start()

            try:
                if migrator_server.is_windows():
                    self._run_transfer_windows(
                        global_context, mail_address_pair,
                        source_account_description_file, target_account_description_file,
                        backup_progress_file, restore_progress_file, message_ids_cache_file
                    )
                else:
                    self._run_transfer_linux(
                        global_context, mail_address_pair,
                        source_account_description_file, target_account_description_file,
                        backup_progress_file, restore_progress_file, message_ids_cache_file
                    )
            finally:
                command_running = False
                poll_status_thread.join()

                global_context.status_storage.set_sync_task_step(StatusStorage.STEP_FINALIZE)
                self._check_errors_in_backup_progress_file(backup_progress_file)
        finally:
            if not global_section(global_context.config).getboolean('do-not-remove-account-files', False):
                with migrator_server.runner() as runner:
                    assert isinstance(runner, BaseRunner)
                    runner.remove_file(source_account_description_file)
                    runner.remove_file(target_account_description_file)

    @staticmethod
    def _run_transfer_linux(
        global_context, mail_address_pair,
        source_account_description_file, target_account_description_file,
        backup_progress_file, restore_progress_file, message_ids_cache_file
    ):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :type source_account_description_file: str | unicode
        :type target_account_description_file: str | unicode
        :type backup_progress_file: str | unicode
        :type restore_progress_file: str | unicode
        :type message_ids_cache_file: str | unicode
        :rtype: None
        """
        migrator_server = global_context.migrator_server

        mail_migrator_executable = get_local_mail_migrator_executable(is_windows=False)
        ca_bundle = get_package_extras_file_path(parallels.core, "cacert/cacert.pem")

        stop_mark_file = global_context.stop_mark.get_stop_file_path()

        log_file = None
        if global_section(global_context.config).getboolean('mail-migrator-log-enabled', False):
            log_file = global_context.session_files.get_new_random_mail_migrator_log_file().path
            logger.fdebug(messages.MIGRATION_LOG_PATH, path=log_file)

        installed_components = global_context.hosting_repository.panel.get_installed_components(
            mail_address_pair.target_subscription_name
        )
        if 'courier-imap' in installed_components:
            target_server_type = 'courier'
        else:
            target_server_type = 'dovecot'

        with migrator_server.runner() as runner:
            assert isinstance(runner, BaseRunner)

            runner.execute_command(
                "{mail_migrator_executable} migrate "
                "--source-account-description-file={source_account_description_file} "
                "--source-message-ids-cache-file={source_message_ids_cache_file} "
                "--target-account-description-file={target_account_description_file} "
                "--backup-progress-file={backup_progress_file} " +
                "--restore-progress-file={restore_progress_file} " +
                "--source-ca-bundle={ca_bundle} " +
                "--target-server-type={target_server_type}" +
                optional_arg(
                    "--source-allow-insecure-connections=true",
                    mail_address_pair.ignore_security_warnings
                ) +
                optional_arg(
                    "--source-host={source_host}",
                    mail_address_pair.imap_host
                ) +
                optional_arg(
                    "--source-port={source_port}",
                    mail_address_pair.imap_port
                ) +
                optional_arg(
                    "--source-folder-separator={source_folder_separator}",
                    not is_empty(mail_address_pair.imap_folder_separator)
                ) +
                optional_arg(
                    "--source-timeout={source_timeout}",
                    not is_empty(mail_address_pair.imap_timeout)
                ) +
                optional_arg(
                    "--source-imap-protocol={source_protocol}",
                    mail_address_pair.imap_encryption in ImapEncryption.get_valid_values_for_mail_migrator()
                ) +
                optional_arg(
                    "--stop-mark-file={stop_mark_file}",
                    stop_mark_file is not None
                ) +
                optional_arg(
                    "--log-file={log_file}",
                    log_file is not None
                ),
                dict(
                    mail_migrator_executable=mail_migrator_executable,
                    source_account_description_file=source_account_description_file,
                    source_host=mail_address_pair.imap_host,
                    source_port=mail_address_pair.imap_port,
                    source_folder_separator=mail_address_pair.imap_folder_separator,
                    source_timeout=mail_address_pair.imap_timeout,
                    source_protocol=mail_address_pair.imap_encryption,
                    source_message_ids_cache_file=message_ids_cache_file,
                    target_account_description_file=target_account_description_file,
                    backup_progress_file=backup_progress_file,
                    restore_progress_file=restore_progress_file,
                    ca_bundle=ca_bundle,
                    target_server_type=target_server_type,
                    stop_mark_file=stop_mark_file,
                    log_file=log_file
                ),
            )

    @staticmethod
    def _run_transfer_windows(
        global_context, mail_address_pair,
        source_account_description_file, target_account_description_file,
        backup_progress_file, restore_progress_file, message_ids_cache_file
    ):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :type source_account_description_file: str | unicode
        :type target_account_description_file: str | unicode
        :type backup_progress_file: str | unicode
        :type restore_progress_file: str | unicode
        :type message_ids_cache_file: str | unicode
        :rtype: None
        """
        migrator_server = global_context.migrator_server

        backup_mail_migrator_executable = get_local_mail_migrator_executable(is_windows=False)
        restore_mail_migrator_executable = get_local_mail_migrator_executable(is_windows=True)

        ca_bundle = get_package_extras_file_path(parallels.core, r"cacert\cacert.pem")

        remote_smarter_mail_ip = get_remote_smarter_mail_ip(global_context.conn.target.plesk_server)
        is_remote_smarter_mail = remote_smarter_mail_ip is not None

        plesk_dir = global_context.conn.target.plesk_server.plesk_dir

        stop_mark_file = global_context.stop_mark.get_stop_file_path()

        backup_log_file = None
        restore_log_file = None
        list_message_ids_log_file = None
        if global_section(global_context.config).getboolean('mail-migrator-log-enabled', False):
            backup_log_file = global_context.session_files.get_new_random_mail_migrator_log_file().path
            restore_log_file = global_context.session_files.get_new_random_mail_migrator_log_file().path
            list_message_ids_log_file = global_context.session_files.get_new_random_mail_migrator_log_file().path
            logger.fdebug(messages.BACKUP_LOG_PATH, path=backup_log_file)
            logger.fdebug(messages.RESTORE_LOG_PATH, path=restore_log_file)
            logger.fdebug(messages.LIST_MESSAGE_IDS_LOG_PATH, path=list_message_ids_log_file)

        exclude_message_ids_file = global_context.session_files.get_new_random_exclude_message_ids_file().path

        list_message_ids_command = (
            "{mail_migrator_executable} list-message-ids "
            "--account-description-file={target_account_description_file} " +
            "--output-file={exclude_message_ids_file} " +
            optional_arg(
                "--log-file={log_file}",
                list_message_ids_log_file is not None
            ) +
            optional_arg(
                # Migrate to remote SmarterMail server using IMAP, as we don't have direct access to the
                # server, and so we can't use native SmarterMail libraries to work with messages.
                # Setting up direct access to remote SmarterMail is a complex task.
                "--provider=imap",
                is_remote_smarter_mail
            ) +
            optional_arg(
                "--host={imap_host}",
                is_remote_smarter_mail
            )
        )
        list_message_ids_command_args = dict(
            mail_migrator_executable=restore_mail_migrator_executable,
            target_account_description_file=target_account_description_file,
            exclude_message_ids_file=exclude_message_ids_file,
            log_file=list_message_ids_log_file,
            imap_host=remote_smarter_mail_ip
        )

        with migrator_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            runner.execute_command(list_message_ids_command, list_message_ids_command_args)

        backup_command = (
            "{python_executable} {mail_migrator_executable} backup "
            "--account-description-file={source_account_description_file} "
            "--message-ids-cache-file={source_message_ids_cache_file} "
            "--exclude-message-ids-file={exclude_message_ids_file} "
            "--progress-file={progress_file} " +
            "--ca-bundle={ca_bundle}" +
            optional_arg(
                "--allow-insecure-connections=true",
                mail_address_pair.ignore_security_warnings
            ) +
            optional_arg(
                "--host={source_host}",
                mail_address_pair.imap_host
            ) +
            optional_arg(
                "--port={source_port}",
                mail_address_pair.imap_port
            ) +
            optional_arg(
                "--folder-separator={source_folder_separator}",
                not is_empty(mail_address_pair.imap_folder_separator)
            ) +
            optional_arg(
                "--timeout={source_timeout}",
                not is_empty(mail_address_pair.imap_timeout)
            ) +
            optional_arg(
                "--imap-protocol={source_protocol}",
                mail_address_pair.imap_encryption in ImapEncryption.get_valid_values_for_mail_migrator()
            ) +
            optional_arg(
                "--stop-mark-file={stop_mark_file}",
                stop_mark_file is not None
            ) +
            optional_arg(
                "--log-file={log_file}",
                backup_log_file is not None
            )
        )
        backup_command_args = dict(
            python_executable=ntpath.join(plesk_dir, 'python', 'python.exe'),
            mail_migrator_executable=backup_mail_migrator_executable,
            source_account_description_file=source_account_description_file,
            exclude_message_ids_file=exclude_message_ids_file,
            source_host=mail_address_pair.imap_host.encode('idna'),
            source_port=mail_address_pair.imap_port,
            source_folder_separator=mail_address_pair.imap_folder_separator,
            source_timeout=mail_address_pair.imap_timeout,
            source_protocol=mail_address_pair.imap_encryption,
            source_message_ids_cache_file=message_ids_cache_file,
            target_account_description_file=target_account_description_file,
            progress_file=backup_progress_file,
            ca_bundle=ca_bundle,
            stop_mark_file=stop_mark_file,
            log_file=backup_log_file,
        )
        restore_command = (
            "{mail_migrator_executable} restore "
            "--account-description-file={target_account_description_file} "
            "--progress-file={restore_progress_file}" +
            optional_arg(
                "--stop-mark-file={stop_mark_file}",
                stop_mark_file is not None
            ) +
            optional_arg(
                "--log-file={log_file}",
                restore_log_file is not None
            ) +
            optional_arg(
                # Migrate to remote SmarterMail server using IMAP, as we don't have direct access to the
                # server, and so we can't use native SmarterMail libraries to work with messages.
                # Setting up direct access to remote SmarterMail is a complex task.
                "--provider=imap",
                is_remote_smarter_mail
            ) +
            optional_arg(
                "--host={imap_host}",
                is_remote_smarter_mail
            )
        )
        restore_command_args = dict(
            mail_migrator_executable=restore_mail_migrator_executable,
            target_account_description_file=target_account_description_file,
            restore_progress_file=restore_progress_file,
            stop_mark_file=stop_mark_file,
            log_file=restore_log_file,
            imap_host=remote_smarter_mail_ip
        )
        result = pipe_commands_local_to_local(
            backup_command, backup_command_args,
            restore_command, restore_command_args,
            # Override default read chunk size to make migration process updates more responsive:
            # mail messages are passed to restore process only when we have read_chunk_size bytes of data backed up.
            # So if the read chunk size is too large (e.g. 1 MB), then messages are started to be restored
            # when many of them were read by IMAP. As IMAP is slow, it takes much time (~minute) to collect 1MB
            # of messages, and all that time no messages are restored and restoration progress keeps showing the same
            # message number.
            read_chunk_size=5 * 1024  # 5 KB
        )

        if result.source_exit_code != 0 or not is_empty(result.source_stderr):
            error_id = 'failed-to-backup-mail'
            error_message = messages.FAILED_TO_BACKUP_MAIL_MESSAGES
            error_details = safe_format(
                messages.FAILED_TO_BACKUP_MAIL_MESSAGES_DETAILS,
                command=windows_utils.format_command_dict(
                    backup_command, clean_dict_args_for_log(backup_command_args)
                ),
                exit_code=result.source_exit_code,
                stderr=result.source_stderr
            )
            if global_context.status_storage.get_reporting_mode() == StatusStorage.REPORTING_MODE_SYNC:
                raise MailMigratorError(error_id, error_message, error_details)
            if global_context.status_storage.get_reporting_mode() == StatusStorage.REPORTING_MODE_ASYNC:
                global_context.status_storage.add_global_issue(
                    mail_address_pair.source_email,
                    mail_address_pair.target_mailname,
                    Issue(
                        error_id, Issue.SEVERITY_ERROR,
                        problem_text=error_message,
                        details_text=error_details
                    )
                )

        if result.target_exit_code != 0 or not is_empty(result.target_stderr):
            error_id = 'failed-to-restore-mail'
            error_message = messages.FAILED_TO_RESTORE_MAIL_MESSAGES
            error_details = safe_format(
                messages.FAILED_TO_RESTORE_MAIL_MESSAGES_DETAILS,
                command=windows_utils.format_command_dict(
                    restore_command, clean_dict_args_for_log(restore_command_args)
                ),
                exit_code=result.target_exit_code,
                stderr=result.target_stderr
            )
            if global_context.status_storage.get_reporting_mode() == StatusStorage.REPORTING_MODE_SYNC:
                raise MailMigratorError(error_id, error_message, error_details)
            elif global_context.status_storage.get_reporting_mode() == StatusStorage.REPORTING_MODE_ASYNC:
                global_context.status_storage.add_global_issue(
                    mail_address_pair.source_email,
                    mail_address_pair.target_mailname,
                    Issue(
                        error_id, Issue.SEVERITY_ERROR,
                        problem_text=error_message,
                        details_text=error_details
                    )
                )

    @staticmethod
    def _update_step_from_backup_progress_file(global_context, mail_address_pair, progress_file):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None:
            step = progress_data.get('step')
            directly_mapped_steps = MailMigratorSteps.get_directly_mapped_steps()
            if step == MailMigratorSteps.STEP_TRANSFER_MESSAGES:
                global_context.status_storage.set_sync_task_step(StatusStorage.STEP_TRANSFER_MESSAGES)
                global_context.status_storage.set_reporting_mode(StatusStorage.REPORTING_MODE_ASYNC)

                global_context.status_storage.set_global_status(
                    mail_address_pair.source_email, mail_address_pair.target_mailname,
                    StatusStorage.STATUS_IN_PROGRESS
                )
            elif step in directly_mapped_steps:
                global_context.status_storage.set_sync_task_step(directly_mapped_steps[step])

    @staticmethod
    def _update_imap_host_from_backup_progress_file(global_context, mail_address_pair, progress_file):
        """Store automatically detected IMAP host to persistent storage to save time on autodetection during resync

        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None:
            imap_host = progress_data.get('autodetected_imap_host')
            if not is_empty(imap_host):
                global_context.settings_storage.set_imap_host(
                    mail_address_pair.source_email, mail_address_pair.target_mailname,
                    imap_host
                )

    @staticmethod
    def _update_imap_vendor_from_backup_progress_file(global_context, progress_file, source_email):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None:
            imap_vendor = progress_data.get('imap_vendor', '')
            if imap_vendor in ['Unknown', '']:
                if re.match("^.+@gmail\.(com|ru)$", source_email, re.IGNORECASE):
                    imap_vendor = "Gmail"
                elif re.match("^.+@yahoo\.(com|ru)$", source_email, re.IGNORECASE):
                    imap_vendor = "Yahoo"
                elif re.match("^.+@ya(ndex)*\.(com|ru)$", source_email, re.IGNORECASE):
                    imap_vendor = "Yandex"
                else:
                    imap_vendor = source_email[source_email.find('@')+1:].lower()
            global_context.statistics_transfer_mail.imap_vendor = imap_vendor
            global_context.statistics_transfer_mail.write()

    @staticmethod
    def _update_task_status_from_backup_progress_file(global_context, progress_file):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None and progress_data.get('error_id') is not None:
            global_context.statistics_transfer_mail.status = progress_data.get('error_id')
            global_context.statistics_transfer_mail.write()

    @staticmethod
    def _update_progress_from_restore_progress_file(global_context, mail_address_pair, progress_file):
        """
        :type global_context: parallels.plesk.source.mail.global_context.MailGlobalMigrationContext
        :type mail_address_pair: parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None:
            current_restored_message_num = progress_data.get('current_restored_message_num')
            global_context.status_storage.set_global_restored_message_num(
                mail_address_pair.source_email, mail_address_pair.target_mailname,
                current_restored_message_num
            )
            global_context.statistics_transfer_mail.restored_overall_messages_length = \
                progress_data.get('restored_overall_messages_length', 0)
            global_context.statistics_transfer_mail.restored_overall_messages_num = \
                progress_data.get('restored_overall_messages_num', 0)
            global_context.statistics_transfer_mail.write()

    @staticmethod
    def _check_errors_in_backup_progress_file(progress_file):
        """
        :type progress_file: str | unicode
        :rtype: None
        """
        progress_data = read_json(progress_file)
        if progress_data is not None:
            if progress_data.get('error_id') is not None:
                raise MailMigratorError(progress_data.get('error_id'), progress_data.get('error_message'))


class MailMigratorSteps(object):
    STEP_DETECT_IMAP_HOST = 'detect-imap-host'
    STEP_CONNECT = 'connect'
    STEP_LOGIN = 'login'
    STEP_DETECT_FOLDER_SEPARATOR = 'detect-folder-separator'
    STEP_TRANSFER_MESSAGES = 'transfer-messages'
    STEP_FINALIZE = 'finalize'

    @staticmethod
    def get_directly_mapped_steps():
        return {
            MailMigratorSteps.STEP_DETECT_IMAP_HOST: StatusStorage.STEP_DETECT_SOURCE_IMAP_HOST,
            MailMigratorSteps.STEP_CONNECT: StatusStorage.STEP_CONNECT_SOURCE,
            MailMigratorSteps.STEP_LOGIN: StatusStorage.STEP_LOGIN_SOURCE,
            MailMigratorSteps.STEP_DETECT_FOLDER_SEPARATOR: StatusStorage.STEP_DETECT_FOLDER_SEPARATOR_SOURCE,
            MailMigratorSteps.STEP_TRANSFER_MESSAGES: StatusStorage.STEP_TRANSFER_MESSAGES
        }


def optional_arg(arg, condition):
    """Shortcut to format command argument which should be added only if specific condition is met

    :type arg: str | unicode
    :type condition: bool
    :rtype: str | unicode
    """
    if condition:
        return " " + arg
    else:
        return ""
