from collections import defaultdict
from itertools import chain
import itertools

from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core import MigrationError
from parallels.core import target_data_model
from parallels.core.converter.business_objects.common import SOURCE_TARGET
from parallels.core.registry import Registry

logger = get_logger(__name__)


def convert_hosting_plans(context, plan_migration_list, report):
    """Convert hosting plans listed in given migration list

    :type context: parallels.core.global_context.GlobalMigrationContext
    :type plan_migration_list: parallels.core.migration_list.entities.plans.MigrationListPlans
    :type report: parallels.core.reports.model.report.Report
    """
    return PlansConverter().convert_plans(
        context.get_primary_sources_info(),
        context.target_panel_obj.get_hosting_plan_adapter(context),
        plan_migration_list,
        report
    )


class PlansConverter(object):
    """Generate list of plans ready to import to target panel.
    
    Plans converter takes the following information:
    - Plesk backup or other source of information 
    about resellers and plans on source servers
    - Import API which provides information about 
    resellers and plans that already exist on target panel
    - Migration list, which contains list of plans that 
    will be used during migration

    Plans converter:
    - Converts each plan we need to migrate to 
    format ready to import to target panel.
    - Performs conflict resolution, if plan exists on multiple
    source panel, or already exists on target panel.

    Result is a list of plans ready to import to target panel.
    """
    def convert_plans(self, sources_info, hosting_plan_adapter, plan_migration_list, report):
        """Convert plans from source and target panel information to target model

        plan_migration_list is a dictionary with key - reseller login, value - list of service templates to create
            for example: {None: ['Plan 1', 'Plan 2'], 'res1': ['Plan 3']} means that we need to have
            'Plan 1' and 'Plan 2' plans for administrator and 'Plan 3' plan for reseller 'res1'

        :type sources_info: list[parallels.core.global_context.SourceInfo]
        :type hosting_plan_adapter: parallels.core.converter.business_objects.plans.HostingPlanAdapter
        :type plan_migration_list: parallels.core.migration_list.entities.plans.MigrationListPlans
        :type report: parallels.core.reports.model.report.Report
        :rtype: dict[str | None, list[parallels.core.target_data_model.Plan]]
        """
        logger.info(messages.LOG_CONVERT_PLANS)

        plans = defaultdict(dict)
        self._add_target_admin_plans(plans)
        self._add_target_admin_addon_plans(plans)
        self._add_target_reseller_all_plans(plans, plan_migration_list)
        self._add_source_panel_plans(plans, hosting_plan_adapter, sources_info, report)

        # filter only one plan with specific name for each owner
        unique_plans = defaultdict(list)
        self._filter_plans(plans, unique_plans, plan_migration_list)
        self._filter_plan_addons(plans, unique_plans, plan_migration_list)

        return unique_plans

    @staticmethod
    def _add_plan(plans, owner_username, plan):
        """Add hosting plan

        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        :type owner_username: str | None
        :type plan: parallels.core.target_data_model.Plan
        """
        if plan.name not in plans[owner_username]:
            plans[owner_username][plan.name] = []
        plans[owner_username][plan.name].append(plan)

    @staticmethod
    def _add_plan_issues(report, issues):
        """Add issue into report

        :type report: parallels.core.reports.model.report.Report
        :type issues: list[parallels.core.reports.model.issue.Issue]
        """
        for issue in issues:
            report.add_issue_obj(issue)

    def _add_target_admin_plans(self, plans):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        """
        admin_id = Registry.get_instance().get_context().hosting_repository.panel.get_administrator_id(None)
        for target_plan in Registry.get_instance().get_context().hosting_repository.service_plan.get_list(
            filter_owner_id=[admin_id]
        ):
            self._add_plan(plans, None, target_data_model.HostingPlan(
                name=target_plan.name, source=SOURCE_TARGET, is_addon=False, settings=None
            ))

    def _add_target_admin_addon_plans(self, plans):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        """
        admin_id = Registry.get_instance().get_context().hosting_repository.panel.get_administrator_id(None)
        for target_plan_addon in Registry.get_instance().get_context().hosting_repository.service_plan_addon.get_list(
            filter_owner_id=[admin_id]
        ):
            self._add_plan(plans, None, target_data_model.HostingPlan(
                name=target_plan_addon.name, source=SOURCE_TARGET, is_addon=True, settings=None
            ))

    def _add_target_reseller_all_plans(self, plans, plan_migration_list):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        :type plan_migration_list: parallels.core.migration_list.entities.plans.MigrationListPlans
        """
        reseller_usernames = [
            reseller_username
            for reseller_username in itertools.chain(
                plan_migration_list.plans.keys(),
                plan_migration_list.addon_plans.keys()
            )
            if reseller_username is not None
        ]
        hosting_repository = Registry.get_instance().get_context().hosting_repository
        for reseller in hosting_repository.reseller.get_list(reseller_usernames):
            for target_plan in hosting_repository.service_plan.get_list(filter_owner_id=[reseller.reseller_id]):
                self._add_plan(plans, reseller.username, target_data_model.HostingPlan(
                    name=target_plan.name, source=SOURCE_TARGET, is_addon=False, settings=None
                ))
            for target_plan_addon in hosting_repository.service_plan_addon.get_list(
                filter_owner_id=[reseller.reseller_id]
            ):
                self._add_plan(plans, reseller.username, target_data_model.HostingPlan(
                    name=target_plan_addon.name, source=SOURCE_TARGET, is_addon=True, settings=None
                ))

    def _add_source_panel_plans(self, plans, hosting_plan_adapter, sources_info, report):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        :type hosting_plan_adapter: parallels.core.converter.business_objects.plans.HostingPlanAdapter
        :type sources_info: list[parallels.core.global_context.SourceInfo]
        :type report: parallels.core.checking.Report
        """
        for source_info in sources_info:
            dump = source_info.load_raw_dump()
            for source_plan in chain(dump.get_plans(), dump.get_addon_plans()):
                settings, issues = hosting_plan_adapter.convert(source_plan)
                self._add_plan(plans, None, target_data_model.HostingPlan(
                    name=source_plan.name,
                    source=source_info.source_id,
                    is_addon=source_plan.is_addon,
                    settings=settings
                ))
                self._add_plan_issues(report, issues)
            for reseller in dump.iter_resellers():
                for source_plan in chain(
                    dump.iter_reseller_plans(reseller.login),
                    dump.iter_reseller_addon_plans(reseller.login)
                ):
                    settings, issues = hosting_plan_adapter.convert(source_plan)
                    self._add_plan(plans, reseller.login, target_data_model.HostingPlan(
                        name=source_plan.name,
                        source=source_info.source_id,
                        is_addon=source_plan.is_addon,
                        settings=settings
                    ))
                    self._add_plan_issues(report, issues)

    @staticmethod
    def _filter_plan_addons(plans, unique_plans, plan_migration_list):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        :type unique_plans: dict[str | None, list[parallels.core.target_data_model.Plan]]
        :type plan_migration_list: parallels.core.migration_list.entities.plans.MigrationListPlans
        """
        for reseller_username, plan_names in plan_migration_list.addon_plans.items():
            for plan_name in plan_names:
                converted_plans = [p for p in plans.get(reseller_username, {}).get(plan_name, []) if p.is_addon]
                if len(converted_plans) == 0:
                    if reseller_username is None:
                        raise MigrationError(messages.CONVERTER_PLAN_ADDON_ADMIN_NOT_EXISTS.format(
                            hosting_plan_name=plan_name
                        ))
                    else:
                        raise MigrationError(messages.CONVERTER_PLAN_ADDON_RESELLER_NOT_EXISTS.format(
                            reseller_username=reseller_username,
                            hosting_plan_name=plan_name
                        ))
                first_converted_plan = converted_plans[0]
                if first_converted_plan.source == SOURCE_TARGET:
                    # hosting plan add-on specified in migration list exists on target
                    logger.debug(messages.CONVERTER_PLAN_ADDON_EXISTS_TARGET.format(hosting_plan_name=plan_name))
                else:
                    # hosting plan add-on specified in migration list not exists on target
                    if len(converted_plans) > 1:
                        logger.debug(messages.CONVERTER_PLAN_ADDON_EXISTS_SOURCE_MULTIPLE.format(
                            hosting_plan_name=plan_name,
                            source_id=first_converted_plan.source
                        ))
                    else:
                        logger.debug(messages.CONVERTER_PLAN_ADDON_EXISTS_SOURCE.format(
                            hosting_plan_name=plan_name,
                            source_id=first_converted_plan.source
                        ))
                unique_plans[reseller_username].append(first_converted_plan)

    @staticmethod
    def _filter_plans(plans, unique_plans, plan_migration_list):
        """
        :type plans: dict[str | None, dict[str, list[parallels.core.target_data_model.Plan]]]
        :type unique_plans: dict[str | None, list[parallels.core.target_data_model.Plan]]
        :type plan_migration_list: parallels.core.migration_list.entities.plans.MigrationListPlans
        """
        for reseller_username, plan_names in plan_migration_list.plans.items():
            for plan_name in plan_names:
                converted_plans = [p for p in plans.get(reseller_username, {}).get(plan_name, []) if not p.is_addon]
                if len(converted_plans) == 0:
                    if reseller_username is None:
                        raise MigrationError(messages.CONVERTER_PLAN_ADMIN_NOT_EXISTS.format(
                            hosting_plan_name=plan_name
                        ))
                    else:
                        raise MigrationError(messages.CONVERTER_PLAN_RESELLER_NOT_EXISTS.format(
                            reseller_username=reseller_username,
                            hosting_plan_name=plan_name
                        ))
                first_converted_plan = converted_plans[0]
                if first_converted_plan.source == SOURCE_TARGET:
                    # hosting plan specified in migration list exists on target
                    logger.debug(messages.CONVERTER_PLAN_EXISTS_TARGET.format(hosting_plan_name=plan_name))
                else:
                    # hosting plan specified in migration list not exists on target
                    if len(converted_plans) > 1:
                        logger.debug(messages.CONVERTER_PLAN_EXISTS_SOURCE_MULTIPLE.format(
                            hosting_plan_name=plan_name,
                            source_id=first_converted_plan.source
                        ))
                    else:
                        logger.debug(messages.CONVERTER_PLAN_EXISTS_SOURCE.format(
                            hosting_plan_name=plan_name,
                            source_id=first_converted_plan.source
                        ))
                unique_plans[reseller_username].append(first_converted_plan)


class HostingPlanAdapter(object):
    def convert(self, backup_plan):
        """Convert hosting dumped plan settings to target panel format

        :type backup_plan: parallels.core.dump.data_model.HostingPlan
        """
        raise NotImplementedError()
