from collections import defaultdict
from parallels.core.migrator_config import MailContent
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import get_mssql_host_for_target_server
from parallels.plesk.source.plesk.infrastructure_checks.checks \
    import NodesPair, DiskSpaceDatabase, DiskSpaceDatabaseAccess, MSSQLConnectionData


class InfrastructureCheckLister(object):
    """Group subscriptions by various criteria.
    
    List different information about subscriptions and databases 
    grouped in various ways to perform different infrastructure checks

    :type data_source: parallels.plesk.source.plesk.infrastructure_checks.lister.InfrastructureCheckListerDataSource
    """

    def __init__(self, data_source):
        """
        :type data_source: parallels.plesk.source.plesk.infrastructure_checks.lister.InfrastructureCheckListerDataSource
        """
        self.data_source = data_source

    # ======================== public functions ========================

    def get_subscriptions_by_web_nodes(self):
        """List subscriptions grouped by source and target web service node.
        
        Returns: dictionary {
            NodesPair(source web node, target web node)
                [list of subscriptions]
        }"""
        return self._get_subscriptions_by_web_nodes(is_windows=None)

    def get_subscriptions_to_target_web_node(self):
        """List subscriptions to target web node.
        
        Returns: dictionary {
            subscription : target web node
        }"""
        return {
            subscription: nodes.target
            for nodes, subscriptions
            in self.get_subscriptions_by_web_nodes().items()
            for subscription in subscriptions
        }

    def get_subscriptions_by_unix_web_nodes(self):
        """List subscriptions that have web service on Unix nodes,
        grouped by source and target web service node.
        
        Returns: dictionary {
            NodesPair(source Unix web node, target Unix web node)
                [list of subscriptions]
        }"""
        return self._get_subscriptions_by_web_nodes(is_windows=False)

    def get_subscriptions_by_windows_web_nodes(self):
        """List subscriptions that have web service on Windows nodes,
        grouped by source and target web nodes.
        
        Returns: dictionary {
            NodesPair(source Windows web node, target Windows web node)
                [list of subscriptions]
        }"""
        return self._get_subscriptions_by_web_nodes(is_windows=True)

    def get_subscriptions_by_unix_mail_nodes(self):
        """List subscriptions that have mail service on Unix nodes,
        grouped by source and target mail nodes.

        For such nodes we copy content by rsync, in all the other
        cases we use IMAP/POP3.
        
        Returns: dictionary {
            NodesPair(source Unix mail node, target Unix mail node): 
                [list of subscriptions]
        } 
        """
        return self._group_subscriptions_by_nodes(
            self._get_subscription_to_source_mail_node(is_windows=False),
            self._get_subscription_to_target_mail_node(is_windows=False)
        )

    def get_subscriptions_by_mail_nodes(self):
        """List subscriptions grouped by source and target mail nodes.

        Returns: dictionary {
            NodesPair(source mail node, target mail node): 
                [list of subscriptions]
        } 
        """
        return self._group_subscriptions_by_nodes(
            self._get_subscription_to_source_mail_node(is_windows=None),
            self._get_subscription_to_target_mail_node(is_windows=None)
        )

    def get_subscriptions_to_target_mail_node(self):
        """List subscriptions to target mail node.
        
        Returns: dictionary {
            subscription : target mail node
        }"""
        return {
            subscription: nodes.target
            for nodes, subscriptions
            in self.get_subscriptions_by_mail_nodes().items()
            for subscription in subscriptions
        }

    def get_subscriptions_by_unix_db_nodes(self):
        """List databases (of all types) on Unix nodes, grouped by source and target
        database nodes.

        Returns dictionary {
            NodesPair(source database node, target database node):
                [ 
                    list of dictionaries {
                        db_name=database name, 
                        db_type=database type (mysql/mssql/postgres), 
                        subscription=subscription name
                    }
                ]
        }
        """
        return self._get_subscriptions_by_db_nodes(is_windows=False)

    def get_subscriptions_by_windows_mysql_nodes(self):
        """List MySQL databases on Windows nodes, grouped by source and target
        database nodes.

        Returns dictionary {
            NodesPair(source database node, target database node):
                [ 
                    list of dictionaries {
                        db_name=database name, 
                        db_type=database type (always 'mysql')
                        subscription=subscription name
                    }
                ]
        }
        """
        return self._get_subscriptions_by_db_nodes(is_windows=True, db_type_filter='mysql')

    def get_subscriptions_by_mssql_instances(self):
        """List MSSQL databases by Windows nodes, grouped by source and target
        database nodes.

        Returns dictionary {
            NodesPair(source database server, target database server):
                [ 
                    list of dictionaries {
                        db_name=database name, 
                        subscription=subscription name
                    }
                ]
        }
        """
        result = defaultdict(list)

        for database_info in self.data_source.list_databases_to_copy():
            source = database_info.source_database_server
            target = database_info.target_database_server
            if source.type() == DatabaseServerType.MSSQL:
                nodes_pair = NodesPair(source=source, target=target)
                result[nodes_pair].append(dict(
                    db_name=database_info.source_database_name,
                    subscription=database_info.subscription_name,
                ))

        return dict(result)

    def get_subscriptions_to_check_source_mail_connections(self, copy_mail_content_mode):
        """List subscriptions for which we should check connection to the
        source server by IMAP or POP3, depending on copy mail content mode. 
        List subscriptions grouped by source node.
        
        Returns dictionary {
            source node: [list of subscriptions]
        }
        """
        result = defaultdict(list)

        subscription_to_target = self._get_subscription_to_target_mail_node()
        subscription_to_source = self._get_subscription_to_source_mail_node()
        for subscription_name in set(subscription_to_target) & set(subscription_to_source):
            source_node = subscription_to_source[subscription_name]
            target_node = subscription_to_target[subscription_name]

            if source_node.ip() == target_node.ip():
                # Assimilated mail server - no need to copy content, no need to check
                continue 

            if not source_node.is_windows() and not target_node.is_windows():
                # Unix to Unix copy mode: no need to check mail server connections as rsync is used
                continue 

            if not source_node.mail_settings.mode == copy_mail_content_mode:
                # Another way of copy mail content should be checked
                continue

            result[source_node].append(subscription_name)

        return dict(result)

    def get_subscriptions_to_check_target_imap(self):
        """List subscriptions for which we should check connection to the
        target server by IMAP. List subscriptions grouped by source node.
        
        Returns dictionary {
            target node: [list of subscriptions]
        }
        """
        result = defaultdict(list)

        subscription_to_target = self._get_subscription_to_target_mail_node()
        subscription_to_source = self._get_subscription_to_source_mail_node()
        for subscription_name in set(subscription_to_target) & set(subscription_to_source):
            source_node = subscription_to_source[subscription_name]
            target_node = subscription_to_target[subscription_name]

            if source_node.ip() == target_node.ip():
                # Assimilated mail server - no need to copy content, no need to check
                continue 

            if source_node.is_windows() == False and target_node.is_windows() == False:
                # Unix to Unix copy mode: no need to check mail server connections as rsync is used
                continue 

            if not source_node.mail_settings.mode in (MailContent.FULL, MailContent.MESSAGES):
                # We don't copy content from that source node, so no need to check IMAP conection
                continue

            result[target_node].append(subscription_name)

        return dict(result)

    def get_mysql_databases_info_for_disk_space_checker(self, is_windows):
        """List MySQL databases to perform disk space checks. 
        
        Returns dictionary {
            NodesPair(source database node, target database node):
                [
                    list of DiskSpaceDatabase objects
                    with DiskSpaceDatabaseAccess as src_access and db_target_node
                ]
        }
        """
        return self._get_databases_info_for_disk_space_checker(
            is_windows=is_windows, dbtype='mysql', 
            convert_db_object_func=self._convert_mysql_db_object
        )

    def get_mssql_databases_info_for_disk_space_checker(self):
        """List MSSQL databases to perform disk space checks.

        Returns dictionary {
            NodesPair(source database node, target database node):
                [
                    list of DiskSpaceDatabase objects
                    with MSSQLConnectionData as src_access and db_target_node
                ]
        }
        convert_db_object_func is a function that accepts database object
        provided by list_databases_to_copy, and returns object suitable for 
        disk space checks (see convert_mysql_db_object and convert_mssql_db_object 
        functions below)
        """
        return self._get_databases_info_for_disk_space_checker(
            is_windows=True, dbtype='mssql', 
            convert_db_object_func=self._convert_mssql_db_object
        )

    # ======================== private functions ========================

    def _get_databases_info_for_disk_space_checker(self, is_windows, dbtype, convert_db_object_func):
        """List databases to perform disk space checks.
        convert_db_object_func is a function that accepts database object
        provided by list_databases_to_copy, and returns object suitable for 
        disk space checks (see _convert_mysql_db_object and _convert_mssql_db_object 
        functions below)
        """
        result = defaultdict(list)

        for database_info in self.data_source.list_databases_to_copy():
            source = database_info.source_database_server
            target = database_info.target_database_server
            if source.type() == dbtype:
                source_node = source.panel_server
                if source_node.is_windows() == is_windows:
                    disk_space_database = convert_db_object_func(database_info)
                    if source.utilities_server is not None and target.utilities_server is not None:
                        nodes_pair = NodesPair(source.utilities_server, target.utilities_server)
                        result[nodes_pair].append(disk_space_database)

        return result

    @staticmethod
    def _convert_mysql_db_object(database_info):
        """Convert MySQL database object returned by list_databases_to_copy to
        DiskSpaceDatabase, ready to be passed into
        infrastructure checker. Use this function with 
        _get_databases_info_for_disk_space_checker function as convert_db_object_func

        :type database_info: parallels.core.utils.database_utils.DatabaseInfo
        """
        source = database_info.source_database_server
        target = database_info.target_database_server
        return DiskSpaceDatabase(
            src_access=DiskSpaceDatabaseAccess(
                host=source.host(),
                port=source.port(),
                user=source.user(),
                password=source.password(),
            ),
            db_target_node=target,
            db_name=database_info.source_database_name
        )

    @staticmethod
    def _convert_mssql_db_object(database_info):
        """Convert MSSQL database object returned by list_databases_to_copy to
        DiskSpaceDatabase, ready to be passed into 
        infrastructure checker. Use this function with 
        _get_databases_info_for_disk_space_checker function as convert_db_object_func

        :type database_info: parallels.core.utils.database_utils.DatabaseInfo
        """
        source = database_info.source_database_server
        target = database_info.target_database_server
        return DiskSpaceDatabase(
            src_access=MSSQLConnectionData(
                host=get_mssql_host_for_target_server(source),
                login=source.user(),
                password=source.password()
            ),
            db_target_node=target,
            db_name=database_info.source_database_name
        )

    def _get_source_mssql_connection_data(self, database_info):
        source = database_info.source_database_server
        return MSSQLConnectionData(
            host=get_mssql_host_for_target_server(source),
            login=source.user(),
            password=source.password()
        )

    def _get_target_mssql_connection_data(self, database_info):
        target = database_info.target_database_server
        return MSSQLConnectionData(
            host=target.host(),
            login=target.user(),
            password=target.password()
        )

    def _get_subscriptions_by_db_nodes(self, is_windows, db_type_filter=None):
        """Returns: dict with 
            key - nodes pair of source and target panel servers
            value - list of subscriptions
        """
        result = defaultdict(list)
        for database_info in self.data_source.list_databases_to_copy():
            source = database_info.source_database_server
            target = database_info.target_database_server
            if (
                # both target and source nodes are of the same platform
                source.is_windows() == is_windows and target.is_windows() == is_windows
                and 
                (db_type_filter is None or source.type() == db_type_filter)
            ): 
                key = NodesPair(
                    source=source.panel_server,
                    target=target.panel_server
                )
                result[key].append(dict(
                    db_name=database_info.source_database_name,
                    db_type=source.type(),
                    subscription=database_info.subscription_name
                ))

        return dict(result)

    def _get_subscriptions_by_web_nodes(self, is_windows):
        return self._group_subscriptions_by_nodes(
            subscription_to_source_node=self._get_subscription_to_source_node(is_windows),
            subscription_to_target_node=self._get_subscription_to_target_web_node()
        )

    def _group_subscriptions_by_nodes(self, subscription_to_source_node, subscription_to_target_node):
        """Group subscriptions by target and source nodes.

        source_subscriptions - dictionary {subscription name: source node}
        target_subscriptions - dictionary {subscription name: target node}
        Result: dictionary {NodesPair(source node, target node): [list of subscriptions]}
        """
        subscriptions_by_nodes = defaultdict(list)

        for subscription in set(subscription_to_source_node.keys()) & set(subscription_to_target_node.keys()):
            source_node = subscription_to_source_node[subscription]
            target_node = subscription_to_target_node[subscription]
            nodes_pair = NodesPair(source_node, target_node)
            subscriptions_by_nodes[nodes_pair].append(subscription)

        return dict(subscriptions_by_nodes)

    def _get_subscription_to_target_web_node(self):
        """Get dictionary {subscription name: target web node}"""
        return self._get_subscription_to_target_node(lambda nodes: nodes.web)

    def _get_subscription_to_target_mail_node(self, is_windows=None):
        """Get dictionary {subscription name: target web node}"""
        return {
            subscription: node
            for subscription, node
            in self._get_subscription_to_target_node(lambda nodes: nodes.mail).items()
            if node.is_windows() == is_windows or is_windows is None
        }

    def _get_subscription_to_source_mail_node(self, is_windows=None):
        target_model = self.data_source.get_target_model()
        subscription_to_node = {
            subscription.name: 
                self.data_source.get_source_mail_node(subscription.name)
            for subscription in target_model.iter_all_subscriptions()
        }

        subscription_to_node_filtered = {}
        for subscription_name, node in subscription_to_node.items():
            if node is None:
                continue
            if node.is_windows() == is_windows or is_windows is None:
                subscription_to_node_filtered[subscription_name] = node

        return subscription_to_node_filtered

    def _get_subscription_to_source_node(self, is_windows=None):
        """Get dictionary {subscription name: source node}"""
        result = {}

        for subscription in self.data_source.get_target_model().iter_all_subscriptions():
            if is_windows is not None and subscription.is_windows != is_windows:
                continue
            source_server = self.data_source.create_migrated_subscription(subscription.name).web_source_server
            if source_server is not None:
                result[subscription.name] = source_server

        return result

    def _get_subscription_to_target_node(self, node_function):
        """Get dictionary {subscription name: target node}, where target 
        node is selected from subscriptions nodes with node_function"""
        result = {}
    
        model_subscriptions = set([s.name for s in self.data_source.get_target_model().iter_all_subscriptions()])

        for subscription_name in model_subscriptions:
            node = node_function(self.data_source.get_target_nodes(subscription_name))
            if node is not None:
                result[subscription_name] = node

        return result


class InfrastructureCheckListerDataSource(object):
    """Get database list or server by subscription name.
    
    Provide necessary information about subscriptions and nodes to
    InfrastructureCheckLister class. This class is abstract, inherit and define
    necessary data sources
    """

    def list_databases_to_copy(self):
        """Provide list of databases migration tool is going to copy
        (list of parallels.plesk.source.plesk.migrator.DatabaseToCopy)

        :rtype: list[parallels.core.utils.database_utils.DatabaseInfo]
        """
        raise NotImplementedError()

    def get_source_node(self, source_node_id):
        """Get SourceServer object by source node ID"""
        raise NotImplementedError()

    def get_source_mail_node(self, subscription_name):
        """Get SourceServer object for source mail node 
        by main (web hosting) source node ID. Mail node may differ
        from web hosting node in case of Expand Centralized Mail 
        for example"""
        raise NotImplementedError()

    def get_target_nodes(self, subscription_name):
        """Get target nodes (SubscriptionNodes) of subscription"""
        raise NotImplementedError()

    def get_target_model(self):
        """Get target data model (common.target_data_model.Model)"""
        raise NotImplementedError()

    def create_migrated_subscription(self, subscription_name):
        """Create MigrationSubscription object for subscription"""
        raise NotImplementedError()
