from paramiko import AuthenticationException
from collections import namedtuple
from contextlib import contextmanager

import parallels.core.runners.unix.ssh
from parallels.core.logging import get_logger
from parallels.plesk.source.expand import messages
import parallels.plesk.config as connections_config
from parallels.plesk.source.plesk.connections.source_connections import PleskMigratorConnections
from parallels.plesk.utils.xml_rpc import expand as expand_api
from parallels.core.utils.config_utils import ConfigSection, read_http_connection_settings, get_sections_list
from parallels.core.migrator_config import read_ssh_auth
from parallels.core.utils import ssh_utils
from parallels.core import MigrationError, MigrationNoContextError
from parallels.plesk.utils.xml_rpc.expand.client import ExpandClientError
from parallels.plesk.utils.xml_rpc.expand.core import ExpandSystemError
from parallels.plesk.utils.xml_rpc.expand.operator import ClientOperator
from parallels.plesk.source.plesk.server import PleskSourceServer
from parallels.plesk.panel import Panel
from parallels.core.utils.common import cached
from parallels.core.utils.common import merge_dicts

logger = get_logger(__name__)


class ExpandMigratorConnections(PleskMigratorConnections):
    def __init__(self, global_context, target_panel):
        super(ExpandMigratorConnections, self).__init__(global_context, target_panel)

        settings = read_expand_settings(global_context.config, 'expand')
        logger.info(messages.LOG_EXPAND_HOST, settings.ip)
        self._settings = settings
        self.expand = ExpandConnections(settings)

        if not self._config.has_option('GLOBAL', 'centralized-mail-servers'):
            self._centralized_mail_servers = {}
        else:
            self._centralized_mail_servers = self._read_plesk_settings_from_config(
                global_context.config, get_sections_list(self._config, 'GLOBAL', 'centralized-mail-servers')
            )
        self._centralized_dns_servers = self._read_dns_settings_from_config(
            global_context.config, 'centralized-dns-servers'
        )
        self._check_config(target_panel)

    @cached
    def get_source_by_id(self, source_id):
        """Return source Plesk server with given identifier

        :type source_id: str
        :rtype: parallels.plesk.source.plesk.server.PleskSourceServer
        """
        source_configs = merge_dicts(
            self.get_source_plesks(),
            self.get_external_db_servers(),
            self.get_centralized_dns_servers(),
            self.get_centralized_mail_servers()
        )
        return PleskSourceServer(source_id, source_configs[source_id], self._global_context.migrator_server)

    def get_stats_server(self):
        """Get source panel server used for statistics reporting (for example, source IP and OS version)

        Source panel could have multiple servers, but this function should return only one of them.
        If there are no source panel servers - return None
        """
        return self.expand_main_server

    @contextmanager
    def get_expand_runner(self):
        with self.expand.ssh() as ssh:
            yield parallels.core.runners.unix.ssh.SSHRunner(
                ssh, self.expand_main_server
            )

    @property
    @cached
    def expand_main_server(self):
        return ExpandMainServer(self, self._settings)

    @cached
    def expand_centralized_dns_server(self, server_id):
        return ExpandCentralizedDNSServer(server_id, self._centralized_dns_servers[server_id])

    def get_centralized_mail_servers(self):
        """Get information about source centralized mail servers
        
        Returns dictionary {server_id: server_settings} with connection
        information.
        """
        return self._centralized_mail_servers

    def get_centralized_dns_servers(self):
        """Get information about source centralized DNS servers
        
        Returns dictionary {server_id: server_settings} with connection
        information.
        """
        return self._centralized_dns_servers

    def check_source_servers(self):
        super(ExpandMigratorConnections, self).check_source_servers()
        self._check_expand()

    @staticmethod
    def _read_dns_settings_from_config(config, list_name):
        settings = {}
        if config.has_option('GLOBAL', list_name):
            list_str = config.get('GLOBAL', list_name)
            if list_str.strip() != '':
                source_sections = [str_elem.strip() for str_elem in list_str.split(',')]
                for section_name in source_sections:
                    settings[section_name] = connections_config.read_source_dns_servers_settings(config, section_name)
                    logger.info(messages.LOG_CDNS_HOST, section_name, settings[section_name].ip)
                return settings
            else:
                return {}
        else:
            return {}

    def _check_config(self, target_panel):
        """Check that migration configuration defined in config file is correct and supported by migrator"""
        if isinstance(target_panel, Panel):
            for cmail_server_id, cmail_server_settings in self._centralized_mail_servers.items():
                if self.target.is_windows and not cmail_server_settings.is_windows:
                    raise MigrationNoContextError(
                        messages.CMAIL_MIGRATION_FROM_UNIX_TO_WINDOWS_NOT_SUPPORTED % (
                            cmail_server_id
                        )
                    )

    def _check_expand(self):
        logger.debug(messages.DEBUG_CHECK_EXPAND_API_CONNECTION)
        try:
            self.expand.api().send(ClientOperator.Get(
                filter=ClientOperator.Get.FilterAll(),
                dataset=[ClientOperator.Get.Dataset.INFO, ClientOperator.Get.Dataset.PERSONAL_INFO]
            ))
        except ExpandSystemError as e:
            logger.exception()
            if e.error_code == 1:
                raise MigrationError(
                    messages.FAILED_TO_LOGIN_TO_EXPAND_BY_EXPAND_API)
            raise
        except (ExpandClientError, EnvironmentError) as err:
            logger.exception()
            raise MigrationError(
                messages.FAILED_TO_CONNECT_TO_EXPAND % (
                    self.expand.conn_settings.expand_api.url, err
                )
            )
        logger.debug(messages.DEBUG_CHECK_EXPAND_SSH)
        try:
            self.expand.ssh()
        except AuthenticationException:
            logger.exception()
            raise MigrationError(messages.FAILED_TO_LOGIN_TO_EXPAND_BY_SSH)
        except Exception as err:
            logger.exception()
            raise MigrationError(
                messages.FAILED_TO_CONNECT_TO_EXPAND_SSH % err
            )


