from xml.etree import ElementTree

from parallels.core.registry import Registry
from parallels.core.utils.common import if_not_none
from parallels.core.utils.plesk_cli_runner import PleskCLIRunnerCLI
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.plesk.utils.xml_rpc.plesk.operator import ServicePlanLogRotation, ServicePlanLogRotationSettings, \
    ServicePlanLogRotationConditionBySize, ServicePlanLogRotationConditionByTime, ServicePlanMail, \
    ServicePlanMailNonexistentUserForward, ServicePlanMailNonexistentUserBounce, \
    ServicePlanMailNonexistentUserReject, ServicePlanPreferences, ServicePlanHostingSetting, \
    ServicePlanWebServerSettings, ServicePlanApsPackage, ServicePlanAddonLimits, \
    ServicePlanAddonHostingSetting, ServicePlanAddonApsPackage
from parallels.plesk.models.target_data_model import HostingPlanLogRotationConditionByTime, \
    HostingPlanLogRotationConditionBySize, HostingPlanMailNonexistentUserBounce, \
    HostingPlanMailNonexistentUserForward, HostingPlanMailNonexistentUserReject


class ServicePlanImporter(object):
    """Import service plans and add-ons to target Plesk via API/CLI"""

    def __init__(self):
        self._conn = Registry.get_instance().get_context().conn.target

    def create_service_plan(self, service_plan_name, owner_username, settings):
        """Create service plan with given name, owner and settings in target Plesk

        :type service_plan_name: str
        :type owner_username: str | None
        :type settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        """
        log_rotation = self._create_log_rotation_api_info(settings)
        plesk_api = self._conn.plesk_api()

        plesk_api.send(
            plesk_ops.ServicePlanOperator.Add(
                name=service_plan_name,
                owner_login=owner_username,
                limits=plesk_ops.ServicePlanLimits(
                    overuse=settings.overuse,
                    limits=[
                        plesk_ops.ServicePlanLimit(name, value)
                        for name, value in settings.limits.items()
                    ]
                ),
                permissions=[
                    plesk_ops.ServicePlanPermission(name, value)
                    for name, value in settings.permissions.items()
                ],
                php_settings=None,
                log_rotation=log_rotation,
                mail=self._create_mail_api_info(settings),
                preferences=self._create_preferences_api_info(settings),
                hosting=self._create_hosting_api_info(settings),
                web_server_settings=self._create_web_server_settings_api_info(settings),
            )
        ).check()

        self._create_aps_bundle_filter(owner_username, service_plan_name, settings, plesk_api)
        self._create_custom_plan_items(owner_username, service_plan_name, settings)
        self._configure_default_db_servers(owner_username, service_plan_name, settings)
        self._set_php_settings(owner_username, service_plan_name, settings.php_settings_node, is_addon=False)
        self._set_no_hosting(owner_username, service_plan_name, settings)

    def create_service_plan_addon(self, service_plan_addon_name, owner_username, settings):
        """Create service plan add-on with given name, owner and settings in target Plesk

        :type service_plan_addon_name: str
        :type owner_username: str | None
        :type settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        """
        self._conn.plesk_api().send(
            plesk_ops.ServicePlanAddonOperator.Add(
                name=service_plan_addon_name,
                owner_login=owner_username,
                limits=ServicePlanAddonLimits(
                    overuse=settings.overuse,
                    limits=[
                        plesk_ops.ServicePlanAddonLimit(name, value)
                        for name, value in settings.limits.items()
                    ]
                ),
                permissions=[
                    plesk_ops.ServicePlanAddonPermission(name, value)
                    for name, value in settings.permissions.items()
                ],
                hosting=self._create_addon_hosting_api_info(settings),
                php_settings=None,
            )
        ).check()
        self._create_addon_aps_bundle_filter(owner_username, service_plan_addon_name, settings)
        self._set_php_settings(owner_username, service_plan_addon_name, settings.php_settings_node, is_addon=True)

    @staticmethod
    def _create_log_rotation_api_info(plan_settings):
        """Create API request part from import API object for service plan log rotation

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanLogRotation
        """
        if plan_settings.log_rotation is not None:
            if plan_settings.log_rotation.settings is not None:
                source_log_condition = plan_settings.log_rotation.settings.log_condition
                if isinstance(source_log_condition, HostingPlanLogRotationConditionByTime):
                    log_condition = ServicePlanLogRotationConditionByTime(
                        source_log_condition.time
                    )
                elif isinstance(source_log_condition, HostingPlanLogRotationConditionBySize):
                    log_condition = ServicePlanLogRotationConditionBySize(
                        source_log_condition.size
                    )
                else:
                    assert False
            else:
                log_condition = None

            log_rotation = ServicePlanLogRotation(
                status=plan_settings.log_rotation.status,
                settings=if_not_none(
                    plan_settings.log_rotation.settings,
                    lambda s: ServicePlanLogRotationSettings(
                        log_condition=log_condition,
                        log_max_num_files=s.log_max_num_files,
                        log_compress=s.log_compress,
                        log_email=s.log_email
                    )
                )
            )
        else:
            log_rotation = None

        return log_rotation

    @staticmethod
    def _create_mail_api_info(plan_settings):
        """Create API request part from import API object for service plan mail settings

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanLogRotation
        """
        if plan_settings.mail is None:
            return None

        if plan_settings.mail.nonexistent_user is not None:
            if isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserBounce):
                nonexistent_user = ServicePlanMailNonexistentUserBounce(
                    plan_settings.mail.nonexistent_user.message
                )
            elif isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserForward):
                nonexistent_user = ServicePlanMailNonexistentUserForward(
                    plan_settings.mail.nonexistent_user.address
                )
            elif isinstance(plan_settings.mail.nonexistent_user, HostingPlanMailNonexistentUserReject):
                nonexistent_user = ServicePlanMailNonexistentUserReject()
            else:
                assert False
        else:
            nonexistent_user = None

        return ServicePlanMail(
            nonexistent_user=nonexistent_user,
            webmail=plan_settings.mail.webmail,
        )

    @staticmethod
    def _create_preferences_api_info(plan_settings):
        """Create API request part from import API object for service plan preferences

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanPreferences
        """
        if plan_settings.preferences is None:
            return None

        return ServicePlanPreferences(
            stat=plan_settings.preferences.stat,
            maillists=plan_settings.preferences.maillists,
            mailservice=plan_settings.preferences.mailservice,
            dns_zone_type=plan_settings.preferences.dns_zone_type,
        )

    @staticmethod
    def _create_hosting_api_info(plan_settings):
        """Create API request part from import API object for service plan hosting settings

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: list[parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanHostingSetting] | None
        """
        if plan_settings.hosting is None:
            return None

        return [
            ServicePlanHostingSetting(hosting.name, hosting.value)
            for hosting in plan_settings.hosting
        ]

    @staticmethod
    def _create_addon_hosting_api_info(plan_settings):
        """Create API request part from import API object for service plan hosting settings

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: list[parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanAddonHostingSetting] | None
        """
        if plan_settings.hosting is None:
            return None

        return [
            ServicePlanAddonHostingSetting(hosting.name, hosting.value)
            for hosting in plan_settings.hosting
            # workaround for PPP-18669: plan creation may fail with the following error if "php" is set to "false"
            # "PHP handler id cannot be specified because PHP support is not enabled"
            if (hosting.name, hosting.value) != ('php', 'false')
        ]

    @staticmethod
    def _create_web_server_settings_api_info(plan_settings):
        """Create API request part from import API object for service plan web server settings

        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: parallels.plesk.utils.xml_rpc.plesk.operator.service_plan.ServicePlanWebServerSettings
        """
        if plan_settings.web_server_settings is None:
            return None

        restrict_follow_symlinks = None
        if plan_settings.web_server_settings.restrict_follow_symlinks is not None:
            if plan_settings.web_server_settings.restrict_follow_symlinks == 'true':
                restrict_follow_symlinks = True
            elif plan_settings.web_server_settings.restrict_follow_symlinks == 'false':
                restrict_follow_symlinks = False

        nginx_proxy_mode = None
        if plan_settings.web_server_settings.nginx_proxy_mode is not None:
            if plan_settings.web_server_settings.nginx_proxy_mode == 'true':
                nginx_proxy_mode = True
            elif plan_settings.web_server_settings.nginx_proxy_mode == 'false':
                nginx_proxy_mode = False

        return ServicePlanWebServerSettings(
            additional=plan_settings.web_server_settings.additional,
            additional_ssl=plan_settings.web_server_settings.additional_ssl,
            additional_nginx=plan_settings.web_server_settings.additional_nginx,
            restrict_follow_symlinks=restrict_follow_symlinks,
            nginx_proxy_mode=nginx_proxy_mode
        )

    @staticmethod
    def _create_aps_bundle_filter(owner_login, plan_name, plan_settings, plesk_api):
        """
        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: None
        """
        if plan_settings.aps_bundle_filter is None:
            return

        if plan_settings.aps_bundle_filter.type != 'white':
            # Other types of APS filters are not supported
            return

        api_filter = plesk_ops.ServicePlanOperator.FilterByName([plan_name])
        enable_aps_filter_request = plesk_ops.ServicePlanOperator.EnableApsFilter(
            filter=api_filter, owner_login=owner_login
        )
        plesk_api.send(enable_aps_filter_request).check()

        if len(plan_settings.aps_bundle_filter.items) > 0:
            add_package_request = plesk_ops.ServicePlanOperator.AddPackage(
                filter=api_filter,
                owner_login=owner_login,
                packages=[
                    ServicePlanApsPackage(name=package.name, value=package.value)
                    for package in plan_settings.aps_bundle_filter.items
                ]
            )
            plesk_api.send(add_package_request).check()

    def _create_addon_aps_bundle_filter(self, owner_login, plan_name, plan_settings):
        """
        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: None
        """
        if plan_settings.aps_bundle_filter is None or len(plan_settings.aps_bundle_filter.items) == 0:
            return

        if plan_settings.aps_bundle_filter.type != 'white':
            # Other types of APS filters are not supported
            return

        plesk_api = self._conn.plesk_api()
        api_filter = plesk_ops.ServicePlanOperator.FilterByName([plan_name])
        request = plesk_ops.ServicePlanAddonOperator.AddPackage(
            filter=api_filter,
            owner_login=owner_login,
            packages=[
                ServicePlanAddonApsPackage(name=package.name, value=package.value)
                for package in plan_settings.aps_bundle_filter.items
            ]
        )
        plesk_api.send(request).check()

    def _set_php_settings(self, owner_login, plan_name, php_settings_node, is_addon):
        """
        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :rtype: None
        """
        if php_settings_node is None:
            return

        migration_server = Registry.get_instance().get_context().migrator_server
        remote_plan_settings_file = self._conn.plesk_server.get_session_file_path('plan_php_settings.xml')
        local_plan_settings_file = migration_server.get_session_file_path('plan_php_settings.xml')
        file_contents = ElementTree.tostring(php_settings_node, 'utf-8', 'xml')
        args = [
            '--set-php-settings', plan_name,
            '-settings', remote_plan_settings_file
        ]
        if owner_login is not None:
            args.extend(['-owner', owner_login])

        with migration_server.runner() as runner:
            runner.upload_file_content(
                local_plan_settings_file, file_contents
            )

        with self._conn.plesk_server.runner() as runner:
            runner.upload_file(local_plan_settings_file, remote_plan_settings_file)
            command = 'domain_template' if not is_addon else 'domain_addon_service_plan'
            plesk_cli_runner = PleskCLIRunnerCLI(self._conn.plesk_server)
            plesk_cli_runner.run(command, args)
            runner.remove_file(remote_plan_settings_file)

        with migration_server.runner() as runner:
            runner.remove_file(local_plan_settings_file)

    def _create_custom_plan_items(self, owner_login, plan_name, plan_settings):
        """Create custom plan items (also known as "Additional Services" in Plesk) for specified plan

        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: None
        """
        for custom_plan_item in plan_settings.custom_plan_items:
            if not self._custom_plan_item_exists(custom_plan_item):
                self._create_custom_plan_item(custom_plan_item)
            self._assign_template_to_custom_plan_item(owner_login, plan_name, custom_plan_item)

    def _custom_plan_item_exists(self, custom_plan_item):
        """Check if custom plan item exists on target

        :type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
        :rtype: bool
        """
        existing_plan_items_list = self._conn.plesk_cli_runner.run(
            'custom_plan_items', ['--list']
        )
        # Simply check presence of GUID in list command output. There should be better way.
        return custom_plan_item.guid in existing_plan_items_list

    def _create_custom_plan_item(self, custom_plan_item):
        """Create custom plan item in target Plesk

        :type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
        :rtype: None
        """
        arguments = [
            '--add',
            custom_plan_item.name,
            '-uuid', custom_plan_item.guid,
            '-visible', custom_plan_item.visible,
        ]
        if custom_plan_item.description is not None:
            arguments.extend([
                '-label', custom_plan_item.description,
            ])
        if custom_plan_item.hint is not None:
            arguments.extend([
                '-tooltip', custom_plan_item.hint,
            ])
        if custom_plan_item.url is not None:
            arguments.extend([
                '-url', custom_plan_item.url
            ])
        if custom_plan_item.options is not None:
            arguments.extend([
                '-url-components', ','.join(self._get_custom_plan_item_url_components(custom_plan_item))
            ])

        self._conn.plesk_cli_runner.run(
            'custom_plan_items', arguments
        )

    @staticmethod
    def _get_custom_plan_item_url_components(custom_plan_item):
        """Get URL component names list from numeric value from backup

        :type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
        :rtype: Iterable[str | unicode]
        """
        # constants from pmm_deploy.h
        url_component_constants = dict(
            CUSTOM_BUTTON_DOM_ID=1,
            CUSTOM_BUTTON_CL_ID=2,
            CUSTOM_BUTTON_CNAME=4,
            CUSTOM_BUTTON_PNAME=8,
            CUSTOM_BUTTON_EMAIL=16,
            CUSTOM_BUTTON_DOM_NAME=32,
            CUSTOM_BUTTON_INTERNAL=256,
            CUSTOM_BUTTON_FTP_USER=512,
            CUSTOM_BUTTON_FTP_PASS=1024,
        )

        option_url_components = dict(
            CUSTOM_BUTTON_DOM_ID='dom_id',
            CUSTOM_BUTTON_DOM_NAME='dom_name',
            CUSTOM_BUTTON_FTP_USER='ftp_user',
            CUSTOM_BUTTON_FTP_PASS='ftp_pass',
            CUSTOM_BUTTON_CL_ID='cl_id',
            CUSTOM_BUTTON_CNAME='cname',
            CUSTOM_BUTTON_PNAME='pname',
            CUSTOM_BUTTON_EMAIL='email'
        )

        url_components = []
        for option_id, url_component_name in option_url_components.items():
            if url_component_constants[option_id] & custom_plan_item.options != 0:
                url_components.append(url_component_name)

        return url_components

    def _assign_template_to_custom_plan_item(self, owner_login, plan_name, custom_plan_item):
        """Assign service template to  custom plan item in target Plesk

        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :type custom_plan_item: parallels.plesk.models.target_data_model.HostingPlanCustomItem
        :rtype: None
        """
        args = [
            '--add-custom-plan-item', plan_name,
            '-custom-plan-item-name', custom_plan_item.name
        ]
        if owner_login is not None:
            args.extend(['-owner', owner_login])
        self._conn.plesk_cli_runner.run('domain_template', args)

    def _configure_default_db_servers(self, owner_login, plan_name, plan_settings):
        """
        :type owner_login: str | unicode | None
        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: None
        """
        if plan_settings.default_db_servers is None:
            return

        available_plesk_servers = {
            (server.dbtype, server.host, str(server.port)): server.id
            for server in self._conn.plesk_server.get_db_servers().values()
        }
        for default_db_server in plan_settings.default_db_servers:
            server_key = (default_db_server.type, default_db_server.host, str(default_db_server.port))
            if server_key in available_plesk_servers:
                args = [
                    '--update', plan_name,
                    '-default_server_%s' % default_db_server.type,
                    '%s:%s' % (default_db_server.host, default_db_server.port)
                ]
                if owner_login is not None:
                    args.extend(['-owner', owner_login])

                self._conn.plesk_cli_runner.run('domain_template', args)

    def _set_no_hosting(self, owner_login, plan_name, plan_settings):
        """Configure plan that have no virtual hosting

        :type owner_login: str | unicode | None
        :type plan_name: str | unicode
        :type plan_settings: parallels.plesk.models.target_data_model.PleskPlanSettings
        :rtype: None
        """
        if plan_settings.virtual_hosting_enabled:
            return

        args = [
            '--update', plan_name,
            '-hosting', 'false'
        ]
        if owner_login is not None:
            args.extend(['-owner', owner_login])
        self._conn.plesk_cli_runner.run('domain_template', args)
