import logging
from collections import defaultdict
from contextlib import closing

from parallels.utils import group_by_id, if_not_none, all_equal, format_multiline_list
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, get_state
from parallels.common import target_data_model
from parallels.target_panel_plesk.models.target_data_model import ResellerSettings
from parallels.common.utils.plesk_limits_permissions import RESELLER_PLAN_LIMIT_NAMES, RESELLER_PLAN_PERMISSION_NAMES


class ResellersConverter(object):
	"""Generate list of resellers ready to import to target panel.
	
	Resellers converter takes the following information:
	- Plesk backup or other source of information 
	about resellers on source servers
	- Information about resellers that already exist on 
	target panel
	- Migration list, which contains list of resellers that 
	will be used during migration

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

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

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

	def convert_resellers(self, source_panel_data, target_resellers, resellers_migration_list, 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
		target_resellers - dictionary with keys - logins of resellers in target
			panel, values - object with various information about reseller
		resellers_migration_list -
			dictionary {reseller login: reseller plan}, if plan is not set and reseller
			subscription is custom, then reseller plan is None.
			If this parameter is None, all resellers should be converted.
		report - report object to put conversion warnings to

		:type resellers_migration_list: dict[basestring, basestring]
		"""
		self.logger.info(u"Convert resellers")
		converted_resellers = []

		# convert resellers from target panel
		self.logger.debug(u"Convert resellers from target panel")
		for reseller in target_resellers.itervalues():
			if resellers_migration_list is None or reseller.contact.username in resellers_migration_list:
				converted_resellers.append(self._convert_target_panel_reseller(reseller))

		# convert resellers from source panel 
		converted_resellers += self._convert_source_panel_resellers(
			source_panel_data, resellers_migration_list, report, password_holder
		)

		# group resellers by logins
		resellers_by_logins = defaultdict(list)
		for reseller in converted_resellers:
			resellers_by_logins[reseller.login].append(reseller)

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

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

					resellers_info = dict(
						login=login,
						info_list=format_multiline_list(
							[self._format_reseller(r) for r in resellers]
						)
					)
					if len(differences) > 0:
						report.add_issue(
							Problem(
								'duplicate_reseller_name_with_another_contact_data', 
								Problem.ERROR, 
								u"Reseller with the same username '{login}' but different {difference} "
								u"exists on a number of servers:\n{info_list}".format(
									difference=u" and ".join(differences), **resellers_info
								)
							),
							u"Either remove the difference (if both accounts belong to the same reseller) "
							u"or change the reseller's username (if these are the different resellers)."
						)
					else:
						report.add_issue(
							Problem(
								'duplicate_reseller_name', 
								Problem.WARNING, 
								u"Reseller with the same username '{login}' exists "
								u"on a number of servers:\n{info_list}".format(**resellers_info)
							),
							u"All reseller data of the checked reseller (including clients and their subscriptions) "
							u"will be transferred to the target panel under a single reseller. "
							u"In this case, ensure that combined resources of all accounts do not exceed the limits. "
							u"If you want migrator to create separate accounts, change the reseller's username."
						)
				else:
					assert False

				merged_resellers.append(resellers[0])

		if resellers_migration_list is not None:
			for login in resellers_migration_list.keys():
				if login not in resellers_by_logins:
					report.add_issue(
						Problem(
							'duplicate_reseller_name_with_another_contact_data', 
							Problem.ERROR, 
							u"Reseller '{login}' is presented in migration list, "
							u"but is presented neither on the source panel, nor in the target panel.".format(
								login=login
							)
						),
						u"Fix migration list."
					)
		
		return merged_resellers

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

	@classmethod
	def _convert_source_panel_resellers(cls, source_panel_data, resellers_migration_list, report, password_holder):
		"""
		:type resellers_migration_list: dict[basestring, basestring]
		"""
		cls.logger.debug(u"Convert resellers from source panel")
		plesks = source_panel_data
		converted_resellers = []
		for plesk_info in plesks:
			server_report = report.subtarget(u"Source server", plesk_info.id)
			with log_context(plesk_info.id):
				cls.logger.debug(u"Convert resellers from '%s'", plesk_info.id)
				with closing(plesk_info.load_raw_backup()) as backup:
					reseller_plans_by_guid = group_by_id(list(backup.iter_admin_reseller_plans()), lambda plan: plan.id)
					for reseller in backup.iter_resellers():
						if resellers_migration_list is None or reseller.login in resellers_migration_list:
							reseller_report = server_report.subtarget(u"Reseller", reseller.login)
							reseller_source_panel_plan = reseller_plans_by_guid.get(reseller.plan_id)
							if resellers_migration_list is not None:
								reseller_migration_list_plan = resellers_migration_list.get(reseller.login)
							else:
								reseller_migration_list_plan = None
							converted_resellers.append(
								cls._create_target_model_reseller(
									plesk_info.id, reseller, plesk_info.settings.is_windows, 
									reseller_report, password_holder,
									reseller_source_panel_plan, reseller_migration_list_plan
								)
							)
		return converted_resellers

	@staticmethod
	def _convert_target_panel_reseller(reseller):
		# result of this function is not imported to the target panel, it is just used for
		# conflict resolution between the source and the target panel resellers
		# so only those properties that are used by conflict resolution should be processed,
		# all the others are set to None
		return target_data_model.Reseller(
			source='ppa',
			login=reseller.contact.username,  # necessary for merging
			personal_info=target_data_model.PersonalInfo(
				first_name=reseller.contact.first_name,  # necessary for merging
				last_name=reseller.contact.last_name,  # necessary for merging
				email=reseller.contact.email,  # necessary for merging
				# below - all information not necessary for merging - set to 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, country_code=None, 
				locale=None, primary_phone=None, additional_phone=None, 
				fax=None, mobile_phone=None,
			),
			# below - all information not necessary for merging - set to None
			password=None, plan_name=None, 
			clients=[],
			company=None,
			auxiliary_user_roles=None, auxiliary_users=[],
			is_enabled=None, plans=None, settings=None,
		)

	@classmethod
	def _create_target_model_reseller(
		cls, plesk_id, reseller, is_windows, reseller_report, password_holder,
		reseller_source_panel_plan, reseller_migration_list_plan
	):
		password = cls._get_reseller_password(reseller, reseller_report, password_holder)

		first_name = reseller.contact
		if first_name is None or not first_name.strip():
			first_name = reseller.personal_info.get('name')
		if first_name is None or not first_name.strip():
			first_name = reseller.login

		if reseller_migration_list_plan is None:
			settings = if_not_none(
				reseller_source_panel_plan,
				cls.convert_reseller_settings_from_plesk_backup_to_plesk_import_api
			)
		else:
			settings = None

		return target_data_model.Reseller(
			login=reseller.login, 
			password=password,
			plan_name=reseller_migration_list_plan,
			clients=[],
			# TODO consider changing .get(key, default) to .get(key) or default
			# as .get() does not protect from present value = None case
			company=reseller.personal_info.get('company', ''),
			personal_info=target_data_model.PersonalInfo(
				first_name=first_name,
				last_name='',
				email=reseller.personal_info.get('email', (u"%s@example.com" % reseller.login)),
				preferred_email_format='plainText',
				address_line_1=reseller.personal_info.get('address', 'No address'),
				address_line_2='',
				city=reseller.personal_info.get('city', 'No city'),
				county='',
				state=get_state(reseller.personal_info.get('state'), reseller.personal_info.get('country', 'US')), 
				postal_code=reseller.personal_info.get('zip', '11111'), 
				language_code=reseller.personal_info.get('locale', 'en')[0:2],
				locale=reseller.personal_info.get('locale', 'en-US'),
				country_code=reseller.personal_info.get('country', 'US'), 
				primary_phone=reseller.personal_info.get('phone', '1111111'), 
				additional_phone='', 
				fax=reseller.personal_info.get('fax', ''), 
				mobile_phone=''
			),
			auxiliary_user_roles=[],
			auxiliary_users=[],
			is_enabled=reseller.is_enabled,
			plans={},
			settings=settings,
			source=plesk_id
		)

	@staticmethod
	def _get_reseller_password(reseller, reseller_report, password_holder):
		if reseller.password.type == 'plain':
			password = reseller.password.text
		else:
			main_auxiliary_user = None
			for user in reseller.auxiliary_users:
				if user.is_built_in and user.name == reseller.login:
					main_auxiliary_user = user

			if main_auxiliary_user is not None and main_auxiliary_user.password.type == 'plain':
				# Hack to get resellers's password if it is stored as a hash:
				# in Plesk 11.0 and 11.1 there is always an SMB (auxiliary) user that has
				# the same login and password as reseller
				# but its password is always stored as plain text (regardless of how resellers's password is stored)
				password = main_auxiliary_user.password.text
			else:
				password = password_holder.get('reseller', reseller.login)
				reseller_report.add_issue(
					Problem(
						'missing_reseller_password',
						Problem.WARNING,
						u"Unable to retrieve the reseller's password as it is stored in the encrypted form"
					),
					u"A new password has been generated for the reseller '%s': '%s'. "
					u"You can change the password after data transfer is finished." % (
						reseller.login, password
					)
				)
		return password

	@staticmethod
	def convert_reseller_settings_from_plesk_backup_to_plesk_import_api(backup_plan):
		return ResellerSettings(
			limits={
				limit_name: backup_plan.properties[limit_name]
				for limit_name in RESELLER_PLAN_LIMIT_NAMES
				if limit_name in backup_plan.properties
			},
			permissions={
				permission_name: backup_plan.properties[permission_name]
				for permission_name in RESELLER_PLAN_PERMISSION_NAMES
				if permission_name in backup_plan.properties
			},
			overuse=backup_plan.properties.get('overuse'),
			oversell=backup_plan.properties.get('oversell'),
		)