from parallels.core.logging import get_logger
from parallels.core import target_data_model as target_model
from parallels.core.converter.business_objects.clients import ClientsConverter
from parallels.core.converter.business_objects.common import \
    index_plesk_backup_domain_objects, \
    check_domain_conflicts, check_subscription_conflicts, \
    check_client_contacts_matching_source_panels, \
    check_client_contacts_matching_source_target, \
    index_target_site_names, \
    EntityConverter
from parallels.core.registry import Registry
from parallels.core.reports.model.issue import Issue
from parallels.core.reports.model.report import Report
from parallels.core.utils.common import group_by_id, find_first, default
from parallels.plesk import messages

logger = get_logger(__name__)


class PleskConverter(object):
    """Generate model of subscriptions and customers 
    ready to import to target panel.
    
    Converter takes the following information:
    - Plesk backup or other source of information 
    about resellers, clients, subscriptions and domains 
    on source servers
    - Information about objects that already exist on
    the target panel: resellers, plans, clients, domains
    and so on
    - Migration list, which contains list of customers and 
    subscriptions to migrate, their mapping between each other,
    plans and resellers

    Converter:
    - Converts each customer and subscription we need to migrate to 
    format ready to import to target panel (clients and subscriptions
    above hosting settings).
    - Performs conflict resolution, for example if client or subscriptions 
    exists on multiple source panel, or already exists on target panel.
    - Performs assignment of subscriptions to clients and plans,
    clients to resellers according to migration list
    - Performs some basic pre-migration checks, for example that plan
    already exists on target panel.

    Result is a model ready to import to target panel.

    Converter is NOT responsible for hosting settings below subscriptions,
    like subdomains, or PHP settings.
    """

    def __init__(self, target_existing_objects, entity_converter=None, encrypted_passwords_supported=False):
        self.target_reseller_plans = {}
        self.target_plans = {}
        self.target_resellers = {}
        self.target_clients = {}
        self.target_site_names = {}
        self.existing_objects = target_existing_objects
        if entity_converter is not None:
            self.entity_converter = entity_converter
        else:
            self.entity_converter = EntityConverter(self.existing_objects)

        self.raw_target_clients = {
            None: ClientsConverter.create_fake_client(None)  # admin fake client
        }  # these special clients are included here, but not distinguished from other - regular - clients
        self.raw_target_subscriptions = {}
        self.existing_site_names = index_target_site_names(self.existing_objects)
        self._encrypted_passwords_supported = encrypted_passwords_supported

    def convert_dumps(
        self, sources_info, plain_report, subscriptions_mapping, customers_mapping,
            reseller_plans, hosting_plans, resellers, password_holder
    ):
        """
        :type sources_info: list[parallels.core.global_context.SourceInfo]
        :type plain_report: parallels.core.reports.plain_report.PlainReport
        :type subscriptions_mapping: dict[str, parallels.core.migration_list.entities.subscription_info.SubscriptionMappingInfo]
        :type customers_mapping: dict[str, str]
        :type reseller_plans: list[parallels.core.target_data_model.ResellerPlan]
        :type hosting_plans: dict[str|None, list[parallels.core.target_data_model.HostingPlan]]
        :type resellers: list[parallels.core.target_data_model.Reseller]
        :type password_holder: parallels.core.utils.password_holder.PasswordHolder
        """
        context = Registry.get_instance().get_context()

        # store reseller plans in model
        self.target_reseller_plans = group_by_id(reseller_plans, lambda p: p.name)
        # store hosting plans owned by administrator in model
        self.target_plans = group_by_id(hosting_plans.get(None, []), lambda p: p.name)
        # store resellers in model
        self.target_resellers = group_by_id(resellers, lambda r: r.login)

        all_source_client_logins = set()
        for source_info in sources_info:
            all_source_client_logins.update({
                client.login for client in source_info.load_raw_dump().iter_all_clients()
            })

        for source_info in sources_info:
            logger.debug(messages.CONVERT_DUMP)
            is_windows = source_info.is_windows if source_info.is_server else context.conn.target.is_windows
            self._convert_dump(
                source_info.id,
                source_info.load_raw_dump(),
                plain_report,
                is_windows,
                subscriptions_mapping,
                customers_mapping,
                hosting_plans,
                password_holder,
                all_source_client_logins
            )

        # Check that each subscription from migration list actually exists in some backup
        for subscription in subscriptions_mapping:
            subscription_exists = any(
                source_info.load_raw_dump().has_subscription(subscription)
                for source_info in sources_info
            )
            if not subscription_exists:
                plain_report.get_root_report().add_issue(
                    "subscription_does_not_exist", Issue.SEVERITY_WARNING,
                    messages.SUBSCRIPTION_DOES_NOT_EXIST_ON_SOURCE.format(subscription=subscription),
                )

    def get_target_model(self):
        return target_model.Model(
            reseller_plans=self.target_reseller_plans,
            plans=self.target_plans,
            resellers=self.target_resellers,
            clients=self.target_clients
        )

    def _convert_dump(
        self, plesk_id, backup, plain_report, is_windows, subscriptions_mapping, customers_mapping, hosting_plans,
        password_holder, all_source_client_logins=None
    ):
        logger.debug(messages.CREATE_TARGET_MODEL_OBJECTS)

        all_source_client_logins = default(all_source_client_logins, set())

        # 1. create model objects (clients and subscriptions), but do not add them to model yet
        for subscription, _, reseller in backup.iter_all_subscriptions_with_owner_and_reseller():
            self._add_subscription(subscription, reseller, plesk_id, is_windows, plain_report, backup)

        # Add clients from source server
        for client, reseller in backup.iter_all_clients_with_owner():
            if client.login in customers_mapping:
                self._add_client(
                    client, reseller, plesk_id, plain_report, password_holder, customers_mapping[client.login],
                    backup.admin_descriptions
                )

        # Add clients that exist on target server only, and do not exist on any of source servers
        for client_login, client in self.existing_objects.customers.items():
            if client_login not in self.raw_target_clients and client_login not in all_source_client_logins:
                self.raw_target_clients[client_login] = EntityConverter.create_client_stub_from_existing_client(client)

        for reseller in self.target_resellers:
            self.raw_target_clients[reseller] = ClientsConverter.create_fake_client(reseller)

        # 2. link model objects
        # 2.1 plans <- subscription, according to migration list

        # dict(owner => set(plan names)), where owner is reseller login or None in case of admin
        plan_names_by_owner = self._get_plans_by_owners(hosting_plans)
        plan_addon_names_by_owner = self._get_plan_addons_by_owners(hosting_plans)
        for subscription in self.raw_target_subscriptions.values():
            if subscription.source == plesk_id:
                if subscription.name in subscriptions_mapping:
                    plan_name = subscriptions_mapping[subscription.name].plan
                    owner_login = subscriptions_mapping[subscription.name].owner
                    if owner_login is not None:
                        if owner_login in customers_mapping:
                            # owner is a customer
                            reseller_login = customers_mapping.get(owner_login)
                        else:
                            reseller_login = owner_login  # owner is reseller
                    else:
                        reseller_login = None  # owner is administrator

                    if plan_name is not None:
                        subscription_report = plain_report.get_subscription_report(subscription.name)
                        self._check_plan_exists(
                            plan_names_by_owner.get(reseller_login, dict()),
                            plan_name, reseller_login, subscription_report
                        )
                        if not subscription_report.has_errors():
                            subscription.plan_name = plan_name

                            # take addon plans mapping
                            for addon_plan_name in subscriptions_mapping[subscription.name].addon_plans:
                                addon_plan = self._get_and_check_addon_plan(
                                    plan_addon_names_by_owner.get(reseller_login, dict()),
                                    addon_plan_name, reseller_login, subscription_report
                                )
                                if addon_plan is not None:
                                    subscription.plan_addon_names.append(addon_plan.name)
                        else:
                            # there are critical errors, so do not consider
                            # that subscription anymore in further checks and conversions
                            del self.raw_target_subscriptions[subscription.name]
                    else:
                        # custom subscription - subscription that is not assigned to any plan
                        # so leave plan_name set to None
                        pass 
                else:
                    logger.debug(
                        messages.DEBUG_SUBSCRIPTION_NOT_LISTED_IN_MIGRATION_LIST,
                        subscription.name
                    )
                    del self.raw_target_subscriptions[subscription.name]

        # 2.2 subscriptions <- client, according to migration list 
        for subscription in self.raw_target_subscriptions.values():
            if subscription.name in subscriptions_mapping:
                subscription_mapping = subscriptions_mapping[subscription.name]
                subscription_owner = subscription_mapping.owner
                if subscription_owner in self.raw_target_clients:
                    # add subscription to model. guard against multiple addition of
                    # the same subscription (if it exists on two Plesk servers)
                    client_subscriptions = self.raw_target_clients[subscription_owner].subscriptions
                    client_subscription_names = [subs.name for subs in client_subscriptions]
                    if subscription.name not in client_subscription_names:
                        self.raw_target_clients[subscription_owner].subscriptions.append(subscription)
                else:
                    plain_report.get_subscription_report(subscription.name).add_issue(
                        "customer_does_not_exist", Issue.SEVERITY_ERROR,
                        messages.CUSTOMER_DOES_NOT_EXIST_ISSUE % subscription_owner,
                        messages.CUSTOMER_DOES_NOT_EXIST_ISSUE_SOLUTION
                    )
                    del self.raw_target_subscriptions[subscription.name]
            else:
                logger.debug(
                    messages.DEBUG_SUBSCRIPTION_NOT_LISTED_IN_MIGRATION_LIST,
                    subscription.name
                )
                del self.raw_target_subscriptions[subscription.name]

        # 2.3 client <- admin, reseller, according to migration list
        def add_client_to_model(client, reseller_login):
            if reseller_login is None:
                if client.login not in self.target_clients:
                    self.target_clients[client.login] = client
            elif reseller_login in self.target_resellers:
                reseller_clients_by_login = group_by_id(
                    self.target_resellers[reseller_login].clients, lambda c: c.login
                )
                if client.login not in reseller_clients_by_login:
                    self.target_resellers[reseller_login].clients.append(client)
            else:
                plain_report.get_customer_report(client.login).add_issue(
                    'reseller_does_not_exist', Issue.SEVERITY_ERROR,
                    messages.CLIENT_MAPPED_TO_RESELLER_THAT_DOES_NOT_EXIST_ISSUE % reseller_login,
                    messages.CLIENT_MAPPED_TO_RESELLER_THAT_DOES_NOT_EXIST_SOLUTION
                )

        backup_reseller_logins = {reseller.login for reseller in backup.iter_resellers()}
        target_reseller_logins = set(self.existing_objects.resellers.keys())
        all_reseller_logins = backup_reseller_logins | target_reseller_logins

        for client in self.raw_target_clients.values():
            if client.login in customers_mapping or client.login is None:
                reseller_login = customers_mapping.get(client.login)
                add_client_to_model(client, reseller_login)
            elif client.login in all_reseller_logins:
                add_client_to_model(client, client.login)
            else:
                logger.debug(messages.DEBUG_CLIENT_IS_NOT_LISTED_IN_MIGRATION_LIST % client.login)
                del self.raw_target_clients[client.login]

    def _add_client(
        self, client, reseller, plesk_id, plain_report, password_holder, customer_mapping, admin_descriptions
    ):
        client_report = plain_report.get_customer_report(client.login)
        if client.login in self.existing_objects.customers:
            existing_client = self.existing_objects.customers[client.login]
            new_client = self._create_target_client_from_backup(
                client, reseller, None, Report('', ''), password_holder, admin_descriptions
            )  # just for easy comparison
            check_client_contacts_matching_source_target(new_client, existing_client, client_report)
            self._check_client_owner(client_report, customer_mapping, existing_client)
            target_model_client = self.entity_converter.create_client_stub_from_existing_client(existing_client)
            self.raw_target_clients[client.login] = target_model_client
        elif client.login in self.raw_target_clients:
            existing_client = self.raw_target_clients[client.login]
            new_client = self._create_target_client_from_backup(
                client, reseller, None, Report('', ''), password_holder, admin_descriptions
            )  # just for easy comparison
            check_client_contacts_matching_source_panels(new_client, existing_client, client_report)
            target_model_client = existing_client
        else:
            target_model_client = self._create_target_client_from_backup(
                client, reseller, plesk_id, client_report, password_holder, admin_descriptions
            )
            self.raw_target_clients[client.login] = target_model_client 

        return target_model_client

    def _check_client_owner(self, client_report, customer_mapping, existing_client):
        admin_id = Registry.get_instance().get_context().hosting_repository.panel.get_administrator_id(None)

        # check existing client owner
        if existing_client.owner_id == admin_id:
            if customer_mapping is not None:
                client_report.add_issue(
                    "owned_by_another_admin", Issue.SEVERITY_ERROR,
                    messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_ADMIN_RESELLER_ISSUE % (customer_mapping,),
                    messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_ADMIN_RESELLER_ISSUE_SOLUTION)
        else:
            # Customer is already owned by some reseller
            target_reseller = find_first(
                self.existing_objects.resellers.values(),
                lambda r: str(r.reseller_id) == str(existing_client.owner_id)
            )

            if target_reseller is None or target_reseller.username != customer_mapping:
                client_report.add_issue(
                    "owned_by_another_reseller", Issue.SEVERITY_ERROR,
                    messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_RESELLER_ISSUE % (
                        'admin' if customer_mapping is None else messages.RESELLER_TITLE_LOWERCASE % (
                            customer_mapping,
                        )
                    ),
                    messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_RESELLER_SOLUTION
                )

    def _create_target_client_from_backup(
        self, client, reseller, source, client_report, password_holder, admin_descriptions
    ):
        return self.entity_converter.create_client_from_plesk_backup_client(
            client, reseller, source, client_report, password_holder,
            encrypted_passwords_supported=self._encrypted_passwords_supported,
            admin_descriptions=admin_descriptions
        )

    @staticmethod
    def _check_plan_exists(reseller_plans, plan_name, reseller_login, subscription_report):
        """Check is plan with given name exists in list of plans, retrieved from
        target panel with the same vendor, and add an error into report if not
        :type reseller_plans: dict
        :type plan_name: str
        :type reseller_login: str
        :type subscription_report: parallels.core.reports.model.report.Report
        """
        if plan_name not in reseller_plans:
            if reseller_login is not None:
                owner_title = messages.RESELLER_TITLE_LOWERCASE % reseller_login
            else:
                owner_title = "admin"

            subscription_report.add_issue(
                "plan_does_not_exist", Issue.SEVERITY_ERROR,
                messages.PLAN_DOES_NOT_EXIST_ISSUE.format(
                    owner=owner_title,
                    plan_name=plan_name
                ),
                messages.PLAN_DOES_NOT_EXIST_SOLUTION
            )

    def _get_and_check_addon_plan(self, addon_plans, addon_plan_name, reseller_login, subscription_report):
        """Get addon plan object for specified owner by name, check addon plan existence

        If addon exists - just return it.
        If addon does not exist - add issue to the report
        and return None.

        :type addon_plans: dict
        :type addon_plan_name: str | unicode
        :type reseller_login: str | unicode
        :type subscription_report: parallels.core.reports.model.report.Report
        :rtype: parallels.core.hosting_repository.service_plan_addon.ServicePlanAddonEntity
        """
        addon_plan = addon_plans.get(addon_plan_name)

        if addon_plan is None:
            if reseller_login is not None:
                owner_title = messages.RESELLER_TITLE_LOWERCASE % reseller_login
            else:
                owner_title = "admin"

            subscription_report.add_issue(
                "addon_plan_does_not_exist", Issue.SEVERITY_ERROR,
                messages.ADDON_PLAN_DOES_NOT_EXIST_ISSUE.format(owner=owner_title, plan_name=addon_plan_name),
                messages.PLAN_DOES_NOT_EXIST_SOLUTION
            )

        return addon_plan

    @classmethod
    def _get_plans_by_owners(cls, hosting_plans):
        """Returns the following structure: plans[owner][plan name] = plan
        where owner is reseller login or None in case of admin

        :type hosting_plans: dict[str|None, list[parallels.core.target_data_model.Plan]]
        :rtype: dict[str | None, dict[str, parallels.core.target_data_model.Plan]]
        """
        plans_by_owners = {}
        for owner_username, owner_plans in hosting_plans.items():
            plans_by_owners[owner_username] = {p.name: p for p in owner_plans if not p.is_addon}
        return plans_by_owners

    @classmethod
    def _get_plan_addons_by_owners(cls, hosting_plans):
        """Returns the following structure: plans[owner][plan_addon_name] = plan
        where owner is reseller login or None in case of admin

        :type hosting_plans: dict[str|None, list[parallels.core.target_data_model.Plan]]
        :rtype: dict[str | None, dict[str, parallels.core.target_data_model.Plan]]
        """
        plans_by_owners = {}
        for owner_username, owner_plans in hosting_plans.items():
            plans_by_owners[owner_username] = {p.name: p for p in owner_plans if p.is_addon}
        return plans_by_owners

    def _add_subscription(self, subscription, reseller, plesk_id, is_windows, plain_report, backup):
        issues = []

        webspaces_by_name = group_by_id(self.existing_objects.subscriptions, lambda s: s.name_canonical)
        if subscription.name_canonical in webspaces_by_name:
            model_subscription = self.entity_converter.create_subscription_stub_from_existing_subscription(
                subscription, plesk_id, is_windows
            )
        else:
            model_subscription = self.entity_converter.create_subscription_from_plesk_backup_subscription(
                subscription, reseller, plesk_id, is_windows, backup.admin_descriptions
            )
            issues += check_subscription_conflicts(
                subscription.name,
                source_webspaces=self.raw_target_subscriptions,
                source_sites=self.target_site_names,
                target_sites=self.existing_site_names,
            )
            issues += check_subscription_conflicts(
                subscription.name[4:] if subscription.name.startswith('www.') else 'www.' + subscription.name,
                source_webspaces=self.raw_target_subscriptions,
                source_sites=self.target_site_names,
                target_sites=self.existing_site_names,
            )
            issues += check_domain_conflicts(
                backup, subscription.name,
                target_webspaces=self.existing_objects.subscriptions,
                target_sites=self.existing_site_names,
                source_webspaces=self.raw_target_subscriptions,
                source_sites=self.target_site_names,
            )

        for issue in issues:
            plain_report.get_subscription_report(subscription.name).add_issue(*issue)

        critical_issues = [issue for issue in issues if issue[1] == Issue.SEVERITY_ERROR]
        if len(critical_issues) == 0:
            self.raw_target_subscriptions[subscription.name] = model_subscription
            domain_objects = index_plesk_backup_domain_objects(backup, subscription.name) 
            for name, kind in domain_objects:
                self.target_site_names[name] = (subscription.name, kind)
