import os

from parallels.core.registry import Registry
from parallels.core.utils.application_utils import get_application_model
from parallels.plesk import messages
from parallels.core.utils.common import find_only, safe_format, cached, is_empty
from parallels.plesk.hosting_description.utils import parse_bool_value
from parallels.core.utils.safe_structures import safe_get_list, safe_get_list_all, safe_get_dict, safe_get_struct


class HostingDescriptionModel(object):
    """Object model over hosting description file, which describes hosting on custom panel that should be migrated"""

    def __init__(self, hosting_description_id, data):
        self._hosting_description_id = hosting_description_id
        self._data = data

    def iter_all_subscriptions(self):
        """Iterate over subscriptions to be migrated

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionSubscription]
        """
        for subscription_data in iter_all_subscriptions(self._data):
            yield HostingDescriptionSubscription(subscription_data)

    def get_subscription(self, name):
        """Get subscription by name

        :type name: str | unicode
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionSubscription
        """
        return find_only(
            self.iter_all_subscriptions(), lambda s: s.name == name,
            messages.FAILED_TO_FIND_SUBSCRIPTION_IN_DUMP % name
        )

    def get_default_mail_server(self):
        """Get information about default mail server

        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionDefaultMailServer | None
        """
        if 'default_mail_server' in self._data:
            return HostingDescriptionDefaultMailServer(safe_get_dict(self._data, 'default_mail_server'))
        else:
            return None


class HostingDescriptionDomain(object):
    """Base class for domain object (webspace, addon domain or subdomain) that provides web files mapping"""
    def __init__(self, data):
        self._data = data

    @property
    def name(self):
        """Name of the domain

        :rtype: str | unicode | None
        """
        return self._data.get('name')

    @property
    def source_document_root(self):
        """Description of a path to the domain document root (which is available by HTTP) on the source server

        :rtype: str | unicode | dict | None
        """
        return self._data.get('source_document_root')

    @property
    def target_document_root(self):
        """Relative path to the domain document root on the target server

        Returns None if target panel's default should be used

        :rtype: str | unicode | None
        """
        return self._data.get('target_document_root')

    @property
    def no_web_hosting(self):
        """Whether that domain should have no web hosting

        :rtype: bool
        """
        return parse_bool_value(self._data.get('no_web_hosting', False), False)

    @property
    def forwarding_url(self):
        """URL of another domain for this domain to forward to

        Return None in case of virtual hosting or no hosting.

        :rtype: str | unicode | None
        """
        return self._data.get('forwarding_url')

    @property
    def forwarding_type(self):
        """Type of forwarding: moved permanently, moved temporarily, frame forwarding

        Return None in case of virtual hosting or no hosting.
        Return one of parallels.plesk.hosting_description.model.ForwardingType in case of forwarding.

        :rtype: str | unicode | None
        """
        if 'forwarding_url' in self._data:
            default = ForwardingType.MOVED_PERMANENTLY
        else:
            default = None

        return self._data.get('forwarding_type', default)

    def iter_web_files(self):
        """Iterate over web file mappings

        :rtype: collections.Iterable[HostingDescriptionWebFilesMapping]
        """
        for web_file_data in safe_get_list(self._data, 'web_files'):
            yield HostingDescriptionWebFilesMapping(web_file_data)
        for application in self.iter_applications():
            if not is_empty(application.document_root_relative_path):
                target_path = (
                    '{document_root}' + os.path.sep + application.document_root_relative_path
                )
            else:
                target_path = '{document_root}'

            yield HostingDescriptionWebFilesMapping(dict(
                source=application.base_path,
                target=target_path
            ))

    def iter_applications(self):
        """Iterate over instances of applications installed on that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionApplication]
        """
        for application_data in safe_get_list(self._data, 'applications'):
            application = HostingDescriptionApplication(application_data)
            if application.base_path is not None:
                yield application


