import random
import re
import string
import sys
import threading

from parallels.core import MigrationError, MigrationNoRepeatError
from parallels.core.logging import get_logger
from parallels.core.hosting_repository.subscription import SubscriptionModel, SubscriptionEntity
from parallels.core.registry import Registry
from parallels.core.runners.exceptions.non_zero_exit_code import NonZeroExitCodeException
from parallels.core.utils.common import default, safe_format, safe_string_repr
from parallels.core.utils.common_constants import ADMIN_USERNAME
from parallels.core.utils.mysql import escape_args_list
from parallels.plesk import messages
from parallels.plesk.hosting_repository.base import PleskBaseModel
from parallels.plesk.hosting_repository.utils.cli.domain import DomainUpdateUsernameCli
from parallels.plesk.hosting_repository.utils.cli.ip_pool import IpPoolAddCli
from parallels.plesk.hosting_repository.utils.cli.repair import RepairSubscriptionSecurityCli
from parallels.plesk.hosting_repository.utils.cli.subscription import SubscriptionEnableHostingCli, \
    SubscriptionSetOwnerCli, SubscriptionCreateCli, SubscriptionAddServicePlan, SubscriptionSyncCli, \
    SubscriptionSettingsUpdateCli, SubscriptionExternalIdCli, SubscriptionRemoveCli
from parallels.plesk.target_data_model import PleskTargetSubscriptionData
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.plesk.utils.xml_rpc.plesk.core import PleskError

logger = get_logger(__name__)


