import re

from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core import target_data_model as target_model
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, str_equals_weak, none_str, find_first, default
from parallels.core.utils.common import if_not_none

logger = get_logger(__name__)

SOURCE_TARGET = 'target-panel'


def format_source(source):
    if source == SOURCE_TARGET:
        return messages.MODEL_SOURCE_TARGET
    else:
        return messages.MODEL_SOURCE_SOURCE % (source,)


def format_contact(personal_info):
    contact_parts = []
    if personal_info.first_name is not None and personal_info.first_name != '':
        contact_parts.append(personal_info.first_name)
    if personal_info.last_name is not None and personal_info.last_name != '':
        contact_parts.append(personal_info.last_name)
    return " ".join(contact_parts).strip()


def index_target_site_names(existing_objects):
    """
    Return dictionary {domain name: (subscription name, domain type)}
    where domain is a domain like object that exists on target panel.
    Domain-like objects types are: addon domains, subdomains, domain aliases.

    Parameters:
    existing_objects - existing data model
    """
    return dict(
        [
            (addon_domain.name, (addon_domain.subscription_name, 'addon domain'))
            for addon_domain in existing_objects.domains
        ] +
        [
            (subdomain.name, (subdomain.subscription_name, 'subdomain'))
            for subdomain in existing_objects.subdomains
        ] +
        [
            (domain_alias.name, (domain_alias.subscription_name, 'alias'))
            for domain_alias in existing_objects.domain_aliases
        ]
    )


def get_client_password(client, report, password_holder, encrypted_passwords_supported=False):
    """Get client password from Plesk backup, working around issue with encrypted passwords.
    :type client: parallels.core.dump.data_model.Client
    Params:
    - client - client object of Plesk backup reader
    - report - report to put issue in case we were
        unable to get password from backup
    - password_holder - PasswordHolder object to create password
        if we were unable to get it from backup
    """
    password = get_plaintext_password(client)
    if password:
        return 'plain', password

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

    logger.debug(messages.DEBUG_AUX_USER_PASSWORD_NOT_FOUND_FOR_CLIENT, client.login)
    if encrypted_passwords_supported and client.password is not None and client.password.password_type == 'encrypted':
        return 'encrypted', client.password.password_text

    logger.debug(messages.DEBUG_ENCRYPTED_PASSWORD_NOT_APPLICABLE_FOR_CLIENT, client.login)
    password = password_holder.get('client', client.login)
    report.add_issue(
        'missing_client_password', Issue.SEVERITY_WARNING,
        messages.UNABLE_RETRIEVE_CUSTOMERS_PASSWORD_AS_IT,
        messages.NEW_PASSWORD_GENERATED_FOR_CUSTOMER_S % (client.login, password)
    )
    return 'plain', password


def get_plaintext_password(client):
    """
    Read client password from XML node (works for resellers as well)
    :type client: parallels.core.dump.data_model.Client
    """
    password = ''
    if client.password is not None and client.password.password_type == 'plain':
        password = client.password.password_text
    return password


def get_auxiliary_user_password(client):
    """ Search for client's password in auxiliary users.

    This is a hack to get client's password if it is stored as a hash: in Plesk 11.0
    and 11.1 there is always an SMB (auxiliary) user that has the same login
    and password as client, but its password is always stored as plain text
    (regardless of how client's password is stored)
    """
    for user in client.auxiliary_users:
        if user.is_built_in and user.name == client.login:
            return get_plaintext_password(user)
    return ''


def index_plesk_backup_domain_objects(backup, subscription_name):
    """
    Return tuple (domain name, type) for all domain-like objects
    under specified subscription in backup.
    Domain-like objects are: addon domains, subdomains, domain aliases.

    Parameters:
    backup - instance of dump.PleskBackupSource*
    subscription_name - list domain-like objects owned by that subscription
    """
    return sum([
        [(d.name, 'addon domain') for d in backup.iter_addon_domains(subscription_name)],
        [(s.name, 'subdomain') for s in backup.iter_subdomains(subscription_name, subscription_name)],
        [(s.name, 'subdomain') for d in backup.iter_addon_domains(subscription_name) for s in backup.iter_subdomains(subscription_name, d.name)],
        [(a.name, 'alias') for a in backup.iter_aliases(subscription_name)],
    ], [])


