from collections import defaultdict

from parallels.core.dump.entity.password import password_is_empty
from parallels.core.logging import get_logger
from parallels.core.logging import log_context
from parallels.core import messages
from parallels.core import target_data_model
from parallels.core.converter.business_objects.common import format_source, format_contact, \
    SOURCE_TARGET, get_plaintext_password, get_auxiliary_user_password
from parallels.core.dump.entity.description import Description
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import (
    group_by_id, format_multiline_list, none_str, all_str_equals_weak,
    default, if_not_none, find_first)
from parallels.core.utils.plesk_limits_permissions import RESELLER_PLAN_LIMIT_NAMES, RESELLER_PLAN_PERMISSION_NAMES
from parallels.plesk.models.target_data_model import ResellerSettings

logger = get_logger(__name__)


class ResellersConverter(object):
    """Generate list of resellers ready to import to target panel.

    Resellers converter takes the following information:
    - Plesk backup or other source of information
    about resellers on source servers
    - Information about resellers that already exist on
    target panel
    - Migration list, which contains list of resellers that
    will be used during migration

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

    Result is a list of resellers ready to import to target panel.
    """

    def __init__(self, encrypted_passwords_supported=False):
        """Class constructor

        :param bool encrypted_passwords_supported: whether target panel allows to create reseller
        with hash instead of plain text password
        """
        self._encrypted_passwords_supported = encrypted_passwords_supported

    def convert_resellers(
        self, sources_info, hosting_plans, existing_resellers, resellers_migration_list, report, password_holder
    ):
        """
        Arguments:
        source_panel_data - abstract data from source panels that will be passed for conversion,
        for plesks source panel it is list of objects with:
            - attribute 'id' (source Plesk id, from config file),
            - attribute 'settings' (source Plesk settings from config file)
            - method 'load_raw_dump' that returns PleskBackup object for corresponding source Plesk
        target_resellers - dictionary with keys - logins of resellers in target
            panel, values - object with various information about reseller
        resellers_migration_list -
            dictionary {reseller login: reseller plan}, if plan is not set and reseller
            subscription is custom, then reseller plan is None.
            If this parameter is None, all resellers should be converted.
        report - report object to put conversion warnings to

        :type sources_info: list[parallels.core.global_context.SourceInfo]
        :type hosting_plans: dict[str|None, list[parallels.core.target_data_model.Plan]]
        :type existing_resellers: dict[str|unicode, parallels.core.hosting_repository.reseller.ResellerEntity]
        :type resellers_migration_list: dict[str|unicode, str|unicode] | None
        :type report: parallels.core.reports.model.report.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolder
        :rtype: list[parallels.core.target_data_model.Reseller]
        """
        logger.info(messages.LOG_CONVERT_RESELLERS)
        converted_resellers = []

        # convert resellers from target panel
        logger.debug(messages.CONVERT_RESELLERS_FROM_TARGET_PANEL)
        for existing_reseller in existing_resellers.values():
            if resellers_migration_list is None or existing_reseller.username in resellers_migration_list:
                converted_resellers.append(self._convert_target_panel_reseller(
                    existing_reseller,
                    hosting_plans.get(existing_reseller.username, [])
                ))

        # convert resellers from source panel
        converted_resellers += self._convert_source_panel_resellers(
            sources_info, hosting_plans, resellers_migration_list, report, password_holder
        )

        # group resellers by logins
        resellers_by_logins = defaultdict(list)
        for reseller in converted_resellers:
            resellers_by_logins[reseller.login].append(reseller)

        # perform conflicts resolution
        merged_resellers = []
        logger.debug(messages.LOG_MERGE_RESELLERS)
        for login, resellers in resellers_by_logins.items():
            with log_context(login):
                if len(resellers) == 1:
                    pass  # reseller login is unique, go ahead
                elif len(resellers) > 1:
                    emails = [none_str(reseller.personal_info.email) for reseller in resellers]
                    contacts = [format_contact(reseller.personal_info) for reseller in resellers]

                    differences = []
                    if not all_str_equals_weak(emails):
                        differences.append(u'e-mail')
                    if not all_str_equals_weak(contacts):
                        differences.append(u'contact (first and last names)')

                    resellers_info = dict(
                        login=login,
                        info_list=format_multiline_list(
                            [self._format_reseller(r) for r in resellers]
                        )
                    )
                    if len(differences) > 0:
                        report.add_issue(
                            'duplicate_reseller_name_with_another_contact_data',
                            Issue.SEVERITY_WARNING,
                            messages.RESELLER_EXISTS_ON_NUMBER_OF_SERVERS_ERROR.format(
                                difference=u" and ".join(differences), **resellers_info
                            ),
                            messages.EITHER_REMOVE_DIFFERENCE_IF_BOTH_ACCOUNTS_1)
                else:
                    assert False

                merged_resellers.append(resellers[0])

        if resellers_migration_list is not None:
            for login in resellers_migration_list.keys():
                if login not in resellers_by_logins:
                    report.add_issue(
                        'duplicate_reseller_name_with_another_contact_data',
                        Issue.SEVERITY_ERROR,
                        messages.RESELLER_LOGIN_IS_PRESENTED_IN_MIGRATION.format(
                            login=login
                        ),
                        u"Fix migration list."
                    )

        return merged_resellers

    @staticmethod
    def _format_reseller(reseller):
        return messages.RESELLER_DESCRIPTION % (
            format_source(reseller.source),
            reseller.login,
            none_str(format_contact(reseller.personal_info)),
            none_str(reseller.personal_info.email),
        )

    def _convert_source_panel_resellers(
        self, sources_info, hosting_plans, resellers_migration_list, report, password_holder
    ):
        """
        :type sources_info: list[parallels.core.global_context.SourceInfo]
        :type hosting_plans: dict[str|None, list[parallels.core.target_data_model.Plan]]
        :type resellers_migration_list: dict[str|unicode, str|unicode]
        :type report: parallels.core.checking.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolder
        :rtype: list[parallels.core.target_data_model.Reseller]
        """
        logger.debug(messages.CONVERT_RESELLERS_FROM_SOURCE_PANEL)
        converted_resellers = []
        for source_info in sources_info:
            server_report = report.subtarget(u"Source server", source_info.source_id)
            with log_context(source_info.source_id):
                logger.debug(messages.DEBUG_CONVERT_RESELLERS_FROM_SERVER, source_info.source_id)
                backup = source_info.load_raw_dump()
                reseller_plans_by_guid = group_by_id(list(backup.iter_admin_reseller_plans()), lambda plan: plan.id)
                for reseller in backup.iter_resellers():
                    if resellers_migration_list is None or reseller.login in resellers_migration_list:
                        reseller_report = server_report.subtarget(u"Reseller", reseller.login)
                        reseller_source_panel_plan = reseller_plans_by_guid.get(reseller.plan_id)
                        if resellers_migration_list is not None:
                            reseller_migration_list_plan = resellers_migration_list.get(reseller.login)
                        else:
                            reseller_migration_list_plan = None
                        converted_resellers.append(
                            self._create_target_model_reseller(
                                source_info.source_id,
                                reseller,
                                hosting_plans.get(reseller.login, []),
                                reseller_report,
                                password_holder,
                                reseller_source_panel_plan,
                                reseller_migration_list_plan,
                                backup.admin_descriptions
                            )
                        )
        return converted_resellers

    @staticmethod
    def _convert_target_panel_reseller(reseller, plans):
        """Convert given reseller entity into target data model format

        :type reseller: parallels.core.hosting_repository.reseller.ResellerEntity
        :type plans: list[parallels.core.target_data_model.Plan]
        :rtype: parallels.core.target_data_model.Reseller
        """
        # result of this function is not imported to the target panel, it is just used for
        # conflict resolution between the source and the target panel resellers
        # so only those properties that are used by conflict resolution should be processed,
        # all the others are set to None
        return target_data_model.Reseller(
            source=SOURCE_TARGET,
            login=reseller.username,  # necessary for merging
            personal_info=target_data_model.PersonalInfo(
                first_name=reseller.first_name,  # necessary for merging
                last_name=reseller.last_name,  # necessary for merging
                email=reseller.email,  # necessary for merging
                # below - all information not necessary for merging - set to None
                address=None, city=None,
                county=None, state=None, postal_code=None, language_code=None, country_code=None,
                locale=None, primary_phone=None, additional_phone=None,
                fax=None, mobile_phone=None, comment=None, im=None, im_type=None
            ),
            # below - all information not necessary for merging - set to None
            password=None, plan_name=None,
            clients=[],
            company=None,
            auxiliary_user_roles=None, auxiliary_users=[],
            is_enabled=None,
            plans={p.name: p for p in plans},
            settings=None,
        )

    def _create_target_model_reseller(
        self, source_id, reseller, plans, reseller_report, password_holder,
        reseller_source_panel_plan, reseller_migration_list_plan, admin_descriptions=None
    ):
        """
        :type source_id: str|unicode
        :type reseller: parallels.core.dump.entity.reseller.Reseller
        :type plans: list[parallels.core.target_data_model.Plan]
        :type reseller_report: parallels.core.checking.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolder
        :type reseller_source_panel_plan: parallels.core.dump.data_model.ResellerPlan
        :type reseller_migration_list_plan: str|unicode
        :type admin_descriptions: list[parallels.core.dump.entity.description.Description] | None
        :rtype: parallels.core.target_data_model.Reseller
        """
        admin_descriptions = default(admin_descriptions, [])
        password_type, password_value = self._get_reseller_password(reseller, reseller_report, password_holder)

        first_name = reseller.contact
        if first_name is None or not first_name.strip():
            first_name = reseller.personal_info.get('name')
        if first_name is None or not first_name.strip():
            first_name = reseller.login

        reseller_source_plan_name = if_not_none(reseller_source_panel_plan, lambda p: p.name)

        if reseller_migration_list_plan == reseller_source_plan_name or reseller_migration_list_plan is None:
            # Migrate settings as they are on the source server, when plan is not reassigned or no plan is
            # assigned in migration list (so reseller should be migrated as "custom")
            settings = self._convert_reseller_settings_from_reseller(reseller)
        else:
            # Plan is reassigned - do not set settings in explicit way, settings will be taken from the new plan
            settings = None

        admin_description = if_not_none(find_first(
            admin_descriptions,
            lambda description: (
                description.object_type == Description.OBJECT_TYPE_RESELLER and
                description.object_name == reseller.login
            )
        ), lambda description: description.value)

        return target_data_model.Reseller(
            login=reseller.login,
            password=password_value,
            password_type=password_type,
            plan_name=reseller_migration_list_plan,
            clients=[],
            company=reseller.personal_info.get('company'),
            personal_info=target_data_model.PersonalInfo(
                first_name=first_name,
                email=reseller.personal_info.get('email'),
                address=reseller.personal_info.get('address'),
                city=reseller.personal_info.get('city'),
                state=reseller.personal_info.get('state'),
                postal_code=reseller.personal_info.get('zip'),
                locale=reseller.personal_info.get('locale'),
                country_code=reseller.personal_info.get('country'),
                primary_phone=reseller.personal_info.get('phone'),
                fax=reseller.personal_info.get('fax'),
                comment=reseller.personal_info.get('comment'),
                im=reseller.personal_info.get('im'),
                im_type=reseller.personal_info.get('im-type')
            ),
            is_enabled=reseller.is_enabled,
            plans={p.name: p for p in plans},
            creation_date=reseller.creation_date,
            settings=settings,
            source=source_id,
            description=admin_description
        )

    def _get_reseller_password(self, reseller, reseller_report, password_holder):
        password = get_plaintext_password(reseller)
        if password:
            return 'plain', password

        logger.debug(messages.DEBUG_PLAIN_PASSWORD_NOT_FOUND_FOR_RESELLER, reseller.login)
        password = get_auxiliary_user_password(reseller)
        if password:
            return 'plain', password

        logger.debug(messages.DEBUG_AUX_USER_PASSWORD_NOT_FOUND_FOR_RESELLER, reseller.login)
        if (
            self._encrypted_passwords_supported and
            not password_is_empty(reseller.password) and reseller.password.password_type == 'encrypted'
        ):
            return 'encrypted', reseller.password.password_text

        logger.debug(messages.DEBUG_ENCRYPTED_PASSWORD_NOT_APPLICABLE_FOR_RESELLER, reseller.login)
        password = password_holder.get('reseller', reseller.login)
        reseller_report.add_issue(
            'missing_reseller_password', Issue.SEVERITY_WARNING,
            messages.UNABLE_RETRIEVE_RESELLERS_PASSWORD_AS_IT,
            messages.NEW_PASSWORD_GENERATED_FOR_RESELLER_S % (reseller.login, password)
        )
        return 'plain', password

    @staticmethod
    def _convert_reseller_settings_from_plan(backup_plan):
        """
        :type backup_plan: parallels.core.dump.data_model.ResellerPlan
        """
        return ResellerSettings(
            limits={
                limit_name: backup_plan.properties[limit_name]
                for limit_name in RESELLER_PLAN_LIMIT_NAMES
                if limit_name in backup_plan.properties
            },
            permissions={
                permission_name: backup_plan.properties[permission_name]
                for permission_name in RESELLER_PLAN_PERMISSION_NAMES
                if permission_name in backup_plan.properties
            },
            overuse=backup_plan.properties.get('overuse'),
            oversell=backup_plan.properties.get('oversell'),
        )

    @staticmethod
    def _convert_reseller_settings_from_reseller(reseller):
        """Convert reseller settings to import API format from backup

        :type reseller: parallels.core.dump.entity.reseller.Reseller
        :rtype: parallels.plesk.models.target_data_model.ResellerSettings
        """
        return ResellerSettings(
            limits={
                limit_name: reseller.limits[limit_name]
                for limit_name in RESELLER_PLAN_LIMIT_NAMES
                if limit_name in reseller.limits
            },
            permissions={
                permission_name: 'true' if reseller.permissions[permission_name] else 'false'
                for permission_name in RESELLER_PLAN_PERMISSION_NAMES
                if permission_name in reseller.permissions
            },
            overuse=reseller.limits.get('overuse'),
            oversell=reseller.limits.get('oversell'),
        )
