import logging
from collections import defaultdict
from contextlib import closing

from parallels.common import target_data_model as target_model
from parallels.common.utils.common_constants import IP_EXCLUSIVE, IP_SHARED
from parallels.common.migration_list import SubscriptionIPMapping
from parallels.common.utils.ip_utils import get_ip_address_types
from parallels.target_panel_plesk.import_api import model as import_api_model
from parallels.utils import group_by_id, group_value_by_id, find_first
from parallels.common.checking import Report, Problem
from parallels.common.converter.business_objects.common import \
	index_plesk_backup_domain_objects, \
	check_domain_conflicts, check_subscription_conflicts, check_subscription_already_exists, \
	check_client_contacts_matching_source_panels, \
	check_client_contacts_matching_source_target, \
	index_target_site_names, \
	EntityConverter, SOURCE_TARGET


class PleskConverter(object):
	"""Generate model of subscriptions and customers 
	ready to import to target panel.
	
	Converter takes the following information:
	- Plesk backup or other source of information 
	about resellers, clients, subscriptions and domains 
	on source servers
	- Information about objects that already exist on
	the target panel: resellers, plans, clients, domains
	and so on
	- Migration list, which contains list of customers and 
	subscriptions to migrate, their mapping between each other,
	plans and resellers

	Converter:
	- Converts each customer and subscription we need to migrate to 
	format ready to import to target panel (clients and subscriptions
	above hosting settings).
	- Performs conflict resolution, for example if client or subscriptions 
	exists on multiple source panel, or already exists on target panel.
	- Performs assignment of subscriptions to clients and plans,
	clients to resellers according to migration list
	- Performs some basic pre-migration checks, for example that plan
	already exists on target panel.

	Result is a model ready to import to target panel.

	Converter is NOT responsible for hosting settings below subscriptions,
	like subdomains, or PHP settings.
	"""

	logger = logging.getLogger(u'%s.Converter' % __name__)

	def __init__(
		self, admin_user_password, ppa_existing_objects,
		options, multiple_webspaces=False, entity_converter=None
	):
		self.target_resellers = {}
		self.target_clients = {}
		self.target_site_names = {}
		self.existing_objects = ppa_existing_objects  # XXX rename to target_existing_objects
		if entity_converter is not None:
			self.entity_converter = entity_converter
		else:
			self.entity_converter = EntityConverter(self.existing_objects)

		self.raw_target_clients = {
			None: self._create_fake_client(None)  # admin fake client
		}  # these special clients are included here, but not distinguished from other - regular - clients
		self.raw_target_subscriptions = {}
		self._ip_addresses_selector = IPAddressesSelector(self.existing_objects.ip_addresses)

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

	def convert_plesks(
		self, plesks, plain_report, 
		subscriptions_mapping, customers_mapping, 
		converted_resellers, 
		password_holder, mail_servers=dict(), custom_subscriptions_allowed=False
	):
		self.existing_site_names = index_target_site_names(self.existing_objects)
		self.target_resellers = group_by_id(converted_resellers, lambda r: r.login)

		for plesk_info in plesks:
			self.logger.debug(u"Convert backup")
			with closing(plesk_info.load_raw_backup()) as backup:
				self.convert_plesk(
					plesk_info.id, backup,
					plain_report, 
					plesk_info.settings.is_windows,
					subscriptions_mapping, customers_mapping,
					password_holder,
				)

	def get_ppa_model(self):  # XXX rename to get_target_model
		return target_model.Model(plans={}, resellers=self.target_resellers, clients=self.target_clients)

	def fix_emails_of_aux_users(self):
		pass  # XXX function applicable for PPA converter only

	def convert_plesk(
		self, plesk_id, backup, plain_report, is_windows,
		subscriptions_mapping, customers_mapping, 
		password_holder
	):
		self.logger.debug(u"Form up target model objects based on source backups")

		# 1. create model objects (clients and subscriptions), but do not add them to model yet
		for subscription in backup.iter_all_subscriptions():
			self._add_subscription(subscription, plesk_id, is_windows, plain_report, backup)

		for client in backup.iter_all_clients():
			if client.login in customers_mapping:
				self._add_client(client, plesk_id, plain_report, password_holder, customers_mapping[client.login])
		for reseller in self.target_resellers:
			self.raw_target_clients[reseller] = self._create_fake_client(reseller)

		# 2. link model objects
		# 2.1 plans <- subscription, according to migration list

		# dict(owner => set(plan names)), where owner is reseller login or None in case of admin
		plan_names_by_owner = self._get_plan_names_by_owners(
			self.existing_objects.resellers, self.existing_objects.service_templates
		)

		for subscription in self.raw_target_subscriptions.values():
			if subscription.source == plesk_id:
				if subscription.name in subscriptions_mapping:
					plan_name = subscriptions_mapping[subscription.name].plan
					owner_login = subscriptions_mapping[subscription.name].owner
					if owner_login is not None:
						if owner_login in customers_mapping:
							# owner is a customer
							reseller_login = customers_mapping.get(owner_login)
						else:
							reseller_login = owner_login  # owner is reseller
					else:
						reseller_login = None  # owner is administrator

					if plan_name is not None:
						subscription_report = plain_report.get_subscription_report(plesk_id, subscription.name)
						self._check_plan_exists(
							plan_names_by_owner.get(reseller_login, set()), 
							plan_name, reseller_login, subscription_report
						)
						if not subscription_report.has_errors():
							subscription.plan_name = plan_name

							# take addon plans mapping
							if len(subscriptions_mapping[subscription.name].addon_plans) > 0:
								reseller_id = find_first(
									self.existing_objects.resellers.itervalues(),
									lambda r: r.contact.username == reseller_login
								).id if reseller_login is not None else 1
								addon_plans = [st for st in self.existing_objects.addon_service_templates if st.owner_id == reseller_id]
								for addon_plan_name in subscriptions_mapping[subscription.name].addon_plans:
									subscription.addon_plan_ids.append(find_first(addon_plans, lambda p: p.name == addon_plan_name).st_id)
						else:
							# there are critical errors, so do not consider
							# that subscription anymore in further checks and conversions
							del self.raw_target_subscriptions[subscription.name]
					else:
						# custom subscription - subscription that is not assigned to any plan
						# so leave plan_name set to None
						pass 
				else:
					self.logger.debug(
						u"Subscription '%s' is not listed in migration list, not including it into target model",
						subscription.name
					)
					del self.raw_target_subscriptions[subscription.name]

		# 2.2 subscriptions <- client, according to migration list 
		for subscription in self.raw_target_subscriptions.values():
			subscription_report = plain_report.get_subscription_report(plesk_id, subscription.name)

			if subscription.name in subscriptions_mapping:
				subscription_mapping = subscriptions_mapping[subscription.name]
				subscription_owner = subscription_mapping.owner
				if subscription_owner in self.raw_target_clients:
					# add subscription to model. guard against multiple addition of
					# the same subscription (if it exists on two Plesk servers)
					client_subscriptions = self.raw_target_clients[subscription_owner].subscriptions
					client_subcription_names = [subs.name for subs in client_subscriptions]
					if subscription.name not in client_subcription_names:
						self.raw_target_clients[subscription_owner].subscriptions.append(subscription)
				else:
					plain_report.add_subscription_issue(
						plesk_id, subscription.name,
						Problem(
							"customer_does_not_exist", Problem.ERROR,
							u"Customer '%s' does not exist in source or in destination panel, "
							u"subscription will not be transferred." % subscription_owner,
						),
						u"Create the customer with such login manually, or map subscription to any other existing customer."
					)
					del self.raw_target_subscriptions[subscription.name]
			else:
				self.logger.debug(
					u"Subscription '%s' is not listed in migration list, not including it into target model",
					subscription.name
				)
				del self.raw_target_subscriptions[subscription.name]

		# 2.3 client <- admin, reseller, according to migration list
		def add_client_to_model(client, reseller_login):
			if reseller_login is None:
				if client.login not in self.target_clients:
					self.target_clients[client.login] = client
			elif reseller_login in self.target_resellers:
				reseller_clients_by_login = group_by_id(self.target_resellers[reseller_login].clients, lambda c: c.login)
				if client.login not in reseller_clients_by_login:
					self.target_resellers[reseller_login].clients.append(client)
			else:
				plain_report.add_customer_issue(
					plesk_id, client.login,
					Problem(
						'reseller_does_not_exist', Problem.ERROR,
						u"Client is mapped to an unexisting reseller '%s'" % reseller_login
					),
					u"Create this reseller in destination panel, or map client to some existing reseller"
				)

		reseller_logins = {reseller.login for reseller in backup.iter_resellers()}
		for client in self.raw_target_clients.values():
			if client.login in customers_mapping or client.login is None:
				reseller_login = customers_mapping.get(client.login)
				add_client_to_model(client, reseller_login)
			elif client.login in reseller_logins:
				add_client_to_model(client, client.login)
			else:
				self.logger.debug(u"Client '%s' is not listed in migration list, not including it into target model" % client.login)
				del self.raw_target_clients[client.login]

		# Map IP addresses for each subscription

		# We are interested only in new subscriptions, existing ones are already created
		# and have some IP address on target system
		new_model_subscriptions = list(self._iter_new_model_subscription(source_id=plesk_id))
		backup_subscriptions = {s.name: s for s in backup.iter_all_subscriptions()}

		self._map_ip_addresses(
			backup, new_model_subscriptions, backup_subscriptions, subscriptions_mapping, plesk_id, plain_report
		)

	def _iter_new_model_subscription(self, source_id):
		webspaces_by_name = group_by_id(self.existing_objects.webspaces, lambda ws: ws.name)

		def does_not_have_hosting_on_target(subscr):
			return (
				subscr.name not in webspaces_by_name or
				webspaces_by_name[subscr.name].htype == 'none'
			)

		for client in self.target_clients.itervalues():
			for subscription in client.subscriptions:
				if does_not_have_hosting_on_target(subscription) and subscription.source == source_id:
					yield None, client.login, subscription
		for reseller in self.target_resellers.itervalues():
			for client in reseller.clients:
				for subscription in client.subscriptions:
					if does_not_have_hosting_on_target(subscription) and subscription.source == source_id:
						yield reseller.login, client.login, subscription

	def _add_client(self, client, plesk_id, plain_report, password_holder, customer_mapping):
		client_report = plain_report.get_customer_report(plesk_id, client.login)
		if client.login in self.existing_objects.customers:
			existing_client = self.existing_objects.customers[client.login]
			new_client = self._create_target_client_from_backup(
				client, None, Report('', ''), password_holder
			)  # just for easy comparison
			check_client_contacts_matching_source_target(new_client, existing_client, client_report)
			self._check_client_owner(client_report, customer_mapping, existing_client)
			target_model_client = self.entity_converter.create_client_stub_from_existing_client(existing_client)
			self.raw_target_clients[client.login] = target_model_client
		elif client.login in self.raw_target_clients:
			existing_client = self.raw_target_clients[client.login]
			new_client = self._create_target_client_from_backup(
				client, None, Report('', ''), password_holder
			)  # just for easy comparison
			check_client_contacts_matching_source_panels(new_client, existing_client, client_report)
			target_model_client = existing_client
		else:
			target_model_client = self._create_target_client_from_backup(client, plesk_id, client_report, password_holder)
			self.raw_target_clients[client.login] = target_model_client 

		return target_model_client

	def _check_client_owner(self, client_report, customer_mapping, existing_client):
		# check existing client owner
		if existing_client.owner_id is None:
			if customer_mapping is not None:
				client_report.add_issue(
					Problem(
						"owned_by_another_admin", Problem.ERROR,
						u"Customer belongs to administrator on target panel, but according to migration list "
						u"it belongs to reseller '%s'" % (customer_mapping,)
					),
					u"Fix migration list so customer belongs to administrator"
				)
		else:
			# Customer is already owned by some reseller
			target_reseller = find_first(
				self.existing_objects.resellers.values(),
				lambda r: str(r.id) == str(existing_client.owner_id)
			)
			if target_reseller is None or target_reseller.contact.username != customer_mapping:
				client_report.add_issue(
					Problem(
						"owned_by_another_reseller", Problem.ERROR,
						u"According to migration list customer belongs to %s, but "
						u"actually on target panel it belongs to another reseller" % (
							'admin' if customer_mapping is None else "reseller '%s'" % (customer_mapping,)
						)
					),
					u"Fix migration list so customer belongs to the actual reseller"
				)

	def _create_target_client_from_backup(self, client, source, client_report, password_holder):
		return self.entity_converter.create_client_from_plesk_backup_client(client, source, client_report, password_holder)

	@staticmethod
	def _check_plan_exists(reseller_plans, plan_name, reseller_login, subscription_report):
		"""
		Get plan object for specified subscription 
		Return tuple (is_success, plan) where:
		- is_success is a boolean that means if plan selection was successful or not.
		- plan is a plan object or None in case of custom subscription
		"""
		if plan_name not in reseller_plans:
			subscription_report.add_issue(
				Problem(
					"plan_does_not_exist", Problem.ERROR,
					u"%s does not have a service template named '%s', unable to make a subscription on it. "
					u"Subscription will not be transferred." % (
						u"reseller '%s'" % reseller_login if reseller_login is not None else "admin", plan_name
					)
				),
				u"Create a service template with this name, or assign subscription to other service template"
			)

	@classmethod
	def _get_plan_names_by_owners(cls, target_resellers, target_service_templates):
		"""
		Returns the following structure: plans[owner] = set(plan names)
		where owner is reseller login or None in case of admin.
		"""

		resellers_by_id = group_value_by_id(
			target_resellers.itervalues(), 
			key_func=lambda r: r.id,
			value_func=lambda r: r.contact.username
		)
		resellers_by_id[import_api_model.ADMIN_ACCOUNT_ID] = None  # special admin owner

		plans = defaultdict(set)
		for plan in target_service_templates:
			if plan.owner_id in resellers_by_id:
				reseller = resellers_by_id.get(plan.owner_id)
				plans[reseller].add(plan.name)

		return dict(plans)

	def _add_subscription(self, subscription, plesk_id, is_windows, plain_report, backup):
		issues = []

		webspaces_by_name = group_by_id(self.existing_objects.webspaces, lambda ws: ws.name)
		if subscription.name in webspaces_by_name:
			model_subscription = self.entity_converter.create_subscription_stub_from_existing_subscription(
				subscription, plesk_id, is_windows
			)
			issues += check_subscription_already_exists(
				subscription.name,
				target_webspaces=self.existing_objects.webspaces
			)
		else:
			model_subscription = self.entity_converter.create_subscription_from_plesk_backup_subscription(
				subscription, plesk_id, is_windows
			)
			model_subscription.group_name = model_subscription.name
			issues += check_subscription_conflicts(
				subscription.name,
				source_webspaces=self.raw_target_subscriptions,
				source_sites=self.target_site_names,
				target_sites=self.existing_site_names,
			)
			issues += check_domain_conflicts(
				backup, subscription.name,
				target_webspaces=self.existing_objects.webspaces,
				target_sites=self.existing_site_names,
				source_webspaces=self.raw_target_subscriptions,
				source_sites=self.target_site_names,
			)

		for issue in issues:
			plain_report.add_subscription_issue(plesk_id, subscription.name, *issue)

		critical_issues = [issue for issue in issues if issue[0].severity == Problem.ERROR]
		if len(critical_issues) == 0:
			self.raw_target_subscriptions[subscription.name] = model_subscription
			domain_objects = index_plesk_backup_domain_objects(backup, subscription.name) 
			for name, kind in domain_objects:
				self.target_site_names[name] = (subscription.name, kind)

	@staticmethod
	def _create_fake_client(login):
		return target_model.Client(
			login=login, password=None, subscriptions=[],
			company=None,
			personal_info=target_model.PersonalInfo(
				first_name=None,
				last_name=None,
				email=None,
				preferred_email_format=None,
				address_line_1=None,
				address_line_2=None,
				city=None,
				county=None,
				state=None, 
				postal_code=None, 
				language_code=None,
				locale=None,
				country_code=None, 
				primary_phone=None, 
				additional_phone=None, 
				fax=None,
				mobile_phone=None,
			),
			auxiliary_user_roles=[],
			auxiliary_users=[],
			is_enabled=True,
			source=SOURCE_TARGET,
			target_client=None
		)

	def _map_ip_addresses(
		self, backup, subscriptions, backup_subscriptions, subscriptions_mapping, source_id, 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 backup: parallels.common.plesk_backup.plesk_backup_xml.PleskBackupSourceBase
		:type subscriptions: list[parallels.common.target_data_model.Subscription]
		:type backup_subscriptions: dict[basestring, parallels.common.plesk_backup.data_model.Subscription]
		:type subscriptions_mapping: dict[basestring, parallels.common.migration_list.SubscriptionMappingInfo]
		:type source_id: basestring | None
		:type report_provider: parallels.common.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(source_id, 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(source_id, 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(backup),
						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(source_id, 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
					)


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

	def __init__(self, ip_addresses):
		"""
		:type ip_addresses: list[parallels.common.utils.plesk_utils.IPAddressInfo]
		"""
		# list of target IP addresses, list[parallels.common.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.common.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.common.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.common.target_data_model.Subscription
		:type backup_subscription: parallels.common.plesk_backup.data_model.Subscription
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type source_default_ip: basestring | None
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report: parallels.common.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
							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.common.target_data_model.Subscription
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report: parallels.common.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)

	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.common.target_data_model.Subscription
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report parallels.common.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.common.target_data_model.Subscription
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type owner_reseller: basestring | None
		:type owner_client: basestring | None
		:type subscription_report parallels.common.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:
			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.common.utils.ip_utils.IPAddressType
		:rtype: parallels.common.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.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'ipv4_address_already_assigned_to_another_owner',
				Problem.WARNING,
				"Subscription is assigned to %s address '%s'"
				"which is also assigned to another reseller/client. "
				"Subscription will be migrated to default shared IP." % (
					ip_type.title, ip_address
				)
			),
			solution=(
				"Migrate subscription to another address "
				"by specifying '%s: <ip>' in the migration list. "
				"Also you can change IP address after migration is finished." % ip_type.title
			)
		)

	@staticmethod
	def _report_no_such_ip_address(ip_address, ip_type, subscription_report):
		"""
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_such_ip_address',
				Problem.WARNING,
				"Subscription is assigned to %s address '%s' "
				"which does not exist on the target Plesk server. "
				"Subscription will be migrated to default shared IP." % (
					ip_type.title, ip_address
				)
			),
			solution=(
				"Either add such IP address to the target Plesk server, "
				"or migrate subscription to another address "
				"by specifying '%s: <ip>' in the migration list. "
				"Also you can change IP address after migration is finished." % ip_type.title
			)
		)

	@staticmethod
	def _report_no_free_new_exclusive_ip(ip_type, subscription_report):
		"""
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_free_exclusive_ip',
				Problem.WARNING,
				"Subscription is assigned to exclusive IP address, "
				"but there are no free exclusive IP addresses left. "
				"Subscription will be migrated to default shared IP address."
			),
			solution=(
				"To migrate subscription to exclusive %s, "
				"add more free exclusive IP addresses on the target Plesk server. "
				"Also you can change IP address after migration is finished." % ip_type.title
			)
		)

	@staticmethod
	def _report_no_shared_ip(ip_type, subscription_report):
		"""
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_shared_ip',
				Problem.ERROR,
				"There is no shared %s address on target Plesk. Subscription can not be migrated." % ip_type.title
			),
			solution=(
				"Fix Plesk configuration so there is at least one shared %s address" % ip_type.title
			)
		)

	@staticmethod
	def _report_no_more_free_exclusive_ips(ip_type, subscription_report):
		"""
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_more_exclusive_ip_address',
				Problem.WARNING,
				"There is not enough free exclusive %s addresses on target Plesk "
				"to map all source exclusive IP addresses. This subscription will be migrated to "
				"default shared IP address" % ip_type.title
			),
			solution=(
				"Add more free exclusive IP addresses to target panel "
				"or change IP addresses of migrated subscriptions after migration"
			)
		)

	@staticmethod
	def _report_no_more_shared_ips(ip_type, subscription_report):
		"""
		:type ip_type: parallels.common.utils.ip_utils.IPAddressType
		:type subscription_report parallels.common.checking.Report
		:rtype: None
		"""
		subscription_report.add_issue(
			problem=Problem(
				'no_more_shared_ip_address',
				Problem.WARNING,
				"There is not enough shared %s addresses on target Plesk "
				"to map all source shared IP addresses. This subscription will be migrated to "
				"default shared IP address" % ip_type.title
			),
			solution=(
				"Add more shared IP addresses to target panel "
				"or change IP addresses of migrated subscriptions after migration"
			)
		)
