import logging
from contextlib import closing

from parallels.common import target_data_model
from parallels.common.checking import Problem
from parallels.common.converter.business_objects.clients import ClientsConverter
from parallels.common.converter.business_objects.common import get_state, format_contact, EntityConverter, SOURCE_TARGET
from parallels.common.logging_context import log_context
from parallels.utils import format_multiline_list, all_equal, group_by_id, group_by

logger = logging.getLogger(__name__)


class ExpandToPleskClientsConverter(object):
	def convert_clients(
		self, source_plesk_servers, expand_data, target_panel_clients, clients_mapping, report, password_holder
	):
		"""
		:type source_plesk_servers: list[parallels.common.global_context.SourceServerInfo]
		:type expand_data: parallels.expand_migrator.expand_data.model.Model
		:type target_panel_clients: dict
		:type clients_mapping: dict[basestring, basestring | None]
		:type report: parallels.common.checking.Report
		:type password_holder: parallels.common.converter.business_objects.password_holder.PasswordHolderBase
		"""
		logger.debug(u"Convert clients")

		plain_converted_clients = self._convert_clients_plain(
			clients_mapping, expand_data, password_holder, report,
			source_plesk_servers, target_panel_clients
		)
		clients_by_logins = group_by(plain_converted_clients, lambda c: c.login)
		merged_clients = self._merge_clients(clients_by_logins, report)
		self._check_all_clients_from_mapping_are_presented(clients_by_logins, clients_mapping, report)

		return merged_clients

	def _convert_clients_plain(
		self, clients_mapping, expand_data, password_holder, report,
		source_plesk_servers, target_panel_clients
	):
		"""Convert clients from target and source to target model and put them in a plain list

		:type clients_mapping: dict[basestring, basestring | None]
		:type expand_data: parallels.expand_migrator.expand_data.model.Model
		:type report: parallels.common.checking.Report
		:type password_holder: parallels.common.converter.business_objects.password_holder.PasswordHolderBase
		:type source_plesk_servers: list[parallels.common.global_context.SourceServerInfo]
		:type target_panel_clients: dict
		:rtype: list[parallels.common.target_data_model.Client]
		"""
		converted_target_clients = self._convert_clients_from_target_panel(
			clients_mapping, target_panel_clients
		)
		converted_expand_clients = self._convert_expand_clients_from_source(
			clients_mapping, expand_data
		)
		converted_plesk_clients = self._convert_clients_from_source_plesks(
			expand_data, clients_mapping, password_holder, report, source_plesk_servers
		)
		converted_clients = sum([
			converted_target_clients, converted_expand_clients, converted_plesk_clients
		], [])
		return converted_clients

	@staticmethod
	def _convert_clients_from_target_panel(clients_mapping, target_panel_clients):
		"""Convert clients from target panel existing objects model to target model clients

		:type clients_mapping: dict[basestring, basestring | None]
		:type target_panel_clients: dict
		:rtype: list[parallels.common.target_data_model.Client]
		"""
		logger.debug(u"Convert clients from target panel")

		converted_clients = []
		for client in target_panel_clients.itervalues():
			if clients_mapping is None or client.contact.username in clients_mapping:
				target_model_client = ClientsConverter.create_client_stub_from_existing_client(client)
				converted_clients.append(target_model_client)

		return converted_clients

	@classmethod
	def _convert_expand_clients_from_source(cls, clients_mapping, expand_data):
		"""Convert Expand clients to target model clients

		:type clients_mapping: dict[basestring, basestring | None]
		:type expand_data: parallels.expand_migrator.expand_data.model.Model
		:rtype: list[parallels.common.target_data_model.Client]
		"""
		logger.debug(u"Convert source Expand clients")

		converted_clients = []
		for expand_client in expand_data.expand_clients:
			expand_client_login = expand_client.info['login']
			if clients_mapping is None or expand_client_login in clients_mapping:
				converted_client = cls._convert_expand_client(expand_client)
				converted_clients.append(converted_client)

		return converted_clients

	@staticmethod
	def _convert_clients_from_source_plesks(
		expand_data, clients_mapping, password_holder, report, source_plesk_servers
	):
		"""Convert Plesk clients to target model clients
		:type expand_data: parallels.expand_migrator.expand_data.model.Model
		:type clients_mapping: dict[basestring, basestring | None]
		:type report: parallels.common.checking.Report
		:type password_holder: parallels.common.converter.business_objects.password_holder.PasswordHolderBase
		:type source_plesk_servers: list[parallels.common.global_context.SourceServerInfo]
		:rtype: list[parallels.common.target_data_model.Client]
		"""
		logger.debug(u"Convert source Plesk clients")

		converted_clients = []
		entity_converter = EntityConverter(None)

		expand_clients_by_id = group_by_id(expand_data.expand_clients, lambda c: c.id)
		plesk_to_expand_client = dict()
		for plesk_client in expand_data.plesk_clients:
			if plesk_client.expand_client_id in expand_clients_by_id:
				plesk_to_expand_client[(
					plesk_client.plesk_id,
					plesk_client.login
				)] = expand_clients_by_id[plesk_client.expand_client_id]

		for source_panel_info in source_plesk_servers:
			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:
						if (source_panel_info.id, client.login) not in plesk_to_expand_client:
							converted_client = entity_converter.create_client_from_plesk_backup_client(
								client, source_panel_info.id, report, password_holder
							)
							converted_clients.append(converted_client)

		return converted_clients

	def _merge_clients(self, clients_by_logins, report):
		"""Merge clients - perform conflict resolution when multiple clients have the same login

		Logic is simple - if they all have the same contact name (first name plus last name) and e-mail,
		then clients are considered the same single client. Otherwise an error is reported.

		:type report: parallels.common.checking.Report
		:type clients_by_logins: dict[basestring, list[parallels.common.target_data_model.Client]]
		:rtype: list[parallels.common.target_data_model.Client]
		"""

		merged_clients = []
		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:
						# seems to be same clients
						pass
				else:
					assert False

				merged_clients.append(clients[0])

		return merged_clients

	@staticmethod
	def _check_all_clients_from_mapping_are_presented(clients_by_logins, clients_mapping, report):
		"""Check that all clients listed in mapping (migration list) actually exist on source or on target

		:type clients_by_logins: dict[basestring, list[parallels.common.target_data_model.Client]]
		:type clients_mapping: dict[basestring, basestring | None]
		:type report: parallels.common.checking.Report
		:rtype: None
		"""
		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 Expand nor on target Plesk.".format(
								login=login
							)
						),
						u"Fix migration list."
					)

	@staticmethod
	def _convert_expand_client(expand_client):
		"""Convert Expand model client to target model client

		:type expand_client: parallels.expand_migrator.expand_data.model.ExpandClient
		:rtype: parallels.common.target_data_model.Client
		"""

		locale = expand_client.personal_info.get('locale', None)

		return target_data_model.Client(
			login=expand_client.info.get('login'),
			password=expand_client.info.get('passwd'),
			subscriptions=[],
			company=expand_client.personal_info.get('company', ''),
			personal_info=target_data_model.PersonalInfo(
				first_name=expand_client.personal_info.get('pname', ''),
				last_name=None,
				email=expand_client.personal_info.get('email') or u"%s@example.com" % expand_client.info.get('login', ''),
				preferred_email_format='plainText',
				address_line_1=expand_client.personal_info.get('address') or 'No address',
				address_line_2='',
				city=expand_client.personal_info.get('city') or 'No city',
				county='',
				state=get_state(
					expand_client.personal_info.get('state'),
					expand_client.personal_info.get('country', 'US')
				),
				postal_code=expand_client.personal_info.get('zip'),
				language_code=locale[0:2] if locale is not None else None,
				locale=locale,
				country_code=expand_client.personal_info.get('country', 'US'),
				primary_phone=expand_client.personal_info.get('phone') or '',
				additional_phone='',
				fax=expand_client.personal_info.get('fax') or '',
				mobile_phone=''
			),
			auxiliary_user_roles=[],
			auxiliary_users=[],
			is_enabled=expand_client.info.get('status', '1') == '0',
			source='expand',
			target_client=None,
		)

	@staticmethod
	def _format_client(client):
		"""Format information about converted client as a string to put into report

		:type client: parallels.common.target_data_model.Client
		:rtype: basestring
		"""
		if client.source == 'expand':
			client_type = u'Expand client on source Expand server'
		elif client.source == SOURCE_TARGET:
			client_type = u'Client on target server'
		else:
			client_type = u"Plesk client on source Plesk server '%s'" % client.source

		return u"%s: username '%s', contact (first and last names) '%s', e-mail '%s'" % (
			client_type,
			client.login,
			format_contact(client.personal_info),
			client.personal_info.email,
		)