from parallels.core.logging import get_logger
from parallels.core.migration_list.entities.ip_mapping import SubscriptionIPMapping
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import find_first
from parallels.core.utils.common_constants import IP_EXCLUSIVE, IP_SHARED
from parallels.plesk.utils.ip_utils import get_ip_address_types, PleskIPv4AddressType
from parallels.plesk import messages
from parallels.plesk.target_data_model import PleskTargetSubscriptionData

logger = get_logger(__name__)


class IPMapper(object):
    @staticmethod
    def map_ip_addresses(
        subscriptions, existing_objects, ip_mapping, subscriptions_mapping, report
    ):
        """Set IP addresses for subscriptions according to migration list, IP mapping file and target IP set

        Current implementation features and limitations:
        1) If you assigned IP address manually, either with migration list or with IP mapping file,
        no additional checks are performed. For example, if this IP is already used by another reseller/client,
        migration will fail when creating subscription. No pre-migration warning will be displayed.
        2) For all automatically allocated IP addresses, except for default shared IP address,
        we always use completely unused IP addresses - addresses which are not assigned to any client/reseller
        and not used by any hosting subscriptions. So, in automatic mode migrator could skip addresses
        which could actually be used. For example, exclusive IP addresses owned by reseller which have no
        hosting subscriptions will be skipped in case if we migrate subscription with
        exclusive IP address under that reseller.
        3) In automatic mode, it is considered that all subscriptions that have the same non-default IP address
        are migrated at once. Otherwise, if you migrated them in several stages, they will get different IP addresses,
        one per each stage.
        4) By default shared IP address on target we take the first shared IP address reported by ipmanage utility.
        Theoretically, it could be possible when default shared IP address is another one, not the first one.
        5) In case of any issue, except for the lack of default shared IP address, we migrate subscription
        to default shared IP address. Migration won't stop, and you can reassign the IP later after migration.

        :type subscriptions: list[parallels.core.migrated_subscription.MigratedSubscription]
        :type ip_mapping: parallels.core.ip_mapping.IPMapping | None
        :type subscriptions_mapping: dict[str | unicode, parallels.core.migration_list.entities.subscription_info.SubscriptionMappingInfo]
        :type report: parallels.core.reports.model.report.Report
        :rtype: None
        """
        ip_addresses_selector = IPAddressesSelector(existing_objects.ip_addresses, ip_mapping)

        # first, map subscriptions that have exactly specified IP address in migration list
        for subscription in subscriptions:
            subscription_mapping = subscriptions_mapping[subscription.name]
            subscription_report = subscription.get_report(report)

            for ip_type in get_ip_address_types():
                ip = ip_type.get_for_ip_mapping(subscription_mapping.ips)
                if ip_type.is_valid_ip(ip):
                    ip_addresses_selector.select_exact_ip(
                        subscription, ip_type, ip, subscription_report
                    )

        # then, map subscriptions that use IP mapping (AUTO in migration list) - either with mapping file, or automatic
        for subscription in subscriptions:
            subscription_mapping = subscriptions_mapping[subscription.name]
            subscription_report = subscription.get_report(report)

            for ip_type in get_ip_address_types():
                if ip_type.get_for_ip_mapping(subscription_mapping.ips) == SubscriptionIPMapping.AUTO:
                    ip_addresses_selector.select_mapping_ip(
                        subscription, subscription.raw_dump,
                        ip_type, ip_type.get_default_ip(subscription.server_raw_dump), subscription_report
                    )

        # then, map subscriptions that have SHARED or DEDICATED in migration list
        for subscription in subscriptions:
            subscription_mapping = subscriptions_mapping[subscription.name]
            subscription_report = subscription.get_report(report)

            for ip_type in get_ip_address_types():
                ip = ip_type.get_for_ip_mapping(subscription_mapping.ips)
                if ip == SubscriptionIPMapping.SHARED:
                    ip_addresses_selector.select_default_shared_ip(subscription, ip_type, subscription_report)
                elif ip == SubscriptionIPMapping.DEDICATED:
                    ip_addresses_selector.select_free_exclusive_ip(
                        subscription, ip_type, subscription_report
                    )

        # if there are still no IP addresses assigned, use the default shared IPv4 address
        for subscription in subscriptions:
            target_data = subscription.model.target_data
            assert isinstance(target_data, PleskTargetSubscriptionData)
            subscription_report = subscription.get_report(report)
            if target_data.web_ipv6 is None and target_data.web_ip is None:
                ip_addresses_selector.select_default_shared_ip(
                    subscription, PleskIPv4AddressType(), subscription_report, report_error=True
                )


