from parallels.core.connections.source_server import SourceServer
from parallels.core.converter.database import SourceDatabaseConflictingObject, DatabaseConflictingObjectDescription
from parallels.core.converter.database_user import SourceDatabaseUserConflictingObject, \
    DatabaseUserConflictingObjectDescription
from parallels.core.migrated_database import MigratedDatabase
from parallels.core.migrated_database_user import MigratedDatabaseUser
from parallels.core.reports.plain_report import PlainReport
from parallels.core.registry import Registry
from parallels.core import messages
from parallels.core.dump.dump import SubscriptionNotFoundException
from parallels.core.utils import plesk_api_utils
from parallels.core.utils.common import find_only, if_not_none, find_first
from parallels.core.utils.common import cached
from parallels.core.utils.common.ip import resolve_all
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import are_same_mssql_servers


class MigratedSubscription(object):
    """Objects that represents subscription that we are migrating"""

    def __init__(self, migrator, name):
        self._migrator = migrator
        self._name = name

    @property
    def name(self):
        return self._name

    @property
    def name_idn(self):
        return self.name.encode('idna')

    @property
    def name_canonical(self):
        return self.name.lower().encode('idna')

    @property
    def is_idn(self):
        return self.name != self.name_idn

    def get_source_info(self):
        return Registry.get_instance().get_context().get_source_info(self.model.source)

    @property
    def is_fake(self):
        """Check if domain is fake - created by technical reasons

        Fake domains may be not existent on source server, and so they should
        not checked.

        :rtype: bool
        """
        return self._migrator._is_fake_domain(self._name)

    # ====================== Subscription dumps =============================
    # There are 2 type of dumps:
    # 1) Regular dumps, with information about web, DNS, database and several other settings
    # 2) Mail dumps, with information about mail settings
    # Usually regular dumps contain information about mail settings, so mail dump of subscription is the same
    # file/object as regular dump. But in special cases, for example for source Expand, mail dumps are different
    # files.
    #
    # Also, there are 2 other type of dump files:
    # 1) Raw dumps - just as they were created on the source servers.
    # 2) Converted dumps - dumps with information in exact way as we are going to restore
    # them on the target server: IP addresses are replaced from source to target, unsupported features
    # are removed, etc.

    @property
    def raw_dump(self):
        """Return subscription object from raw dump

        Raw dump contains subscriptions like they are presented on source panel.

        :rtype: parallels.core.dump.data_model.Subscription | parallels.core.dump.data_model.Subscription8
        """
        return self.server_raw_dump.get_subscription(self._name)

    @property
    def converted_dump(self):
        """Return subscription object from converted dump

        Converted dump contains subscription how we are going to restore them on the target panel.
        So IP addresses are changed, unsupported features are removed, etc.

        :rtype: parallels.core.dump.data_model.Subscription
        """
        return self.server_converted_dump.get_subscription(self._name)

    @property
    def server_raw_dump(self):
        """Return full raw dump of the server

        Returns an object parallels.core.dump.PleskBackupSource*
        """
        return self.get_source_info().load_raw_dump()

    @property
    def server_converted_dump(self):
        """Return full converted dump of the server

        Returns an object parallels.core.dump.PleskBackupSource*
        """
        return self._migrator.load_converted_dump(self.model.source)

    @property
    def mail_raw_dump(self):
        """Return subscription object from raw mail dump

        :rtype: parallels.core.dump.data_model.Subscription
        """
        try:
            return self.mail_server_raw_dump.get_subscription(self._name)
        except SubscriptionNotFoundException:
            # subscription is not presented in the mail dump, so we consider it has no mail
            return None

    @property
    def mail_converted_dump(self):
        """Return subscription object from converted mail dump

        :rtype: parallels.core.dump.data_model.Subscription
        """
        try:
            return self.mail_server_converted_dump.get_subscription(self._name)
        except SubscriptionNotFoundException:
            # subscription is not presented in the mail dump, so we consider it has no mail
            return None

    @property
    def mail_server_raw_dump(self):
        """Return full raw mail dump of the server

         Returns an object parallels.core.dump.PleskBackupSource*
         """
        # default implementation: mail dump is the same file as regular dump
        return self.server_raw_dump

    @property
    def mail_server_converted_dump(self):
        """Return full raw mail converted of the server

         Returns an object parallels.core.dump.PleskBackupSource*
         """
        # default implementation: mail dump is the same file as regular dump
        return self.server_converted_dump

    # ====================== Target data models ===============================

    @property
    def model(self):
        """Return target data model subscription
        
        :rtype: parallels.core.target_data_model.Subscription
        """
        target_model = self._migrator.get_target_model(False)

        return find_only(
            target_model.iter_all_subscriptions(),
            lambda s: s.name == self._name,
            messages.FAILED_FIND_SUBSCRIPTION_BY_NAME_2)

    @property
    def model_client(self):
        target_model = self._migrator.get_target_model(False)

        return find_only(
            target_model.iter_all_owners(),
            lambda c: self._name in {s.name for s in c.subscriptions},
            messages.FAILED_FIND_CLIENT_THAT_OWNS_SUBSCRIPTION % self._name
        )

    @property
    def model_reseller(self):
        target_model = self._migrator.get_target_model(False)
        for reseller in target_model.resellers.values():
            if self.model_client in reseller.clients:
                return reseller

        return None

    # ====================== Owner logins =====================================

    @property
    def customer_username(self):
        """Login of customer which owns the subscription

        :rtype: str | unicode | None
        """
        target_model = self._migrator.get_target_model(False)
        customer = find_first(
            target_model.iter_all_customers(),
            lambda c: self._name in {s.name for s in c.subscriptions}
        )
        if customer is not None:
            return customer.login
        else:
            return None

    @property
    def reseller_username(self):
        """Login of customer which owns customer which owns the subscription, or owns the subscription directly

        :rtype: str | unicode | None
        """
        if self.model_reseller is not None:
            return self.model_reseller.login
        else:
            return None

    # ====================== IP addresses =====================================

    @property
    def target_mail_ip(self):
        """Return target mail server IP"""
        return if_not_none(
            if_not_none(
                self._get_target_services(),
                lambda addresses: addresses.mail_ips
            ),
            lambda mail_ips: mail_ips.v4
        )

    @property
    def target_web_ip(self):
        """Return target web server IP"""
        return if_not_none(
            if_not_none(
                self._get_target_services(),
                lambda addresses: addresses.web_ips
            ),
            lambda web_ips: web_ips.v4
        )

    @property
    def source_mail_ip(self):
        """Get IPv4 address on which mail service of subscription works on the source panel

        :rtype: str | unicode
        """
        source_mail_ipv4 = self.mail_raw_dump.mail_service_ips.v4
        if source_mail_ipv4 is None:
            # Mail service IP addresses can absent in backup, for example for PfU8.
            # In this case we use server IP.
            source_mail_ipv4 = self.mail_source_server.ip()
        return source_mail_ipv4

    @property
    def source_mail_ipv6(self):
        """Get IPv6 address on which mail service of subscription works on the source panel

        :rtype: str | unicode
        """
        return self.mail_raw_dump.mail_service_ips.v6

    @property
    def target_public_mail_ipv4(self):
        return self._get_public_ip(self.target_mail_ip)

    @property
    def target_public_mail_ipv6(self):
        return self._get_public_ip(self.target_mail_ipv6)

    @property
    def target_public_web_ipv4(self):
        return self._get_public_ip(self.target_web_ip)

    @property
    def target_public_web_ipv6(self):
        return self._get_public_ip(self.target_web_ipv6)

    @property
    def target_mail_ipv6(self):
        return if_not_none(
            if_not_none(
                self._get_target_services(),
                lambda addresses: addresses.mail_ips
            ),
            lambda mail_ips: mail_ips.v6
        )

    @property
    def target_web_ipv6(self):
        return if_not_none(
            if_not_none(
                self._get_target_services(),
                lambda addresses: addresses.web_ips
            ),
            lambda web_ips: web_ips.v6
        )

    @property
    def target_dns_ips(self):
        """Retrive list of IP addresses of target DNS servers

        :rtype: list[str]
        """
        return [self.panel_target_server.ip()]

    @property
    def source_dns_ips(self):
        """Return IP addresses of source DNS servers"""
        return self._migrator._get_source_dns_ips(self.model.source)

    # ====================== Server objects ===================================

    @property
    def panel_target_server(self):
        return self._migrator._get_target_panel().get_subscription_plesk_node(
            self._migrator.global_context, self._name
        )

    @property
    def web_target_server(self):
        """Return target server for this subscription, where web content is stored

        :rtype: parallels.plesk.connections.target_server.PleskTargetServer | None
        """
        return self._migrator._get_subscription_nodes(self._name).web

    @property
    def web_source_server(self):
        """
        :rtype: parallels.core.connections.source_server.SourceServer
        """
        return self._migrator._get_source_web_node(self._name)

    @property
    def mail_target_server(self):
        return self._migrator._get_subscription_nodes(self._name).mail

    @property
    def mail_source_server(self):
        # First check if it is external mail server
        connections = Registry.get_instance().get_context().conn
        external_mail_server = connections.get_external_mail_server(self.mail_source_server_id)
        if external_mail_server is not None:
            return external_mail_server

        # Otherwise, look for it in the list of source servers
        if not self._migrator._has_source_node(self.mail_source_server_id):
            return None
        return self._migrator._get_source_node(self.mail_source_server_id)

    @property
    def mail_source_server_id(self):
        """Get ID (name of a section in configuration file) of the server with mail messages of subscription

        :rtype: str | unicode
        """
        return self.model.source

    @property
    def db_target_servers(self):
        return self._migrator._get_subscription_nodes(self._name).database

    @property
    def dns_target_servers(self):
        return self._migrator._get_subscription_nodes(self._name).dns

    # =============== Hosting objects of subscription =========================

    @property
    @cached
    def databases(self):
        """Databases of subscription

        :rtype: list[parallels.core.migrated_database.MigratedDatabase]
        """
        databases = []

        converted_dump_databases = {
            (database.dbtype, database.name): database for database in self.converted_dump.iter_all_databases()
        }
        conflicting_object_description = DatabaseConflictingObjectDescription(self._migrator.global_context)

        for raw_dump_database in self.raw_dump.iter_all_databases():
            new_name = self._migrator.global_context.name_mapping.get_new_name(
                conflicting_object_description.get_type_id(),
                SourceDatabaseConflictingObject(self, raw_dump_database)
            )

            new_name_key = (raw_dump_database.dbtype, new_name)
            if new_name_key in converted_dump_databases:
                databases.append(MigratedDatabase(raw_dump_database, converted_dump_databases[new_name_key]))
                continue

            old_name_key = (raw_dump_database.dbtype, raw_dump_database.name)
            if old_name_key in converted_dump_databases:
                databases.append(MigratedDatabase(raw_dump_database, converted_dump_databases[old_name_key]))

        return databases

    @property
    @cached
    def database_users(self):
        """Database users of subscription

        :rtype: list[parallels.core.migrated_database_user.MigratedDatabaseUser]
        """
        db_users = []

        converted_dump_db_users = {
            (db_user.dbtype, db_user.name): db_user for db_user in self.converted_dump.iter_all_database_users()
        }
        conflicting_object_description = DatabaseUserConflictingObjectDescription(self._migrator.global_context)

        for raw_dump_dbuser in self.raw_dump.iter_all_database_users():
            new_name = self._migrator.global_context.name_mapping.get_new_name(
                conflicting_object_description.get_type_id(),
                SourceDatabaseUserConflictingObject(self, raw_dump_dbuser)
            )

            new_name_key = (raw_dump_dbuser.dbtype, new_name)
            if new_name_key in converted_dump_db_users:
                db_users.append(MigratedDatabaseUser(raw_dump_dbuser, converted_dump_db_users[new_name_key]))
                continue

            old_name_key = (raw_dump_dbuser.dbtype, raw_dump_dbuser.name)
            if old_name_key in converted_dump_db_users:
                db_users.append(MigratedDatabaseUser(raw_dump_dbuser, converted_dump_db_users[old_name_key]))

        return db_users

    # =============== Additional auxiliary functions ==========================

    @property
    @cached
    def target_sysuser_name(self):
        return plesk_api_utils.get_subscription_sysuser_name(
            self.panel_target_server.plesk_api(), self.name
        )

    @cached
    def is_assimilate_database(self, database_name, database_type):
        """Check if given database should be assimilated

        :type database_name: str
        :type database_type: str
        :rtype: bool
        """
        database_dump = self.raw_dump.get_database(database_name, database_type)
        # retrieve source and target database servers for given database
        source_database_server = self._get_source_database_server(
            database_dump.host,
            database_dump.port,
            database_dump.dbtype
        )
        if source_database_server is None:
            # source database server is undefined; it is a common case when we don't have
            # an administrator access to source database server; assume that database
            # couldn't be assimilated in this case
            return False
        target_database_server = self._get_target_database_server(database_dump.dbtype)

        # compare source and target database servers
        return self._is_same_database_server(source_database_server, target_database_server, database_dump.dbtype)

    @cached
    def is_assimilate_database_user(self, database_user_name, database_user_type):
        """Check if given database user should be assimilated

        :type database_user_name: str
        :type database_user_type: str
        :rtype: bool
        """
        database_user_dump = self.raw_dump.get_database_user(database_user_name, database_user_type)
        # retrieve source and target database servers for given database user
        source_database_server = self._get_source_database_server(
            database_user_dump.host,
            database_user_dump.port,
            database_user_dump.dbtype
        )
        if source_database_server is None:
            # source database server is undefined; it is a common case when we don't have
            # an administrator access to source database server; assume that database
            # couldn't be assimilated in this case
            return False
        target_database_server = self._get_target_database_server(database_user_dump.dbtype)

        # compare source and target database servers
        return self._is_same_database_server(source_database_server, target_database_server, database_user_dump.dbtype)

    def add_report_issue(self, report, *args, **kwargs):
        """Add issue to subscription's report, provided by overall report

        :type report: parallels.core.reports.model.report.Report
        :rtype: None
        """
        plain_report = PlainReport(report, self._migrator.global_context.migration_list_data)
        plain_report.get_subscription_report(self._name).add_issue(*args, **kwargs)

    def get_report(self, report):
        """Get report of this subscription from overall report

        :type report: parallels.core.reports.model.report.Report | parallels.core.reports.report_writer.ReportWriter
        :rtype: parallels.core.reports.model.report.Report
        """
        plain_report = PlainReport(report, self._migrator.global_context.migration_list_data)
        return plain_report.get_subscription_report(self.name)

    def _get_public_ip(self, ip):
        ip_to_public_ip = {
            ip.ip_address: ip.public_ip_address or ip.ip_address
            for ip in self.panel_target_server.get_all_ips()
        }
        return ip_to_public_ip.get(ip)

    def _get_target_services(self):
        return self._migrator.global_context.target_panel_obj.get_subscription_target_services(
            self._migrator.global_context, self.name
        )

    def _get_source_database_server(self, database_server_host, database_server_port, database_server_type):
        """Retrive source database server by given hostname and port

        :type database_server_host: str
        :type database_server_port: str
        :rtype: parallels.core.connections.database_servers.base.DatabaseServer | None
        """
        database_server_dump = self._migrator._get_src_db_server(
            database_server_host,
            database_server_port,
            database_server_type,
            self.server_raw_dump
        )
        if database_server_dump is not None:
            return self._migrator._get_source_database_server(self.model.source, database_server_dump)
        return None

    def _get_target_database_server(self, database_server_type):
        """Retrive source database server of given type

        :type database_server_type: str
        :rtype: parallels.core.connections.database_servers.base.DatabaseServer | None
        """
        return self._migrator._get_target_database_server(self.model.name, database_server_type)

    def _is_same_database_server(self, source_database_server, target_database_server, database_server_type):
        if database_server_type == DatabaseServerType.MSSQL:
            is_same_database_servers = are_same_mssql_servers(source_database_server, target_database_server)
        else:
            if source_database_server.host() == 'localhost':
                # for local database server retrieve ip-address of source server
                source_server = self._migrator._get_source_db_node(self.model.source)
                source_host = source_server.ip() if isinstance(source_server, SourceServer) else None
            else:
                source_host = source_database_server.host()
            same_host = target_database_server.ip() in resolve_all(source_host) if source_host is not None else False
            same_port = source_database_server.port() == target_database_server.port()
            is_same_database_servers = same_port and same_host

        # when detecting whether target and source database servers are actually the same we consider that
        # database server hostnames are resolved in the same way on source server (management node for
        # multiserver sources) and on the server where Plesk Migrator is running;
        # also we consider that customer won't register database server by different ip-addresses in
        # source and target systems;
        # otherwise Plesk Migrator will not detect that database should be assimilated
        return is_same_database_servers


class HostingDescriptorMigratedSubscription(MigratedSubscription):
    @property
    @cached
    def hosting_description(self):
        """Get hosting description of specified subscription

        Returns object model over part of the hosting description file which is related to that subscription.

        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionSubscription | None
        """
        if self._migrator.global_context.conn.has_hosting_description_config(self.model.source):
            hosting_description = self._migrator.global_context.get_hosting_description(self.model.source)
            return hosting_description.get_subscription(self.name)
        else:
            return None

    @property
    def hosting_description_config(self):
        """Hosting description config - section from config.ini which corresponds to hosting description of subscription

        :rtype: parallels.plesk.hosting_description.config.HostingDescriptionConfig | None
        """
        connections = self._migrator.global_context.conn
        source_id = self.model.source

        if connections.has_hosting_description_config(source_id):
            return connections.get_hosting_description_config(source_id)
        else:
            return None
