from parallels.common import messages
from parallels.utils import group_by_id, join_nonempty_strs
from parallels.utils import if_not_none
from parallels.common.checking import Problem
from parallels.common import target_data_model as target_model
from parallels.utils import fill_none_namedtuple

SOURCE_TARGET = 'ppa'


def format_source(source):
	if source == SOURCE_TARGET:
		return u'PPA'
	else:
		return u"Plesk '%s'" % (source,)



def format_contact(personal_info):
	contact_parts = []
	if personal_info.first_name is not None and personal_info.first_name != '':
		contact_parts.append(personal_info.first_name)
	if personal_info.last_name is not None and personal_info.last_name != '':
		contact_parts.append(personal_info.last_name)
	return " ".join(contact_parts)



def get_state(state, country):
	if country == 'US':
		return state or 'Washington'
	elif country == 'CA':
		return state or 'ON' # set 'Ontario' (ON) as default state for canadian people
	elif state is None:
		return ''
	return state



def index_target_site_names(existing_objects):
	"""
	Return dictionary {domain name: (subscription name, domain type)}
	where domain is a domain like object that exists on target panel.
	Domain-like objects types are: addon domains, subdomains, domain aliases.

	Parameters:
	existing_objects - existing data model
	"""
	return dict(
		[(name, (sub_name, 'addon domain')) for (name, (sub_name, _)) in existing_objects.plesk_sites.iteritems()] +
		[(name, (sub_name, 'subdomain')) for (name, (sub_name, _)) in existing_objects.plesk_subdomains.iteritems()] +
		[(name, (sub_name, 'alias')) for (name, (sub_name, _)) in existing_objects.plesk_site_aliases.iteritems()]
	)



def get_client_password(client, report, password_holder):
	"""Get client password from Plesk backup, working around issue with encrypted passwords. 
	Params:
	- client - client object of Plesk backup reader
	- report - report to put issue in case we were
		unable to get password from backup
	- password_holder - PasswordHolder object to create password 
		if we were unable to get it from backup
	"""
	if client.password is not None and client.password.type == 'plain':
		password = client.password.text
	else:
		main_auxiliary_user = None
		for user in client.auxiliary_users:
			if user.is_built_in and user.name == client.login:
				main_auxiliary_user = user

		if main_auxiliary_user is not None and main_auxiliary_user.password.type == 'plain':
			# Hack to get client'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 client
			# but its password is always stored as plain text (regardless of how client's password is stored)
			password = main_auxiliary_user.password.text
		else:
			password = password_holder.get('client', client.login)
			report.add_issue(
				Problem('missing_client_password', Problem.WARNING, messages.UNABLE_RETRIEVE_CUSTOMERS_PASSWORD_AS_IT),
				messages.NEW_PASSWORD_GENERATED_FOR_CUSTOMER_S % (client.login, password)
			)
	return password



def index_plesk_backup_domain_objects(backup, subscription_name):
	"""
	Return tuple (domain name, type) for all domain-like objects 
	under specified subscription in backup. 
	Domain-like objects are: addon domains, subdomains, domain aliases.

	Parameters:
	backup - instance of plesk.backup.plesk_backup_xml.PleskBackupSource*
	subscription_name - list domain-like objects owned by that subscription
	"""
	return sum([
		[(d.name, 'addon domain') for d in backup.iter_addon_domains(subscription_name)],
		[(s.name, 'subdomain') for s in backup.iter_subdomains(subscription_name, subscription_name)],
		[(s.name, 'subdomain') for d in backup.iter_addon_domains(subscription_name) for s in backup.iter_subdomains(subscription_name, d.name)],
		[(a.name, 'alias') for a in backup.iter_aliases(subscription_name)],
	], [])



