from parallels.target.ppa import messages
import logging
from itertools import chain
from collections import defaultdict
from contextlib import closing

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

				self.convert_plesk(
					plesk_info.id, backup, plesk_info.ip,
					plain_report, 
					plesk_info.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, messages.CUSTOMER_WITH_SPECIAL_USERNAME_ALREADY_EXISTS % login),
					messages.PPA_HAS_SPECIAL_CUSTOMER_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, messages.CLIENT_WAS_NOT_ADDED_INTO_SPECIAL_PPA_ACCOUNT
			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, messages.SUBSCRIPTION_DOES_NOT_HAVE_ENOUGH_RESOURCES % (group_id, subscription.name)),
					messages.ADJUST_LIMITS_OR_TRANSFER_IN_SINGLE_MODE_SOLUTION)

			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, messages.SUBSCRIPTION_MAY_NOT_HAVE_ENOUGH_RESOURCES % (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, messages.SUBSCRIPTION_HAS_NO_PLAN_ID_AND_PLAN_NAME % 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, messages.SERVICE_TEMPLATE_HAS_NOT_ENOUGH_RESOURCES % (service_template.st_id, service_template.name, service_template.owner_id, subscription.name)),
					messages.ADJUST_SERVICE_TEMPLATES_LIMITS)

			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, messages.SERVICE_TEMPLATE_MAY_HAVE_NOT_ENOUGH_RESOURCES % (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(messages.FORM_UP_TARGET_MODEL)
		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(messages.SUBSCRIPTION_IS_NOT_LISTED_IN_MIGRATION_LIST % 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,
							messages.CUSTOMER_DOES_NOT_EXIST % customer_login,
						),
						messages.SOLUTION_CREATE_CUSTOMER_WITH_SUCH_LOGIN_MANUALLY)
					del self.raw_ppa_subscriptions[subscription.name]
			else:
				self.logger.debug(messages.SUBSCRIPTION_IS_NOT_LISTED_IN_MIGRATION_LIST % 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,
						messages.CLIENT_MAPPED_TO_UNEXISTING_RESELLER % reseller_login
					),
					messages.SOLUTION_CREATE_RESELLER_IN_DESTINATION_PANEL)

		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(messages.SPECIAL_CLIENT_HAS_NO_SUBSCRIPTIONS_ASSIGNED % client.login)
			else:
				self.logger.debug(messages.CLIENT_IS_NOT_LISTED_IN_MIGRATION_LIST % 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(messages.CLIENT_NOT_LISTED_IN_MIGRATION_LIST % 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, messages.SUBSCRIPTION_NOT_ASSIGNED_TO_SERVICE_TEMPLATE),
					messages.ASSIGN_SUBSCRIPTION_TO_SERVICE_TEMPLATE))
			else:
				plan = None
		else:
			plan = reseller_plans.get(plan_name)
			if plan is None:
				issues.append((
					Problem("plan_does_not_exist", Problem.ERROR,
						messages.NO_SERVICE_TEMPLATE_WITH_SUCH_NAME % (u"reseller '%s'" % reseller_login if reseller_login is not None else "admin", plan_name)					),
					messages.CREATE_SERVICE_TEMPLATE_WITH_NAME))
			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,
						messages.SUBSCRIPTION_IS_ASSIGNED_TO_SERVICE_TEMPLATE_WITH_BAD_MAIL_QUOTA % (
							plan.name, _format_mailbox_quota(plan.mailbox_quota), mailbox.full_name, _format_mailbox_quota(mailbox.quota)
						),
					),
					messages.MAILBOX_QUOTA_SOLUTION))

		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,
						messages.SUBSCRIPTION_ASSIGNED_TO_SERVICE_TEMPLATE_WITH_NO_FEATURE % (ppa_plan.name, hosting_details[resource].description),
					),
					messages.ASSIGN_SUBSCRIPTION_TO_SERVICE_TEMPLATE_WITH_FEATURE % 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, messages.TRANSFER_OF_INSTANT_MESSENGER_FIELD_NOT_SUPPORTED),
				u"Transfer this info manually."
			))
		if 'comment' in user.personal_info:
			issues.append((
				Problem('auxiliary_user_contact_info_comment', Problem.WARNING, messages.TRANSFER_OF_ADDITIONAL_CONTACT_INFO_FIELD_NO_SUPPORTED),
				messages.MIGRATE_ADDITIONAL_CONTACT_INFO_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=None,
				locale=None,
				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(messages.CONVERT_AUXILIARY_USER_ROLE, 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, messages.ROLE_FOR_AUXILIARY_USERS_ALREADY_EXISTS_WITH_DIFFERENT_PERMISSIONS % role.name),
						messages.ROLE_WILL_NOT_BE_TRANSFERRED)
				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, messages.ROLE_FOR_AUX_USERS_EXISTS_ON_OTHER_SERVER % role.name),
						messages.ONE_ROLE_WILL_BE_TRANSFERRED)
			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, messages.AUXILIARY_USER_ALREADY_EXISTS % user.name),
				messages.USER_WILL_NOT_BE_TRANSFERRED)]
			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,
						messages.AUXILIARY_USER_WITH_DIFFERENT_CONTACT_INFO % user.name
					),
					messages.ONE_AUXILIARY_USER_WILL_BE_TRANSFERRED))

			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,
					messages.IDN_DOMAIN_ADMIN_WILL_NOT_BE_TRANSFERRED % 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, messages.UNABLE_TO_GET_AUXILIARY_USER_PASSWORD),
				messages.NEW_PASSWORD_FOR_AUX_USER % (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,
						messages.WEBSPACE_ALREADY_EXISTS_BUT_DOES_NOT_INCLUDE_HOSTINGS % (subscription_name, ", ".join(missing_resources))
					),
					messages.ADD_MISSING_HOSTINGS_INTO_WEBSPACE))
			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(messages.CHECK_DNS_ZONES_OF_SUBSCRIPTION, 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(messages.SKIP_CHECKING_DNS_ZONE_OF_DOMAIN, 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 = (messages.CHANGE_DNS_RECORDS_MANUALLY_ON_EXT_DNS +
				messages.RUN_TOOL_WITH_COPY_MAIL_CONTENT_COMMAND)
			else:
				solution = messages.CHANGE_DNS_RECORDS_ON_EXT_DNS

			if not domain.dns_zone.enabled:
				subscription_report.subtarget('DNS zone', domain.name).add_issue(
					Problem('external_dns', Problem.WARNING, messages.DNS_ZONE_MANAGED_BY_EXT_DNS % (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, messages.DNS_SERVER_IN_SLAVE_MODE % (domain.name,)),
					solution
				)

