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.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 as ppa
from parallels.target_panel_plesk.import_api.import_api_base import PleskResellerSettings
from parallels.common.utils.plesk_limits_permissions import PLESK_LIMIT_NAMES, PLESK_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 - list with reseller logins to migrate, or None if all resellers should be migrated
			report - report object to put conversion warnings to
		"""
		self.logger.info(u"Convert resellers")
		converted_resellers = []

		# convert resellers from target panel
		self.logger.info(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_ppa_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
		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,
			)

		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([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} 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) 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 on a number of servers:\n{info_list}".format(**resellers_info)
							),
							u"All reseller data of the checked reseller (including clients and their subscriptions) will be transferred to PPA under a single reseller. " + \
							u"In this case, ensure that combined resources of all accounts do not exceed the limits. " + 
							u"If you want PPA 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:
				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, but is presented neither on source Plesk(s) nor in PPA.".format(
								login=login
							)
						),
						u"Fix migration list."
					)
		
		return merged_resellers

	@classmethod
	def _convert_source_panel_resellers(cls, source_panel_data, resellers_migration_list, report, password_holder):
		cls.logger.info(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.info(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_plan = reseller_plans_by_guid.get(reseller.plan_id)
							converted_resellers.append(
								cls._create_ppa_reseller(
									plesk_info.id, reseller, plesk_info.settings.is_windows, 
									reseller_report, password_holder, reseller_plan
								)
							)
		return converted_resellers

	@staticmethod
	def _convert_ppa_reseller(reseller):
		# result of this function is not imported to PPA, it is just used for conflict resolution between PPA and Plesk resellers
		# so only those properties that are used by conflict resolution should be processed, all the others are set to None
		return ppa.Reseller(
			source='ppa',
			login=reseller.contact.username, # necessary for merging
			personal_info=ppa.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_ppa_reseller(cls, plesk_id, reseller, is_windows, reseller_report, password_holder, reseller_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

		return ppa.Reseller(
			login=reseller.login, 
			password=password,
			plan_name=None,
			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=ppa.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=if_not_none(reseller_plan, cls.convert_reseller_settings_from_plesk_backup_to_plesk_import_api),
			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'. 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 PleskResellerSettings(
			limits={
				limit_name: backup_plan.properties[limit_name]
				for limit_name in PLESK_LIMIT_NAMES 
				if limit_name in backup_plan.properties
			},
			permissions={
				permission_name: backup_plan.properties[permission_name]
				for permission_name in PLESK_PERMISSION_NAMES 
				if permission_name in backup_plan.properties
			},
			overuse=backup_plan.properties.get('overuse'),
			oversell=backup_plan.properties.get('oversell'),
		)