class PleskSubscriptionModel(SubscriptionModel, PleskBaseModel):
    UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_ALREADY_EXIST = (
        'user {username} already exists'
    )
    UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_INPROPER_VALUE = (
        'Some fields are empty or contain an improper value'
    )
    CREATE_SUBSCRIPTION_CLI_ERROR_MARKER_DOMAIN_ALREADY_EXIST = (
        'Domain with name ".*" already exists'
    )

    def get_list(self, filter_name=None, filter_owner_id=None):
        """Retrive list of subscription from target Plesk by given filter

        :type filter_name: list[str] | None
        :type filter_owner_id: list[int] | None
        :rtype: list[parallels.core.hosting_repository.subscription.SubscriptionEntity]
        """
        if filter_name is not None and len(filter_name) == 0:
            #  filter by name is empty, so no one subscription could be retrieved
            return []

        if filter_owner_id is not None and len(filter_owner_id) == 0:
            #  filter by owner_id is empty, so no one subscription could be retrieved
            return []

        subscriptions = {}

        # retrieve list of subscriptions
        query = """
            SELECT
                Subscriptions.id as subscription_id,
                domains.id as domain_id,
                domains.name as name,
                domains.status as status,
                domains.htype as hosting_type,
                domains.vendor_id as owner_id
            FROM
                Subscriptions JOIN domains ON Subscriptions.object_id = domains.id
            WHERE
                Subscriptions.object_type = "domain" AND domains.webspace_id = 0
        """
        query_args = {}
        if filter_name is not None:
            filter_name_placeholders, filter_name_values = escape_args_list(filter_name, 'name')
            query += ' AND domains.name in ({filter_name_placeholders_str})'.format(
                filter_name_placeholders_str=', '.join(filter_name_placeholders)
            )
            # idn domains stored in database in idna encoding, so perform encoding of given filter
            query_args.update({key: value.encode('idna') for key, value in filter_name_values.items()})
        if filter_owner_id is not None:
            filter_owner_id_placeholders, filter_owner_id_values = escape_args_list(filter_owner_id, 'owner_id')
            query += ' AND domains.vendor_id in ({filter_owner_id_placeholders_str})'.format(
                filter_owner_id_placeholders_str=', '.join(filter_owner_id_placeholders)
            )
            query_args.update(filter_owner_id_values)

        rows = self.plesk_server.query_panel_db(query, query_args)
        for row in rows:
            subscription_id = row['subscription_id']
            if sys.version_info.major >= 3:
                name = bytes(row['name'], 'ascii').decode('idna')
            else:
                name = row['name'].decode('idna')
            subscriptions[subscription_id] = SubscriptionEntity(
                subscription_id,
                row['domain_id'], name, row['status'], row['hosting_type'], row['owner_id']
            )

        if len(subscriptions) == 0:
            # no one subscription found, so return
            return []

        # retrieve list of service plans, associated with just retrieved subscriptions
        query = """
            SELECT
                Subscriptions.id as subscription_id,
                Templates.id as service_plan_id
            FROM
                Subscriptions JOIN
                PlansSubscriptions ON Subscriptions.id = PlansSubscriptions.subscription_id JOIN
                Templates ON PlansSubscriptions.plan_id = Templates.id AND Templates.type = "domain"
            WHERE
                1
        """
        query_args = {}
        filter_subscription_id_placeholders, filter_subscription_id_values = escape_args_list(
            subscriptions.keys(),
            'id'
        )
        query += ' AND Subscriptions.id in ({filter_subscription_id_placeholders_str})'.format(
            filter_subscription_id_placeholders_str=', '.join(filter_subscription_id_placeholders)
        )
        query_args.update(filter_subscription_id_values)

        rows = self.plesk_server.query_panel_db(query, query_args)
        for row in rows:
            subscription_id = row['subscription_id']
            if subscription_id not in subscriptions:
                continue
            subscriptions[subscription_id].service_plan_id = row['service_plan_id']

        return subscriptions.values()

    def is_exists(self, subscription_name, guid=None):
        """Check if subscription with given name or guid exists in target Plesk

        :type subscription_name: str
        :type guid: str | None
        :rtype: bool
        """
        if not guid:
            result = self.plesk_server.query_panel_db(
                """
                    SELECT id
                    FROM domains
                    WHERE webspace_id = 0 AND name = %(subscription_name)s
                """,
                dict(subscription_name=subscription_name.encode('idna'))
            )
        else:
            result = self.plesk_server.query_panel_db(
                """
                    SELECT id
                    FROM domains
                    WHERE webspace_id = 0 AND (name = %(subscription_name)s or guid = %(guid)s)
                """,
                dict(
                    subscription_name=subscription_name.encode('idna'),
                    guid=guid
                )
            )
        return len(result) > 0

    def create(self, subscription, owner, reseller, is_enable_hosting):
        """Create given subscription in target Plesk

        Returns list of non-critical errors (warnings).

        :type subscription: parallels.core.target_data_model.Subscription
        :type owner: parallels.core.target_data_model.Client | parallels.core.target_data_model.Reseller
        :type reseller: parallels.core.target_data_model.Reseller
        :type is_enable_hosting: bool
        :rtype: list[str | unicode]
        """
        target_data = subscription.target_data
        if isinstance(target_data, PleskTargetSubscriptionData):
            ips = [target_data.web_ip, target_data.web_ipv6]
            ips_with_type = [
                (target_data.web_ip, target_data.web_ip_type),
                (target_data.web_ipv6, target_data.web_ipv6_type)
            ]
        else:
            raise TypeError()

        if reseller is not None:
            # add ip-addresses into ip pool of given reseller
            for ip_address, ip_type in ips_with_type:
                if ip_address is None:
                    continue
                command = IpPoolAddCli(self.plesk_cli_runner, ip_address, ip_type, reseller.login)
                try:
                    command.run()
                except NonZeroExitCodeException as e:
                    if e.exit_code == 2:  # ip address already in pool
                        pass
                    else:
                        logger.exception()
                        raise MigrationError(
                            messages.FAILED_TO_ADD_IP_TO_RESELLER_POOL % (
                                ip_address, reseller.login, e.stdout, e.stderr
                            )
                        )

        random_username = 'sub_%s' % ''.join(random.choice(string.digits) for _ in range(10))
        if subscription.sysuser_login is None:
            actual_subscription_username = random_username
        else:
            actual_subscription_username = subscription.sysuser_login

        def _create_command(subscription_username):
            return SubscriptionCreateCli(
                runner=self.plesk_cli_runner,
                subscription_name=subscription.name,
                subscription_username=subscription_username,
                is_enable_hosting=is_enable_hosting,
                ips=ips,
                owner_username=owner.login,
                service_plan_name=subscription.plan_name,
                admin_description=subscription.admin_description,
                reseller_description=subscription.reseller_description,
                guid=subscription.guid,
                creation_date=subscription.creation_date,
                mail_service=subscription.mail_service
            )

        def _process_command(_command, is_allow_retry=False):
            """
            :type _command: parallels.plesk.hosting_repository.utils.cli.base.BaseCli
            :type is_allow_retry: bool
            """
            try:
                _command.run()
            except NonZeroExitCodeException as e:
                # process error
                if re.search(self.CREATE_SUBSCRIPTION_CLI_ERROR_MARKER_DOMAIN_ALREADY_EXIST, e.stderr) is not None:
                    # domain with given name already exist so no way to continue
                    raise MigrationNoRepeatError(messages.SUBSCRIPTION_WAS_ALREADY_CREATED)
                elif is_allow_retry:
                    # process issues which could be solved via rerun with differ options
                    if self.UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_ALREADY_EXIST.format(
                        username=actual_subscription_username.lower()
                    ) in e.stderr.lower() and subscription.sysuser_login is not None:
                        # if system user with given username already exists then retry operation
                        # with randomly generated username
                        return _process_command(_create_command(random_username))
                    else:
                        raise
                else:
                    raise

        try:
            _process_command(_create_command(actual_subscription_username), True)
        except Exception as e:
            logger.exception()
            raise MigrationError(safe_format(
                messages.CREATE_SUBSCRIPTION_ERROR,
                subscription_name=subscription.name, reason=e
            ))

        # assign subscription with service plan add-ons
        owner_username = reseller.login if (reseller is not None and reseller.login is not None) else ADMIN_USERNAME
        service_plan_addons = Registry.get_instance().get_context().hosting_repository.service_plan_addon.get_list(
            filter_name=subscription.plan_addon_names, filter_owner_username=[owner_username]
        )
        for service_plan_addon in service_plan_addons:
            command = SubscriptionAddServicePlan(self.plesk_cli_runner, subscription.name, service_plan_addon.name)
            command.run()

        return []

    def set_username(self, subscription_name, username):
        """Set username of system user associated with given subscription in target panel

        :type subscription_name: str
        :type username: str
        """
        command = DomainUpdateUsernameCli(self.plesk_cli_runner, subscription_name, username)
        if Registry.get_instance().get_context().conn.target.is_windows:
            exit_code, _, stderr = command.run(True)
        else:
            with threading.Lock():
                # there is a problem in Plesk for Unix which could lead to incorrect Apache configs
                # if we run change system user login in parallel, so we don't allow two threads
                # to change system user login at the same time
                exit_code, _, stderr = command.run(True)

        if exit_code != 0:
            if self.UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_ALREADY_EXIST.format(
                username=username.lower()
            ) in stderr.lower():
                raise UserAlreadyExistsException()
            elif (
                self.UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_INPROPER_VALUE in stderr and
                u"'login'" in stderr
            ):
                raise LoginIsInvalidException()
            else:
                raise MigrationError(messages.UPDATE_SUBSCRIPTION_SET_USERNAME_ERROR.format(
                    subscription_name=subscription_name,
                    username=username
                ))

    def set_owner(self, subscription_name, owner_username):
        """Set owner of subscription with given name in target Plesk

        :type subscription_name: str
        :type owner_username: str
        """
        command = SubscriptionSetOwnerCli(self.plesk_cli_runner, subscription_name, owner_username)
        command.run()

    def set_limits(
        self, subscription_name,
        max_domains=None, max_subdomains=None, max_domain_aliases=None, max_databases=None, max_mssql_databases=None,
        max_mail_accounts=None
    ):
        """Set limits for given subscription

        :type subscription_name: str
        :type max_domains: int | None
        :type max_subdomains: int | None
        :type max_domain_aliases: int | None
        :type max_databases: int | None
        :type max_mssql_databases: int | None
        :type max_mail_accounts: int | None
        """
        command = SubscriptionSettingsUpdateCli(
            self.plesk_cli_runner, subscription_name,
            max_domains, max_subdomains, max_domain_aliases, max_databases, max_mssql_databases, max_mail_accounts
        )
        command.run()

    def set_external_id(self, subscription_name, external_id):
        """Set External ID for given subscription in target Plesk

        :type subscription_name: str
        :type external_id: str
        """
        command = SubscriptionExternalIdCli(self.plesk_cli_runner, subscription_name, external_id)
        command.run()

    def enable_virtual_hosting(self, subscription):
        """Enable virtual hosting for subscription with given name in target Plesk

        :type subscription: parallels.core.target_data_model.Subscription
        """
        random_username = 'sub_%s' % ''.join(random.choice(string.digits) for _ in range(10))
        actual_subscription_username = default(subscription.sysuser_login, random_username)

        target_data = subscription.target_data
        if isinstance(target_data, PleskTargetSubscriptionData):
            ips = [target_data.web_ip, target_data.web_ipv6]
        else:
            raise TypeError()

        def _create_command(subscription_username):
            return SubscriptionEnableHostingCli(
                self.plesk_cli_runner, subscription.name, subscription_username, ips
            )

        def _process_command(command, is_allow_retry=False):
            exit_code, _, stderr = command.run(True)
            if exit_code == 0:
                return True
            # process error
            if is_allow_retry:
                # process issues which could be solved via rerun with differ options
                if self.UPDATE_SUBSCRIPTION_SET_USERNAME_CLI_ERROR_MARKER_ALREADY_EXIST.format(
                    actual_subscription_username.lower()
                ) in stderr.lower() and subscription.sysuser_login is not None:
                    # if system user with given username already exists then retry operation
                    # with randomly generated username
                    return _process_command(_create_command(random_username))
            return False

        if not _process_command(_create_command(actual_subscription_username), True):
            raise MigrationError(messages.UPDATE_SUBSCRIPTION_ENABLE_HOSTING_ERROR.format(
                subscription_name=subscription.name
            ))

    def sync(self, subscription_name):
        """Sync subscription with given name with associated service plan in target Plesk

        :type subscription_name: str
        """
        try:
            # try to perform synchronization via API because in this case
            # we have a chance to catch errors if any
            Registry.get_instance().get_context().conn.target.plesk_api().send(
                plesk_ops.SubscriptionOperator.SyncSubscription(
                    filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name])
                )
            ).check()
        except PleskError as e:
            if (
                e.code == 1023 and
                'the component' in safe_string_repr(e).lower() and
                'was not installed' in safe_string_repr(e).lower()
            ):
                # synchronization via API failed and we have an explanation whats going wrong,
                # so let's force synchronization via CLI and then display an error
                command = SubscriptionSyncCli(self.plesk_cli_runner, subscription_name)
                command.run()
                raise SubscriptionCannotBeSyncedWithPlan(
                    messages.SUBSCRIPTION_CANNOT_BE_SYNCED_WITH_PLAN_MISSING_COMPONENT
                )
            else:
                raise

    def get_dedicated_application_pool_user(self, subscription_name):
        """Retrieve name of dedicated application pool user for given subscription on target Plesk

        :type subscription_name: str
        """
        if not Registry.get_instance().get_context().conn.target.is_windows:
            raise NotImplementedError(messages.UNIX_SUBSCRIPTION_GET_DEDICATED_APP_POOL_USER_NOT_APPLICABLE)

        query = """
            SELECT identity
            FROM domains JOIN IisAppPools ON IisAppPools.ownerType = 'domain' AND IisAppPools.ownerId = domains.id
            WHERE domains.name = %(subscription_name)s
        """
        rows = self.plesk_server.query_panel_db(query, dict(subscription_name=subscription_name))
        if len(rows) < 1:
            return None
        return rows[0]['identity']

    def update_security(self, subscription_name):
        """Actualize security metadata for given subscription in target Plesk

        :type subscription_name: str
        """
        if not Registry.get_instance().get_context().conn.target.is_windows:
            raise NotImplementedError(messages.UNIX_SUBSCRIPTION_REPAIR_WEBSPACE_NOT_APPLICABLE)

        command = RepairSubscriptionSecurityCli(self.plesk_cli_runner, subscription_name)
        command.run()

    def remove(self, subscription_name):
        """Remove subscription from the server

        :type subscription_name: str | unicode
        """
        command = SubscriptionRemoveCli(self.plesk_cli_runner, subscription_name)
        command.run()


class UserAlreadyExistsException(Exception):
    pass


class LoginIsInvalidException(Exception):
    pass


class SubscriptionCannotBeSyncedWithPlan(Exception):
    pass
