from parallels.core.logging import get_logger
from parallels.core.subscription_target_info import TargetServices, SubscriptionServiceIPs, TargetServers
from parallels.core.utils import plesk_api_utils
from parallels.core.utils.plesk_api_utils import request_single_optional_item
from parallels.core.utils.plesk_utils import get_php_handlers
from parallels.plesk import messages
from parallels.core import MigrationNoContextError
from parallels.core.utils.common import cached, format_list
from parallels.core.target_panel_base import TargetPanelBase, TargetServiceInfo
from parallels.core.utils.plesk_db_credentials_fetcher import PleskDbCredentialsFetcher
from parallels.plesk.connections.database_target_server import PleskDatabaseTargetServer
from parallels.plesk.converter.adapter.client_subscription_converter import \
    TargetPleskClientSubscriptionConverterAdapter
from parallels.plesk.converter.adapter.reseller_plan_converter import PleskResellerPlanConverterAdapter
from parallels.plesk.converter.hosting_plan import PleskHostingPlanAdapter
from parallels.plesk.hosting_repository.model import PleskHostingRepositoryModel
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.operator.subscription import SubscriptionHostingNone
from parallels.plesk.connections.target_connections import PleskTargetConnections

logger = get_logger(__name__)


class Panel(TargetPanelBase):
    @property
    def name(self):
        return messages.TARGET_PANEL_PLESK_TITLE

    def has_custom_subscriptions_feature(self):
        """Whether subscriptions not assigned to plan are allowed

        :rtype: bool
        """
        return True

    def has_admin_reseller_plan_feature(self):
        """Whether admin reseller plans are supported for that target panel migration

        :rtype: bool
        """
        return True

    def has_admin_subscriptions_feature(self):
        """Whether subscriptions assigned directly to admin are allowed

        :rtype: bool
        """
        return True

    def has_reseller_subscriptions_feature(self):
        """Whether subscriptions assigned directly to reseller are allowed

        :rtype: bool
        """
        return True

    def has_subscriptions_only(self, global_context):
        """Whether panel have subscriptions only, but not clients, resellers, plans, etc

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: bool
        """
        return global_context.conn.target.plesk_server.is_power_user_mode

    def has_resellers(self, global_context):
        """Whether panel has resellers

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: bool
        """
        return global_context.conn.target.plesk_server.can_manage_resellers

    def has_dns_forwarding(self):
        """Whether panel should support DNS forwarding migration feature

        :rtype: bool
        """
        return True

    def is_test_dns_forwarding(self):
        """Whether to test DNS forwarding feature on post-migration checks

        :rtype: bool
        """
        # Do not test DNS forwarding, as it is experimental not documented feature
        # and should not used by customers by default, so we should not force testing it.
        # Better for user, but more complex solution is to store domains for which we set DNS forwarding
        # and check only that domains
        return False

    def is_encrypted_passwords_supported(self):
        """Whether encrypted passwords (hashes) are supported when creating resellers and clients

        :rtype: bool
        """
        return True

    @cached
    def get_subscription_nodes(self, global_context, subscription_name):
        """Get servers of subscription on target panel

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription_name: str | unicode
        :rtype: parallels.core.subscription_target_info.TargetServers
        """
        plesk_server = self.get_subscription_plesk_node(global_context, subscription_name)
        subscription_target_services = self.get_subscription_target_services(global_context, subscription_name)

        database_servers = {}

        for db_type, db_params in subscription_target_services.db_servers.items():
            if db_params is None:
                continue

            login, password = PleskDbCredentialsFetcher.get_instance().get(
                plesk_server, db_type, db_params
            )

            database_servers[db_type] = PleskDatabaseTargetServer(
                db_type, db_params.host, db_params.port,
                login, password, plesk_server
            )

        if global_context.conn.target.remote_smarter_mail_server is None:
            mail_server = plesk_server
        else:
            mail_server = global_context.conn.target.remote_smarter_mail_server

        return TargetServers(
            web=plesk_server,
            mail=mail_server,
            database=database_servers,
            dns=[plesk_server]
        )

    @cached
    def get_subscription_target_services(self, global_context, subscription_name):
        """Get location of subscriptions's hosting services on target panel

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription_name: str | unicode
        :rtype: parallels.core.subscription_target_info.TargetServices
        """
        plesk_server = self.get_subscription_plesk_node(global_context, subscription_name)
        plesk_api = global_context.conn.target.plesk_server.plesk_api()

        request = plesk_ops.SubscriptionOperator.Get(
            plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
            [
                plesk_ops.SubscriptionOperator.Dataset.GEN_INFO,
                plesk_ops.SubscriptionOperator.Dataset.HOSTING,
                plesk_ops.SubscriptionOperator.Dataset.MAIL,
            ]
        )

        subscription_info_response = request_single_optional_item(plesk_api, request)

        if subscription_info_response is None:
            # Subscription was not created on target Plesk yet. Predict services location by service template.
            return self.predict_subscription_target_services(global_context, subscription_name)

        subscription_info = subscription_info_response[1]
        hosting = subscription_info.hosting

        raw_dump = global_context.get_subscription(subscription_name).raw_dump
        if raw_dump is not None and isinstance(hosting, SubscriptionHostingNone) and raw_dump.hosting_type != 'none':
            # Subscription has no virtual hosting and has no information about services location.
            # Wa may enable hosting during migration, so here we predict target services location by service template.
            return self.predict_subscription_target_services(global_context, subscription_name)

        db_servers_info = plesk_api_utils.get_subscription_db_servers(plesk_api, subscription_name)
        db_servers = {
            server.server_type: global_context.conn.target.plesk_server.get_db_servers().get(server.server_id)
            for server in db_servers_info[1]
        }

        if isinstance(hosting, (
            plesk_ops.SubscriptionHostingVirtual,
            plesk_ops.SubscriptionHostingStandardForwarding,
            plesk_ops.SubscriptionHostingFrameForwarding
        )):
            web_ips = SubscriptionServiceIPs(v4=hosting.ip_addresses.v4, v6=hosting.ip_addresses.v6)
        else:
            web_ips = SubscriptionServiceIPs()

        mail_info = subscription_info.mail

        if global_context.conn.target.remote_smarter_mail_ip is None:
            if mail_info is not None:
                mail_ips = SubscriptionServiceIPs(v4=mail_info.ip_addresses.v4, v6=mail_info.ip_addresses.v6)
            else:
                mail_ips = SubscriptionServiceIPs()
        else:
            mail_ips = SubscriptionServiceIPs(v4=global_context.conn.target.remote_smarter_mail_ip, v6=None)

        return TargetServices(
            web_ips=web_ips,
            mail_ips=mail_ips,
            db_servers=db_servers,
            dns_ips=[SubscriptionServiceIPs(v4=plesk_server.ip(), v6=None)]
        )

    def predict_subscription_target_services(self, global_context, subscription_name):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type subscription_name: str | unicode
        :rtype: parallels.core.subscription_target_info.TargetServices
        """
        subscription_database_nodes = {}

        subscription = global_context.get_subscription(subscription_name)

        service_plan = global_context.hosting_repository.service_plan.get_by_name(
            subscription.model.plan_name,
            None if subscription.model_reseller is None else subscription.model_reseller.login
        )

        plesk_server = self._get_plesk_target_server(global_context.conn.target)

        database_server_types = {database.dbtype for database in subscription.raw_dump.iter_all_databases()}

        for database_server_type in database_server_types:
            target_database_server = None
            # try to detect target database server based on database server details,
            # specified as default for target service template
            if service_plan is not None:
                default_database_server = service_plan.get_database_server(database_server_type)
                if default_database_server is not None and plesk_server.has_database_server(
                    default_database_server.host,
                    default_database_server.type
                ):
                    target_database_server = plesk_server.get_database_server(
                        default_database_server.host,
                        default_database_server.type
                    )
            # try to detect target database server based on target panel database servers settings
            if target_database_server is None:
                target_database_server = plesk_server.get_default_database_server(database_server_type)
            # raise exception, if unable to detect target database server
            if target_database_server is None:
                raise Exception(messages.UNABLE_TO_FIND_DB_SERVER_ON_TARGET_ISSUE % database_server_type)

            subscription_database_nodes[target_database_server.dbtype] = target_database_server

        target_subscription_data = subscription.model.target_data

        if isinstance(target_subscription_data, PleskTargetSubscriptionData):
            web_ipv4 = target_subscription_data.web_ip
            web_ipv6 = target_subscription_data.web_ipv6
        else:
            web_ipv4 = None
            web_ipv6 = None

        web_ips = SubscriptionServiceIPs(v4=web_ipv4, v6=web_ipv6)

        return TargetServices(
            web_ips=web_ips,
            mail_ips=web_ips,
            db_servers=subscription_database_nodes,
            dns_ips=plesk_server.ip(),
        )

    def get_subscription_plesk_node(self, global_context, subscription_name):
        return self._get_plesk_target_server(global_context.conn.target)

    def get_service_nodes(self, conn):
        service_nodes = [
            TargetServiceInfo(
                service_type='web',
                node=self._get_plesk_target_server(conn)
            ),
            TargetServiceInfo(
                service_type='mail',
                node=self._get_plesk_target_server(conn)
            ),
            TargetServiceInfo(
                service_type='dns',
                node=self._get_plesk_target_server(conn)
            )
        ]
        for result in conn.plesk_api().send(
            plesk_ops.DbServerOperator.Get(
                filter=plesk_ops.DbServerOperator.FilterAll()
            )
        ):
            # We can not use the database server until credentials is not
            # set. Example: for postgresql credentials don't set after
            # install.
            if result.data.status == 'CREDENTIALS_NOT_SET':
                continue
            # Check only local database server.
            if result.data.local:
                service_nodes.append(
                    TargetServiceInfo(
                        service_type=result.data.dbtype,
                        node=self._get_plesk_target_server(conn)
                    )
                )
        return service_nodes

    def get_connections(self, global_context):
        """Get target panel connections

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        return PleskTargetConnections(global_context.config)

    def get_hosting_repository(self, global_context):
        """Retrive hosting repository of target Plesk

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: parallels.plesk.hosting_repository.model.PleskHostingRepositoryModel
        """
        return PleskHostingRepositoryModel(self.get_connections(global_context).plesk_server)

    def get_converter_adapter(self):
        """
        :rtype: parallels.plesk.converter.adapter.client_subscription_converter.TargetPleskClientSubscriptionConverterAdapter
        """
        return TargetPleskClientSubscriptionConverterAdapter()

    def get_reseller_plan_converter_adapter(self):
        """
        :rtype: parallels.plesk.converter.adapter.reseller_plan_converter.PleskResellerPlanConverterAdapter
        """
        return PleskResellerPlanConverterAdapter()

    def get_hosting_plan_adapter(self, global_context):
        """Retrieve hosting plan adapter

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: parallels.plesk.converter.hosting_plan.PleskHostingPlanAdapter
        """
        plesk_server = global_context.conn.target.plesk_server
        plesk_api = plesk_server.plesk_api()

        allowed_limits = plesk_api_utils.get_service_template_allowed_limits(plesk_api)
        logger.debug(messages.CONVERTER_PLAN_ALLOWED_HOSTING_PLAN_LIMITS.format(
            hosting_plan_limits=format_list(allowed_limits)
        ))

        allowed_permissions = plesk_api_utils.get_service_template_allowed_permissions(plesk_api)
        logger.debug(messages.CONVERTER_PLAN_ALLOWED_HOSTING_PLAN_PERMISSIONS.format(
            hosting_plan_permissions=format_list(allowed_permissions)
        ))

        allowed_settings = plesk_api_utils.get_service_template_allowed_hosting_settings(plesk_api)
        logger.debug(messages.CONVERTER_PLAN_ALLOWED_HOSTING_PLAN_SETTINGS.format(
            hosting_plan_settings=format_list(allowed_settings)
        ))

        plesk_version = plesk_server.get_plesk_version()
        if plesk_version < (12, 5):
            allowed_php_handlers = None
        else:
            allowed_php_handlers = get_php_handlers(plesk_server)

        remote_smarter_mail = global_context.conn.target.remote_smarter_mail_server is not None
        allowed_webmails = global_context.hosting_repository.panel.get_available_webmails()

        return PleskHostingPlanAdapter(
            allowed_limits, allowed_permissions, allowed_settings, allowed_php_handlers,
            is_windows=plesk_server.is_windows(), remote_smarter_mail=remote_smarter_mail,
            allowed_webmails=allowed_webmails
        )

    def get_hosting_check_messages_panel_id(self):
        return 'plesk'

    @staticmethod
    def _get_plesk_target_server(conn):
        return conn.plesk_server

    def check_version(self, global_context):
        """Check that target panel version is ok for migration purposes. Raise MigrationError otherwise.

        Raised exception should contain information about supported versions for migration.

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: None
        """
        plesk_version = global_context.conn.target.plesk_server.get_plesk_version()
        plesk_version_str = '.'.join([str(i) for i in plesk_version])

        if plesk_version < (12, 0):
            raise MigrationNoContextError(
                messages.TARGET_PLESK_VERSION_NOT_SUPPORTED % (
                    plesk_version_str
                )
            )

    def get_import_dump_additional_env_vars(self, global_context):
        """Get additional target panel-specific environment variables to pass to pmmcli when importing dump

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: dict[str, str]
        """
        # Tell pmmcli to convert imported dump into latest format with hierarchical directory structure
        return {'PPA_IGNORE_FILE_MATCHING': 'true'}