class HostingDescriptionSubscription(HostingDescriptionDomain):
    """Object model over subscription in hosting description file"""

    @property
    def source_webspace_root(self):
        """Description of a path to webspace root (which contains all web files of webspace) on the source server

        :rtype: str | unicode | dict | None
        """
        return self._data.get('source_webspace_root')

    @property
    def mail_service(self):
        """Mail service of the subscription

        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionMailService
        """
        data = safe_get_struct(self._data, 'mail_service')
        if data is not None:
            return HostingDescriptionMailService(data)
        else:
            return None

    def iter_databases(self):
        """Iterate over databases of that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionDatabase]
        """
        for database_data in safe_get_list(self._data, 'databases'):
            database = HostingDescriptionDatabase(database_data)
            if database.name is not None:
                yield database
        for application in iter_all_applications(self._data):
            if application.instance is None:
                continue
            for application_database in (application.instance.get_databases()):
                database_data = dict(
                    server=dict(
                        host=application_database.host,
                        port=application_database.port,
                        type=application_database.type
                    ),
                    users=[
                        dict(
                            login=application_database.username,
                            password=application_database.password
                        )
                    ],
                    name=application_database.name,
                    type=application_database.type
                )
                database = HostingDescriptionDatabase(database_data)
                if database.name is not None:
                    yield database

    def iter_database_users(self):
        """Iterate over database users of that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionDatabaseUser]
        """
        for database_user_data in safe_get_list(self._data, 'database_users'):
            database_user = HostingDescriptionDatabaseUser(database_user_data)
            if database_user.username is not None:
                yield database_user

    def iter_mailboxes(self):
        """Iterate over mailboxes of that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionMailbox]
        """
        domains = [self._data] + safe_get_list(self._data, 'addon_domains')
        for domain in domains:
            mail_service = safe_get_struct(domain, 'mail_service')
            if not mail_service:
                continue
            for mail_user_data in safe_get_list(mail_service, 'mail_users'):
                mailbox = HostingDescriptionMailbox(mail_user_data, domain['name'])
                if mailbox.name is not None:
                    yield mailbox

    def get_mailbox(self, mailbox_name):
        """Get mailbox by name

        :type mailbox_name: str | unicode
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionMailbox
        """
        return find_only(
            self.iter_mailboxes(), lambda m: m.name == mailbox_name,
            safe_format(
                messages.FAILED_TO_FIND_MAILBOX_IN_DUMP,
                mailbox=mailbox_name,
                subscription=self.name
            )
        )

    def iter_addon_domains(self):
        """Iterate over addon domains of that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description_model.HostingDescriptionAddonDomain]
        """
        for addon_domain_data in safe_get_list(self._data, 'addon_domains'):
            addon = HostingDescriptionAddonDomain(addon_domain_data)
            if addon.name is not None:
                yield addon

    def iter_subdomains(self):
        """Iterate over subdomains of that subscription

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionSubdomain]
        """
        for subdomain_data in safe_get_list(self._data, 'subdomains'):
            subdomain = HostingDescriptionSubdomain(subdomain_data)
            if subdomain.name is not None:
                yield subdomain

    def get_addon_domain(self, name):
        """Get addon domain by name

        :type name: str | unicode
        :rtype: parallels.custom_panel_migrator.hosting_description.HostingDescriptionAddonDomain
        """
        return find_only(
            self.iter_addon_domains(), lambda d: d.name == name,
            messages.FAILED_TO_FIND_ADDON_IN_DUMP % name
        )

    def get_subdomain(self, name):
        """Get subdomain by name

        :type name: str | unicode
        :rtype: parallels.custom_panel_migrator.hosting_description.HostingDescriptionSubdomain
        """
        return find_only(
            self.iter_subdomains(), lambda d: d.name == name,
            messages.FAILED_TO_FIND_SUBDOMAIN_IN_DUMP % name
        )

    def is_fake(self):
        """Whether subscription is fake or not

        Fake subscriptions are created for technical reasons and does not exist on source. For example it is necessary
        when you have several domains on webspace, but can not select main domain of them. Then you create a
        fake subscription which will hold all domains as addon domains.

        :rtype: bool
        """
        return parse_bool_value(self._data.get('is_fake', False), False)


