import random
import string

from paramiko import AuthenticationException

from parallels.core.actions.utils.logging_properties import LoggingProperties
from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.utils.common import safe_format
from parallels.core.utils.ftp import InvalidFTPCredentials
from parallels.plesk.source.web import messages
from parallels.plesk.source.web.actions.base.site_migration import SiteMigrationAction
from parallels.plesk.source.web.config import FileTransportOption
from parallels.plesk.source.web.utils.status_storage import StatusStorage

logger = get_logger(__name__)


class ConnectToSourceDomain(SiteMigrationAction):
    """Connect to the source server by FTP/SSH"""

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

        :rtype: str | unicode
        """
        return messages.ACTION_CONNECT_TO_SOURCE_DOMAIN_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_CONNECT_TO_SOURCE_DOMAIN_FAILURE

    def get_logging_properties(self):
        """Get how action should be logged to migration tools end-user

        :rtype: parallels.core.actions.utils.logging_properties.LoggingProperties
        """
        return LoggingProperties(compound=False)

    def run(self, global_context):
        """
        :type global_context: parallels.plesk.source.web.global_context.WebGlobalMigrationContext
        """
        source = global_context.conn.get_source()
        ssh_exception = None

        # On initial fetch with "get-site-info" command, use file transport specified in configuration file,
        # including automatic mode.
        # On "site-transfer" command, use file transport previously detected by "get-site-info" command.
        # That is necessary because:
        # 1) Key point: file paths could be different for SSH and FTP, e.g. document root retrieved by
        # SSH may look like "/var/www/vhosts/example.com/httpdocs", while for FTP it may look like "/httpdocs".
        # So paths detected by SSH could be invalid for FTP and vice verso.
        # 2) When SSH was initially selected and now there is a temporary issue with it (e.g. network issue),
        # switch to FTP will break transfer because of paths problems.
        # 3) Old versions of migrator, which used FTP, should continue to use FTP because of paths problems.
        # 4) When we detected that there is no SSH on the server, trying to detect whether we should use SSH or FTP
        # for each transfer is a waste of time.
        if Registry.get_instance().get_command_name() == 'get-site-info':
            file_transport = source.config.file_transport
        else:  # "site-transfer" command
            file_transport = global_context.site_info_storage.get_file_transport()

        try:
            if file_transport == FileTransportOption.FTP:
                logger.info(messages.CONNECT_BY_FTP)
                source.connect_by_ftp()
            elif file_transport == FileTransportOption.SSH:
                logger.info(messages.CONNECT_BY_SSH)
                source.connect_by_ssh()
            else:  # automatic detection (source.config.file_transport == FileTransportOption.AUTO)
                try:
                    logger.info(messages.TRY_TO_CONNECT_BY_SSH)
                    source.connect_by_ssh(
                        # customized timeouts and attempts settings, so migrator won't wait too long
                        # if firewall drops packets to SSH server
                        # otherwise, with standard settings it could take several
                        # minutes to understand that SSH is not available)
                        max_connect_attempts=2, interval_between_connect_attempts=5, tcp_timeout=15
                    )
                except Exception as e:
                    with logger.info_level_context(messages.CONNECT_BY_SSH_FAILED):
                        logger.exception()
                    ssh_exception = e
                    source.connect_by_ftp()
        except Exception as e:
            issue_id = None
            if file_transport == FileTransportOption.FTP:
                if source.config.effective_allow_insecure_connections:
                    message = safe_format(
                        messages.STATUS_MESSAGE_CONNECT_TO_SOURCE_FTP_FAILURE, reason=e
                    )
                else:
                    message = safe_format(
                        messages.STATUS_MESSAGE_CONNECT_TO_SOURCE_FTPS_FAILURE, reason=e
                    )
                    issue_id = StatusStorage.ISSUE_ID_FTPS_CONNECT_FAILED
            elif file_transport == FileTransportOption.SSH:
                message = safe_format(
                    messages.STATUS_MESSAGE_CONNECT_TO_SOURCE_SSH_FAILURE, reason=e
                )
            else:  # automatic detection - tried both FTP and SSH
                ftp_exception = e

                if (
                    isinstance(ftp_exception, InvalidFTPCredentials) and
                    isinstance(ssh_exception, AuthenticationException)
                ):
                    message = messages.INVALID_CREDENTIALS
                else:
                    if source.config.effective_allow_insecure_connections:
                        message = safe_format(
                            messages.STATUS_MESSAGE_CONNECT_TO_SOURCE_BOTH_FAILURE,
                            reason_ssh=ssh_exception, reason_ftp=ftp_exception
                        )
                    else:
                        message = safe_format(
                            messages.STATUS_MESSAGE_SECURE_CONNECT_TO_SOURCE_BOTH_FAILURE,
                            reason_ssh=ssh_exception, reason_ftp=ftp_exception
                        )
                        issue_id = StatusStorage.ISSUE_ID_SECURE_CONNECT_FAILED

            global_context.status_storage.set_operation_failure(StatusStorage.OPERATION_CONNECT, message, issue_id)
            raise

        global_context.status_storage.set_operation_success(StatusStorage.OPERATION_CONNECT)
        self._configure_source_agent_id(global_context)
        if source.is_ftp_available():
            global_context.site_info_storage.set_ftp_version(source.ftp.server_version)

        global_context.site_info_storage.set_file_transport(
            FileTransportOption.SSH if source.is_ssh_available() else FileTransportOption.FTP
        )

    @staticmethod
    def _configure_source_agent_id(global_context):
        """Configure source agent ID - unique ID to name directory with agent on the source server

        :type global_context: parallels.plesk.source.web.global_context.WebGlobalMigrationContext
        :rtype: None
        """
        plesk_agent_id_file = global_context.session_files.get_plesk_agent_id_file()
        if plesk_agent_id_file.is_exists():
            plesk_agent_id = plesk_agent_id_file.read().strip()
        else:
            # 32-symbols length gives better uniqueness than GUID:
            # GUID has 2^128 (128 bits, each could have 2 values) combinations,
            # while we have 36^32 combinations (32 symbols, each could be one of 36 ASCII letters and digits)
            # That unique ID has 2 goals:
            # - to avoid intersection of several target servers migrating from a single source
            # - to avoid malicious user from guessing where Web RPC agent resides
            # We should keep it short enough to avoid problems on Windows. It has full path limitation
            # at around 256 symbols when using old filesystem API, which is a common case for us
            # (PHP 5.x is affected by that limitation)
            unique_id_len = 32

            plesk_agent_id = ''.join(
                random.choice(string.ascii_lowercase + string.digits) for _ in range(unique_id_len)
            )
            plesk_agent_id_file.write(plesk_agent_id)
        global_context.conn.get_source().set_plesk_agent_id(plesk_agent_id)
