from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.plesk.source.expand import messages
from parallels.plesk.source.expand.client_subscription_adapter import ExpandToPleskClientSubscriptionConverterAdapter
from parallels.plesk.source.expand.global_context import ExpandGlobalMigrationContext
from parallels.plesk.source.expand.migrated_subscription import ExpandMigratedSubscription
from parallels.plesk.source.expand.session_files import ExpandSessionFiles
from parallels.plesk.source.plesk.migrator import Migrator as PlesksMigrator
from parallels.core.utils.common import cached, group_value_by
from parallels.plesk.utils.xml_rpc.expand.operator import PleskDnsOperator, PleskCentralizedDnsZoneOperator
from parallels.core.utils.common import find_only, find_first
from parallels.plesk.source.expand.connections import ExpandMigratorConnections
from parallels.plesk.source.expand.converter.reseller import ExpandResellersConverter
from parallels.plesk.source.expand.migration_list import ExpandMigrationListSourceData


logger = get_logger(__name__)


class Migrator(PlesksMigrator):
    def _load_connections_configuration(self, global_context, target_panel_type):
        conn = ExpandMigratorConnections(global_context, self._get_target_panel_by_name(target_panel_type))
        self.centralized_dns_servers = conn.get_centralized_dns_servers()
        return conn

    def _create_global_context(self):
        return ExpandGlobalMigrationContext()

    def _create_session_files(self):
        return ExpandSessionFiles(self.global_context.conn, self._get_migrator_server())

    def _get_source_servers(self):
        return dict(
            self.global_context.conn.get_source_plesks().items() +
            self.global_context.conn.get_centralized_mail_servers().items()
        )

    def get_migration_list_source_data(self):
        return ExpandMigrationListSourceData(self.global_context)

    def _convert_resellers(self, hosting_plans, existing_resellers, resellers_migration_list, report):
        converter = ExpandResellersConverter(self.global_context.target_panel_obj.is_encrypted_passwords_supported())
        return converter.convert_resellers(
            self.global_context.expand_data.expand_resellers,
            hosting_plans,
            existing_resellers,
            resellers_migration_list,
            report,
            self.global_context.password_holder
        )

    def _convert_accounts(self, reseller_plans, hosting_plans, resellers, report):
        converter_adapter = ExpandToPleskClientSubscriptionConverterAdapter()
        return converter_adapter.convert(self.global_context, reseller_plans, hosting_plans, resellers, report)

    @cached
    def _get_centralized_mail_server_by_plesk_server(self):
        result = {}
        plesk_server_config_id_by_id = {
            server.id: server.plesk_id for server in self.global_context.expand_data.servers
        }
        for cmail_server in self.global_context.expand_data.centralized_mail_servers:
            for server_id in cmail_server.assigned_server_ids:
                if server_id in plesk_server_config_id_by_id:
                    result[plesk_server_config_id_by_id[server_id]] = cmail_server.plesk_id

        return result

    def _is_mail_centralized(self, plesk_id):
        return plesk_id in self._get_centralized_mail_server_by_plesk_server()

    def get_mail_server_id(self, server_id):
        cmail_server = self._get_centralized_mail_server_by_plesk_server().get(server_id)
        if cmail_server is not None:
            return cmail_server
        else:
            return server_id

    def _get_source_dns_ips(self, source_id):
        ips = super(Migrator, self)._get_source_dns_ips(source_id)
        expand_dns_ips = [
            mapping.dns_server_ip
            for mapping in self.global_context.expand_data.dns_server_mapping
        ]
        ips += expand_dns_ips
        return ips

    def _switch_domains_dns_to_slave(self, domains_info, domains_masters, plesk_id):
        """When Expand centralized DNS is serving regular (master) Plesk zones in slave mode, 
        it may not re-transfer them after DNS forwarding is set, because target server may have lower serial numbers.
        So it takes to make Expand CDNS retransfer these zones once.

        Plesk migrator should set up the DNS forwarding on Plesk servers prior to updating Expand CDNS.
        """

        err_zones = super(Migrator, self)._switch_domains_dns_to_slave(domains_info, domains_masters, plesk_id)
        err_zones.update(self._change_cdns_slave_zones(plesk_id, domains_masters, u"set up the DNS forwarding"))
        return err_zones

    def _change_cdns_slave_zones(self, plesk_id, domains_masters, operation_name):

        if len(domains_masters) == 0:
            return set()    # no domains - nothing to process and no errors

        logger.debug(messages.LOG_REFRESH_EXPAND_ZONES_DATABASE)
        expand_server_id = find_only(
            self.global_context.expand_data.servers,
            lambda x: x.plesk_id == plesk_id, messages.FAILED_TO_FIND_EXPAND_SERVER % (plesk_id,)
        ).id

        self.global_context.conn.expand.api().send(
            PleskDnsOperator.Refresh(
                filter=PleskDnsOperator.Refresh.FilterByServerId(expand_server_id)
            )
        )

        logger.debug(messages.LOG_UPDATE_EACH_DNS_ZONE)

        err_zones = {}
        try:
            dns_servers_map = group_value_by(
                [m for m in self.global_context.expand_data.dns_server_mapping if m.mode == 'slave'],
                key_func=lambda mapping: mapping.plesk_id,
                value_func=lambda mapping: mapping.dns_server_ip
            )
            if plesk_id not in dns_servers_map:
                pass  # Expand CDNS does not serve zones of this Plesk, skip changing zones
            else:
                dns_server_ip_id = {s.ip: server_id for server_id, s in self.centralized_dns_servers.items()}
                for dns_server_ip in dns_servers_map[plesk_id]:
                    if dns_server_ip in dns_server_ip_id:
                        dns_server_id = dns_server_ip_id[dns_server_ip]
                        centralized_dns_server = self.global_context.conn.expand_centralized_dns_server(dns_server_id)
                        with centralized_dns_server.runner() as runner:
                            assert isinstance(runner, BaseRunner)
                            for domain, _ in domains_masters.items():
                                try:
                                    logger.debug(messages.LOG_GET_CDNS_ZONE_INFO, domain)
                                    expand_domain = find_first(
                                        self.global_context.expand_data.plesk_domains,
                                        lambda x: x.name == domain
                                    )
                                    if expand_domain is not None:
                                        expand_cdns_zone_id = self.global_context.conn.expand.api().send(
                                            PleskCentralizedDnsZoneOperator.GetZone(
                                                filter=PleskCentralizedDnsZoneOperator.GetZone.FilterByDomainId(
                                                    [expand_domain.id]
                                                )
                                            )
                                        )[0].data.id

                                        logger.debug(messages.LOG_SYNC_ZONE_FROM_EXPAND_TO_CDNS)
                                        for result in self.global_context.conn.expand.api().send(
                                            PleskCentralizedDnsZoneOperator.SyncZone(
                                                filter=PleskCentralizedDnsZoneOperator.SyncZone.FilterByCdnsZoneId(
                                                    [expand_cdns_zone_id]
                                                ),
                                                force=True
                                            )
                                        ):
                                            result.check()
                                        logger.debug(
                                            messages.LOG_FORCE_RETRANSFER_ZONE_DATA_FROM_TARGET)
                                        runner.execute_command(
                                            "rndc retransfer {zone}", dict(zone=domain.encode('idna'))
                                        )
                                    else:
                                        # Aliases configured with domains
                                        expand_domain_alias = find_first(
                                            self.global_context.expand_data.plesk_domain_aliases,
                                            lambda x: x.name == domain
                                        )
                                        if expand_domain_alias is None:
                                            raise Exception(messages.FAILED_TO_FIND_EXPAND_DOMAIN % domain)
                                except Exception as e:
                                    logger.exception()
                                    errmsg = messages.FAILED_TO_PERFORM_OPERATION_FOR_DOMAIN % (
                                        operation_name, domain, e
                                    )
                                    logger.error(errmsg)
                                    err_zones[domain] = errmsg
                    else:
                        logger.error(
                            messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_ALL_CDNS_NOT_DESCRIBED,
                            operation_name, plesk_id, dns_server_ip
                        )
                        for zone in domains_masters:
                            errmsg = (
                                messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_DOMAIN_CDNS_NOT_DESCRIBED % (
                                    operation_name, zone, dns_server_ip
                                )
                            )
                            err_zones[zone] = errmsg
        except Exception as e:
            logger.exception()
            logger.error(messages.FAILED_TO_PERFORM_OPERATION_FOR_ALL_DOMAINS, operation_name, plesk_id, e)
            for zone in domains_masters:
                errmsg = (
                    messages.UNABLE_TO_PERFORM_DNS_FORWARDING_OPERATION_FOR_DOMAIN_CNDS_FAILURE % (
                        operation_name, zone, e
                    )
                )
                err_zones[zone] = errmsg

        return err_zones

    def _switch_domains_dns_to_master(self, domains_info, domains_masters, plesk_id):
        """Restore Expand CDNS <-> Plesk DNS connection for the Plesk zones hosted in CDNS in slave mode.
        Plesk migrator should undo the DNS forwarding on Plesk servers prior to undoing Expand CDNS.
        """
        err_zones = super(Migrator, self)._switch_domains_dns_to_master(domains_info, domains_masters, plesk_id)
        err_zones.update(self._change_cdns_slave_zones(plesk_id, domains_masters, messages.UNDO_DNS_FORWARDING))

        return err_zones

    @staticmethod
    def is_expand_mode():
        return True

    @cached
    def _create_migrated_subscription(self, name):
        return ExpandMigratedSubscription(self, name)