def check_domain_conflicts(backup, subscription_name, target_webspaces, target_sites, source_webspaces, source_sites):
    """Check uniqueness of addon domains, subdomains and aliases"""

    # list of tuples (domain name, object type - addon domain/subdomain/domain alias)
    subobjects = index_plesk_backup_domain_objects(backup, subscription_name)
    issues = []

    target_webspaces_by_name = group_by_id(target_webspaces, lambda ws: ws.name)
    for name, kind in subobjects:
        if name in target_webspaces_by_name:
            issues.append((
                'duplicate_site_name', Issue.SEVERITY_ERROR,
                messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS % (kind, name, name),
                messages.FIX_DOMAINS_CONFLICT
            ))
        elif name in source_webspaces:
            sub = source_webspaces[name]
            issues.append((
                'duplicate_site_name', Issue.SEVERITY_ERROR,
                messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_1 % (kind, name, name, sub.source),
                messages.FIX_DOMAINS_CONFLICT
            ))
        elif name in target_sites and target_sites[name] != (subscription_name, kind):
            sub_name, obj_type = target_sites[name]
            issues.append((
                'duplicate_site_name', Issue.SEVERITY_ERROR,
                messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_2 % (kind, name, obj_type, name, sub_name),
                messages.FIX_DOMAINS_CONFLICT
            ))
        elif name in source_sites:
            sub_name, obj_type = source_sites[name]
            issues.append((
                'duplicate_site_name', Issue.SEVERITY_ERROR,
                messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_3 % (kind, name, obj_type, name, sub_name),
                messages.FIX_DOMAINS_CONFLICT
            ))

    return issues


def check_subscription_conflicts(subscription_name, source_webspaces, source_sites, target_sites):
    """Return list of problems related to conflicts between this subscription and other subscriptions or their domains.
    """
    issues = []
    if subscription_name in source_webspaces:
        sub = source_webspaces[subscription_name]
        if sub.source != SOURCE_TARGET:
            issues.append((
                'duplicate_subscription_name', Issue.SEVERITY_ERROR,
                messages.SUBSCRIPTION_ALREADY_EXISTS_ON_OTHER_SERVER_ISSUE % (subscription_name, sub.source),
                messages.FIX_SUBSCRIPTION_NAME_CONFLICT
            ))

    elif subscription_name in target_sites:
        sub_name, obj_type = target_sites[subscription_name]
        issues.append((
            'duplicate_site_name', Issue.SEVERITY_ERROR,
            messages.SITE_NAME_ALREADY_EXIST_ON_TARGET_ISSUE % (obj_type, subscription_name, sub_name),
            messages.SITE_CONFLICT_TARGET_SOLUTION % (obj_type, subscription_name)
        ))

    elif subscription_name in source_sites:
        sub_name, obj_type = source_sites[subscription_name]
        issues.append((
            'duplicate_site_name', Issue.SEVERITY_ERROR,
            messages.SITE_NAME_ALREADY_EXIST_ON_SOURCE_ISSUE % (obj_type, subscription_name, sub_name),
            messages.SITE_CONFLICT_SOLUTION % (obj_type, subscription_name)
        ))

    return issues