class ExpandMainServer(object):
    def __init__(self, conn, settings):
        self._conn = conn
        self._settings = settings

    @staticmethod
    def description():
        return messages.EXPAND_MAIN_SERVER_DESCRIPTION

    @contextmanager
    def runner(self):
        with self._conn.get_expand_runner() as runner:
            yield runner

    @property
    @cached
    def expand_api(self):
        return self._conn.expand.api()

    @staticmethod
    def is_windows():
        return False

    def ip(self):
        return self._settings.ip

    @property
    def plesk_version(self):
        return None


class ExpandCentralizedDNSServer(object):
    def __init__(self, server_id, settings):
        self._server_id = server_id
        self._settings = settings

    @contextmanager
    def runner(self):
        with ssh_utils.connect(self._settings) as ssh:
            yield parallels.core.runners.unix.ssh.SSHRunner(ssh, self)

    def description(self):
        return messages.CDNS_SERVER_DESCRIPTION % (self._server_id, self._settings.ip)

    def ip(self):
        return self._settings.ip


EXPAND_URL_DEFAULTS = dict(
    protocol='https',
    port=8442,
    path='/webgate.php'
)

ExpandConfig = namedtuple('ExpandConfig', ('ip', 'ssh_auth', 'expand_api'))


def read_expand_settings(config, section_name):
    section = ConfigSection(config, section_name)

    ip = section['ip']
    ssh_auth = read_ssh_auth(section)

    expand_settings = dict(
        host=ip,
        username=section.get('panel-username'),
        password=section.get_password('panel-password'),
    )
    expand_api_settings = read_http_connection_settings(
        section.prefixed('plesk-').with_defaults(dict(EXPAND_URL_DEFAULTS, **expand_settings))
    )

    return ExpandConfig(ip=ip, ssh_auth=ssh_auth, expand_api=expand_api_settings)


class ExpandConnections(object):
    def __init__(self, conn_settings):
        self.conn_settings = conn_settings

    def api(self):
        return expand_api.Client(
            self.conn_settings.expand_api.url, 
            self.conn_settings.expand_api.auth.username,
            self.conn_settings.expand_api.auth.password, 
            '9.0'
        )

    def ssh(self):
        return ssh_utils.connect(self.conn_settings)