class HostingDescriptionApplication(object):
    """Object model over application in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def type(self):
        """Type of that application

        :rtype: str
        """
        return self._data.get('type')

    @property
    def source_id(self):
        """Identifier of source where that application located

        :rtype: str
        """
        return self._data.get('source_id')

    @property
    def base_path(self):
        """Base to the files of that application

        :rtype: str
        """
        return self._data.get('base_path')

    @property
    def document_root_relative_path(self):
        """Path relative to document root

        :rtype: str | unicode
        """
        return self._data.get('document_root_relative_path')

    @property
    @cached
    def instance(self):
        """Instance of that application

        :rtype: parallels.core.application.base.ApplicationInstance | None
        """
        if self.type is None:
            return None
        application_model = get_application_model(self.type)
        if application_model is None:
            return None
        source_info = Registry.get_instance().get_context().get_source_info(self.source_id)
        if source_info is None:
            return None
        return application_model.get_instance(source_info.source, self.base_path)


class HostingDescriptionDatabase(object):
    """Object model over database in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def db_type(self):
        """Type of the database (either 'mysql' or 'mssql')

        :rtype: str | unicode
        """
        return self._data.get('type', 'mysql')

    @property
    def version(self):
        return self._data.get('version')

    @property
    def charset(self):
        return self._data.get('charset')

    @property
    def collation(self):
        return self._data.get('collation')

    @property
    def aps_registry_id(self):
        return self._data.get('aps_registry_id')

    @property
    def related_domain_name(self):
        return self._data.get('related_domain_name')

    @property
    def name(self):
        """Name of the database

        :rtype: str | unicode
        """
        return self._data.get('name')

    @property
    def dump(self):
        """Path to database dump file

        :rtype: str | unicode | None
        """
        return self._data.get('dump')

    @property
    def server_id(self):
        server_id = self._data.get('server')
        if isinstance(server_id, basestring):
            return server_id
        return None

    @property
    def server(self):
        """
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseServer | None
        """
        server_data = self._data.get('server')
        if isinstance(server_data, dict):
            return HostingDescriptionDatabaseServer(server_data)
        return None

    def iter_database_users(self):
        """Iterate over database users of that database

        :rtype: collections.Iterable[parallels.plesk.hosting_description.model.HostingDescriptionDatabaseUser]
        """
        for database_user_data in safe_get_list(self._data, 'users'):
            database_user = HostingDescriptionDatabaseUser(database_user_data)
            if database_user.username is not None:
                yield database_user


class HostingDescriptionDatabaseUser(object):
    def __init__(self, data):
        self._data = data

    @property
    def username(self):
        return self._data.get('login')

    @property
    def password(self):
        return self._data.get('password')

    @property
    def password_type(self):
        return self._data.get('password_type')

    @property
    def aps_registry_id(self):
        return self._data.get('aps_registry_id')

    @property
    def server_id(self):
        server_id = self._data.get('server')
        if isinstance(server_id, basestring):
            return server_id
        return None

    @property
    def server(self):
        """
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseServer | None
        """
        server_data = self._data.get('server')
        if isinstance(server_data, dict):
            return HostingDescriptionDatabaseServer(server_data)
        return None


class HostingDescriptionDatabaseServer(object):
    def __init__(self, data):
        self._data = data

    @property
    def db_type(self):
        return self._data.get('type')

    @property
    def host(self):
        return self._data.get('host')

    @property
    def port(self):
        return self._data.get('port')

    @property
    def administrator(self):
        """
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseServerAdministrator
        """
        return HostingDescriptionDatabaseServerAdministrator(self._data.get('admin', {}))


class HostingDescriptionDatabaseServerAdministrator(object):
    def __init__(self, data):
        self._data = data

    @property
    def username(self):
        return self._data.get('login')

    @property
    def password(self):
        return self._data.get('password')

    @property
    def password_type(self):
        return self._data.get('password_type')


class HostingDescriptionDefaultMailServer(object):
    """Object model over default mail server in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def server_type(self):
        """Type of mail server

        :rtype: str | unicode | None
        """
        return self._data.get('type')

    @property
    def administrator(self):
        """Administrator of default mail server

        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionMailServerAdministrator
        """
        return HostingDescriptionMailServerAdministrator(safe_get_dict(self._data, 'admin'))


class HostingDescriptionMailServerAdministrator(object):
    """Object model over administrator of default mail server in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def username(self):
        """Default mail server administrator's login

        :rtype: str | unicode | None
        """
        return self._data.get('login')

    @property
    def password(self):
        """Default mail server administrator's password

        :rtype: str | unicode | None
        """
        return self._data.get('password')


class CopyMessagesMode(object):
    AUTO = 'auto'
    REMOTE_DIRECTORY = 'remote-directory'
    LOCAL_DIRECTORY = 'local-directory'
    IMAP = 'imap'
    MAIL_MIGRATOR = 'mail-migrator'
    NONE = 'none'

    @classmethod
    def all_values(cls):
        return [
            cls.AUTO,
            cls.REMOTE_DIRECTORY,
            cls.LOCAL_DIRECTORY,
            cls.IMAP,
            cls.MAIL_MIGRATOR,
            cls.NONE,
        ]


