import logging
from parallels.core import messages
from parallels.core.utils.common import group_by_id
from parallels.core.utils.common import if_not_none
from parallels.core.checking import Problem
from parallels.core import target_data_model as target_model
from parallels.core.utils.common import fill_none_namedtuple


SOURCE_TARGET = 'target-panel'
logger = logging.getLogger(__name__)

def format_source(source):
	if source == SOURCE_TARGET:
		return messages.MODEL_SOURCE_TARGET
	else:
		return messages.MODEL_SOURCE_SOURCE % (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
	"""
	password = get_plaintext_password(client)
	if not password:
		logger.debug("Plain password not found for client %s" % client.login)
		password = get_auxiliary_user_password(client)
	if not password:
		logger.debug("Auxiliary user password not found, generating a new temporary password")
		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 get_plaintext_password(client):
	"""Read client password from XML node (works for resellers as well)"""
	password = ''
	if client.password.type == 'plain' and client.password is not None:
		password = client.password.text
	return password


def get_auxiliary_user_password(client):
	""" Search for client's password in auxiliary users.

	This is a 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)
	"""
	for user in client.auxiliary_users:
		if user.is_built_in and user.name == client.login:
			return get_plaintext_password(user)
	return ''


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 dump.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.SITE_NAME_ALREADY_EXIST_ON_TARGET % (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(
				messages.EMAIL_DIFFERENCE % (
					existing_client.personal_info.email, new_client.personal_info.email
				)
			)
		if format_contact(new_client.personal_info) != format_contact(existing_client.personal_info):
			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.REMOVE_ACCOUNTS_DIFFERENCE)
	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(
				messages.EMAIL_DIFFERENCE_DEST % (
					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.REMOVE_ACCOUNTS_DIFFERENCE)
	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_target_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 some 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 _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
		)
