import ntpath
import posixpath
from contextlib import contextmanager

from parallels.core import MigrationError
from parallels.core.connections.ftp.connection_pool import FTPConnectionPool
from parallels.core.connections.source import Source
from parallels.core.connections.server import Server
from parallels.core.migrator_config import PhysicalUnixServerConfig, SSHAuthPassword
from parallels.core.runners.ftp.runner import FTPRunner
from parallels.core.runners.unix.ssh import SSHRunner, TestCommandNonZeroExitCodeFailure
from parallels.core.runners.web.runner import WebRPCAgentRunner
from parallels.core.runners.web.utils.agent import WebRPCAgent
from parallels.core.utils import ssh_utils
from parallels.core.utils.common import cached, is_empty
from parallels.core.utils.common.ip import resolve
from parallels.core.utils.windows_mysql_client_deploy import deploy_mysqldump, deploy_mysql
from parallels.plesk.source.web.config import TransferFilesModeOption
from parallels.plesk.source.web.session_dir import WebSessionDir
from parallels.plesk.source.web import messages


class WebSource(Source, Server):
    """Source for migration from web
    """

    def __init__(self, source_id, config, migrator_server):
        Source.__init__(self, source_id, config, migrator_server)
        Server.__init__(self)

        self._web_source_ssh_server = None
        self._ssh_runner = None

        self._ftp = None
        self._ftp_runner = None

        self._plesk_agent_id = ''
        self._web_rpc_agent = None
        self._web_rpc_agent_runner = None

    @property
    def config(self):
        """Return web source config

        :rtype: parallels.plesk.source.web.connections.WebSourceConfig
        """
        return super(WebSource, self).config

    @contextmanager
    def runner(self):
        """Get runner to execute commands on the source

        :rtype: parallels.core.runners.web.runner.WebRPCAgentRunner
        """
        yield self.get_web_rpc_runner()

    def get_filesystem_runner(self):
        """Get runner which should be used to work with files (upload, download, remove, etc) on the source site

        :rtype: parallels.core.runners.base.BaseRunner
        """
        if self.is_ssh_available():
            return self.get_ssh_runner()
        elif self.is_ftp_available():
            return self.get_ftp_runner()
        else:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FILE_TRANSPORT)

    def get_filesystem_runner_title(self):
        """Get human-readable title of the used filesystem runner ("SSH" or "FTP")

        :rtype: str
        """
        if self.is_ssh_available():
            return 'SSH'
        elif self.is_ftp_available():
            return 'FTP'
        else:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FILE_TRANSPORT)

    def get_filesystem_runner_encoding(self):
        """Get encoding in which filesystem runner works with file names

        :rtype: str
        """
        if self.is_ssh_available():
            # do not actually detect encoding, consider that all modern Linux servers use UTF-8
            return 'utf-8'
        elif self.is_ftp_available():
            return self.ftp.get_encoding()
        else:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FILE_TRANSPORT)

    @contextmanager
    def filesystem_runner(self):
        """Get runner which should be used to work with files (upload, download, remove, etc) on the source site
        """
        yield self.get_filesystem_runner()

    def connect_by_ftp(self):
        """
        :rtype: None
        """
        self._ftp = FTPConnectionPool.get_instance().get(
            self.config.host, self.config.username, self.config.password,
            self.config.ftp_encoding, self.config.ignore_passive_mode_server_ip,
            ftps_only=not self.config.effective_allow_insecure_connections
        )
        self._ftp_runner = FTPRunner(self._ftp)

    @property
    @cached
    def ftp(self):
        """
        :rtype: parallels.core.utils.ftp.Ftp
        """
        if self._ftp is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FTP)
        return self._ftp

    def get_ftp_runner(self):
        """Get runner based on FTP connection

        :rtype: parallels.core.runners.ftp.runner.FTPRunner
        """
        if self._ftp_runner is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FTP)
        return self._ftp_runner

    @contextmanager
    def ftp_runner(self):
        """Get runner based on FTP connection
        """
        if self._ftp_runner is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_FTP)
        yield self._ftp_runner

    def is_ftp_available(self):
        """
        :rtype: bool
        """
        return self._ftp_runner is not None

    def connect_by_ssh(self, max_connect_attempts=5, interval_between_connect_attempts=10, tcp_timeout=15):
        """
        :rtype: None
        """
        try:
            self._web_source_ssh_server = WebSSHServer(
                self.config.host, self.config.username, self.config.password, self.config.ssh_port,
                self.config.transfer_files_mode,
                max_connect_attempts, interval_between_connect_attempts, tcp_timeout
            )
            ssh_runner = self._web_source_ssh_server.get_runner()
        except TestCommandNonZeroExitCodeFailure:
            raise MigrationError(messages.FAILED_TO_EXECUTE_TEST_COMMAND_BY_SSH)

        self._check_ssh_required_utils(ssh_runner)
        self._ssh_runner = ssh_runner

    @staticmethod
    def _check_ssh_required_utils(ssh_runner):
        """
        :type ssh_runner: parallels.core.runners.unix.ssh.SSHRunner
        :rtype: None
        :raises: parallels.core.MigrationError
        """
        if not ssh_runner.is_utility_find_available() and not ssh_runner.is_utility_ls_available():
            raise MigrationError(messages.NO_FIND_OR_LS_ON_SERVER)
        if not ssh_runner.is_utility_rsync_available() and not ssh_runner.is_utility_scp_available():
            raise MigrationError(messages.NO_RSYNC_OR_SCP_ON_SERVER)

    def get_ssh_runner(self):
        """Get runner based on SSH connection

        :rtype: parallels.core.runners.unix.ssh.SSHRunner
        """
        if self._ssh_runner is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_SSH)
        return self._ssh_runner

    @contextmanager
    def ssh_runner(self):
        """Get runner based on SSH connection
        """
        if self._ssh_runner is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_SSH)
        yield self._ssh_runner

    def get_ssh_server(self):
        """
        :rtype: parallels.plesk.source.web.source.WebSSHServer
        """
        if self._web_source_ssh_server is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_SSH)
        return self._web_source_ssh_server

    def is_ssh_available(self):
        """
        :rtype: bool
        """
        return self._ssh_runner is not None

    def is_rsync_available(self):
        """
        :rtype: bool
        """
        return self._ssh_runner is not None and self._ssh_runner.is_utility_rsync_available()

    def connect_by_web_rpc_agent(self):
        """
        :rtype: None
        """
        web_rpc_agent = WebRPCAgent(
            self.get_filesystem_runner(),
            self.config.document_root,
            self.config.base_url,
            self._plesk_agent_id,
            verify_certificates=not self.config.effective_allow_insecure_connections,
            single_file_agent=self.config.single_file_web_rpc_agent
        )
        web_rpc_agent.deploy()
        self._web_rpc_agent = web_rpc_agent
        self._web_rpc_agent_runner = WebRPCAgentRunner(self.web_rpc_agent, self.config.host)
        self._web_rpc_agent_runner.connect()

    @cached
    def get_web_rpc_runner(self):
        """
        :rtype: parallels.core.runners.web.runner.WebRPCAgentRunner
        """
        if self._web_rpc_agent_runner is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_WEB_RPC_AGENT)
        return self._web_rpc_agent_runner

    @cached
    def get_web_rpc_runner_encoding(self):
        """Get encoding in which web RPC runner works with file names

        :rtype: str
        """
        # For Linux, we consider that all modern servers use UTF-8
        # For Windows, we don't know actual encoding (detection implementation is required),
        # and use UTF-8, while it is usually not correct
        return 'utf-8'

    def is_web_rpc_agent_deployed(self):
        """
        :rtype: bool
        """
        return self._web_rpc_agent is not None

    @property
    @cached
    def web_rpc_agent(self):
        """
        :rtype: parallels.core.runners.web.utils.agent.WebRPCAgent
        """
        if self._web_rpc_agent is None:
            raise MigrationError(messages.INTERNAL_ERROR_NO_WEB_RPC_AGENT)
        return self._web_rpc_agent

    @cached
    def get_absolute_filesystem_root_path(self):
        """Retrieve path where located files of this source

        :rtype: str
        """
        web_rpc_runner = self.get_web_rpc_runner()
        return web_rpc_runner.get_absolute_filesystem_root_path()

    @cached
    def get_files_root(self):
        """Path to start scan for files, applications and document root, related to FTP/SSH root.

        When not specified, default is used: when transport is FTP, then FTP root is used, when transport is SSH,
        then home directory of SSH user is used.

        :rtype: str | unicode
        """
        if not is_empty(self.config.files_root):
            return self.config.files_root
        else:
            if self.is_ssh_available():
                # By default, for SSH start search from a directory, which is the current right after login
                # (in most cases - home directory of system user). We can not always use "/" as retrieving files
                # tree and detection of document root and applications could take too much time and memory.
                return self.get_ssh_runner().execute_command('pwd').stdout.strip()
            else:
                return '/'

    def get_absolute_files_root(self):
        """Get absolute path for files root path.

        :rtype: str | unicode
        """
        files_root = self.get_files_root()
        absolute_fs_root_path = self.get_absolute_filesystem_root_path()
        files_root_parts = [item for item in files_root.split('/') if item != '']
        if self.is_windows():
            return ntpath.join(absolute_fs_root_path, *files_root_parts)
        else:
            return posixpath.join(absolute_fs_root_path, *files_root_parts)

    @cached
    def get_path_to_mysqldump(self):
        """
        :rtype: str | unicode
        """
        if self.is_windows():
            return deploy_mysqldump(self)
        else:
            return 'mysqldump'

    @cached
    def get_path_to_mysql(self):
        """
        :rtype: str | unicode
        """
        if self.is_windows():
            return deploy_mysql(self)
        else:
            return 'mysql'

    def is_windows(self):
        """
        :rtype: bool
        """
        web_rpc_runner = self.get_web_rpc_runner()
        return web_rpc_runner.is_windows

    def _create_session_dir(self):
        """
        :rtype: parallels.plesk.source.web.session_dir.WebSessionDir
        """
        return WebSessionDir(self.runner, self.config.session_dir)

    def set_plesk_agent_id(self, plesk_agent_id):
        """
        :type plesk_agent_id: str | unicode
        :rtype: None
        """
        # convert agent ID to binary string, to avoid issues when using URL parse/unparse methods while combining
        # agent ID into URLs
        self._plesk_agent_id = str(plesk_agent_id)

    def get_plesk_agent_id(self):
        """
        :rtype: str | unicode | None 
        """
        return self._plesk_agent_id