def check_client_contacts_matching_source_panels(
    new_client, existing_client, report
):
    if any([
        not str_equals_weak(new_client.personal_info.email, existing_client.personal_info.email),
        not str_equals_weak(new_client.personal_info.first_name, existing_client.personal_info.first_name),
        # skip last name as it is always empty according to conversion defined
        # in _create_client_from_plesk_backup_client, compare first name only
    ]):
        differences = []
        if not str_equals_weak(new_client.personal_info.email, existing_client.personal_info.email):
            differences.append(
                messages.EMAIL_DIFFERENCE % (
                    none_str(existing_client.personal_info.email), none_str(new_client.personal_info.email)
                )
            )
        if not str_equals_weak(format_contact(new_client.personal_info), format_contact(existing_client.personal_info)):
            differences.append(
                messages.CONTACT_NAME_CUSTOMER_ANOTHER_SERVER_S %
                (none_str(existing_client.personal_info.first_name), none_str(new_client.personal_info.first_name))
            )
        report.add_issue(
            'duplicate_customer_name_with_another_contact_data',
            Issue.SEVERITY_WARNING,
            messages.CUSTOMER_WITH_USERNAME_EXISTS_FOR_SOURCES % ("\n".join([u"- %s" % s for s in differences])),
            messages.SAME_CUSTOMER_SOLUTION
        )


def check_client_contacts_matching_source_target(
    new_client, existing_client, report
):
    existing_contact_name = format_contact(existing_client)

    if any([
        not str_equals_weak(new_client.personal_info.email, existing_client.email),
        not str_equals_weak(format_contact(new_client.personal_info), existing_contact_name),
    ]):
        differences = []
        if not str_equals_weak(new_client.personal_info.email, existing_client.email):
            differences.append(
                messages.EMAIL_DIFFERENCE_DEST % (
                    none_str(existing_client.email), none_str(new_client.personal_info.email)
                )
            )
        if not str_equals_weak(format_contact(new_client.personal_info), existing_contact_name):
            differences.append(
                messages.CONTACT_NAME_FIRST_NAME_LAST_NAME %
                (none_str(existing_contact_name), none_str(format_contact(new_client.personal_info)))
            )

        report.add_issue(
            'duplicate_customer_name_with_another_contact_data',
            Issue.SEVERITY_WARNING,
            messages.CUSTOMER_WITH_USERNAME_EXISTS_FOR_SOURCE_AND_TARGET % (
                "\n".join(["- %s" % s for s in differences])
            ),
            messages.SAME_CUSTOMER_SOLUTION
        )