class PasswordType(object):
    PLAIN = 'plain'
    HASH = 'hash'

    @classmethod
    def all_values(cls):
        return [
            cls.PLAIN,
            cls.HASH,
        ]


class HostingDescriptionMailService(object):
    """Object model over mail service in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def mail_server(self):
        """ID of mail server where mail messages are stored

        :rtype: str | unicode | None
        """
        return self._data.get('mail_server')

    @property
    def mail_ip(self):
        """IPv4 address of mail service of subscription

        :rtype: str | unicode | None
        """
        return self._data.get('mail_ip')

    @property
    def mail_ipv6(self):
        """IPv6 address of mail service of subscription

        :rtype: str | unicode | None
        """
        return self._data.get('mail_ipv6')

    @property
    def copy_messages(self):
        """In which way migrator should try to copy mail messages for mailboxes of that mail service

        :return: str | unicode
        """
        value = self._data.get('copy_messages')
        if value not in CopyMessagesMode.all_values():
            return CopyMessagesMode.AUTO
        else:
            return value


class HostingDescriptionMailbox(object):
    """Object model over mailbox in hosting description file"""

    def __init__(self, data, domain_name):
        self._data = data
        self._domain_name = domain_name

    @property
    def name(self):
        """Full name of the mailbox

        :rtype: str
        """
        return '%s@%s' % (self._data['name'], self._domain_name)

    @property
    def short_name(self):
        """Short name of the mailbox - everything before '@' symbol

        :rtype: str
        """
        return self._data['name']

    @property
    def domain_name(self):
        """Name of domain of the mailbox

        :rtype: str
        """
        return self._domain_name

    @property
    def source_name(self):
        """Full name of mailbox on the source server. Specified only if it differs from mailbox name on target.

        :rtype: str | unicode | None
        """
        return self._data.get('source_full_name')

    @property
    def source_domain_name(self):
        """Domain name of mailbox on the source server.
        Specified only if mailbox on source differs from mailbox name on target.

        :rtype: str | unicode | None
        """
        if self.source_name is not None and '@' in self.source_name:
            return self.source_name.split('@', 1)[1]
        else:
            return None

    @property
    def source_short_name(self):
        """Short name of mailbox on the source server - everything before '@' symbol.
          Specified only if mailbox on source differs from mailbox name on target.

          :rtype: str | unicode | None
          """
        if self.source_name is not None and '@' in self.source_name:
            return self.source_name.split('@', 1)[0]
        else:
            return None

    @property
    def source_password(self):
        """Password of the mailbox on the source server.
        Specified only if it differs from password on the target server. Always has plain type.

        :rtype: str | unicode | None
        """
        return self._data.get('source_password')

    @property
    def source_directory(self):
        """Directory on the source server that contains mail messages (specific to Unix only)

        :rtype: str | unicode | dict | None
        """
        return self._data.get('directory')

    @property
    def source_maildir_directory(self):
        """'Maildir" directory on the source server (specific to Unix only)

        :rtype: str | unicode | dict | None
        """
        return self._data.get('maildir_directory')

    def iter_mail_files(self):
        """Iterate over mail file mappings

        :rtype: collections.Iterable[HostingDescriptionMailFilesMapping]
        """
        for mail_file_data in safe_get_list(self._data, 'mail_files'):
            yield HostingDescriptionMailFilesMapping(mail_file_data)

    @property
    def quota(self):
        """Mailbox quota, as string

        :rtype: str | None
        """
        return self._data.get('disk_quota')

    @property
    def password(self):
        """Password of the mailbox

        :rtype: str | unicode | None
        """
        return self._data.get('password')

    @property
    def password_type(self):
        """Password type of the mailbox

        :rtype: str | unicode | None
        """
        password_type = self._data.get('password_type')
        if self.password is None:
            return None
        else:
            if password_type not in PasswordType.all_values():
                return PasswordType.PLAIN
            else:
                return password_type

    @property
    def mail_ip(self):
        """IPv4 address of mail service of mailbox

        :rtype: str | unicode | None
        """
        return self._data.get('mail_ip')

    @property
    def mail_ipv6(self):
        """IPv6 address of mail service of mailbox

        :rtype: str | unicode | None
        """
        return self._data.get('mail_ipv6')

    @property
    def copy_messages(self):
        """In which way migrator should try to copy mail messages for that mailbox

        :return: str | unicode
        """
        value = self._data.get('copy_messages')

        if value is None:
            return None
        elif value not in CopyMessagesMode.all_values():
            return CopyMessagesMode.AUTO
        else:
            return value


class HostingDescriptionAddonDomain(HostingDescriptionDomain):
    """Object model over addon domain in hosting description file"""


class HostingDescriptionSubdomain(HostingDescriptionDomain):
    """Object model over subdomain in hosting description file"""

    @property
    def parent_domain(self):
        """Name of parent domain, if specified

        :rtype: str | None
        """
        return self._data.get('parent_domain')


class HostingDescriptionWebFilesMapping(object):
    """Object model over web files mapping in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def source(self):
        """Source path of the web files to be copied

        :rtype: str | unicode
        """
        return self._data['source']

    @property
    def target(self):
        """Target path of the web files to be copied

        :rtype: str | unicode
        """
        return self._data['target']

    @property
    def exclude(self):
        """List of files to exclude

        :rtype: list[str | unicode]
        """
        return safe_get_list(self._data, 'exclude')

    @property
    def skip_if_source_not_exists(self):
        """Whether to silently skip this item if source path does not exist on the source server

        :rtype: bool
        """
        return self._data.get('skip_if_source_not_exists', False)

    @property
    def source_system_user_login(self):
        """Login of system user that owns these files on the source server.

        Should be specified if it differs from main system user of subscription.
        Mutually exclusive with 'source_system_user_uid' and 'source_system_user_gid' properties.

        :rtype: str | unicode | None
        """
        return self._data.get('source_system_user_login')

    @property
    def source_system_user_uid(self):
        """UID (user id) of system user that owns these files on the source server.

        Should be specified if it differs from main system user of subscription.
        Mutually exclusive with 'source_system_user_login' property.
        If that property is set, 'source_system_user_gid' property expected to be set too.

        :rtype: str | unicode | None
        """
        return self._data.get('source_system_user_uid')

    @property
    def source_system_user_gid(self):
        """GID (group id) of system user that owns these files on the source server.

        Should be specified if it differs from main system user of subscription.
        Mutually exclusive with 'source_system_user_login' property.
        If that property is set, 'source_system_user_uid' property is expected to be set too.

        :rtype: str | unicode | None
        """
        return self._data.get('source_system_user_gid')


