from parallels.plesk import messages
import logging

from parallels.core.utils.common_constants import IP_EXCLUSIVE, IP_SHARED
from parallels.core.migration_list import SubscriptionIPMapping
from parallels.core.utils.ip_utils import get_ip_address_types, IPv4AddressType
from parallels.core.utils.common import find_first
from parallels.core.checking import Problem


logger = logging.getLogger(__name__)


class IPMapper(object):
	def __init__(self, existing_objects):
		self._ip_addresses_selector = IPAddressesSelector(existing_objects.ip_addresses)

	def set_ip_mapping(self, ip_mapping):
		"""
		:type ip_mapping: parallels.core.ip_mapping.IPMapping
		"""
		self._ip_addresses_selector.set_ip_mapping(ip_mapping)

	def map_ip_addresses(
		self, backups, subscriptions, backup_subscriptions, subscriptions_mapping, report_provider
	):
		"""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 backups: dict[basestring, parallels.core.dump.dump.PleskBackupSourceBase]
		:type subscriptions: list[parallels.core.target_data_model.Subscription]
		:type backup_subscriptions: dict[basestring, parallels.core.dump.data_model.Subscription]
		:type subscriptions_mapping: dict[basestring, parallels.core.migration_list.SubscriptionMappingInfo]
		:type report_provider: parallels.core.checking.SubscriptionReportProvider
		:rtype: None
		"""
		# first, map subscriptions that have exactly specified IP address in migration list
		for reseller_login, customer_login, subscription in subscriptions:
			subscription_mapping = subscriptions_mapping[subscription.name]
			subscription_report = report_provider.get_subscription_report(
				subscription.source, subscription.name
			)

			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):
					self._ip_addresses_selector.select_exact_ip(
						subscription, ip_type, ip, reseller_login, customer_login, subscription_report
					)

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

			backup_subscription = backup_subscriptions[subscription.name]
			for ip_type in get_ip_address_types():
				if ip_type.get_for_ip_mapping(subscription_mapping.ips) == SubscriptionIPMapping.AUTO:
					self._ip_addresses_selector.select_mapping_ip(
						subscription, backup_subscription,
						ip_type, ip_type.get_default_ip(backups[subscription.name]),
						reseller_login, customer_login,
						subscription_report
					)

		# then, map subscriptions that have SHARED or DEDICATED in migration list
		for reseller_login, customer_login, subscription in subscriptions:
			subscription_mapping = subscriptions_mapping[subscription.name]
			subscription_report = report_provider.get_subscription_report(
				subscription.source, subscription.name
			)

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

		# if there are still no IP addresess assigned, use the default shared IP4 address
		for reseller_login, customer_login, subscription in subscriptions:
			subscription_mapping = subscriptions_mapping[subscription.name]
			subscription_report = report_provider.get_subscription_report(
				subscription.source, subscription.name
			)
			if subscription.web_ipv6 is None and subscription.web_ip is None:
				self._ip_addresses_selector.select_default_shared_ip(
						subscription, IPv4AddressType(), subscription_report)


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

	def __init__(self, ip_addresses):
		"""
		:type ip_addresses: list[parallels.core.utils.plesk_utils.IPAddressInfo]
		"""
		# list of target IP addresses, list[parallels.core.utils.plesk_utils.IPAddressInfo]
		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 = None

	def set_ip_mapping(self, ip_mapping):
		"""Set IP mapping specified by customer

		:type ip_mapping: parallels.core.ip_mapping.IPMapping
		"""
		self._customer_ip_mapping = ip_mapping

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

		:type subscription: parallels.core.target_data_model.Subscription
		:type backup_subscription: parallels.core.dump.data_model.Subscription
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type source_default_ip: basestring | None
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report: parallels.core.checking.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, target_ip)
			self._ip_owner[target_ip] = (owner_reseller, owner_client)
		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:
					next_shared_ip = find_first(
						self._target_ip_addresses,
						lambda ip: (
							ip.ip_type == IP_SHARED and
							ip_type.is_valid_ip(ip.public_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
							ip.public_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
						self._report_no_more_shared_ips(ip_type, subscription_report)
						self.select_default_shared_ip(subscription, ip_type, subscription_report)
					else:
						ip_type.set_for_target_model_subscription(subscription, next_shared_ip.public_ip_address)
						self._automatic_ip_mapping[source_ip] = next_shared_ip.public_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.public_ip_address) and
						ip.ip_address not in self._automatic_ip_mapping.values() and
						ip.public_ip_address not in self._automatic_ip_mapping.values() and
						(
							self._ip_owner.get(ip.public_ip_address) is None  # not owned by anybody
							or
							# owned by this current reseller and client
							self._ip_owner.get(ip.public_ip_address) == (owner_reseller, owner_client)
						) 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, next_exclusive_ip.public_ip_address)
					self._automatic_ip_mapping[source_ip] = next_exclusive_ip.public_ip_address
					self._ip_owner[next_exclusive_ip.public_ip_address] = (owner_reseller, owner_client)
		else:
			ip_type.set_for_target_model_subscription(subscription, self._automatic_ip_mapping[source_ip])

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

		:type subscription: parallels.core.target_data_model.Subscription
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type subscription_report: parallels.core.checking.Report
		:rtype: None
		"""
		shared_ip = self._get_default_shared_ip(ip_type)

		if shared_ip is None:
			self._report_no_shared_ip(ip_type, subscription_report)
		else:
			ip_type.set_for_target_model_subscription(subscription, shared_ip.public_ip_address)
			logger.debug(messages.IP_MAPPING_ASSIGNED_DEFAULT_IP, shared_ip.public_ip_address, subscription.name)

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

		:type subscription: parallels.core.target_data_model.Subscription
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report parallels.core.checking.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.public_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, ip_address.public_ip_address)
			self._ip_owner[ip_address.ip_address] = (owner_reseller, owner_client)

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

		:type subscription: parallels.core.target_data_model.Subscription
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report parallels.core.checking.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, 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, target_ip.public_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 != owner_client or
						existing_owner_reseller != owner_reseller
					):
						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, target_ip.public_ip_address)
				else:
					self._ip_owner[ip_address] = (owner_reseller, owner_client)
					ip_type.set_for_target_model_subscription(subscription, target_ip.public_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.public_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.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'ipv4_address_already_assigned_to_another_owner',
				Problem.WARNING,
				messages.IP_MAPPING_IP_ASSIGNED_TO_ANOTHER_OWNER_ISSUE % (
					ip_type.title, ip_address
				)
			),
			solution=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.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_such_ip_address',
				Problem.WARNING,
				messages.IP_MAPPING_NO_SUCH_IP_ISSUE % (
					ip_type.title, ip_address
				)
			),
			solution=(
				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.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_free_exclusive_ip',
				Problem.WARNING,
				messages.IP_MAPPING_NO_FREE_EXCLUSIVE_IP_ISSUE
			),
			solution=messages.IP_MAPPING_NO_FREE_EXCLUSIVE_IP_SOLUTION % ip_type.title
		)

	@staticmethod
	def _report_no_shared_ip(ip_type, subscription_report):
		"""
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type subscription_report parallels.core.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_shared_ip',
				Problem.ERROR,
				messages.IP_MAPPING_NO_SHARED_IP_ISSUE % ip_type.title
			),
			solution=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.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_more_exclusive_ip_address',
				Problem.WARNING,
				messages.IP_MAPPING_NO_MORE_EXCLUSIVE_IP_ISSUE % ip_type.title
			),
			solution=messages.IP_MAPPING_NO_MORE_EXCLUSIVE_IP_SOLUTION
		)

	@staticmethod
	def _report_no_more_shared_ips(ip_type, subscription_report):
		"""
		:type ip_type: parallels.core.utils.ip_utils.IPAddressType
		:type subscription_report parallels.core.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_more_shared_ip_address',
				Problem.WARNING,
				messages.IP_MAPPING_NO_MORE_SHARED_IP_ISSUE % ip_type.title
			),
			solution=messages.IP_MAPPING_NO_MORE_SHARED_IP_SOLUTION
		)
