from parallels.plesk import messages
import logging
from collections import defaultdict
from contextlib import closing

from parallels.core import target_data_model as target_model
from parallels.core.converter.business_objects.clients import ClientsConverter
from parallels.plesk.converter.ip_mapping import IPMapper
from parallels.plesk.import_api import model as import_api_model
from parallels.core.utils.common import group_by_id, group_value_by_id, find_first
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 PleskConverter(object):
	"""Generate model of subscriptions and customers 
	ready to import to target panel.
	
	Converter takes the following information:
	- Plesk backup or other source of information 
	about resellers, clients, subscriptions and domains 
	on source servers
	- Information about objects that already exist on
	the target panel: resellers, plans, clients, domains
	and so on
	- Migration list, which contains list of customers and 
	subscriptions to migrate, their mapping between each other,
	plans and resellers

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

	Result is a model ready to import to target panel.

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

	logger = logging.getLogger(__name__)

	def __init__(self, target_existing_objects, entity_converter=None, skip_ip_mapping=False):
		self.target_resellers = {}
		self.target_clients = {}
		self.target_site_names = {}
		self.existing_objects = target_existing_objects
		if entity_converter is not None:
			self.entity_converter = entity_converter
		else:
			self.entity_converter = EntityConverter(self.existing_objects)

		self.raw_target_clients = {
			None: ClientsConverter.create_fake_client(None)  # admin fake client
		}  # these special clients are included here, but not distinguished from other - regular - clients
		self.raw_target_subscriptions = {}
		self.existing_site_names = index_target_site_names(self.existing_objects)

		self._ip_mapper = IPMapper(self.existing_objects)
		self._skip_ip_mapping = skip_ip_mapping

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

	def convert_dumps(
		self, plesks, plain_report, subscriptions_mapping, customers_mapping, converted_resellers, password_holder
	):
		self.target_resellers = group_by_id(converted_resellers, lambda r: r.login)

		for plesk_info in plesks:
			self.logger.debug(messages.CONVERT_DUMP)
			with closing(plesk_info.load_raw_dump()) as backup:
				self._convert_dump(
					plesk_info.id, backup, plain_report, plesk_info.is_windows,
					subscriptions_mapping, customers_mapping, password_holder)

	def get_target_model(self):
		return target_model.Model(plans={}, resellers=self.target_resellers, clients=self.target_clients)

	def _convert_dump(
			self, plesk_id, backup, plain_report, is_windows,
			subscriptions_mapping, customers_mapping, password_holder):
		self.logger.debug(messages.CREATE_TARGET_MODEL_OBJECTS)

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

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

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

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

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

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

							# take addon plans mapping
							if len(subscriptions_mapping[subscription.name].addon_plans) > 0:
								reseller_id = find_first(
									self.existing_objects.resellers.itervalues(),
									lambda r: r.contact.username == reseller_login
								).id if reseller_login is not None else 1
								addon_plans = [
									st for st in self.existing_objects.addon_service_templates
									if st.owner_id == reseller_id
								]

								for addon_plan_name in subscriptions_mapping[subscription.name].addon_plans:
									addon_plan = self._get_addon_plan(
										addon_plans, addon_plan_name, reseller_login, subscription_report
									)
									if addon_plan is not None:
										subscription.addon_plan_ids.append(addon_plan.st_id)
						else:
							# there are critical errors, so do not consider
							# that subscription anymore in further checks and conversions
							del self.raw_target_subscriptions[subscription.name]
					else:
						# custom subscription - subscription that is not assigned to any plan
						# so leave plan_name set to None
						pass 
				else:
					self.logger.debug(
						messages.SUBSCRIPTION_NOT_LISTED_IN_MIGRATION_LIST,
						subscription.name
					)
					del self.raw_target_subscriptions[subscription.name]

		# 2.2 subscriptions <- client, according to migration list 
		for subscription in self.raw_target_subscriptions.values():
			if subscription.name in subscriptions_mapping:
				subscription_mapping = subscriptions_mapping[subscription.name]
				subscription_owner = subscription_mapping.owner
				if subscription_owner in self.raw_target_clients:
					# add subscription to model. guard against multiple addition of
					# the same subscription (if it exists on two Plesk servers)
					client_subscriptions = self.raw_target_clients[subscription_owner].subscriptions
					client_subcription_names = [subs.name for subs in client_subscriptions]
					if subscription.name not in client_subcription_names:
						self.raw_target_clients[subscription_owner].subscriptions.append(subscription)
				else:
					plain_report.add_subscription_issue(
						plesk_id, subscription.name,
						Problem(
							"customer_does_not_exist", Problem.ERROR,
							messages.CUSTOMER_DOES_NOT_EXIST_ISSUE % subscription_owner,
						),
						messages.CUSTOMER_DOES_NOT_EXIST_ISSUE_SOLUTION
					)
					del self.raw_target_subscriptions[subscription.name]
			else:
				self.logger.debug(
					messages.SUBSCRIPTION_NOT_LISTED_IN_MIGRATION_LIST,
					subscription.name
				)
				del self.raw_target_subscriptions[subscription.name]

		# 2.3 client <- admin, reseller, according to migration list
		def add_client_to_model(client, reseller_login):
			if reseller_login is None:
				if client.login not in self.target_clients:
					self.target_clients[client.login] = client
			elif reseller_login in self.target_resellers:
				reseller_clients_by_login = group_by_id(self.target_resellers[reseller_login].clients, lambda c: c.login)
				if client.login not in reseller_clients_by_login:
					self.target_resellers[reseller_login].clients.append(client)
			else:
				plain_report.add_customer_issue(
					plesk_id, client.login,
					Problem(
						'reseller_does_not_exist', Problem.ERROR,
						messages.CLIENT_MAPPED_TO_RESELLER_THAT_DOES_NOT_EXIST_ISSUE % reseller_login
					),
					messages.CLIENT_MAPPED_TO_RESELLER_THAT_DOES_NOT_EXIST_SOLUTION
				)

		backup_reseller_logins = {reseller.login for reseller in backup.iter_resellers()}
		target_reseller_logins = set(self.existing_objects.resellers.keys())
		all_reseller_logins = backup_reseller_logins | target_reseller_logins

		for client in self.raw_target_clients.values():
			if client.login in customers_mapping or client.login is None:
				reseller_login = customers_mapping.get(client.login)
				add_client_to_model(client, reseller_login)
			elif client.login in all_reseller_logins:
				add_client_to_model(client, client.login)
			else:
				self.logger.debug(messages.CLIENT_IS_NOT_LISTED_IN_MIGRATION_LIST % client.login)
				del self.raw_target_clients[client.login]

		# Map IP addresses for each subscription

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

		if not self._skip_ip_mapping:
			self._ip_mapper.map_ip_addresses(
				backups, new_model_subscriptions, backup_subscriptions, subscriptions_mapping, plain_report
			)

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

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

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

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

		return target_model_client

	def _check_client_owner(self, client_report, customer_mapping, existing_client):
		# check existing client owner
		if existing_client.owner_id is None:
			if customer_mapping is not None:
				client_report.add_issue(
					Problem(
						"owned_by_another_admin", Problem.ERROR,
						messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_ADMIN_RESELLER_ISSUE % (customer_mapping,)
					),
					messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_ADMIN_RESELLER_ISSUE_SOLUTION)
		else:
			# Customer is already owned by some reseller
			target_reseller = find_first(
				self.existing_objects.resellers.values(),
				lambda r: str(r.id) == str(existing_client.owner_id)
			)
			if target_reseller is None or target_reseller.contact.username != customer_mapping:
				client_report.add_issue(
					Problem(
						"owned_by_another_reseller", Problem.ERROR,
						messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_RESELLER_ISSUE % (
							'admin' if customer_mapping is None else messages.RESELLER_TITLE_LOWERCASE % (
								customer_mapping,
							)
						)
					),
					messages.CUSTOMER_BELONGS_TO_DIFFERENT_OWNER_RESELLER_SOLUTION
				)

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

	@staticmethod
	def _check_plan_exists(reseller_plans, plan_name, reseller_login, subscription_report):
		"""
		Get plan object for specified subscription 
		Return tuple (is_success, plan) where:
		- is_success is a boolean that means if plan selection was successful or not.
		- plan is a plan object or None in case of custom subscription
		"""
		if plan_name not in reseller_plans:
			if reseller_login is not None:
				owner_title = messages.RESELLER_TITLE_LOWERCASE % reseller_login
			else:
				owner_title = "admin"

			subscription_report.add_issue(
				Problem(
					"plan_does_not_exist", Problem.ERROR,
					messages.PLAN_DOES_NOT_EXIST_ISSUE.format(
						owner=owner_title,
						plan_name=plan_name
					)
				),
				messages.PLAN_DOES_NOT_EXIST_SOLUTION
			)

	@staticmethod
	def _get_addon_plan(addon_plans, addon_plan_name, reseller_login, subscription_report):
		"""Get addon plan object for specified owner by name. If it does not exist - add issue to the report"""
		addon_plan = find_first(addon_plans, lambda p: p.name == addon_plan_name)

		if addon_plan is None:
			if reseller_login is not None:
				owner_title = messages.RESELLER_TITLE_LOWERCASE % reseller_login
			else:
				owner_title = "admin"

			subscription_report.add_issue(
				Problem(
					"addon_plan_does_not_exist", Problem.ERROR,
					messages.ADDON_PLAN_DOES_NOT_EXIST_ISSUE.format(owner=owner_title, plan_name=addon_plan_name)
				),
				messages.PLAN_DOES_NOT_EXIST_SOLUTION
			)

		return addon_plan

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

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

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

		return dict(plans)

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

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

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

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