class IPAddressesSelector(object):
    """Get IP addresses for new subscriptions

    :type _target_ip_addresses: list[parallels.core.utils.plesk_utils.IPAddressInfo]
    """

    def __init__(self, ip_addresses, ip_mapping):
        """
        :type ip_addresses: list[parallels.core.utils.plesk_utils.IPAddressInfo]
        :type ip_mapping: parallels.core.ip_mapping.IPMapping | None
        """
        self._target_ip_addresses = ip_addresses
        # dictionary of IP owners, {ip address: (reseller login, customer login)}
        self._ip_owner = dict()
        # dictionary of automatic IP mapping, {source ip address: target ip address}
        self._automatic_ip_mapping = dict()
        # IP mapping specified by IP mapping file by customer,
        # parallels.core.ip_mapping.IPMapping object or None if mapping was not specified
        self._customer_ip_mapping = ip_mapping

    def select_mapping_ip(
        self, subscription, backup_subscription, ip_type, source_default_ip,
        subscription_report
    ):
        """Select IP according to IP mapping - either manual (with IP mapping file) or automatic

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type backup_subscription: parallels.core.dump.data_model.Subscription
        :type ip_type: parallels.plesk.utils.ip_utils.PleskIPAddressType
        :type source_default_ip: str | unicode | None
        :type subscription_report: parallels.core.reports.model.report.Report
        :rtype: None
        """
        source_ip = ip_type.get_for_backup_subscription(backup_subscription)
        source_ip_type = ip_type.get_type_for_backup_subscription(backup_subscription)

        if source_ip is None:
            # There were no IP of such type on source, then there should be no
            # IP of such type on target in AUTO mode
            return

        if self._customer_ip_mapping is not None and self._customer_ip_mapping.get(source_ip) is not None:
            target_ip = self._customer_ip_mapping.get(source_ip)
            ip_type.set_for_target_model_subscription(subscription.model, target_ip)
            self._ip_owner[target_ip] = (subscription.reseller_username, subscription.customer_username)
        elif source_ip not in self._automatic_ip_mapping:
            if source_ip_type == IP_SHARED:
                if source_default_ip is not None and source_ip == source_default_ip:
                    self.select_default_shared_ip(subscription, ip_type, subscription_report)
                else:
                    # find mapping for source non-default IP address
                    next_shared_ip = find_first(
                        self._target_ip_addresses,
                        lambda ip: (
                            ip.ip_type == IP_SHARED and
                            ip_type.is_valid_ip(ip.ip_address) and
                            (source_default_ip is None or ip != self._get_default_shared_ip(ip_type)) and
                            ip.ip_address not in self._automatic_ip_mapping.values() and
                            (source_default_ip is None or ip.is_completely_free)
                        )
                    )
                    if next_shared_ip is None:
                        # If no IP was found - map to default shared IP,  but do not report anything to customer:
                        # we consider this situation is ok, nothing bad will happen if some shared IP address
                        # is mapped to default shared IP address.
                        self.select_default_shared_ip(subscription, ip_type, subscription_report)
                    else:
                        ip_type.set_for_target_model_subscription(subscription.model, next_shared_ip.ip_address)
                        self._automatic_ip_mapping[source_ip] = next_shared_ip.ip_address
            elif source_ip_type == IP_EXCLUSIVE:
                next_exclusive_ip = find_first(
                    self._target_ip_addresses,
                    lambda ip: (
                        ip.ip_type == IP_EXCLUSIVE and
                        ip_type.is_valid_ip(ip.ip_address) and
                        ip.ip_address not in self._automatic_ip_mapping.values() and
                        (
                            # not owned by anybody
                            self._ip_owner.get(ip.ip_address) is None or
                            # owned by this current reseller and client
                            self._ip_owner.get(ip.ip_address) == (
                                subscription.reseller_username, subscription.customer_username
                            )
                        ) and
                        ip.is_completely_free
                    )
                )
                if next_exclusive_ip is None:
                    # If no IP was found - map to default shared IP
                    self._report_no_more_free_exclusive_ips(ip_type, subscription_report)
                    self.select_default_shared_ip(subscription, ip_type, subscription_report)
                else:
                    ip_type.set_for_target_model_subscription(subscription.model, next_exclusive_ip.ip_address)
                    self._automatic_ip_mapping[source_ip] = next_exclusive_ip.ip_address
                    self._ip_owner[next_exclusive_ip.ip_address] = (
                        subscription.reseller_username, subscription.customer_username
                    )
        else:
            ip_type.set_for_target_model_subscription(subscription.model, self._automatic_ip_mapping[source_ip])

    def select_default_shared_ip(self, subscription, ip_type, subscription_report, report_error=False):
        """Select default shared IP address for subscription

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type ip_type: parallels.plesk.utils.ip_utils.PleskIPAddressType
        :type subscription_report: parallels.core.reports.model.report.Report
        :type report_error: bool
        :rtype: None
        """
        shared_ip = self._get_default_shared_ip(ip_type)

        if shared_ip is None:
            if report_error:
                self._report_no_shared_ip_error(ip_type, subscription_report)
            else:
                self._report_no_shared_ip_warning(ip_type, subscription_report)
        else:
            ip_type.set_for_target_model_subscription(subscription.model, shared_ip.ip_address)
            logger.debug(messages.IP_MAPPING_ASSIGNED_DEFAULT_IP, shared_ip.ip_address, subscription.name)

    def select_free_exclusive_ip(self, subscription, ip_type, subscription_report):
        """Select new free exclusive IP address for subscription

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type ip_type: parallels.plesk.utils.ip_utils.PleskIPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        ip_address = find_first(
            self._target_ip_addresses,
            lambda ip: (
                ip.ip_type == IP_EXCLUSIVE and
                ip.ip_address not in self._ip_owner and
                ip_type.is_valid_ip(ip.ip_address) and
                ip.is_completely_free
            )
        )
        if ip_address is None:
            self._report_no_free_new_exclusive_ip(ip_type, subscription_report)
            self.select_default_shared_ip(subscription, ip_type, subscription_report)
        else:
            ip_type.set_for_target_model_subscription(subscription.model, ip_address.ip_address)
            self._ip_owner[ip_address.ip_address] = (subscription.reseller_username, subscription.customer_username)

    def select_exact_ip(
        self, subscription, ip_type, ip_address, subscription_report
    ):
        """Select exactly specified IP address for subscription

        :type subscription: parallels.core.migrated_subscription.MigratedSubscription
        :type ip_type: parallels.plesk.utils.ip_utils.PleskIPAddressType
        :type ip_address: str | unicode
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        target_ip = find_first(
            self._target_ip_addresses,
            lambda ip: ip.ip_address == ip_address
        )

        if target_ip is None:
            self._report_no_such_ip_address(ip_address, ip_type, subscription_report)
            self.select_default_shared_ip(subscription, ip_type, subscription_report)
        else:
            ip_type.set_type_for_target_model_subscription(subscription.model, target_ip.ip_type)
            logger.debug(messages.IP_MAPPING_SUBSCRIPTION_ASSIGNED_IPS, target_ip.ip_address, target_ip.ip_type)
            if target_ip.ip_type == IP_SHARED:
                ip_type.set_for_target_model_subscription(subscription.model, target_ip.ip_address)
            elif target_ip.ip_type == IP_EXCLUSIVE:
                owner = self._ip_owner.get(ip_address)
                if owner is not None:
                    existing_owner_reseller, existing_owner_client = owner
                    if (
                        existing_owner_client != subscription.customer_username or
                        existing_owner_reseller != subscription.reseller_username
                    ):
                        self._report_ip_assigned_to_another_owner(ip_address, ip_type, subscription_report)
                        self.select_default_shared_ip(subscription, ip_type, subscription_report)
                    else:
                        ip_type.set_for_target_model_subscription(subscription.model, target_ip.ip_address)
                else:
                    self._ip_owner[ip_address] = (subscription.reseller_username, subscription.customer_username)
                    ip_type.set_for_target_model_subscription(subscription.model, target_ip.ip_address)

    def _get_default_shared_ip(self, ip_type):
        """Get default shared IP address of specified type

        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :rtype: parallels.core.utils.plesk_utils.IPAddressInfo
        """
        shared_ip = find_first(
            self._target_ip_addresses,
            lambda ip: ip.ip_type == IP_SHARED and ip_type.is_valid_ip(ip.ip_address)
        )
        return shared_ip

    @staticmethod
    def _report_ip_assigned_to_another_owner(ip_address, ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'ipv4_address_already_assigned_to_another_owner',
            Issue.SEVERITY_WARNING,
            messages.IP_MAPPING_IP_ASSIGNED_TO_ANOTHER_OWNER_ISSUE % (
                ip_type.title, ip_address
            ),
            messages.IP_MAPPING_IP_ASSIGNED_TO_ANOTHER_OWNER_SOLUTION % ip_type.title
        )

    @staticmethod
    def _report_no_such_ip_address(ip_address, ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'no_such_ip_address',
            Issue.SEVERITY_WARNING,
            messages.IP_MAPPING_NO_SUCH_IP_ISSUE % (
                ip_type.title, ip_address
            ),
            messages.IP_MAPPING_NO_SUCH_IP_SOLUTION % ip_type.title
        )

    @staticmethod
    def _report_no_free_new_exclusive_ip(ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'no_free_exclusive_ip',
            Issue.SEVERITY_WARNING,
            messages.IP_MAPPING_NO_FREE_EXCLUSIVE_IP_ISSUE,
            messages.IP_MAPPING_NO_FREE_EXCLUSIVE_IP_SOLUTION % ip_type.title
        )

    @staticmethod
    def _report_no_shared_ip_error(ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'no_shared_ip_error',
            Issue.SEVERITY_ERROR,
            messages.IP_MAPPING_NO_SHARED_IP_ISSUE_ERROR % ip_type.title,
            messages.IP_MAPPING_NO_SHARED_IP_SOLUTION % ip_type.title
        )

    @staticmethod
    def _report_no_shared_ip_warning(ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'no_shared_ip_warning',
            Issue.SEVERITY_WARNING,
            messages.IP_MAPPING_NO_SHARED_IP_ISSUE_WARNING % ip_type.title,
            messages.IP_MAPPING_NO_SHARED_IP_SOLUTION % ip_type.title
        )

    @staticmethod
    def _report_no_more_free_exclusive_ips(ip_type, subscription_report):
        """
        :type ip_type: parallels.core.utils.ip_utils.IPAddressType
        :type subscription_report parallels.core.reports.model.report.Report
        :rtype: None
        """
        subscription_report.add_issue(
            'no_more_exclusive_ip_address',
            Issue.SEVERITY_WARNING,
            messages.IP_MAPPING_NO_MORE_EXCLUSIVE_IP_ISSUE % ip_type.title,
            messages.IP_MAPPING_NO_MORE_EXCLUSIVE_IP_SOLUTION
        )
