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.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, u"Unable to retrieve the customer's password as it is stored in the encrypted form"),
				u"A new password has been generated for the customer '%s': '%s'. You can change the password after data transfer is finished." % (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,
				u"The name of the %s '%s' in this subscription conflicts with the name of destination panel webspace '%s'" % (kind, name, name)
			))
		elif name in source_webspaces:
			sub = source_webspaces[name]
			problems.append(Problem(
				'duplicate_site_name', Problem.ERROR,
				u"The name of the %s '%s' in this subscription conflicts with the name of subscription '%s' which exists on the other server '%s'"
				% (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,
				u"The name of %s '%s' in this subscription conflicts with the name of %s '%s' which already exists in a subscription '%s'"
				% (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,
				u"The name of %s '%s' in this subscription conflicts with the name of %s '%s' which exists in subscription '%s' on the other server" % (kind, name, obj_type, name, sub_name)
			))

	return [
		(problem, u"Either change the conflicting names, or exclude this subscription from transfer using the migration list file.")
		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,
					u"A subscription '%s' already exists on the other server '%s'" % (subscription_name, sub.source)
				),
				u"Either rename one of the subscriptions (if you want to transfer both of them) or exclude one of the subscriptions from the migration list file."
			))

	elif subscription_name in target_sites:
		sub_name, obj_type = target_sites[subscription_name]
		issues.append((
			Problem(
				'duplicate_site_name', Problem.ERROR,
				u"A %s '%s' already exists in PPA within the '%s' subscription" % (obj_type, subscription_name, sub_name)
			),
			u"Either rename this subscription or the %s '%s' or exclude this subscription from transfer using the migration list file." % (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,
				u"A %s '%s' already exists on the other server within the '%s' subscription" % (obj_type, subscription_name, sub_name)
			),
			u"Either rename this subscription or the %s '%s' or exclude this subscription from transfer using the migration list file." % (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,
				u"The webspace '%s' already exists" % subscription_name,
			),
			u"The existing webspace will be overwritten (including hosting settings, site content, databases, and so on).\n"
			u"If you want to transfer the subscription into a separate webspace, rename this subscription or the existing webspace."
		))
	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(
				u"the contact name of a customer on another server: '%s', the contact name of the checked customer: '%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
				u"A customer with the same username but different contact data already exists on another server. The list of differences:\n%s" % ("\n".join([u"- %s" % s for s in differences]))
			),
			u"Either remove the difference (if both accounts belong to the same customer) or change the customer's username (if these are the different customers)."
		)
	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, u"A customer with the same username already exists on another server"),
			u"All subscriptions of both customers will be transferred to the same account. If you want to transfer the subscriptions under separate customer accounts, change the username of one of the customers."
		)


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(
				u"the Contact Name (First Name + Last Name) parameter of a customer in the destination panel: '%s', the contact name of the verified customer: '%s'" % 
				(existing_contact_name, format_contact(new_client.personal_info))
			)

		report.add_issue(
			Problem(
				'duplicate_customer_name_with_another_contact_data', 
				Problem.ERROR, 
				u"A customer with the same username but different contact data already exists in the destination panel. The list of differences:\n%s" % ("\n".join(["- %s" % s for s in differences]))
				),
			u"Either remove the difference (if both accounts belong to the same customer) or change the customer's username (if these are the different customers)."
		)
	else:
		report.add_issue(
			Problem('duplicate_customer_name', Problem.WARNING, u"A customer with the same user name already exists in destination panel"),
			u"All subscriptions of this customer will be transferred to existing customer's account. If you want to transfer the subscriptions to a new customer account, change the client's username."
		)


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

		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=(client.personal_info.get('locale') or 'en')[0:2],
				locale=client.personal_info.get('locale') or 'en-US',
				# 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
		)
