import logging
from collections import defaultdict
from contextlib import closing

from parallels.common import target_data_model as target_model
from parallels.utils import all_equal, format_multiline_list, group_by_id, fill_none_namedtuple
from parallels.common.logging_context import log_context
from parallels.common.checking import Problem
from parallels.common.converter.business_objects.common import format_source, format_contact, EntityConverter, \
	SOURCE_TARGET


class ClientsConverter(object):
	"""Generate list of customers ready to import to target panel.

	Clients converter takes the following information:
	- Plesk backup or other source of information
	about clients on source servers
	- Information about clients that already exist on
	target panel
	- Migration list, which contains list of resellers that
	will be used during migration

	Clients converter:
	- Converts each client we need to migrate to
	format ready to import to target panel.
	- Performs conflict resolution, if client exists on multiple
	source panel, or already exists on target panel.

	Result is a list of clients ready to import to target panel.
	"""

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

	def convert_clients(self, source_panel_data, target_panel_clients, clients_mapping, report, password_holder):
		"""Arguments:
			source_panel_data - abstract data from source panels that will be passed for conversion,
			for plesks source panel it is list of objects with:
				- attribute 'id' (source Plesk id, from config file),
				- attribute 'settings' (source Plesk settings from config file)
				- method 'load_raw_backup' that returns PleskBackup object for corresponding source Plesk
			clients_mapping - dictionary with keys - logins of clients in target panel,
				values - logins of clients to put clients under
			report - report object to put conversion warnings to
		"""
		self.logger.info(u"Convert clients")
		converted_clients = []

		# convert clients from target panel
		self.logger.info(u"Convert clients from target panel")
		for client in target_panel_clients.itervalues():
			if clients_mapping is None or client.contact.username in clients_mapping:
				target_model_client = self._create_client_stub_from_existing_client(client)
				converted_clients.append(target_model_client)

		# convert clients from source panel
		converted_clients += self._convert_source_panel_clients(
			source_panel_data, clients_mapping, report, password_holder
		)

		# group clients by logins
		clients_by_logins = defaultdict(list)
		for client in converted_clients:
			clients_by_logins[client.login].append(client)

		# perform conflicts resolution
		merged_clients = []
		self.logger.debug(u"Merge clients")
		for login, clients in clients_by_logins.iteritems():
			with log_context(login):
				if len(clients) == 1:
					pass  # client login is unique, go ahead
				elif len(clients) > 1:
					emails = [client.personal_info.email for client in clients]
					contacts = [format_contact(client.personal_info) for client in clients]

					differences = []
					if not all_equal(emails):
						differences.append(u'e-mail')
					if not all_equal(contacts):
						differences.append(u'contact (first and last names)')

					clients_info = dict(
						login=login,
						info_list=format_multiline_list([self._format_client(r) for r in clients])
					)
					if len(differences) > 0:
						report.add_issue(
							Problem(
								'duplicate_client_name_with_another_contact_data',
								Problem.ERROR,
								u"Client with the same username '{login}' but different {difference} "
								u"exists on a number of servers:\n{info_list}".format(
									difference=u" and ".join(differences), **clients_info
								)
							),
							u"Either remove the difference (if both accounts belong to the same client) "
							u"or change the client's username (if these are the different clients)."
						)
					else:
						pass  # seems to be same clients
				else:
					assert False

				merged_clients.append(clients[0])

		if clients_mapping is not None:
			for login in clients_mapping:
				if login not in clients_by_logins:
					report.add_issue(
						Problem(
							'client_is_not_presented_on_panels',
							Problem.ERROR,
							u"Client '{login}' is presented in migration list, "
							u"but is presented neither on source Plesk(s) nor in PPA.".format(
								login=login
							)
						),
						u"Fix migration list."
					)

		return merged_clients

	@staticmethod
	def assign_clients_to_resellers(admin_clients, resellers, clients, clients_mapping, report):
		clients_by_logins = group_by_id(clients, lambda c: c.login)
		resellers_by_logins = group_by_id(resellers, lambda r: r.login)
		for client_login, reseller_login in clients_mapping.iteritems():
			if client_login not in clients_by_logins:
				# If customer exists in migration list, but does not exist on source
				# and target panels - simply ignore it here. Issue should be reported by
				# "convert_clients" function
				continue

			client = clients_by_logins[client_login]

			if reseller_login is not None and reseller_login in resellers_by_logins:
				reseller = resellers_by_logins[reseller_login]
				reseller.clients.append(client)
			elif reseller_login is None:
				admin_clients[client.login] = client
			else:
				report.add_issue(
					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, "
					u"or map client to some existing reseller or administrator"
				)

	@classmethod
	def _convert_source_panel_clients(cls, source_panel_data, clients_mapping, report, password_holder):
		cls.logger.info(u"Convert clients from source panel")
		entity_converter = EntityConverter(None)
		converted_clients = []
		for source_panel_info in source_panel_data:
			server_report = report.subtarget(u"Source server", source_panel_info.id)
			with log_context(source_panel_info.id):
				cls.logger.info(u"Convert clients from '%s'", source_panel_info.id)
				with closing(source_panel_info.load_raw_backup()) as backup:
					for client in backup.iter_all_clients():
						if clients_mapping is None or client.login in clients_mapping:
							client_report = server_report.subtarget(u"Client", client.login)
							converted_client = entity_converter.create_client_from_plesk_backup_client(
								client, source_panel_info.id,  client_report, password_holder
							)
							entity_converter.fix_poa_company_name(converted_client)
							converted_clients.append(converted_client)

		return converted_clients

	@staticmethod
	def _create_client_stub_from_existing_client(existing_client):
		return target_model.Client(
			login=existing_client.contact.username,
			source=SOURCE_TARGET,
			personal_info=fill_none_namedtuple(
				target_model.PersonalInfo,
				email=existing_client.contact.email,
				first_name=existing_client.contact.first_name,
				last_name=existing_client.contact.last_name
			),
			password=None, subscriptions=[], company=None,
			auxiliary_user_roles=[], auxiliary_users=[], is_enabled=None,
			target_client=existing_client
		)

	@staticmethod
	def _format_client(client):
		return u"%s: username '%s', contact (first and last names) '%s', e-mail '%s'" % (
			format_source(client.source),
			client.login,
			format_contact(client.personal_info),
			client.personal_info.email,
		)