class EntityConverter(object):
    """
    Entity converter is responsible for conversions of objects (clients and subscription),
    without any complex logic, conflict resolution or something like that. Just take
    object from source panel (source Plesk backup or other data file), or from target panel
    and create target model entity object (Subscription or Client), filling all necessary
    fields
    """

    def __init__(self, existing_objects):
        self.existing_objects = existing_objects

    @staticmethod
    def create_subscription_from_plesk_backup_subscription(
        subscription, reseller, plesk_id, is_windows, admin_descriptions=None
    ):
        """Create target model subscription out of subscription in dump

        :type subscription: parallels.core.dump.data_model.Subscription
        :type reseller: parallels.core.dump.entity.reseller.Reseller | None
        :type plesk_id: str | unicode
        :type is_windows: bool
        :type admin_descriptions: list[parallels.core.dump.entity.description.Description] | None
        """
        admin_descriptions = default(admin_descriptions, [])

        sysuser = subscription.get_phosting_sysuser()
        sysuser_login = if_not_none(sysuser, lambda s: s.name)
        if reseller is not None:
            reseller_description = if_not_none(find_first(
                reseller.descriptions,
                lambda description: (
                    description.object_type == Description.OBJECT_TYPE_DOMAIN and
                    description.object_name == subscription.name
                )
            ), lambda description: description.value)
        else:
            reseller_description = None

        admin_description = if_not_none(find_first(
            admin_descriptions,
            lambda description: (
                description.object_type == Description.OBJECT_TYPE_DOMAIN and
                description.object_name == subscription.name
            )
        ), lambda description: description.value)

        creation_date = None
        if (
            isinstance(subscription.creation_date, basestring) and
            # Ignore date in invalid format. Format check is the same as we have in Plesk CLI utilities.
            re.match('^\d{4}-\d{1,2}-\d{1,2}$', subscription.creation_date)
        ):
            creation_date = subscription.creation_date

        return target_model.Subscription(
            subscription.name,
            plan_name=None,
            plan_addon_names=[],
            is_enabled=subscription.is_enabled,
            is_locked=subscription.locked,
            source=plesk_id,
            is_windows=is_windows,
            sysuser_login=sysuser_login,
            admin_description=admin_description,
            reseller_description=reseller_description,
            guid=subscription.guid,
            creation_date=creation_date,
            mail_service=subscription.has_mail_accounts or (
                    subscription.mailsystem is not None and subscription.mailsystem.enabled
            )
        )

    @staticmethod
    def create_subscription_stub_from_existing_subscription(subscription, plesk_id, is_windows):
        return target_model.Subscription(
            subscription.name,
            plan_name=None,
            plan_addon_names=[],
            is_enabled=True,
            is_locked=subscription.locked,
            source=plesk_id,
            is_windows=is_windows,
            sysuser_login=None,
            guid=subscription.guid,
            mail_service=subscription.has_mail_accounts or (
                    subscription.mailsystem is not None and subscription.mailsystem.enabled
            )
        )

    @staticmethod
    def create_client_from_plesk_backup_client(
        client, reseller, source, client_report, password_holder, encrypted_passwords_supported=False,
        admin_descriptions=None
    ):
        """
        :type client: parallels.core.dump.data_model.Client
        :type reseller: parallels.core.dump.entity.reseller.Reseller | None
        :type source: str|unicode
        :type client_report: parallels.core.checking.Report
        :type password_holder: parallels.core.converter.business_objects.password_holder.PasswordHolder
        :type encrypted_passwords_supported: bool
        :type admin_descriptions: list[parallels.core.dump.entity.description.Description] | None
        :rtype: parallels.core.target_data_model.Client
        """
        admin_descriptions = default(admin_descriptions, [])
        password_type, password_text = get_client_password(
            client, client_report, password_holder, encrypted_passwords_supported
        )

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

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

        if reseller is not None:
            client_description = if_not_none(find_first(
                reseller.descriptions,
                lambda description: (
                    description.object_type == Description.OBJECT_TYPE_CLIENT and
                    description.object_name == client.login
                )
            ), lambda description: description.value)
        else:
            client_description = None

        if client_description is None:
            client_description = if_not_none(find_first(
                admin_descriptions,
                lambda description: (
                    description.object_type == Description.OBJECT_TYPE_CLIENT and
                    description.object_name == client.login
                )
            ), lambda description: description.value)

        return target_model.Client(
            login=client.login,
            password=password_text,
            password_type=password_type,
            company=client.personal_info.get('company'),
            personal_info=target_model.PersonalInfo(
                first_name=first_name,
                email=client.personal_info.get('email'),
                address=client.personal_info.get('address'),
                city=client.personal_info.get('city'),
                state=client.personal_info.get('state'),
                postal_code=client.personal_info.get('zip'),
                language_code=locale[0:2] if locale is not None else None,
                locale=locale,
                country_code=client.personal_info.get('country'),
                primary_phone=client.personal_info.get('phone'),
                fax=client.personal_info.get('fax'),
                comment=client.personal_info.get('comment', ''),
                im=client.personal_info.get('im', ''),
                im_type=client.personal_info.get('im-type', 'Other')
            ),
            is_enabled=client.is_enabled,
            creation_date=client.creation_date,
            source=source,
            description=client_description
        )

    @staticmethod
    def create_client_stub_from_existing_client(existing_client):
        return target_model.Client(
            login=existing_client.username,
            source=SOURCE_TARGET,
            personal_info=target_model.PersonalInfo.create_none_filled(email=existing_client.email),
            password=None, subscriptions=[], company=None,
            auxiliary_user_roles=[], auxiliary_users=[], is_enabled=None,
            target_client=existing_client
        )