def check_domain_conflicts(backup, subscription_name, target_webspaces, target_sites, source_webspaces, source_sites):
	"""Check uniqueness of addon domains, subdomains and aliases"""

	# list of tuples (domain name, object type - addon domain/subdomain/domain alias)
	subobjects = index_plesk_backup_domain_objects(backup, subscription_name) 
	problems = []

	target_webspaces_by_name = group_by_id(target_webspaces, lambda ws: ws.name)
	for name, kind in subobjects:
		if name in target_webspaces_by_name:
			problems.append(Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS % (kind, name, name)
			))
		elif name in source_webspaces:
			sub = source_webspaces[name]
			problems.append(Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_1% (kind, name, name, sub.source)
			))
		elif name in target_sites and target_sites[name] != (subscription_name, kind):
			sub_name, obj_type = target_sites[name]
			problems.append(Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_2% (kind, name, obj_type, name, sub_name)
			))
		elif name in source_sites:
			sub_name, obj_type = source_sites[name]
			problems.append(Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.NAME_S_S_IN_SUBSCRIPTION_CONFLICTS_3 % (kind, name, obj_type, name, sub_name)
			))

	return [
		(problem, messages.EITHER_CHANGE_CONFLICTING_NAMES_EXCLUDE_SUBSCRIPTION)
		for problem in problems
	]



def check_subscription_conflicts(subscription_name, source_webspaces, source_sites, target_sites):
	"""Return list of problems related to conflicts between this subscription and other subscriptions or their domains.
	"""
	issues = []
	if subscription_name in source_webspaces:
		sub = source_webspaces[subscription_name]
		if sub.source != SOURCE_TARGET:
			issues.append((
				Problem(
					'duplicate_subscription_name', Problem.ERROR,
					messages.SUBSCRIPTION_S_ALREADY_EXISTS_OTHER_SERVER % (subscription_name, sub.source)
				),
				messages.EITHER_RENAME_ONE_SUBSCRIPTIONS_IF_YOU))

	elif subscription_name in target_sites:
		sub_name, obj_type = target_sites[subscription_name]
		issues.append((
			Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.S_S_ALREADY_EXISTS_IN_PPA % (obj_type, subscription_name, sub_name)
			),
			messages.EITHER_RENAME_SUBSCRIPTION_S_S_EXCLUDE % (obj_type, subscription_name)
		))

	elif subscription_name in source_sites:
		sub_name, obj_type = source_sites[subscription_name]
		issues.append((
			Problem(
				'duplicate_site_name', Problem.ERROR,
				messages.S_S_ALREADY_EXISTS_OTHER_SERVER % (obj_type, subscription_name, sub_name)
			),
			messages.EITHER_RENAME_SUBSCRIPTION_S_S_EXCLUDE_1 % (obj_type, subscription_name)
		))

	return issues



def check_subscription_already_exists(subscription_name, target_webspaces):
	"""Checks whether subscription already exists in target panel
	   Return list of problems.
	"""
	issues = []

	webspaces_by_name = group_by_id(target_webspaces, lambda ws: ws.name)
	if subscription_name in webspaces_by_name:
		issues.append((
			Problem(
				'duplicate_webspace_name', Problem.WARNING,
				messages.WEBSPACE_S_ALREADY_EXISTS % subscription_name,
			),
			messages.EXISTING_WEBSPACE_OVERWRITTEN_INCLUDING_HOSTING_SETTINGS))
	return issues



