from parallels.core.logging import get_logger
from parallels.core.logging import log_context
from parallels.core import target_data_model
from parallels.core.converter.business_objects.clients import ClientsConverter
from parallels.core.converter.business_objects.common import format_contact, EntityConverter, SOURCE_TARGET
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import format_multiline_list, group_by_id, group_by, all_str_equals_weak
from parallels.plesk.source.expand import messages

logger = get_logger(__name__)


class ExpandToPleskClientsConverter(object):
    def convert_clients(
        self, source_plesk_servers, expand_data, target_panel_clients, clients_mapping, report, password_holder
    ):
        """
        :type source_plesk_servers: list[parallels.core.global_context.SourceInfo]
        :type expand_data: parallels.plesk.source.expand.expand_data.model.Model
        :type target_panel_clients: dict
        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type report: parallels.core.reports.model.report.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolderBase
        """
        logger.debug(messages.DEBUG_CONVERT_CLIENTS)

        plain_converted_clients = self._convert_clients_plain(
            clients_mapping, expand_data, password_holder, report,
            source_plesk_servers, target_panel_clients
        )
        clients_by_logins = group_by(plain_converted_clients, lambda c: c.login)
        merged_clients = self._merge_clients(clients_by_logins, report)
        self._check_all_clients_from_mapping_are_presented(clients_by_logins, clients_mapping, report)

        return merged_clients

    def _convert_clients_plain(
        self, clients_mapping, expand_data, password_holder, report,
        source_plesk_servers, target_panel_clients
    ):
        """Convert clients from target and source to target model and put them in a plain list

        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type expand_data: parallels.plesk.source.expand.expand_data.model.Model
        :type report: parallels.core.reports.model.report.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolderBase
        :type source_plesk_servers: list[parallels.core.global_context.SourceInfo]
        :type target_panel_clients: dict
        :rtype: list[parallels.core.target_data_model.Client]
        """
        converted_target_clients = self._convert_clients_from_target_panel(
            clients_mapping, target_panel_clients
        )
        converted_expand_clients = self._convert_expand_clients_from_source(
            clients_mapping, expand_data
        )
        converted_plesk_clients = self._convert_clients_from_source_plesks(
            expand_data, clients_mapping, password_holder, report, source_plesk_servers
        )
        converted_clients = sum([
            converted_target_clients, converted_expand_clients, converted_plesk_clients
        ], [])
        return converted_clients

    @staticmethod
    def _convert_clients_from_target_panel(clients_mapping, target_panel_clients):
        """Convert clients from target panel existing objects model to target model clients

        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type target_panel_clients: dict
        :rtype: list[parallels.core.target_data_model.Client]
        """
        logger.debug(messages.LOG_CONVERT_CLIENTS_FROM_TARGET_PANEL)

        converted_clients = []
        for client in target_panel_clients.values():
            if clients_mapping is None or client.username in clients_mapping:
                target_model_client = ClientsConverter.create_client_stub_from_existing_client(client)
                converted_clients.append(target_model_client)

        return converted_clients

    @classmethod
    def _convert_expand_clients_from_source(cls, clients_mapping, expand_data):
        """Convert Expand clients to target model clients

        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type expand_data: parallels.plesk.source.expand.expand_data.model.Model
        :rtype: list[parallels.core.target_data_model.Client]
        """
        logger.debug(messages.DEBUG_CONVERT_EXPAND_CLIENTS)

        converted_clients = []
        for expand_client in expand_data.expand_clients:
            expand_client_login = expand_client.info['login']
            if clients_mapping is None or expand_client_login in clients_mapping:
                converted_client = cls._convert_expand_client(expand_client)
                converted_clients.append(converted_client)

        return converted_clients

    @staticmethod
    def _convert_clients_from_source_plesks(
        expand_data, clients_mapping, password_holder, report, source_plesk_servers
    ):
        """Convert Plesk clients to target model clients
        :type expand_data: parallels.plesk.source.expand.expand_data.model.Model
        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type report: parallels.core.reports.model.report.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolderBase
        :type source_plesk_servers: list[parallels.core.global_context.SourceInfo]
        :rtype: list[parallels.core.target_data_model.Client]
        """
        logger.debug(messages.DEBUG_CONVERT_PLESK_CLIENTS)

        converted_clients = []
        entity_converter = EntityConverter(None)

        expand_clients_by_id = group_by_id(expand_data.expand_clients, lambda c: c.id)
        plesk_to_expand_client = dict()
        for plesk_client in expand_data.plesk_clients:
            if plesk_client.expand_client_id in expand_clients_by_id:
                plesk_to_expand_client[(
                    plesk_client.plesk_id,
                    plesk_client.login
                )] = expand_clients_by_id[plesk_client.expand_client_id]

        for source_panel_info in source_plesk_servers:
            backup = source_panel_info.load_raw_dump()
            for client in backup.iter_all_clients():
                if clients_mapping is None or client.login in clients_mapping:
                    if (source_panel_info.id, client.login) not in plesk_to_expand_client:
                        converted_client = entity_converter.create_client_from_plesk_backup_client(
                            client, None, source_panel_info.id, report, password_holder
                        )
                        converted_clients.append(converted_client)

        return converted_clients

    def _merge_clients(self, clients_by_logins, report):
        """Merge clients - perform conflict resolution when multiple clients have the same login

        Logic is simple - if they all have the same contact name (first name plus last name) and e-mail,
        then clients are considered the same single client. Otherwise an error is reported.

        :type report: parallels.core.reports.model.report.Report
        :type clients_by_logins: dict[str | unicode, list[parallels.core.target_data_model.Client]]
        :rtype: list[parallels.core.target_data_model.Client]
        """

        merged_clients = []
        logger.debug(messages.DEBUG_MERGE_CLIENTS)
        for login, clients in clients_by_logins.items():
            with log_context(login):
                if len(clients) == 1:
                    pass  # client login is unique, go ahead
                elif len(clients) > 1:
                    emails = [client.personal_info.email for client in clients]
                    contacts = [format_contact(client.personal_info) for client in clients]

                    differences = []
                    if not all_str_equals_weak(emails):
                        differences.append(messages.CLIENT_DIFFERENCE_EMAIL)
                    if not all_str_equals_weak(contacts):
                        differences.append(messages.CLIENT_DIFFERENCE_CONTACT)

                    clients_info = dict(
                        login=login,
                        info_list=format_multiline_list([self._format_client(r) for r in clients])
                    )
                    if len(differences) > 0:
                        report.add_issue(
                            'duplicate_client_name_with_another_contact_data',
                            Issue.SEVERITY_ERROR,
                            messages.CLIENT_WITH_SAME_USERNAME_EXISTS.format(
                                difference=u" and ".join(differences), **clients_info
                            ),
                            messages.SOLUTION_REMOVE_DIFFERENCE_BETWEEN_CLIENTS)
                    else:
                        # seems to be same clients
                        pass
                else:
                    assert False

                merged_clients.append(clients[0])

        return merged_clients

    @staticmethod
    def _check_all_clients_from_mapping_are_presented(clients_by_logins, clients_mapping, report):
        """Check that all clients listed in mapping (migration list) actually exist on source or on target

        :type clients_by_logins: dict[str | unicode, list[parallels.core.target_data_model.Client]]
        :type clients_mapping: dict[str | unicode, str | unicode | None]
        :type report: parallels.core.reports.model.report.Report
        :rtype: None
        """
        if clients_mapping is not None:
            for login in clients_mapping:
                if login not in clients_by_logins:
                    report.add_issue(
                        'client_is_not_presented_on_panels',
                        Issue.SEVERITY_ERROR,
                        messages.CLIENT_DOES_NOT_EXIST.format(
                            login=login
                        ),
                        messages.SOLUTION_FIX_MIGRATION_LIST
                    )

    @staticmethod
    def _convert_expand_client(expand_client):
        """Convert Expand model client to target model client

        :type expand_client: parallels.plesk.source.expand.expand_data.model.ExpandClient
        :rtype: parallels.core.target_data_model.Client
        """

        locale = expand_client.personal_info.get('locale', None)

        return target_data_model.Client(
            login=expand_client.info.get('login'),
            password=expand_client.info.get('passwd'),
            company=expand_client.personal_info.get('company'),
            personal_info=target_data_model.PersonalInfo(
                first_name=expand_client.personal_info.get('pname'),
                email=expand_client.personal_info.get('email'),
                address=expand_client.personal_info.get('address'),
                city=expand_client.personal_info.get('city'),
                state=expand_client.personal_info.get('state'),
                postal_code=expand_client.personal_info.get('zip'),
                language_code=locale[0:2] if locale is not None else None,
                locale=locale,
                country_code=expand_client.personal_info.get('country', 'US'),
                primary_phone=expand_client.personal_info.get('phone'),
                fax=expand_client.personal_info.get('fax'),
            ),
            is_enabled=expand_client.info.get('status', '1') == '0',
            source='expand'
        )

    @staticmethod
    def _format_client(client):
        """Format information about converted client as a string to put into report

        :type client: parallels.core.target_data_model.Client
        :rtype: str | unicode
        """
        if client.source == 'expand':
            client_type = messages.EXPAND_CLIENT_ON_SOURCE_EXPAND_SERVER
        elif client.source == SOURCE_TARGET:
            client_type = messages.CLIENT_ON_TARGET_SERVER
        else:
            client_type = messages.PLESK_CLIENT_ON_SOURCE_PLESK_SERVER % client.source

        return messages.CLIENT_FORMAT % (
            client_type,
            client.login,
            format_contact(client.personal_info),
            client.personal_info.email,
        )
