import logging
from itertools import chain
from collections import defaultdict
from contextlib import closing

from parallels.common import target_data_model as ppa
from parallels.common.utils import poa_api_helper
from parallels.utils import group_by_id, group_by, obj, if_not_none, is_ascii_string
from parallels.common.plesk_backup import data_model as plesk
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


class PPAConverter(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
	exists on target panel and has all necessary resources.
	- Also, it performs grouping according to multiple webspaces mode
	and resource checks to avoid resource overuse in that mode.

	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.ppa_resellers = {}
		self.ppa_clients = {}
		self.ppa_subscriptions = {}
		self.ppa_site_names = {}
		self.admin_user_password = admin_user_password
		self.existing_objects = ppa_existing_objects 
		self.options = options
		self.multiple_webspaces = multiple_webspaces
		self.existing_site_names = index_target_site_names(self.existing_objects)
		if entity_converter is not None:
			self.entity_converter = entity_converter
		else:
			self.entity_converter = EntityConverter(self.existing_objects)

		self.special_ppa_clients = {}	# ppa-admin, ppa-reseller1 and so on - clients generated by converter
		self.raw_ppa_clients = {}	# these special clients are included here, but not distinguished from other - regular - clients
		self.raw_ppa_subscriptions = {}
		self.raw_aux_users = defaultdict(list)	# { client_login: [aux_user1,...] }

	def set_ip_mapping(self, ip_mapping):
		pass  # do nothing for PPA migrator as it has no IP mapping actually

	def _get_admin_special_ppa_account(self):
		for account_login, reseller_login in self.special_ppa_clients.items():
			if reseller_login is None:
				return self.raw_ppa_clients.get(account_login)	# return first matching login
		return None

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

		self._add_special_accounts(plesks, customers_mapping, plain_report, password_holder)

		for plesk_info in plesks:
			self.logger.info(u"Convert backup")
			with closing(plesk_info.load_raw_backup()) as backup:
				if plesk_info.id in mail_servers:
					mail_is_windows = mail_servers[plesk_info.id].is_windows
				else:
					mail_is_windows = plesk_info.settings.is_windows

				self.convert_plesk(
					plesk_info.id, backup, plesk_info.settings.ip, 
					plain_report, 
					plesk_info.settings.is_windows,
					mail_is_windows,
					subscriptions_mapping, customers_mapping,
					password_holder,
					custom_subscriptions_allowed=custom_subscriptions_allowed
				)

	def get_ppa_model(self):
		return ppa.Model(plans={}, resellers=self.ppa_resellers, clients=self.ppa_clients)

	def fix_emails_of_aux_users(self):
		for aux_user in (
			[aux_user for client in self.ppa_clients.itervalues() for aux_user in client.auxiliary_users] +
			[aux_user for reseller in self.ppa_resellers.itervalues() for aux_user in reseller.auxiliary_users] +
			[aux_user for reseller in self.ppa_resellers.itervalues() for client in reseller.clients for aux_user in client.auxiliary_users]
		):
			if aux_user.personal_info.get('email', '') == '':
				aux_user.personal_info['email'] = 'admin@%s' % aux_user.login if aux_user.is_domain_admin else aux_user.login


	def _add_special_ppa_client(self, login, reseller_login, plesk_id, plain_report):
		if login not in self.raw_ppa_clients:
			if login in self.existing_objects.customers:
				plain_report.add_server_issue(
					plesk_id,
					Problem('duplicate_customer_name', Problem.WARNING, u"A customer with the special username '%s' already exists in PPA" % login),
					u"PPA has the special '{login}' customer account reserved for subscriptions owned by {owner}. As '{login}' already exists in PPA, all such subscriptions from this server will be transferred under this account.".format(owner='the administrator' if reseller_login is None else u"reseller '%s'" % reseller_login, login=login)
				)
			ppa_client = self._create_ppa_fake_client(login, plesk_id)
			self.special_ppa_clients[login] = reseller_login
			self.raw_ppa_clients[login] = ppa_client
			return ppa_client
		else:
			assert login in self.special_ppa_clients, u"Client '%s' should have been added into special_ppa_clients, but it is not so"
			return self.raw_ppa_clients[login]

	def _group_subscriptions(self, ppa_client, vendor_id=None):
		"""Logically group domains by PPA client and service template name.
		   Then, if one of these domains is bound to a PPA subscription,
		   bind all other domains to this subscription as well.
		   If there are several domains already bound to different subscriptions,
		   bind all not-yet-bound domains to the subscription of the first bound domain.

		   Effectively it means, if a customer will have 3 multiple-webspaces subscriptions to the plan 'Linux Shared Hosting',
		   all domains of this customer that aren't yet transferred to PPA will be transferred to the first ot these subscriptions.

		   vendor_id is an additional parameter just for Plesks implementation, allowing to get plan_id by subscription.plan_name
		"""
		subscriptions_by_plan_name = group_by(ppa_client.subscriptions, lambda s: s.plan_name)
		for plan_name, subscriptions in subscriptions_by_plan_name.iteritems():
			# make sure that all subscriptions have group_name set. FIXME relies on that when plan_name is None - in PBAS case - group_name is already set for all subscriptions.
			if plan_name is not None:
				default_group_name = plan_name
				for subscription in subscriptions:
					subscription.group_name = subscription.group_name or default_group_name


			def get_group_id(client_id, plan_id, group_name):
				matching_groups = [
					subscr for subscr in self.existing_objects.raw_ppa_subscriptions if all([
						subscr.owner_id == client_id,
						subscr.st_id == plan_id,
						subscr.name == group_name,
					])
				]
				if len(matching_groups) > 0:
					return matching_groups[0].subscription_id
				else:
					return None

			for subscription in subscriptions:
				client = self.existing_objects.customers.get(ppa_client.login)
				client_id = if_not_none(client, lambda c: c.id)

				if subscription.plan_id is None:	# Plesks case
					st = poa_api_helper.get_service_template_by_owner_and_name(self.existing_objects.service_templates, vendor_id, plan_name)
					plan_id = if_not_none(st, lambda st: st.st_id)
				else:
					plan_id = subscription.plan_id	# PBAS case

				group_id = get_group_id(client_id, plan_id, subscription.group_name)

				subscription.group_id = subscription.group_id or group_id

	@staticmethod
	def _check_usage_vs_avail(current_usage, hostings_wanted, hostings_available, sub_report, problem, solution):
		for hosting in hostings_wanted:
			current_usage[hosting] += 1
		hostings_lacking = [
			hosting for hosting, qty in current_usage.iteritems()
			if hosting not in hostings_available
			or hostings_available[hosting] != -1 and hostings_available[hosting] < qty
		]

		if len(hostings_lacking) > 0:
			sub_report.add_issue(problem, solution.format(hostings_lacking = ", ".join(hostings_lacking)))

	def _check_group_limit(self, subscriptions, plain_report):
		subscriptions_by_group = group_by(subscriptions, lambda s: s.group_id)
		raw_subcriptions_by_id = group_by_id(self.existing_objects.raw_ppa_subscriptions, lambda s: s.subscription_id)
		service_templates_by_id = group_by_id(self.existing_objects.service_templates, lambda st: st.st_id)
		for group_id, group_subscriptions in subscriptions_by_group.iteritems():
			if group_id is None:
				continue	# skip subscriptions for which group is not yet created
			raw_subscription = raw_subcriptions_by_id[group_id]
			service_template = service_templates_by_id[raw_subscription.st_id]

			hostings_available = poa_api_helper.getSubscriptionHostingsAvailable(raw_subscription, service_template)
			hostings_required = defaultdict(int)
			for subscription in group_subscriptions:
				if subscription.sub_id is not None:
					continue	# skip subscriptions that are already created in PPA - they do not need more resources

				sub_report = plain_report.get_subscription_report(subscription.source, subscription.name)
				PPAConverter._check_usage_vs_avail(
					hostings_required, subscription.required_resources, hostings_available, sub_report,
					Problem('not_enough_resources', Problem.ERROR, u"The subscription #%s does not have enough resources to add the webspace '%s'" % (group_id, subscription.name)),
					u"You can either adjust the subscription's limits on the hosting types: ({hostings_lacking}) and re-transfer this subscription, or transfer this subscription in the single-webspace mode."
				)

			hostings_desired = hostings_required	# hostings_desired contains maximum resources we would allocate if limits allow, so it includes hostings_required
			for subscription in group_subscriptions:
				if subscription.sub_id is not None:
					continue	# skip subscriptions that are already created in PPA - they do not need more resources

				sub_report = plain_report.get_subscription_report(subscription.source, subscription.name)
				PPAConverter._check_usage_vs_avail(
					hostings_desired, list((set(subscription.additional_resources) - set(subscription.required_resources)) & set(hostings_required.keys())), hostings_available, sub_report,
					Problem('not_enough_resources', Problem.ERROR, u"The subscription #%s may not have enough resources to add the webspace '%s'. For your convenience, the migration tool will allocate resources to a webspace even if they are not required by the subscription being transferred. However, because the order of creating webspaces is not defined, resources may be exhausted before creating a webspace for the subscription that requires them." % (group_id, subscription.name)),
					u"You can do either of the following: (1) disable the allocation of optional resources by passing the --allocate-only-required-resources option to the migration tool; (2) adjust subscription's limits on the hosting types: ({hostings_lacking}) and re-transfer this subscription; or (3) transfer this subscription in the single-webspace mode."
				)

	def _check_service_template_limit(self, customer_subscriptions, service_template_owner_id, plain_report):
		"""This function also checks subscriptions group limit, but does so for subscriptions that do not have corresponding subscription (group_id) yet
		   In this case, it takes to check if a new subscription created by given service template would fit all source subscriptions
		   linked to the same group_name.
		"""
		service_templates_by_id = group_by_id(self.existing_objects.service_templates, lambda st: st.st_id)

		subscriptions_by_group_name = group_by(customer_subscriptions, lambda s: s.group_name)
		for _, subscriptions in subscriptions_by_group_name.iteritems():
			service_template = None
			if subscriptions[0].plan_id is not None:
				service_template = service_templates_by_id[subscriptions[0].plan_id]
			elif subscriptions[0].plan_name is not None:
				service_template = poa_api_helper.get_service_template_by_owner_and_name(self.existing_objects.service_templates, service_template_owner_id, subscriptions[0].plan_name)
			assert service_template is not None, u"somehow, subscription '%s' has neither plan_id nor plan_name defined" % subscriptions[0].name

			hostings_available = poa_api_helper.getServiceTemplateHostingsAvailable(service_template)
			hostings_required = defaultdict(int)
			for subscription in subscriptions:
				if subscription.sub_id is not None:
					continue	# subscription already exists, skip it

				sub_report = plain_report.get_subscription_report(subscription.source, subscription.name)
				PPAConverter._check_usage_vs_avail(
					hostings_required, subscription.required_resources, hostings_available, sub_report,
					Problem('not_enough_resources', Problem.ERROR, u"The service template #%s (named '%s', owned by account #%s) does not have enough resources to add the webspace '%s' into a new subscription based on that service template." % (service_template.st_id, service_template.name, service_template.owner_id, subscription.name)),
					u"You can adjust this service template's limits on the hosting types: ({hostings_lacking}) and re-transfer this subscription; or you can transfer this subscription in the single-webspace mode."
				)

			hostings_desired = hostings_required
			for subscription in subscriptions:
				if subscription.sub_id is not None:
					continue	# subscription already exists, skip it

				sub_report = plain_report.get_subscription_report(subscription.source, subscription.name)
				PPAConverter._check_usage_vs_avail(
					hostings_desired, list((set(subscription.additional_resources) - set(subscription.required_resources)) & set(hostings_required.keys())), hostings_available, sub_report,
					Problem('not_enough_resources', Problem.ERROR, u"The service template #%s (named '%s', owned by account #%s) may not have enough resources to add the webspace '%s'. For your convenience, the migration tool will allocate resources to a webspace even if they are not required by the subscription being transferred. However, because the order of creating webspaces is not defined, resources may be exhausted before creating a webspace for the subscription that requires them." % (service_template.st_id, service_template.name, service_template.owner_id, subscription.name)),
					u"You can do either of the following: (1) disable the allocation of optional resources by passing the --allocate-only-required-resources option to the migration tool; (2) adjust subscription's limits on the hosting types: ({hostings_lacking}) and re-transfer this subscription; or (3) transfer this subscription in the single-webspace mode."
				)

	def _add_special_accounts(self, plesks, customers_mapping, plain_report, password_holder):
		"""Add only the special accounts as listed in customers mapping.
		   E.g., for :
		   Plan: LSH for Migration
		       Customer: ppa-admin # automatically generated client for 'admin'
		           subscription1.com
		       Customer: vasya # Vassiliy # exists on plesk1
			   subscription2.com
		   Create only ppa-admin, but not vasya.
		"""
		clients = {}	# { login: plesk_id }
		resellers = {}	# { login: plesk_id }
		for plesk_info in plesks:
			with closing(plesk_info.load_raw_backup()) as backup:
				clients.update({ client.login: (client, plesk_info.id) for client in backup.iter_all_clients() })
				resellers.update({ reseller.login: (reseller, plesk_info.id) for reseller in backup.iter_resellers() })
		if len(plesks) > 0:
			first_plesk_id = plesks[0].id
		else:
			first_plesk_id = None

		for acc_login, reseller_login in customers_mapping.items():
			self._make_special_account_if_needed(acc_login, reseller_login, clients, resellers, first_plesk_id, plain_report)

	def _make_special_account_if_needed(self, acc_login, reseller_login, clients, resellers, first_plesk_id, plain_report):
		if acc_login not in clients:
			self._add_special_ppa_client(acc_login, reseller_login, first_plesk_id, plain_report)

	def _make_resellers_client_and_subscriptions(self, plesk_id, is_windows, mail_is_windows, plain_report, backup, password_holder):
		"""Add only the subscriptions. Special user ppa-reseller (or other) will be added by convert_plesks according to customers_mapping"""
		for reseller in backup.iter_resellers():
			for subscription in reseller.subscriptions:		
				self._add_subscription(subscription, plesk_id, is_windows, mail_is_windows, plain_report, backup)

	def _add_client_if_needed(self, plesk_client, plesk_id, customers_mapping, plain_report, password_holder):
		if plesk_client.login in customers_mapping:
			self._add_client(plesk_client, plesk_id, plain_report, password_holder)

	# 1. subscriptions_mapping and customers_mapping will be always defined. either user explicitly provides them or migrator implicitly generates them on start.
	def convert_plesk(
		self, plesk_id, backup, ip, plain_report, is_windows, mail_is_windows, 
		subscriptions_mapping, customers_mapping, 
		password_holder, custom_subscriptions_allowed=False
	):
		self.logger.info(u"Form up target model objects based on source backups")
		converted_plans = self._convert_plans(self.existing_objects.resellers, self.existing_objects.service_templates)

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

		for client in backup.iter_all_clients():
			self._add_client_if_needed(client, plesk_id, customers_mapping, plain_report, password_holder)

			for subscription in client.subscriptions:
				self._add_subscription(subscription, plesk_id, is_windows, mail_is_windows, plain_report, backup)

		# 1.2 (clients and) subscriptions - for resellers
		self._make_resellers_client_and_subscriptions(plesk_id, is_windows, mail_is_windows, plain_report, backup, password_holder)

		# 1.3 add aux users and aux roles, only for admin and clients
		# it's done separately from creation of corresponding ppa_client's, because admin's ppa_client is created out of scope of this method.
		# Note: we never add aux users for Plesk *resellers* - neither in Plesk migration nor in Expand migration - it's so from the beginning.
		admin_special_account = self._get_admin_special_ppa_account()
		if admin_special_account is not None:	# admin_special_account may not be specified, if admin does not have subscriptions, or user decided to assign admin's subscriptions to another existing customer
			server_report = plain_report.get_server_report(plesk_id)
			self._add_auxiliary_user_roles(admin_special_account, backup.iter_admin_auxiliary_user_roles(), server_report)
			self._add_auxiliary_users(admin_special_account.login, backup.iter_admin_auxiliary_users(), server_report, password_holder)
					
		for client in backup.iter_all_clients():
			if client.login in customers_mapping:
				ppa_client = self.raw_ppa_clients[client.login]	# we create this client earlier in this function, it must exist now.
				client_report = plain_report.get_customer_report(plesk_id, client.login)
				self._add_auxiliary_user_roles(ppa_client, client.auxiliary_user_roles, client_report)
				self._add_auxiliary_users(ppa_client.login, client.auxiliary_users, client_report, password_holder)

		# 2. link model objects
		def get_client_login_by_subscription_owner(subscription_owner):
			reseller_to_client = { reseller: client for client, reseller in self.special_ppa_clients.items() }
			if subscription_owner in reseller_to_client:
				return reseller_to_client[subscription_owner]
			else:
				return subscription_owner

		# 2.1 plan <- subscription, according to ML
		for subscription in self.raw_ppa_subscriptions.values():
			if subscription.source == plesk_id:
				if subscription.name in subscriptions_mapping:
					plan_name = subscriptions_mapping[subscription.name].plan
					customer_login = get_client_login_by_subscription_owner(subscriptions_mapping[subscription.name].owner)
					reseller_login = customers_mapping.get(customer_login) or self.special_ppa_clients.get(customer_login)
	
					is_plan_selection_successful, plan = self._get_plan(
						subscription, backup, converted_plans[reseller_login], 
						plan_name, reseller_login, plain_report.get_subscription_report(plesk_id, subscription.name),
						custom_subscriptions_allowed
					)

					if is_plan_selection_successful:
						if plan is not None:
							subscription.plan_name = plan.name
						else:
							subscription.plan_name = None # custom subscription - subscription that is not assigned to any plan
					else:
						del self.raw_ppa_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_ppa_subscriptions[subscription.name]

		# 2.2 subscriptions <- client, according to ML
		for subscription in self.raw_ppa_subscriptions.values():
			if subscription.name in subscriptions_mapping:
				subscription_owner = subscriptions_mapping[subscription.name].owner
				customer_login = get_client_login_by_subscription_owner(subscription_owner)
				if customer_login in self.raw_ppa_clients:
					self.ppa_subscriptions[subscription.name] = subscription
					# add subscription to model. guard against multiple addition of the same subscription (if it exists on two Plesk servers)
					# TODO think of using different data type for subscriptions of client
					client_subscriptions = self.raw_ppa_clients[customer_login].subscriptions
					client_subcription_names = [ subs.name for subs in client_subscriptions ]
					if subscription.name not in client_subcription_names:
						self.raw_ppa_clients[customer_login].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, subscription will not be transferred." % customer_login,
						),
						u"Create the customer with such login manually, or map subscription to any other existing customer."
					)
					del self.raw_ppa_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_ppa_subscriptions[subscription.name]

		# 2.3 client <- admin, reseller, according to ML and self.special_ppa_clients
		def add_client_to_model(client, reseller_login):
			if reseller_login is None:
				if client.login not in self.ppa_clients:
					self.ppa_clients[client.login] = client
			elif reseller_login in self.ppa_resellers:
				reseller_clients_by_login = group_by_id(self.ppa_resellers[reseller_login].clients, lambda c: c.login)
				if client.login not in reseller_clients_by_login:
					self.ppa_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"
				)

		for client in self.raw_ppa_clients.values():
			if client.login in customers_mapping:
				reseller_login = customers_mapping[client.login]
				if client.login not in self.special_ppa_clients:	# regular clients have value even if they don't have subscription, migrate them
					add_client_to_model(client, reseller_login)
				elif len(client.subscriptions) > 0:	# special clients make sense only if they receive some subscriptions
					add_client_to_model(client, reseller_login)
				else:
					self.logger.debug(u"Special client '%s' has no subscriptions assigned to it, not including it into target model" % 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_ppa_clients[client.login]

		for client_login, users in self.raw_aux_users.items():
			if client_login in self.raw_ppa_clients:
				ppa_client = self.raw_ppa_clients[client_login]
				ppa_client_aux_users = group_by(ppa_client.auxiliary_users, lambda user: user.name)
				for user in users:
					if user.name in ppa_client_aux_users:	# user was already added, during conversion of other plesk
						pass
					elif user.subscription_name is None or user.subscription_name in [s.name for s in ppa_client.subscriptions]:
						ppa_client.auxiliary_users.append(user)
					else:
						pass # user won't be migrated as subscription it is restricted to does not exist in and won't be created
			else:
				self.logger.debug(u"Client '%s' is not listed in migration list, not including its aux users into target model" % client_login)
				del self.raw_aux_users[client_login]

		# 3. now that subscriptions are assigned to specific client and plan, we do multiple-webspaces stuff.
		# the checks here are still related to converter as they can invalidate model.
		def get_vendor_account_id(client_login):
			reseller_login = customers_mapping[client_login]	# clients without mapping were deleted in 2.3
			if reseller_login is None:
				return poa_api_helper.ADMIN_ACCOUNT_ID
			elif reseller_login in self.existing_objects.resellers:
				return self.existing_objects.resellers[reseller_login].id
			return None

		if self.multiple_webspaces:
			for ppa_client in self.raw_ppa_clients.values():
				vendor_account_id = get_vendor_account_id(ppa_client.login)
				self._group_subscriptions(ppa_client, vendor_account_id)	# this procedure modifies subscriptions of given ppa_client
				self._check_group_limit(ppa_client.subscriptions, plain_report)
				if vendor_account_id is not None:
					self._check_service_template_limit(ppa_client.subscriptions, vendor_account_id, plain_report)

		# 4. FIXME any checks wrongly placed into converter as not related to it
		for subscription_name in self.raw_ppa_subscriptions:
			# note that raw_ppa_subcriptions (and model as well) can contain subscriptions from other sources, not retrievable from current backup. filter them out.
			# FIXME: thus, the DNS zones will not be checked for subscriptions that already exist in PPA
			if self.raw_ppa_subscriptions[subscription_name].source == plesk_id:
				self._check_subscription_dns_zones(backup, subscription_name, plain_report.get_subscription_report(plesk_id, subscription_name))

	def _add_client(self, client, plesk_id, plain_report, password_holder):
		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_ppa_client(
				client, None, Report('', ''), password_holder
			) # just for easy comparison
			check_client_contacts_matching_source_target(new_client, existing_client, client_report)
			ppa_client = self.entity_converter.create_client_stub_from_existing_client(existing_client)
			self.raw_ppa_clients[client.login] = ppa_client
		elif client.login in self.raw_ppa_clients:
			existing_client = self.raw_ppa_clients[client.login]
			new_client = self._create_ppa_client(client, None, Report('', ''), password_holder) # just for easy comparison
			check_client_contacts_matching_source_panels(new_client, existing_client, client_report)
			ppa_client = existing_client
		else:
			ppa_client = self._create_ppa_client(client, plesk_id, client_report, password_holder)
			self.raw_ppa_clients[client.login] = ppa_client

		return ppa_client

	@classmethod
	def _convert_plans(cls, ppa_resellers, ppa_service_templates):
		"""
		Returns the following structure: converted_plans[owner][service_template_name] = service_template,
		where owner is reseller login or None in case of admin.
		"""
		cls.logger.info(u"Convert service templates")
		converted_plans = defaultdict(dict)

		resellers_by_id = group_by_id(ppa_resellers.itervalues(), lambda r: r.id)

		for st in ppa_service_templates:
			if st.owner_id == poa_api_helper.ADMIN_ACCOUNT_ID:
				owner = None
			elif st.owner_id in resellers_by_id:
				owner = resellers_by_id[st.owner_id].contact.username
			converted_plans[owner][st.name] = st
		return converted_plans


	def _get_plan(self, subscription, backup, reseller_plans, plan_name, reseller_login, subscription_report, custom_subscriptions_allowed):
		"""
		Get plan object for specified subscription and perform several plan compatibility pre-checks.
		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
		"""
		issues = []

		if plan_name is None :
			if not custom_subscriptions_allowed:
				issues.append((
					Problem('custom_subscription', Problem.ERROR, u"This subscription is not assigned to any service template. Migration of such subscriptions is not supported"),
					u"Assign the subscription to a service template using the migration list file."
				))
			else:
				plan = None
		else:
			plan = reseller_plans.get(plan_name)
			if plan is None:
				issues.append((
					Problem("plan_does_not_exist", Problem.ERROR,
						u"%s does not have a service template named '%s', unable to make a subscription on it. 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"
				))
			else:
				issues += self._check_subscription_plan_compatibility(subscription, plan)
				issues += self._check_plan_mailbox_quota_compatibility(subscription, backup, plan)

		# TODO think, as it looks not in place
		for issue in issues:
			subscription_report.add_issue(*issue)
		critical_issues = [ issue for issue in issues if issue[0].severity == Problem.ERROR ]
		if len(critical_issues) > 0:
			return (False, None)
		else:
			return (True, plan)

	def _check_plan_mailbox_quota_compatibility(self, subscription, backup, plan):
		def _format_mailbox_quota(bytes_count):
			KB = 1024.0
			MB = 1024.0 * KB
			GB = 1024.0 * MB

			if bytes_count == -1:
				return 'unlimited'
			elif bytes_count < KB:
				return u"%s bytes" % (bytes_count,)
			elif bytes_count < MB:
				return u"%.2fKB" % (bytes_count / KB)
			elif bytes_count < GB:
				return u"%.2fMB" % (bytes_count / MB)
			else:
				return u"%.2fGB" % (bytes_count / GB)

		if plan.mailbox_quota == -1:
			return []

		issues = []
		# In Plesk backup:
		# 1) mailbox quota that is equal to "-1" means that mailbox quota is
		# equal to subscription's mailbox quota
		# 2) subscription's mailbox quota that is equal to "-1" means
		# unlimited (no quota at all)
		# Considering that we remove limits and permissions from backup when
		# restoring in on PPA, there is no special need to handle "-1" value:
		# subscription's limit from PPA service template will be used for
		# mailbox. So, it is enough to just compare mailbox limit and PPA
		# service template limit.
		for mailbox in backup.iter_subscription_mailboxes(subscription.name):
			if mailbox.quota is not None and mailbox.quota > plan.mailbox_quota:
				issues.append((
					Problem(
						'mailbox_quota_limit', Problem.ERROR,
						u"Subscription is assigned to a service template '%s' that has mailbox quota limit (%s) less than mailbox quota of mailbox '%s' (%s). Mailbox can not be transferred. " % (
							plan.name, _format_mailbox_quota(plan.mailbox_quota), mailbox.full_name, _format_mailbox_quota(mailbox.quota)
						),
					),
					u"Increase service template limit on the destination panel, or decrease mailbox quota on the source server and run migration tool with --reload-source-data option."
				))

		return issues

	def _check_subscription_plan_compatibility(self, subscription, ppa_plan):
		hosting_details = {
			'mail':		obj(description='mail service'),
			'iis':		obj(description='Web IIS service'),
			'apache':	obj(description='Web Apache service'),
			'mysql':	obj(description='MySQL database service'),
			'mssql':	obj(description='Microsoft SQL database service'),
			'postgresql':	obj(description='PostgreSQL database service'),
		}

		issues = []
		hostings_avail = ppa_plan.resources

		for resource in subscription.required_resources:
			# if it's the resource we can check, and that resource is absent or has zero limit - plan is not compatible, add issue
			if resource in hosting_details and hostings_avail.get(resource, 0) == 0:
				issues.append((
					Problem(
						'plan_is_not_compatible', Problem.ERROR,
						u"Subscription is assigned to a service template '%s' that has no '%s'" % (ppa_plan.name, hosting_details[resource].description),
					),
					u"Assign this subscription to a service template which offers %s." % hosting_details[resource].description.capitalize()
				))
		
		return issues

	def _check_auxiliary_user(self, user):
		issues = []
		if 'im' in user.personal_info:
			issues.append((
				Problem('auxiliary_user_contact_info_im', Problem.WARNING, u"The transfer of the instant messenger field in the contact information is not supported"),
				u"Transfer this info manually."
			))
		if 'comment' in user.personal_info:
			issues.append((
				Problem('auxiliary_user_contact_info_comment', Problem.WARNING, u"The transfer of the additional contact information field is not supported"),
				u"Migrate additional contact information field manually"
			))
		return issues

	def _create_ppa_fake_client(self, login, source):
		return ppa.Client(
			login=login, password=self.admin_user_password, subscriptions=[],
			company='',
			personal_info=ppa.PersonalInfo(
				first_name=login,
				last_name='',
				email=u'%s@example.com' % (login,),
				preferred_email_format='plainText',
				address_line_1='No address',
				address_line_2='',
				city='No city',
				county='',
				state='Washington', 
				postal_code='11111', 
				language_code='en',
				locale='en-US',
				country_code='US', 
				primary_phone='1111111', 
				additional_phone='', 
				fax='',
				mobile_phone='',
			),
			auxiliary_user_roles=[],
			auxiliary_users=[],
			is_enabled=True,
			source=source,
			target_client=None
		)

	@staticmethod
	def _get_postal_code(code):
		return code if code is not None and code.isdigit() else '11111'

	def _create_ppa_client(self, client, source, client_report, password_holder):
		converted_client = self.entity_converter.create_client_from_plesk_backup_client(client, source, client_report, password_holder)

		company = client.personal_info.get('company')
		# In PPA POA UI on many pages customer is identified by company name,
		# and you don't see its first and last names. As company is often empty
		# on source panels, a workaround is applied: we put client's contact as
		# a company name. Otherwise you see a lot of rows it tables that differ
		# only by id. This workaround should be removed as soon as bug #120645
		# is fixed on PPA side
		if company is None or company == '': 
			company = client.contact

		converted_client.company = company
		return converted_client

	def _add_auxiliary_user_roles(self, ppa_client, new_roles, report):
		self.logger.debug(u"Convert auxiliary user roles")
		roles_by_name = dict((role.name, role) for role in ppa_client.auxiliary_user_roles)
		for role in new_roles:
			self.logger.debug(u"Convert auxiliary user role '%s'", role.name)
			if (ppa_client.login, role.name) in self.existing_objects.auxiliary_user_roles:
				existing_plesk_role = self.existing_objects.auxiliary_user_roles[(ppa_client.login, role.name)]
				if role.permissions != existing_plesk_role.permissions:
					report.add_issue(
						Problem('duplicate_role_name', Problem.WARNING, u"A role '%s' for auxiliary users already exists in the destination panel and has the different set of permissions" % role.name),
						u"This role will not be transferred. The users with this role will be reassigned to the existing role. If you want to transfer the role, change its name."
					)
				ppa_client.auxiliary_user_roles.append(self._convert_role(existing_plesk_role))
			elif role.name in roles_by_name:
				existing_role = roles_by_name[role.name]
				if role.permissions != existing_role.permissions:
					report.add_issue(
						Problem('duplicate_role_name', Problem.WARNING, u"A role '%s' for auxiliary users exists on the other server and has the different set of permissions" % role.name),
						u"Only one role will be transferred. The priority will be given to the role from the other server. If you want to transfer both roles separately, rename one of them."
					)
			else:
				ppa_client.auxiliary_user_roles.append(self._convert_role(role))

	def _convert_role(self, plesk_role):
		return ppa.AuxiliaryUserRole(name=plesk_role.name, permissions=plesk_role.permissions)

	def _make_auxiliary_user(self, client_login, user, password_holder):
		"""Check if auxiliary user does not conflict with others.
		   Return (issues, model aux user) if so.
		   Return (issues, None) otherwise.
		"""
		if (client_login, user.name) in self.existing_objects.auxiliary_users:
			# We can't be sure if existing and new user are equal or not because we can't get 
			# some user settings like user subscription name or password via API
			# (however it is possible to try another way: use Plesk backup instead of API).
			# So, we just add a message for each existing user
			issues = [(
				Problem('duplicate_auxiliary_user_name', Problem.WARNING, u"An auxiliary user with the username '%s' already exists in the destination panel" % user.name),
				u"The user will not be transferred. If you want this user to be transferred, change its username."
			)]
			return issues, self._convert_user_from_plesk_api(self.existing_objects.auxiliary_users[(client_login, user.name)])

		issues, converted_user = self._convert_user_from_plesk_backup(user, client_login, password_holder)
		users_by_name = { user.login: user for user in self.raw_aux_users.get(client_login, []) }
		if user.name in users_by_name:
			if users_by_name[user.name] != converted_user:
				issues.append((
					Problem(
						'duplicate_auxiliary_user_name', Problem.WARNING,
						u"An auxiliary user with the username '%s' but different contact information exists on the other server" % user.name
					),
					u"Only one auxiliary user will be transferred. The priority will be given to the user from the other server. If you want to transfer both users, rename one of them."
				))

			return issues, None
		
		issues += self._check_auxiliary_user(user)
		if converted_user.is_domain_admin and not is_ascii_string(converted_user.login): # check for bug TP #139750
			issues.append((
				Problem(
					'idn_auxiliary_user', Problem.WARNING,
					u"Domain administrator of domain '%s' will not be migrated: migration of domain administrators for IDN domains is not supported." % user.name
				),
				""
			))
			return issues, None
		else:
			return issues, converted_user

	def _add_auxiliary_users(self, client_login, new_users, report, password_holder):
		self.logger.debug(u"Convert auxiliary users")

		for user in new_users:
			if not user.is_built_in:
				self.logger.debug(u"Convert auxiliary user '%s'", user.name)
				issues, converted_user = self._make_auxiliary_user(client_login, user, password_holder)
				if converted_user is not None:
					self.raw_aux_users[client_login].append(converted_user)

				aux_user_report = report.subtarget("Auxiliary user", user.name)
				for issue in issues:
					aux_user_report.add_issue(*issue)

	def _convert_user_from_plesk_api(self, user):
		return ppa.AuxiliaryUser(
			login=user.gen_info.login,
			password=None,
			name=user.gen_info.name,
			roles=set(user.roles),
			personal_info=user.gen_info.contact_info._asdict(),
			is_active=user.gen_info.status=='enabled',
			subscription_name=None,
			is_domain_admin=False,
		)

	def _convert_user_from_plesk_backup(self, user, ppa_client_login, password_holder):
		issues = []
		if user.password.type == 'plain':
			password = user.password.text
		else:
			password = password_holder.get('auxiliary_user', (ppa_client_login, user.name))
			issues.append((
				Problem('missing_auxiliary_user_password', Problem.WARNING, u"Unable to retrieve the auxiliary user's password as it is stored in the encrypted form"),
				u"A new password was generated for the auxiliary user '%s': '%s'. You can change the password for the corresponding auxiliary user in the destination panel once the data transfer is finished." % (user.name, password)
			))
		personal_info = dict(user.personal_info)
		if 'email' not in personal_info:	# 'email' is mandatory parameter (according to importer), but it may be missing in Plesk 8 domain admin or mail accounts
			guessed_email = user.name if '@' in user.name else u"admin@%s" % user.name
			personal_info['email'] = user.email or guessed_email

		return issues, ppa.AuxiliaryUser(
			login=user.name, password=password,
			name=user.contact if user.contact is not None else user.name, roles=set(user.roles), personal_info=personal_info,
			is_active=user.is_active,
			subscription_name=user.subscription_name,
			is_domain_admin=user.is_domain_admin
		)

	def _check_required_resources_missing_in_webspace(self, webspace, required_resources):
		"""Check if webspace has enough resources.
		   Return list of missing resources.
		"""

		missing_resources = set(required_resources) - set(webspace.resources)
		return list(missing_resources)

	@staticmethod
	def _get_required_hostings(backup_subscription, is_windows):
		required_hostings = []
		if backup_subscription.mailsystem is not None and backup_subscription.mailsystem.enabled:
			required_hostings.append('mail')	
		# subscription resource is required by PPA
		required_hostings.append('subscription')

		# databases were available when there were db servers and limit on number of databases was non-zero
		required_db_types = set([db.dbtype for db in chain(backup_subscription.get_databases(), backup_subscription.get_aps_databases())])
		required_hostings += required_db_types

		# web hostings was always available, but not always used
		if (
			backup_subscription.hosting_type != 'none'
			or len(required_db_types) > 0	# PPA 11.5 does not allow to create webspace without web hosting but with DB hosting
							# work it around by including web hosting. Web hosting will be disabled by pleskrestore.
							# P2 drawback of this workaround is that web hosting resource will be spent even if webspace doesn't have web
		):
			required_hostings.append('iis' if is_windows else 'apache')
		return required_hostings

	def _add_subscription(self, subscription, plesk_id, is_windows, mail_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._make_stub_for_existing_subscription(subscription, plesk_id, is_windows, mail_is_windows)
			issues += self._validate_existing_subscription(subscription.name, model_subscription.required_resources)
		else:
			model_subscription = self._make_new_ppa_subscription(subscription, plesk_id, is_windows, mail_is_windows)
			issues += check_subscription_conflicts(
				subscription.name,
				source_webspaces=self.raw_ppa_subscriptions,
				source_sites=self.ppa_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_ppa_subscriptions,
				source_sites=self.ppa_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_ppa_subscriptions[subscription.name] = model_subscription
			domain_objects = index_plesk_backup_domain_objects(backup, subscription.name) 
			for name, kind in domain_objects:
				self.ppa_site_names[name] = (subscription.name, kind)

	def _make_new_ppa_subscription(self, subscription, plesk_id, is_windows, mail_is_windows):
		"""Form up the target model object for source subscription.
		"""
		converted_subscription = self.entity_converter.create_subscription_from_plesk_backup_subscription(
			subscription, plesk_id, is_windows
		)
		converted_subscription.mail_is_windows = mail_is_windows

		converted_subscription.required_resources = PPAConverter._get_required_hostings(
			subscription, converted_subscription.is_windows
		)
		# If service template offers web or db hosting, include it into webspace [regardless of whether if was actually used before migration],
		# to keep 11.1's migrator behavior. This behavior is optional, can be disabled by specifying --allocate-only-required-resources
		if self.options.allocate_only_required_resources:
			converted_subscription.additional_resources = []
		else:
			if converted_subscription.is_windows:
				converted_subscription.additional_resources = \
					converted_subscription.required_resources + ['iis', 'mysql', 'mssql', 'mail']
			else:
				converted_subscription.additional_resources = \
					converted_subscription.required_resources + ['apache', 'mysql', 'postgresql', 'mail']

		# define group_name only for single-webspace mode
		converted_subscription.group_name = subscription.name if not self.multiple_webspaces else None

		return converted_subscription

	def _make_stub_for_existing_subscription(self, subscription, plesk_id, is_windows, mail_is_windows):
		converted_subscription = self.entity_converter.create_subscription_stub_from_existing_subscription(subscription, plesk_id, is_windows)
		converted_subscription.mail_is_windows = mail_is_windows
		return converted_subscription

	def _validate_existing_subscription(self, subscription_name, required_hostings):
		"""Checks whether existing subscription is suitable. Actually only lack of resources can make it not suitable.
		   Return list of problems.
		"""
		issues = []

		webspaces_by_name = group_by_id(self.existing_objects.webspaces, lambda ws: ws.name)
		if subscription_name in webspaces_by_name:
			webspace = webspaces_by_name[subscription_name]
			missing_resources = self._check_required_resources_missing_in_webspace(webspace, required_hostings)
			if len(missing_resources) > 0:
				issues.append((
					Problem(
						'webspace_exists_with_insufficient_resources', Problem.ERROR,
						u"The webspace '%s' already exists, but does not include the following hostings: %s" % (subscription_name, ", ".join(missing_resources))
					),
					u"Add missing hostings into that webspace manually and then re-transfer this subscription"
				))
			else:
				issues += check_subscription_already_exists(
					subscription_name, 
					target_webspaces=self.existing_objects.webspaces
				)
		return issues

	def _check_subscription_dns_zones(self, backup, subscription_name, subscription_report):
		self.logger.debug(u"Check DNS zones of subscription '%s'", subscription_name)

		# the iterator also lists main domain of the subscription
		for domain in chain([backup.get_subscription(subscription_name)], backup.iter_addon_domains(subscription_name), backup.iter_aliases(subscription_name)):
			if domain.dns_zone is None:
				self.logger.debug(u"Skip checking DNS zone of domain '%s': it has no DNS zone", domain.name)
				continue

			self.logger.debug(u"Check DNS status for '%s' zone (type: '%s', is_enabled: '%s')", domain.name, domain.dns_zone.zone_type, domain.dns_zone.enabled)

			if domain.mailsystem is not None and domain.mailsystem.enabled:
				solution = (u"Once the transfer is finished, manually change DNS records on the external DNS server(s) (including MX records) and " +
				"run the tool with the 'copy-mail-content' command to complete mail content sync.")
			else:
				solution = u"Once the transfer is finished, manually change DNS records on the external DNS server(s)." 

			if not domain.dns_zone.enabled:
				subscription_report.subtarget('DNS zone', domain.name).add_issue(
					Problem('external_dns', Problem.WARNING, u"The DNS zone '%s' of this subscription is managed by an external server." % (domain.name,)), 
					solution
				)
			elif domain.dns_zone.zone_type == 'slave' and not isinstance(domain, plesk.DomainAlias): # for aliases slave zone type == zone is synched with parent domain zone, so we don't check it
				subscription_report.subtarget('DNS zone', domain.name).add_issue(
					Problem('slave_dns', Problem.WARNING, u"The DNS server in Plesk Panel works in the slave mode for the subscription's DNS zone '%s'." % (domain.name,)),
					solution
				)

