import logging
from collections import defaultdict
from contextlib import closing

from parallels.common import target_data_model as ppa
from parallels.utils import group_by_id, if_not_none
from parallels.common.checking import Problem
from parallels.target_panel_ppa.converter.converter import PPAConverter
from parallels.common.converter.business_objects.common import check_domain_conflicts, index_plesk_backup_domain_objects

# Conversion rules:
# * ignore Plesk plans, convert only PPA service templates; do not check Plesk plans conflicts and ownership
# * ignore Plesk resellers, convert only PPA resellers; do not check Plesk resellers conflicts
# * ignore Plesk clients, convert only PPA customers; do not check Plesk clients conflicts
# * convert only Plesk customer subscriptions (but customer may be under admin as well as under reseller);
#   must not check that client and plan have same owner, in PBAS it is possible for reseller to subscribe own client to admin's template
# * ignore Plesk admin's hosting subscriptions
class Converter(PPAConverter):
	logger = logging.getLogger('%s.Converter' % __name__)
	ppa_admin_login = None	# not going to migrate admin's subscriptions

	def __init__(self, ppa_existing_objects, pbas_data, options):
		PPAConverter.__init__(self, None, ppa_existing_objects, options)
		self.pbas_data = dict(pbas_data)	# { plesk_login: ppa_login, ppa_st_id, ppa_group_name }
		# all aux users, to check if such aux user already exists on other Plesk
		self.raw_aux_users = defaultdict(list)	# { client_login: [aux_user1,...] }

	def convert_plesks(self, plesks, plain_report, subscriptions_mapping, password_holder, mail_servers=dict()):
		for plesk_info in plesks:
			self.logger.info(u"Convert backup")
			with closing(plesk_info.load_raw_backup()) as backup:
				self.convert_plesk(
					plesk_info.id, backup, plesk_info.settings.ip,
					plain_report, 
					plesk_info.settings.is_windows,
					plesk_info.settings.is_windows,
					subscriptions_mapping,
					password_holder
				)

		# check if all Plesk accounts specified in migration_data.xml are found in Plesk backups
		for plesk_login, data in self.pbas_data.iteritems():
			if data.source is None:
				plain_report.add_customer_issue(data.source, plesk_login,
					Problem(
						'nonexistent_plesk_account', Problem.ERROR,
						u"Account with login '%s' does not exist on Plesk servers specified in migration tool's configuration. Any subscriptions of this account will not be transferred." % plesk_login
					),
					u"Either add the Plesk server where this account exists into the migration tool's configuration file, or exclude the corresponding PBAS account from transfer."
				)

		# group PPA account subscriptions and check limits
		for _, ppa_client in self.ppa_clients.iteritems():
			self._group_subscriptions(ppa_client)
			self._check_group_limit(ppa_client.subscriptions, plain_report)
			self._check_service_template_limit(ppa_client.subscriptions, None, plain_report)

	def convert_plesk(self, plesk_id, backup, ip, plain_report, is_windows, mail_is_windows, subscriptions_mapping, password_holder):
		def add_all_subscriptions(plesk_account, ppa_client, ppa_group_name, ppa_st_id, report):
			subscriptions = plesk_account.subscriptions
			if hasattr(plesk_account, 'clients'):
				for client in plesk_account.clients:
					subscriptions += client.subscriptions

			for subscription in subscriptions:
				if subscription.name not in subscriptions_mapping:
					self.logger.debug(u"Skip subscription '%s', it is omitted in migration list", subscription.name)
				else:
					self.logger.debug(u"Convert subscription '%s'", subscription.name)
					sub_report = plain_report.get_subscription_report(plesk_id, subscription.name)
					self._add_subscription(plesk_id, ppa_client, subscription, backup, sub_report, is_windows, mail_is_windows, ppa_st_id, ppa_group_name)
					if ppa_client.login not in self.existing_objects.customers:
						sub_report.add_issue(
							Problem(
								'nonexistent_ppa_account', Problem.ERROR,
								u"Unable to transfer subscription '%s', as destination customer account with login '%s' does not exist in PPA." % (subscription.name, ppa_client.login)
							),
							u"Create PPA customer account with this login."
						)

		def add_all_aux_users(plesk_account, ppa_client, report):
			aux_users = [
				aux_user for aux_user in plesk_account.auxiliary_users
				if aux_user.is_built_in == False	# skip login that is auto-generated by PBAS
			]
			aux_users_bindings = {}	# { login: subscription } *subscription* is the name of a subscription to which login must be bound
			if hasattr(plesk_account, 'clients'):
				for client in plesk_account.clients:
					if len(client.subscriptions) == 0:
						continue	# if we transfer panel access for client without subscriptions,
								# he will have access to all webspaces (not own) - therefore we do not transfer it.
					aux_users += client.auxiliary_users
					subscription_name = client.subscriptions[0].name
					aux_users_bindings.update({ user.name: subscription_name for user in client.auxiliary_users })

			for aux_user in aux_users:
				issues, converted_user = self._make_auxiliary_user(ppa_client.login, aux_user, password_holder)
				if converted_user is not None:
					subscription_name = aux_users_bindings.get(aux_user.name)
					if subscription_name is not None:
						converted_user.subscription_name = subscription_name
					self.raw_aux_users[ppa_client.login].append(converted_user)
					ppa_client.auxiliary_users.append(converted_user)

					if ppa_client.login not in self.existing_objects.customers:
						issues.append((
							Problem(
								'nonexistent_ppa_account', Problem.ERROR,
								u"Unable to transfer auxiliary user '%s', as destination customer account with login '%s' does not exist in PPA." % (converted_user.login, ppa_client.login)
							),
							u"Create PPA customer account with this login."
						))

				for issue in issues:
					report.add_issue(*issue)

		def add_all_aux_roles(plesk_account, ppa_client, report):
			self._add_auxiliary_user_roles(ppa_client, plesk_account.auxiliary_user_roles, report)
			if hasattr(plesk_account, 'clients'):
				for client in plesk_account.clients:
					self._add_auxiliary_user_roles(ppa_client, client.auxiliary_user_roles, report)

		def make_ppa_client(customer_login):
			if customer_login not in self.ppa_clients:
				ppa_client = ppa.Client(
					login=customer_login,
					source='ppa',
					personal_info=ppa.PersonalInfo(
						email=None, first_name=None, last_name=None,
						preferred_email_format=None, address_line_1=None,
						address_line_2=None, city=None, county=None,
						state=None, postal_code=None, language_code=None,
						locale=None, country_code=None, primary_phone=None,
						additional_phone=None, fax=None, mobile_phone=None,
					),
					password=None, subscriptions=[], company=None,
					auxiliary_user_roles=[], auxiliary_users=[], is_enabled=None,
				)
				self.ppa_clients[customer_login] = ppa_client
			else:
				ppa_client = self.ppa_clients[customer_login]
			return ppa_client

		resellers_by_login = group_by_id(backup.iter_resellers(), lambda r: r.login)
		clients_by_login = group_by_id(backup.iter_all_clients(), lambda c: c.login)
		users_by_login = dict(resellers_by_login.items() + clients_by_login.items())

		for plesk_login, data in self.pbas_data.iteritems():
			if plesk_login not in users_by_login:
				continue

			if data.source is None:
				data.source = plesk_id

			ppa_client = make_ppa_client(data.ppa_login)
			if plesk_login in resellers_by_login:
				account_report = plain_report.get_reseller_report(plesk_id, plesk_login)
			else:
				account_report = plain_report.get_customer_report(plesk_id, plesk_login)
			add_all_subscriptions(plesk_account=users_by_login[plesk_login], ppa_client=ppa_client, ppa_group_name=data.ppa_group_name, ppa_st_id=data.ppa_st_id, report=account_report)

			add_all_aux_roles(plesk_account=users_by_login[plesk_login], ppa_client=ppa_client, report=account_report)
			add_all_aux_users(plesk_account=users_by_login[plesk_login], ppa_client=ppa_client, report=account_report)

	@classmethod
	def convert_resellers(cls, source_panel_data, existing_ppa_resellers, resellers_migration_list, report, password_holder):
		cls.logger.info(u"Convert resellers from PPA")

		converted_resellers = []
		for reseller in existing_ppa_resellers.itervalues():
			if resellers_migration_list is None or reseller.contact.username in resellers_migration_list:
				converted_resellers.append(cls._convert_ppa_reseller(reseller))
		return converted_resellers

	# TODO make it even more flat
	# 1) need to check if subscription conflicts with: existing in ppa, existing in sources, existing sites, existing subdomains
	# 2) need to form up subscription with proper source
	def _add_subscription(self, plesk_id, ppa_client, new_subscription, backup, report, is_windows, mail_is_windows, plan_id, group_name):
		required_hostings = PPAConverter._get_required_hostings(new_subscription, is_windows)
		if self.options.allocate_only_required_resources:
			desired_hostings = required_hostings
		else:
			if is_windows:
				desired_hostings = required_hostings + ['iis', 'mysql', 'mssql']
			else:
				desired_hostings = required_hostings + ['apache', 'mysql', 'postgresql']

		webspaces_by_name = group_by_id(self.existing_objects.webspaces, lambda ws: ws.name)
		if new_subscription.name in webspaces_by_name:
			webspace = webspaces_by_name[new_subscription.name]
			ppa_subscriptions_by_id = group_by_id(self.existing_objects.raw_ppa_subscriptions, lambda s: s.subscription_id)
			ppa_subscription = ppa_subscriptions_by_id[webspace.subscription_id]
			if webspace.owner_id != if_not_none(self.existing_objects.customers.get(ppa_client.login, None), lambda c: c.id):
				existing_customer_username_by_id = dict((customer.id, customer.contact.username) for customer in self.existing_objects.customers.values())
				problem = Problem(
					'subscription_owned_by_another_customer', Problem.WARNING,
					u"The subscription '%s' already exists in PPA but belongs to the other customer '%s'" % (
						new_subscription.name, 
						existing_customer_username_by_id[webspace.owner_id],
					)
				)
			else:
				problem = Problem(
					'duplicate_subscription_name', Problem.WARNING,
					u"The subscription '%s' already exists in PPA" % new_subscription.name
				)

			solution_msg = u"The existing subscription in PPA will be overwritten (including hosting settings, site content, databases, and so on).  If you want to transfer the subscription into a separate one, rename one of the subscriptions."
			report.add_issue(problem, solution_msg)

			sub = ppa.Subscription(
				new_subscription.name, plan_name=None, plan_id=plan_id,
				addon_plan_ids=[],
				web_ip=None,
				web_ip_type=new_subscription.ip_type,
				web_ipv6=None,
				web_ipv6_type=new_subscription.ipv6_type,
				is_enabled=True,
				# XXX: source=plesk_id is logically wrong
				source=plesk_id, is_windows=is_windows,
				mail_is_windows=is_windows,
				sub_id=webspace.webspace_id, group_name=ppa_subscription.name, group_id=webspace.subscription_id, required_resources=[], additional_resources=[],
			)
			ppa_client.subscriptions.append(sub)
			self.ppa_subscriptions[sub.name] = sub

		elif new_subscription.name in self.ppa_subscriptions:
			sub = self.ppa_subscriptions[new_subscription.name]
			report.add_issue(
				Problem(
					'duplicate_subscription_name', Problem.ERROR,
					u"A subscription '%s' already exists on the other server '%s'" % (new_subscription.name, sub.source)
				),
				u"Either rename one of the subscriptions (if you want to transfer both of them) or exclude one of the subscriptions from the migration list file."
			)

		elif new_subscription.name in self.existing_site_names:
			sub_name, obj_type = self.existing_site_names[new_subscription.name]
			report.add_issue(
				Problem(
					'duplicate_site_name', Problem.ERROR,
					u"A %s '%s' already exists in PPA within the '%s' subscription" % (obj_type, new_subscription.name, sub_name)
				),
				u"Either rename this subscription or the %s '%s' or exclude this subscription from transfer using the migration list file." % (obj_type, new_subscription.name)
			)

		elif new_subscription.name in self.ppa_site_names:
			sub_name, obj_type = self.ppa_site_names[new_subscription.name]
			report.add_issue(
				Problem(
					'duplicate_site_name', Problem.ERROR,
					u"A %s '%s' already exists on the other server within the '%s' subscription" % (obj_type, new_subscription.name, sub_name)
				),
				u"Either rename this subscription or the %s '%s' or exclude this subscription from transfer using the migration list file." % (obj_type, new_subscription.name)
			)

		else:
			# TODO refine checks, as it looks like above checks are partially the same as the following
			problems = check_domain_conflicts(
				backup, new_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,
			)
			if not problems:
				ppa_subscription = ppa.Subscription(
					new_subscription.name, plan_name=None, plan_id=plan_id,
					addon_plan_ids=[],
					web_ip=None,
					web_ip_type=new_subscription.ip_type,
					web_ipv6=None,
					web_ipv6_type=new_subscription.ipv6_type,
					is_enabled=new_subscription.is_enabled,
					source=plesk_id, is_windows=is_windows,
					mail_is_windows=is_windows,
					sub_id=None, group_name=group_name,
					group_id=None, required_resources=required_hostings, additional_resources=desired_hostings,
				)
				ppa_client.subscriptions.append(ppa_subscription)
				self.ppa_subscriptions[ppa_subscription.name] = ppa_subscription
				self._check_subscription_dns_zones(backup, new_subscription.name, report)
				domain_objects = index_plesk_backup_domain_objects(backup, new_subscription.name) 
				for name, kind in domain_objects:
					self.ppa_site_names[name] = (new_subscription.name, kind)
			else:
				for problem in problems:
					report.add_issue(problem, u"Either change the conflicting names, or exclude this subscription from transfer using the migration list file.")