class HostingDescriptionMailFilesMapping(object):
    """Object model over mail files mapping in hosting description file"""

    def __init__(self, data):
        self._data = data

    @property
    def source(self):
        """Source path of the mail files to be copied

        :rtype: str | unicode
        """
        return self._data['source']

    @property
    def target(self):
        """Target path of the mail files to be copied

        :rtype: str | unicode
        """
        return self._data['target']

    @property
    def exclude(self):
        """List of files to exclude

        :rtype: list[str | unicode]
        """
        return safe_get_list(self._data, 'exclude')

    @property
    def skip_if_source_not_exists(self):
        """Whether to silently skip this item if source path does not exist on the source server

        :rtype: bool
        """
        return self._data.get('skip_if_source_not_exists', False)


class ForwardingType(object):
    MOVED_PERMANENTLY = 'moved-permanently'
    MOVED_TEMPORARILY = 'moved-temporarily'
    FRAME_FORWARDING = 'frame-forwarding'


def iter_all_subscriptions(data):
    """
    :type data: dict
    :rtype: collections.Iterable[dict]
    """
    for reseller_data in safe_get_list(data, 'resellers'):
        for subscription_data in safe_get_list(reseller_data, 'subscriptions'):
            yield subscription_data
        for customer_data in safe_get_list(reseller_data, 'customers'):
            for subscription_data in safe_get_list(customer_data, 'subscriptions'):
                yield subscription_data
    for customer_data in safe_get_list(data, 'customers'):
        for subscription_data in safe_get_list(customer_data, 'subscriptions'):
            yield subscription_data
    for subscription_data in safe_get_list(data, 'subscriptions'):
        yield subscription_data


def iter_all_applications(data):
    """Iterate over application instances introduced by hosting description

    :type data: dict
    :rtype: list[parallels.plesk.hosting_description.model.HostingDescriptionApplication]
    """
    for application_data in safe_get_list_all(data, 'applications'):
        yield HostingDescriptionApplication(application_data)


def iter_all_database_servers(data):
    for database_server_data in safe_get_list(data, 'database_servers'):
        yield HostingDescriptionDatabaseServer(database_server_data)