def check_client_contacts_matching_source_panels(
		new_client, existing_client, report
	):
	if any([
		new_client.personal_info.email != existing_client.personal_info.email,
		new_client.personal_info.first_name != existing_client.personal_info.first_name,
		# skip last name as it is always empty according to conversion defined
		# in _create_client_from_plesk_backup_client, compare first name only
	]):
		differences = []
		if new_client.personal_info.email != existing_client.personal_info.email:
			differences.append(
				u"e-mail of a customer on another server: '%s', e-mail of the checked customer: '%s'" % 
				(existing_client.personal_info.email, new_client.personal_info.email)
			)
		if new_client.personal_info.first_name != existing_client.personal_info.first_name:
			# according to conversion first name in terms of PPA == contact in terms of Plesk
			differences.append(
				messages.CONTACT_NAME_CUSTOMER_ANOTHER_SERVER_S % 
				(existing_client.personal_info.first_name, new_client.personal_info.first_name)
			)
		report.add_issue(
			Problem(
				'duplicate_customer_name_with_another_contact_data', 
				Problem.ERROR, 
				# TODO now that we have existing_client.source, would be good to specify it in issue
				messages.CUSTOMER_SAME_USERNAME_BUT_DIFFERENT_CONTACT % ("\n".join([u"- %s" % s for s in differences]))
			),
			messages.EITHER_REMOVE_DIFFERENCE_IF_BOTH_ACCOUNTS_2)
	else:
		report.add_issue(
			# TODO now that we have existing_client.source, would be good to specify it in issue
			Problem('duplicate_customer_name', Problem.WARNING, messages.CUSTOMER_SAME_USERNAME_ALREADY_EXISTS_ANOTHER),
			messages.ALL_SUBSCRIPTIONS_BOTH_CUSTOMERS_TRANSFERRED_SAME)



def check_client_contacts_matching_source_target(
		new_client, existing_client, report
	):
	existing_contact_name = format_contact(existing_client.contact)

	if any([
		new_client.personal_info.email != existing_client.contact.email,
		format_contact(new_client.personal_info) != existing_contact_name,
	]):
		differences = []
		if new_client.personal_info.email != existing_client.contact.email:
			differences.append(
				u"e-mail of a customer on destination panel: '%s', e-mail of the verified customer: '%s'" %
				(existing_client.contact.email, new_client.personal_info.email)
			)
		if format_contact(new_client.personal_info) != existing_contact_name:
			differences.append(
				messages.CONTACT_NAME_FIRST_NAME_LAST_NAME % 
				(existing_contact_name, format_contact(new_client.personal_info))
			)

		report.add_issue(
			Problem(
				'duplicate_customer_name_with_another_contact_data', 
				Problem.ERROR, 
				messages.CUSTOMER_SAME_USERNAME_BUT_DIFFERENT_CONTACT_1 % ("\n".join(["- %s" % s for s in differences]))
				),
			messages.EITHER_REMOVE_DIFFERENCE_IF_BOTH_ACCOUNTS_3)
	else:
		report.add_issue(
			Problem('duplicate_customer_name', Problem.WARNING, messages.CUSTOMER_SAME_USER_NAME_ALREADY_EXISTS),
			messages.ALL_SUBSCRIPTIONS_CUSTOMER_TRANSFERRED_EXISTING_CUSTOMERS)



