from parallels.core import messages, migrator_config, MigrationConfigurationFileError
from parallels.core.connections.checker import ConnectionChecker
from parallels.core.connections.source import Source
from parallels.core.connections.source_server import SourceServer
from parallels.core.connections.physical_server import create_physical_server
from parallels.core.migrator_config import read_source_config
from parallels.core.utils import config_utils
from parallels.core.utils.common import cached, group_value_by_id
from parallels.core.utils.config_utils import get_option, get_sections_list
from parallels.core.panels import get_additional_source_panel


class Connections(object):
    """
    :type _global_context: parallels.core.global_context.GlobalMigrationContext
    """
    def __init__(self, global_context, target_panel):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        self._global_context = global_context
        self._config = global_context.config
        self.target = target_panel.get_connections(global_context)
        self._external_db_servers = self._read_external_db_settings_from_config(
            global_context.config, 'external-db-servers'
        )
        self._external_mail_servers = self._read_external_mail_settings_from_config(
            global_context.config, 'external-mail-servers'
        )

    @cached
    def get_source_config_by_id(self, source_id):
        """Return configuration of source with given identifier

        :type source_id: str
        :rtype: parallels.core.migrator_config.SourceConfig
        """
        return read_source_config(self._config, source_id)

    @cached
    def get_source_by_id(self, source_id):
        """
        :type source_id: str | unicode
        :rtype: parallels.core.connections.source.Source
        """
        source_config = self.get_source_config_by_id(source_id)
        return Source(source_id, source_config, self._global_context.migrator_server)

    def get_source_server_by_id(self, source_id):
        source = self.get_source_by_id(source_id)
        if not isinstance(source, SourceServer):
            return None
        return source

    def get_sources(self):
        """
        :rtype: list[parallels.core.connections.source.Source]
        """
        return [self.get_source_by_id(source_id) for source_id in self._get_source_config_section_names()]

    def get_source_servers(self):
        """Get servers which could provide info about subscriptions and customers"""
        return {
            source.source_id: source.config
            for source in self.get_sources() if isinstance(source, SourceServer)
        }

    @cached
    def has_source_node(self, source_id):
        return source_id in self._get_source_config_section_names()

    @cached
    def get_source_node(self, source_id):
        return self.get_source_by_id(source_id)

    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
        """
        raise NotImplementedError()

    def get_panel_version(self):
        """Get version of source panel
        """
        return None

    def check_source_servers(self):
        """Check source servers: connections, versions, licenses, etc

        By default, check external database servers only
        Override in child classes and add more checks
        """
        for external_db_server_id in self._external_db_servers.keys():
            physical_server = self.get_external_db_physical_server(external_db_server_id)
            if physical_server.is_windows():
                with physical_server.runner() as runner:
                    runner.check(physical_server.description())
            else:
                ConnectionChecker().check_ssh(physical_server.description(), physical_server.settings())

    def get_external_db_servers(self):
        """Get information about source external database servers

        Returns dictionary {server_id: server_settings} with connection
        information.

        :rtype: dict[str, parallels.core.migrator_config.ExternalDBConfigBase]
        """
        return self._external_db_servers

    def get_external_db_server_id(self, db_type, host):
        """Get external database server ID by database type and host

        Return external database server ID - name of a section in configuration file which describes this
        external database server. If no server with such database type and host was found - return None.

        :param str db_type: type of database server, 'mysql' or 'mssql' or 'postgresql'
        :param str host: hostname of database server, for example '192.168.1.15', or '10.52.1.55\MSSQLSERVER2008'
        :rtype: str | None
        """
        external_db_servers_by_host = group_value_by_id(
            self._external_db_servers.items(),
            key_func=lambda s: (s[1].db_type, s[1].host),
            value_func=lambda s: s[0]
        )
        return external_db_servers_by_host.get((db_type, host))

    @cached
    def get_external_db_physical_server(self, server_id):
        """Get physical server object that corresponds to external database server with specified ID

        :param str server_id: external database server ID name of a section in configuration file
        :rtype: parallels.core.connections.physical_server.PhysicalServer
        """
        return create_physical_server(self._external_db_servers[server_id])

    @cached
    def get_external_mail_server(self, server_id):
        """Get source external mail server object by specified ID

        If server with such ID is not external mail server or completely does not exist in configuration file,
        then this function returns None.

        :type server_id: str | unicode
        :rtype: parallels.core.connections.source_server.SourceServer | None
        """
        if server_id in self._external_mail_servers:
            return SourceServer(server_id, self._external_mail_servers[server_id])
        else:
            return None

    def get_external_mail_servers(self):
        """Get all external mail servers specified in configuration file

        :rtype: list[parallels.core.connections.source_server.SourceServer]
        """
        servers = []

        for server_id in self._external_mail_servers.keys():
            servers.append(self.get_external_mail_server(server_id))

        return servers

    @staticmethod
    def _read_external_db_settings_from_config(config, list_name):
        """
        :rtype: dict[str, parallels.core.migrator_config.ExternalDBConfigBase]
        """
        sections = config_utils.get_sections_list(config, 'GLOBAL', list_name)
        return {
            section_name: migrator_config.read_external_db_settings(config, section_name)
            for section_name in sections
        }

    @staticmethod
    def _read_external_mail_settings_from_config(config, list_name):
        """
        :type config: ConfigParser.RawConfigParser
        :type list_name: str | unicode
        :rtype: dict[
            str,
            parallels.core.migrator_config.ExternalMailUnixServerConfig |
            parallels.core.migrator_config.ExternalMailWindowsServerConfig
        ]
        """
        sections = config_utils.get_sections_list(config, 'GLOBAL', list_name)
        return {
            section_name: migrator_config.read_external_mail_settings(config, section_name)
            for section_name in sections
        }

    def _get_source_config_section_names(self):
        """Get names of sections describing sources of data

        :rtype: list[str | unicode]
        """
        if self._config.has_option('GLOBAL', 'sources'):
            return get_sections_list(self._config, 'GLOBAL', 'sources')
        elif self._config.has_option('GLOBAL', 'source-servers'):
            # backward compatible check of previous option name (was changed in scope of PMT-2798)
            return get_sections_list(self._config, 'GLOBAL', 'source-servers')
        else:
            if get_additional_source_panel(self._global_context):
                return [get_option('source-type')]
            raise MigrationConfigurationFileError(messages.EXCEPTION_MIGRATION_CONFIGURATION_FILE_SOURCES_MISSED)