class WebSSHServer(object):
    def __init__(
        self, host, username, password, port, transfer_files_mode,
        max_connect_attempts=5, interval_between_connect_attempts=10, tcp_timeout=15
    ):
        self._host = host
        self._username = username
        self._password = password
        self._port = port
        self._tcp_timeout = tcp_timeout
        self._ssh = ssh_utils.connect(
            self.settings(), max_connect_attempts, interval_between_connect_attempts
        ).__enter__()

        if transfer_files_mode == TransferFilesModeOption.SSH_SCP:
            copy_files_mode = SSHRunner.COPY_FILES_MODE_SCP
        elif transfer_files_mode == TransferFilesModeOption.SSH_RSYNC:
            copy_files_mode = SSHRunner.COPY_FILES_MODE_RSYNC
        else:
            copy_files_mode = SSHRunner.COPY_FILES_MODE_AUTO

        self._ssh_runner = SSHRunner(
            self._ssh, self, SSHRunner.LIST_FILES_MODE_AUTO, copy_files_mode
        )

    @contextmanager
    def runner(self):
        yield self.get_runner()

    def get_runner(self):
        """
        :rtype: parallels.core.runners.unix.ssh.SSHRunner
        """
        return self._ssh_runner

    def description(self):
        return 'remote server'

    def ip(self):
        return resolve(self._host)

    def settings(self):
        return PhysicalUnixServerConfig(
            'web', self.ip(), None, SSHAuthPassword(self._port, self._username, self._password, self._tcp_timeout)
        )

    def ssh_key(self):
        return None
