import logging
import uuid

from parallels.ppa import messages
from parallels.core import MigrationError
from parallels.core.utils.common import find_first, get_executable_file_name
from parallels.core.utils.common import cached
from parallels.ppa.utils.xml_rpc.poa import poa_api
from parallels.core.utils.yaml_utils import read_yaml, write_yaml
from parallels.ppa.utils import poa_api_helper
from parallels.core.utils.common.ip import is_ipv4
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.ppa.import_api import model
from parallels.core.import_api.import_api import ImportAPI
from parallels.core.import_api.utils import plesk_unix_change_sysuser_login, plesk_unix_get_dedicated_app_pool_user


class PPAImportAPI(ImportAPI):
	logger = logging.getLogger(__name__)
	system_name = 'PPA'

	poa_to_ppa_statuses = {
		poa_api.RequestStatus.SUCCESS: model.WebspaceStatus.ACTIVE,
		poa_api.RequestStatus.RUNNING: model.WebspaceStatus.PENDING,
		poa_api.RequestStatus.FAILED: model.WebspaceStatus.FAILED
	}

	def __init__(self, conn, webspaces_requests_holder):
		self.conn = conn
		self.poa = conn.poa_api() 
		self.webspaces_requests_holder = webspaces_requests_holder

	def has_addon_service_templates(self):
		return False

	def create_reseller(self, reseller):
		return self._createAccount(poa_api.AccountTypes.RESELLER, poa_api.Identifiers.OID_ADMIN, reseller)

	def create_customer(self, owner_id, customer):
		return self._createAccount(poa_api.AccountTypes.CLIENT, owner_id, customer)
	
	def _createAccount(self, account_type, owner_id, acc_data):
		pinfo = acc_data.personal_info

		person = poa_api.PersonInfo(
			first_name=pinfo.first_name, last_name=pinfo.last_name, company_name=acc_data.company
		)
		# Note that country code in Account's address and in Member's contact should be converted to lower case
		# otherwise POA UI treats it as unknown and shows 'United States'
		address = poa_api.AddressInfo(
			zipcode=pinfo.postal_code, city=pinfo.city, country=pinfo.country_code.lower(),
			state=pinfo.state, street_name=pinfo.address_line_1, address2=pinfo.address_line_2
		)
		phone = poa_api.PhoneInfo(country_code='', area_code='', phone_num=pinfo.primary_phone, ext_num='')
		fax = poa_api.FaxInfo(country_code='', area_code='', phone_num=pinfo.fax, ext_num='')

		# Here is a very simple locale processing: only locales supported by PPA are transferred as is,
		# all the others are changed to 'en_US'.
		# Also 'zh-TW' is not supported: need to implement selection by of locale by country,
		# or take locale from Plesk backup "as is".
		# Another disadvantage of the current implementation as it sets unknown locale to 'en_US' silently,
		# without any pre-migration or post-migration warnings.
		# Yet another issue is a PPA limitation: PPA POA locales set is less than PPA Plesk locales set,
		# so some locales that are supported by PPA Plesk
		# can not be selected while creating subscription with PPA POA.
		language_code_to_contry_code = {
			'de': 'DE', 'en': 'US', 'es': 'ES', 'fr': 'FR', 'it': 'IT',
			'ja': 'JP', 'nl': 'NL', 'pt': 'BR', 'ru': 'RU', 'zh': 'CN'
		}
		if pinfo.language_code in language_code_to_contry_code:
			locale = poa_api.LocaleInfo(
				language_code=pinfo.language_code,
				country_code=language_code_to_contry_code[pinfo.language_code], variant=''
			)
		else:
			locale = poa_api.LocaleInfo(language_code='en', country_code='US', variant='')

		parent_account_id = owner_id if owner_id is not None else poa_api.Identifiers.OID_ADMIN

		request_id = uuid.uuid4()
		txn_id = self.poa.txnBegin(request_id)
		account_id = self.poa.addAccount(account_type, parent_account_id, person, address, phone, pinfo.email, fax, locale, txn_id)

		auth = poa_api.AuthInfo(login=acc_data.login, password=acc_data.password)
		self.poa.addAccountMember(account_id, auth, person, address, phone, pinfo.email, fax, txn_id)
		self.poa.txnCommit(txn_id)

		return account_id

	def create_hosting_subscription(
			self, account_id, plan_id, addon_plan_names, subscription, client=None,
			reseller=None, hosting_type='phosting'):
		"""Create hosting subscription
		1. subscription is being just added, to avoid auto-provisioning and creation of default webspace
		2. subscription is added with specified group_name. if empty group_name specified,
		subscription will be named after service template
		3. subscription is added synchronously
		"""
		subscription_name = subscription.group_name
		if subscription_name == '':
			subscription_name = subscription.plan_name
		act_params = [
			{'var_name': 'domain_name', 'var_value': subscription_name},
			{'var_name': 'plesk_disable_provisioning', 'var_value': 'false'},
		]

		subscription_id = self.poa.addSubscription(account_id, plan_id, act_params, subscription_name)

		return subscription_id

	def create_webspace(self, model_subscription):
		sub_id = model_subscription.group_id
		name = model_subscription.name
		required_resources = model_subscription.required_resources + ['subscription']
		subs_info = self.poa.getSubscription(sub_id, get_resources=True)
		st_info = self.poa.getServiceTemplate(subs_info.st_id, get_resources=True)

		# by subscription, determine which resources are not overused
		avail_rts = [ res.rt_id for res in subs_info.resources if res.limit == -1 or res.usage < res.limit ]

		# by service template, determine true type of included resources
		st_avail_rts = poa_api_helper.list_service_template_hostings(st_info, avail_rts)

		missing_resources = set(required_resources) - set(st_avail_rts.keys())

		if len(missing_resources) > 0:
			raise MigrationError(messages.SUBSCRIPTION_HAS_NOT_ENOUGH_RESOURCES_TO_CREATE_WEBSPACE % (sub_id, name, ",".join(missing_resources)))

		def makeWebspaceResource(hosting_type, rt_id):
			params = []
			if hosting_type in ['iis', 'apache'] and any([
				# cut off 'none', 'none' combination
				model_subscription.web_ip_type in ['shared', 'exclusive'],
				model_subscription.web_ipv6_type in ['exclusive'],
			]):
				params.append(
					poa_api.Param(name='ipv4_type',
						value='shared' if model_subscription.web_ip_type == 'shared'
						else 'dedicated' if model_subscription.web_ip_type is not None
						else 'none'
					)
				)
				params.append(
					poa_api.Param(name='ipv6_type',
						value='dedicated' if model_subscription.web_ipv6_type == 'exclusive'
						else 'none'
					)
				)
			return poa_api.WebspaceResource(rt_id=rt_id, params=params)

		desired_resources = model_subscription.additional_resources + required_resources
		resources = [
			makeWebspaceResource(req_name, rt_id=rt_ids[0])
			for (req_name, rt_ids) in st_avail_rts.items() if req_name in desired_resources
		]

		request_id = uuid.uuid4()
		txn_id = self.poa.txnBegin(request_id)
		webspace_id = self.poa.createWebspace(sub_id, name, resources, txn_id)
		self.poa.txnCommit(txn_id)
		self.webspaces_requests_holder.put(webspace_id, request_id)
		return webspace_id

	def get_service_template_info_list(self, service_template_ids):
		def get_mailbox_quota(resource_types):
			mailbox_quota_rt = find_first(resource_types, lambda rt: rt.resclass_name == 'plesk_mail.mailbox_size')
			if mailbox_quota_rt is not None:
				if mailbox_quota_rt.limit > 0:
					return mailbox_quota_rt.limit * 1024 # RT limit is in Kbytes, convert to bytes
				else:
					return mailbox_quota_rt.limit
			else:
				return -1 # consider unlimited

		service_templates = []
		for st_id in set(service_template_ids):
			st_details = self.poa.getServiceTemplate(st_id, get_resources=True)
			service_templates.append(
				model.PPAServiceTemplateDetails(
					st_id=st_details.st_id, name=st_details.name, owner_id=st_details.owner_id,
					autoprovidable=st_details.autoprovidable,
					resources=poa_api_helper.get_service_template_hostings_available(st_details),
					mailbox_quota=get_mailbox_quota(st_details.resource_types),
					resource_types=st_details.resource_types
				)
			)
		return service_templates

	def get_service_template_list(self, owner_id, service_template_names=None):
		service_templates_brief = []
		for st in self.poa.getServiceTemplateList(owner_id, active=True):
			if service_template_names is None or st.name in service_template_names:
				service_templates_brief.append(model.PPAServiceTemplateInfo(
					st_id=st.st_id, name=st.name, owner_id=st.owner_id
				))
		return service_templates_brief

	def get_addon_service_template_list(self, owner_id, active=None):
		# There are no addon templates in PPA, return empty list
		return []

	def list_resellers(self, reseller_logins):
		return self._findAccounts(poa_api.AccountTypes.RESELLER, reseller_logins)

	def list_customers(self, customer_logins):
		return self._findAccounts(poa_api.AccountTypes.CLIENT, customer_logins)

	def _findAccounts(self, accounts_type, logins):
		assert accounts_type in [poa_api.AccountTypes.RESELLER, poa_api.AccountTypes.CLIENT], \
			messages.MIGRATOR_INTERNAL_ERROR_ACCOUNT_OF_UNSUPPORTED_TYPE % accounts_type
		accounts = []
		for login in set(logins):
			try:
				member = self.poa.getAccountMemberByLogin(login)
			except poa_api.PoaApiError as e:
				if e.module_id == 'AccountManagement' and e.extype_id == 41:	# NoSuchMemberWithName
					continue
				else:
					raise
			account_info = self.poa.getAccountInfo(member.account_id)
			if account_info.account_type != accounts_type:
				continue

			member_info = self.poa.getMemberFullInfo(member.user_id)
			contact = model.PPAContactInfo(
				username=member_info.auth_info.login, email=member_info.pers_contact.email,
				first_name=member_info.pers_contact.first_name, last_name=member_info.pers_contact.last_name
			)
			if accounts_type == poa_api.AccountTypes.RESELLER:
				account = model.PPAResellerInfo(id=account_info.account_id, contact=contact)
			else:	# accounts_type == poa_api.AccountTypes.CLIENT:
				account = model.PPACustomerInfo(id=account_info.account_id, contact=contact)

			accounts.append(account)
		return accounts

	def get_request_status_for_webspace(self, webspace_id):
		request_id = self.webspaces_requests_holder.get(webspace_id)
		return self.get_request_status(request_id)

	def get_request_status(self, request_id):
		assert request_id is not None, messages.INTERNAL_ERROR_MISSING_REQUEST_ID

		req_status, status_messages = self.poa.getRequestStatus(request_id)
		return self.poa_to_ppa_statuses[req_status], u"\n".join(status_messages)

	def list_webspaces(self, domain_names):
		def get_status(webspace):
			"""Get webspace status.
			Return tuple(status, status_message).
			"""
			if webspace.status == 'disabled':
				return model.WebspaceStatus.SUSPENDED, ''
			else:
				request_id = self.webspaces_requests_holder.get(webspace.webspace_id)
				if request_id is None:
					return None, messages.MIGRATOR_HAS_NO_INFO_ABOUT_WEBSPACE_PROVISIONING
				else:
					req_status, status_messages = self.poa.getRequestStatus(request_id)
					return self.poa_to_ppa_statuses[req_status], "\n".join(status_messages)

		webspaces = []
		for domain_name in set(domain_names):
			try:
				webspace_id = self.poa.getWebspaceIDByPrimaryDomain(domain_name)
			except poa_api.PoaApiError as e:
				if e.module_id == 'MSPCommon' and e.extype_id == 52:	# NoWebspaceByDomain
					continue
				else:
					raise
			webspace = self.poa.getWebspace(webspace_id)

			status, status_reason = get_status(webspace)
			subscription = self.poa.getSubscription(webspace.sub_id, get_resources=True)
			ws = model.PPAWebspaceInfo(
				webspace_id=webspace.webspace_id, name=webspace.domain, subscription_id=webspace.sub_id,
				owner_id=webspace.owner_id, status=status, 
				resources=poa_api_helper.list_webspace_resources(subscription, webspace.rt_ids), 
				status_reason=status_reason
			)

			webspaces.append(ws)

		self._validate_webspaces(webspaces)

		return webspaces

	def _validate_webspaces(self, webspaces):
		"""Check database consistency for retrieved webspaces"""

		subscription_owner_ids = [webspace.owner_id for webspace in webspaces]
		subscriptions = self._list_hosting_subscriptions(subscription_owner_ids, get_resources=False)
		subscription_ids = set([subscription.subscription_id for subscription in subscriptions])

		hanged_webspaces = []
		for webspace in webspaces:
			if webspace.subscription_id not in subscription_ids:
				hanged_webspaces.append((webspace.webspace_id, webspace.name))

		if len(hanged_webspaces) > 0:
			for (webspace_id, webspace_name) in hanged_webspaces:
				self.logger.error(
					messages.PPA_WEBSPACE_INCONSISTENCY.format(
						util_name=get_executable_file_name(),
						webspace_id=webspace_id,
						webspace_name=webspace_name
					)
				)
			raise MigrationError(messages.FIX_INCONSISTENCIES_LISTED_ABOVE_AND_RERUN_MIGRATION)

	def list_webspaces_brief(self, domain_names):
		webspaces = []
		for domain_name in set(domain_names):
			try:
				webspace_id = self.poa.getWebspaceIDByPrimaryDomain(domain_name)
			except poa_api.PoaApiError as e:
				if e.module_id == 'MSPCommon' and e.extype_id == 52:	# NoWebspaceByDomain
					continue
				else:
					raise
			webspaces.append(model.PPAWebspaceInfoBrief(webspace_id, domain_name))
		return webspaces

	def list_hosting_subscriptions(self, account_ids):
		return self._list_hosting_subscriptions(account_ids, get_resources=True)

	def _list_hosting_subscriptions(self, account_ids, get_resources):
		subs = []
		for account_id in set(account_ids):
			for subscription_id in self.poa.getAccountSubscriptions(account_id):
				subscription = self.poa.getSubscription(subscription_id, get_resources=get_resources)
				subs.append(subscription)
		return subs

	def _is_hosting_service_template(self, st_id):
		return self.poa.getServiceTemplate(st_id, get_resources=False).autoprovidable

	def suspend_customer(self, account_id):
		self.poa.disableAccount(account_id)

	def suspend_subscription(self, subscription_id):
		self.poa.disableSubscription(subscription_id)

	def get_webspace_id_by_primary_domain(self, domain_name):
		return self.poa.get_webspace_id_by_primary_domain(domain_name)

	def get_subscription_id_by_webspace_id(self, webspace_id):
		return self.poa.get_webspace(webspace_id)['sub_id']

	def list_service_node_ids(self, service_name):
		services_list = self.poa.listServices()
		return [ s.host_id for s in services_list if s.service_name == service_name ]

	def change_sysuser_login(self, subscription_name, sysuser_login):
		with self.conn.main_node_runner() as runner:
			plesk_unix_change_sysuser_login(runner, subscription_name, sysuser_login)

	@staticmethod
	def repair_webspace_security(subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		with subscription.panel_target_server.runner() as runner:
			runner.run(
				u'%s/bin/repair' % subscription.panel_target_server.plesk_dir, [
					'--repair-webspace-security', '-webspace-name', subscription.name_idn
				]
			)

	def update_webspace_subdomains_security_metadata_location(self, subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		:rtype: None
		"""
		server = self.conn.plesk_server
		with server.runner() as runner:
			runner.run(
				ur'%s/bin/repair' % server.plesk_dir, [
					'--update-webspace-subdomains-security-metadata-location',
					'-webspace-name', subscription.name_idn
				]
			)

	@staticmethod
	def get_dedicated_app_pool_user(subscription):
		"""
		:type subscription: parallels.core.migrated_subscription.MigratedSubscription
		"""
		with subscription.panel_target_server.runner() as runner:
			return plesk_unix_get_dedicated_app_pool_user(runner, subscription.name)

	def refresh_node_components(self, node):
		server = self.conn.plesk_server
		with server.runner() as runner:
			runner.sh("{plesk_root}/bin/service_node --update {service_node_ip}", dict(
				plesk_root=server.plesk_dir,
				service_node_ip=node.ip()
			))

	def set_ip_access_restrictions(self, domain_name, ip_allow, ip_deny):
		server = self.conn.plesk_server
		with server.runner() as runner:
			cmd = u"{plesk_root}/bin/virtdir --update / -vhost {domain_name}".format(
				plesk_root=server.plesk_dir, domain_name=domain_name
			)
			if ip_allow is not None:
				cmd += u" -ip_allow '{ip_allow}'".format(ip_allow=ip_allow)
			if ip_deny is not None:
				cmd += u" -ip_deny '{ip_deny}'".format(ip_deny=ip_deny)
			runner.sh(cmd)

	def get_domain_dns_server_ips(self, target_server, domain_name):
		"""
		:type target_server: parallels.core.connections.target_servers.TargetServer
		"""
		return [
			info['ip_address'] 
			for info in self.conn.poa_api().get_domain_name_servers(domain_name) 
			if is_ipv4(info['ip_address'])
		]

	@cached
	def get_dns_template(self):
		self.logger.info(messages.DEBUG_FETCH_DNS_TEMPLATE)

		ppa_dns_template_raw = self.conn.plesk_api().send(
			plesk_ops.dns.DnsOperator.GetRec(
				filter=plesk_ops.dns.DnsOperator.FilterAll(),
				template=True
			)
		)

		ppa_dns_template = [record.data for record in ppa_dns_template_raw]

		# API returns template PTR records with excess suffix like ' / 24',
		# strip this suffix
		ppa_dns_template = [
			data._replace(src = data.src.split()[0])
			if data.rec_type == 'PTR' else data
			for data in ppa_dns_template
		]

		# Throw away default DNS template records related to NS as NS records
		# are controlled by PPA, not by Plesk. The behaviour is the same as
		# for Plesk which does not consider these records too when creating
		# subscription.
		ppa_dns_template = [ 
			rec for rec in ppa_dns_template
			if (rec.src, rec.dst, rec.opt, rec.rec_type) not in (
				('<domain>.', 'ns.<domain>.', '', 'NS'),
				('ns.<domain>.', '<ip.dns>', '', 'A')
			)
		]
		return ppa_dns_template


class WebspacesRequestsHolder(object):
	def __init__(self, filename):
		self.filename = filename
		self.webspaces_requests = read_yaml(self.filename, True, {})

	def get(self, webspace_id):
		return self.webspaces_requests.get(webspace_id)

	def put(self, webspace_id, request_id):
		self.webspaces_requests[webspace_id] = request_id
		write_yaml(self.filename, self.webspaces_requests)