class EntityConverter(object):
	"""
	Entity converter is responsible for convestions of objects (clients and subsciption), 
	without any complex logic, conflict resolution or something like that. Just take
	object from source panel (source Plesk backup or other data file), or from target panel
	and create target model entity object (Subscription or Client), filling all necessary
	fields
	"""
	def __init__(self, existing_objects):
		self.existing_objects = existing_objects

	def create_subscription_from_plesk_backup_subscription(self, subscription, plesk_id, is_windows):
		sysuser = subscription.get_phosting_sysuser()
		sysuser_login = if_not_none(sysuser, lambda s: s.name)
		return target_model.Subscription(
			subscription.name, plan_name=None, plan_id=None,
			addon_plan_ids=[],
			web_ip=None,
			web_ip_type=subscription.ip_type,
			web_ipv6=None,
			web_ipv6_type=subscription.ipv6_type,
			is_enabled=subscription.is_enabled,
			source=plesk_id, is_windows=is_windows,
			mail_is_windows=is_windows,
			sub_id=None, group_name=None, group_id=None,
			required_resources=None, additional_resources=None,
			sysuser_login=sysuser_login
		)

	def create_subscription_stub_from_existing_subscription(self, subscription, plesk_id, is_windows):
		webspaces_by_name = group_by_id(self.existing_objects.webspaces, lambda ws: ws.name)
		webspace = webspaces_by_name[subscription.name]
		target_subscriptions_by_id = group_by_id(self.existing_objects.raw_ppa_subscriptions, lambda s: s.subscription_id)
		target_subscription = target_subscriptions_by_id[webspace.subscription_id]
		return target_model.Subscription(
			subscription.name, plan_name=None, plan_id=None,
			addon_plan_ids=[],
			web_ip=None,
			web_ip_type=subscription.ip_type,
			web_ipv6=None,
			web_ipv6_type=subscription.ipv6_type,
			is_enabled=True, 
			# XXX: source=plesk_id is logically incorrect. however can't just set it to source=SOURCE_TARGET
			# as verify_hosting will not be performed for it, after hosting settings restored
			source=plesk_id, is_windows=is_windows,
			mail_is_windows=is_windows,
			sub_id=webspace.webspace_id, group_name=target_subscription.name, group_id=webspace.subscription_id,
			required_resources=[], additional_resources=[],
			sysuser_login=None
		)

	def create_client_from_plesk_backup_client(self, client, source, client_report, password_holder):
		password = get_client_password(client, client_report, password_holder)

		# Plesk combines first and last name into a single mandatory field
		# called 'contact name'. For that to work in a case of 'empty first +
		# empty last name', we pass client login 'first name'.
		first_name = self._get_client_first_name(client)
		last_name = self._get_client_last_name(client)
		if(
			(first_name is None or not first_name.strip()) and
			(last_name is None or not last_name.strip())
		):
			first_name = client.login

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

		return target_model.Client(
			login=client.login, 
			password=password,
			subscriptions=[],
			company=client.personal_info.get('company'),
			personal_info=target_model.PersonalInfo(
				first_name=first_name,
				last_name=last_name,
				email=client.personal_info.get('email') or u"%s@example.com" % client.login,
				preferred_email_format='plainText',
				address_line_1=client.personal_info.get('address') or 'No address',
				address_line_2='',
				city=client.personal_info.get('city') or 'No city',
				county='',
				state=get_state(
					client.personal_info.get('state'),
					client.personal_info.get('country', 'US')
				), 
				postal_code=self._get_postal_code(client.personal_info.get('zip')), 
				language_code=locale[0:2] if locale is not None else None,
				locale=locale,
				# country could be None in case of migration from Plesk 9.5:
				# you can create client without country with Plesk API in such
				# case set country to POA default - 'United States' 
				country_code=client.personal_info.get('country', 'US'), 
				primary_phone=client.personal_info.get('phone') or '1111111', 
				additional_phone='', 
				fax=client.personal_info.get('fax') or '', 
				mobile_phone=''
			),
			auxiliary_user_roles=[],
			auxiliary_users=[],
			is_enabled=client.is_enabled,
			source=source,
			target_client=None,
		)

	@staticmethod
	def fix_poa_company_name(client):
		"""Fix company name for client to workaround issue with empty name in POA UI

		In PPA POA UI on many pages customer is identified by company name,
		and you don't see its first and last names. As company is often empty
		on source panels, a workaround is applied: we put client's contact as
		a company name. Otherwise you see a lot of rows it tables that differ
		only by id. This workaround should be removed as soon as bug #120645
		is fixed on PPA side

		:type client: parallels.common.target_data_model.Client
		"""
		if client.company is None or client.company == '':
			client.company = join_nonempty_strs([
				client.personal_info.first_name,
				client.personal_info.last_name
			])

	@staticmethod
	def _get_postal_code(code):
		return code if code is not None and code.isdigit() else '11111'

	def _get_client_first_name(self, client):
		return client.contact

	def _get_client_last_name(self, client):
		return ''

	@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,
			),
			password=None, subscriptions=[], company=None, 
			auxiliary_user_roles=[], auxiliary_users=[], is_enabled=None,
			target_client=existing_client
		)
