import re
from parallels.plesk import messages
import logging
import random 
import string

from parallels.core import MigrationError, MigrationNoRepeatError
from parallels.core.utils.plesk_cli_runner import PleskCLIRunnerCLI
from parallels.core.import_api.utils import plesk_get_service_plan_database_server_data
from parallels.plesk.import_api.hosting_plans_importer import HostingPlansImporter
from parallels.core.safe import FailedObjectInfo
from parallels.core.checking import Problem

from parallels.core.import_api.import_api import ImportAPI, SubscriptionCannotBeSyncedWithPlan
from parallels.plesk.import_api import model
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.plesk.utils.xml_rpc.plesk.core import PleskError, PleskErrorCode
from parallels.core.utils.common import unique_list, generate_random_password, join_nonempty_strs, cached, unused

OWNER_ADMIN = 1


class PleskBaseImportAPI(ImportAPI):
	logger = logging.getLogger(__name__)
	system_name = messages.TARGET_PANEL_PLESK_TITLE

	def __init__(self, conn):
		"""
		:type conn: parallels.plesk.connections.target_connections.PleskTargetConnections
		"""
		self.conn = conn

	def has_addon_service_templates(self):
		return True

	def create_reseller(self, reseller):
		result = self.conn.plesk_api().send(
			plesk_ops.ResellerOperator.Add(
				gen_info=plesk_ops.ResellerAddGenInfo(
					cname=reseller.company, 
					pname=join_nonempty_strs([reseller.personal_info.first_name, reseller.personal_info.last_name]),
					passwd=reseller.password,
					login=reseller.login, 
					status=0, 
					phone=reseller.personal_info.primary_phone, 
					fax=reseller.personal_info.fax, 
					email=reseller.personal_info.email, 
					address=join_nonempty_strs([reseller.personal_info.address_line_1, reseller.personal_info.address_line_2], delim='\n'),
					city=reseller.personal_info.city, 
					state=reseller.personal_info.state, 
					pcode=reseller.personal_info.postal_code, 
					country=reseller.personal_info.country_code, 
					locale=None, 
					external_id=None
				),
				limits=plesk_ops.ResellerAddLimits(
					limits=[
						plesk_ops.ResellerAddLimit(limit_name, limit_value)	
						for limit_name, limit_value in reseller.settings.limits.iteritems()
					],
					resource_policy=plesk_ops.ResellerAddResourcePolicy(
						overuse=reseller.settings.overuse,
						oversell=reseller.settings.oversell,
					) if (
						reseller.settings.overuse is not None 
						and 
						reseller.settings.oversell is not None
					) else None
				) if reseller.settings is not None else None,
				permissions=[
					plesk_ops.ResellerAddPermission(permission_name, permission_value)
					for permission_name, permission_value in reseller.settings.permissions.iteritems()
				] if reseller.settings is not None else None,
				plan_name=reseller.plan_name,
			)
		)
		
		reseller_id = result.data.reseller_id

		return reseller_id

	def list_webspaces(self, domain_names):
		webspaces = []
		for domain_name in unique_list(domain_names):
			try:
				webspace_info = self.conn.plesk_api().send(
					plesk_ops.SubscriptionOperator.Get(
						dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO],
						filter=plesk_ops.SubscriptionOperator.FilterByName([domain_name])
					)
				)[0].data
			except PleskError as e:
				if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST:  # webspace with such name does not exist
					continue
				else:
					raise

			webspace_id = webspace_info[0]
			gen_info = webspace_info[1].gen_info
			webspace = model.PleskWebspaceInfo(
				webspace_id=webspace_id,
				name=gen_info.name,
				# in Plesk, webspace and subscription are the same entities, so subscription id is equal to webspace id
				subscription_id=webspace_id, 
				owner_id=gen_info.owner_id,
				status=model.WebspaceStatus.ACTIVE if gen_info.status == 0 else model.WebspaceStatus.SUSPENDED,
				htype=gen_info.htype,
			)
			webspaces.append(webspace)

		return webspaces

	def list_webspaces_brief(self, domain_names):
		webspaces = []
		for domain_name in unique_list(domain_names):
			try:
				webspace_info = self.conn.plesk_api().send(
					plesk_ops.SubscriptionOperator.Get(
						dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO],
						filter=plesk_ops.SubscriptionOperator.FilterByName([domain_name])
					)
				)[0].data
			except PleskError as e:
				if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST:  # webspace with such name does not exist
					continue
				else:
					raise

			webspace = model.PleskWebspaceInfoBrief(
				webspace_id=webspace_info[0], 
				name=webspace_info[1].gen_info.name
			)
			webspaces.append(webspace)

		return webspaces

	def list_resellers(self, reseller_logins):
		resellers = []
		for reseller_login in unique_list(reseller_logins):
			try:
				reseller_info = self.conn.plesk_api().send(
					plesk_ops.ResellerOperator.Get(
						dataset=[plesk_ops.ResellerOperator.Dataset.GEN_INFO],
						filter=plesk_ops.ResellerOperator.FilterByLogin([reseller_login])
					)
				)[0].data
			except PleskError as e:
				if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST:  # reseller with such login does not exist
					continue
				else:
					raise

			gen_info = reseller_info[1].gen_info
			contact = model.PleskContactInfo(
				username=gen_info.login, email=gen_info.email,
				first_name=gen_info.pname, last_name=''
			)
			reseller = model.PleskResellerInfo(
				id=reseller_info[0],
				contact=contact,
			)
			resellers.append(reseller)

		return resellers

	def get_service_template_list(self, owner_id=None, active=None):
		plans = [
			result.data for result in self.conn.plesk_api().send(
				plesk_ops.ServicePlanOperator.Get(
					filter=plesk_ops.ServicePlanOperator.FilterAll(),
					owner_id=None if (owner_id == OWNER_ADMIN or owner_id is None) else owner_id,
					owner_all=None
				)
			)
		]
		return [
			model.PleskServiceTemplateInfo(
				st_id=plan.plan_id, name=plan.name, owner_id=owner_id
			)
			for plan in plans
		]

	def get_addon_service_template_list(self, owner_id, active=None):
		plans = [
			result.data for result in self.conn.plesk_api().send(
				plesk_ops.ServicePlanAddonOperator.Get(
					filter=plesk_ops.ServicePlanAddonOperator.FilterAll(),
					owner_id=None if owner_id == OWNER_ADMIN else owner_id,
				)
			)
		]
		return [
			model.PleskServiceTemplateInfo(
				# we use guid as when adding addon plan to subscription, 
				# Plesk API requires either guid or external ID, 
				# but there is no way to use just plan ID
				st_id=plan.guid, 
				name=plan.name, owner_id=owner_id
			)
			for plan in plans
		]

	def get_service_template_info_list(self, service_template_ids):
		"""
		Retrive serivce templates details by gived IDs
		:param service_template_ids: list of service templates IDs
		:return: list[parallels.plesk.import_api.model.PleskServiceTemplateDetails]
		"""
		service_templates = [
			result.data for result in self.conn.plesk_api().send(
				plesk_ops.ServicePlanOperator.Get(
					filter=plesk_ops.ServicePlanOperator.FilterById(service_template_ids),
					owner_id=None,
					owner_all=True
				)
			)
		]

		if self.conn.plesk_server.is_windows():
			database_server_types = ['mysql', 'mssql']
		else:
			database_server_types = ['mysql', 'postgresql']

		service_template_info_list = []
		for service_template in service_templates:
			# supply the basic service template information with database servers details
			database_servers = []
			for database_server_type in database_server_types:
				database_server_data = plesk_get_service_plan_database_server_data(
					self.conn.plesk_server,
					service_template.plan_id,
					database_server_type
				)
				if database_server_data is not None:
					database_servers.append(model.PleskDatabaseServerInfo(
						database_server_data['host'],
						database_server_data['port'],
						database_server_data['type'],
					))
			service_template_info_list.append(model.PleskServiceTemplateDetails(
				st_id=service_template.plan_id,
				name=service_template.name,
				owner_id=service_template.owner_id if service_template.owner_id is not None else OWNER_ADMIN,
				database_servers=database_servers
			))
		return service_template_info_list
	
	def list_customers(self, customer_logins):
		accounts = []
		for login in unique_list(customer_logins):
			try:
				customer_info = self.conn.plesk_api().send(
					plesk_ops.CustomerOperator.Get(
						dataset=[plesk_ops.CustomerOperator.Dataset.GEN_INFO],
						filter=plesk_ops.CustomerOperator.FilterByLogin([login])
					)
				)[0].data[1]
			except PleskError as e:
				if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST:  # customer with such login does not exist
					continue
				else:
					raise

			contact = model.PleskContactInfo(
				username=customer_info.login, email=customer_info.email,
				first_name=customer_info.pname, last_name=''
			)
			account = model.PleskCustomerInfo(id=customer_info.id, contact=contact, owner_id=customer_info.owner_id)
			accounts.append(account)

		return accounts

	def list_hosting_subscriptions(self, account_ids):
		subscriptions = []
		for account_id in unique_list(account_ids):
			api_response = self.conn.plesk_api().send(
				plesk_ops.SubscriptionOperator.Get(
					dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO, plesk_ops.SubscriptionOperator.Dataset.SUBSCRIPTIONS],
					filter=plesk_ops.SubscriptionOperator.FilterByOwnerId([
						# '1' means admin's subscriptions
						account_id if account_id is not None else '1'
					])
				)
			)
			for subscription_response in api_response:
				try:
					subscription_info = subscription_response.data
				except PleskError as e:
					if e.code == PleskErrorCode.OWNER_DOES_NOT_EXIST:  # owner with such id does not exist
						continue
					else:
						raise

				if len(subscription_info[1].subscriptions.subscriptions) > 0:
					plan_guid = subscription_info[1].subscriptions.subscriptions[0].plan.guid
					plan_id = self.conn.plesk_api().send(
						plesk_ops.ServicePlanOperator.Get(
							filter=plesk_ops.ServicePlanOperator.FilterByGuid([plan_guid]),
							owner_id=None,
							owner_all=True
						)
					)[0].data.plan_id
				else:  # custom subscription - not assigned to any plan
					plan_id = None

				gen_info = subscription_info[1].gen_info 
				subscription = model.PleskSubscriptionInfo(
					subscription_id=subscription_info[0],
					owner_id=account_id,
					st_id=plan_id, 
					name=gen_info.name,
					is_active=bool(gen_info.status == 0),
				)
				subscriptions.append(subscription)

		return subscriptions

	def create_customer(self, owner_id, customer):
		result = self.conn.plesk_api().send(
			plesk_ops.CustomerOperator.Add(
				plesk_ops.CustomerAddInfo(
					gen_info=plesk_ops.CustomerAddGenInfo(
						cname=customer.company,
						pname=join_nonempty_strs([
							customer.personal_info.first_name,
							customer.personal_info.last_name
							]),
						login=customer.login,
						passwd=customer.password,
						email=customer.personal_info.email,
						owner_id=None if owner_id == OWNER_ADMIN else owner_id,
						address=customer.personal_info.address_line_1, 
						city=customer.personal_info.city, 
						state=customer.personal_info.state, 
						country=customer.personal_info.country_code, 
						pcode=customer.personal_info.postal_code, 
						phone=customer.personal_info.primary_phone,
						fax=customer.personal_info.fax,
						locale=customer.personal_info.locale,
					)
				)
			)
		)
		return result.data.customer_id

	def create_hosting_subscription(
			self, account_id, plan_id, addon_plan_names, subscription, client=None,
			reseller=None, hosting_type='phosting'):
		if reseller is not None:
			self._add_ips_to_reseller_pool(reseller, subscription)

		if subscription.sysuser_login is not None:
			sysuser_login = subscription.sysuser_login
		else:
			sysuser_login = self.generate_random_login()

		utility_name = 'subscription'

		try:
			command_arguments = self._create_subscription_command_args(
				client, subscription, sysuser_login, hosting_type)
			self.conn.plesk_cli_runner.run(utility_name, command_arguments)
		except Exception as e:
			# If user with such login already exists - just try
			# to create subscription with randomly generated login.
			# Then on further steps migrator will try to change it
			# to the source one and report issue to customer.
			if (
				(u'user %s already exists' % sysuser_login.lower()) in unicode(e).lower()
				and subscription.sysuser_login is not None
			):
				command_arguments = self._create_subscription_command_args(
					client, subscription, self.generate_random_login(), hosting_type)
				self.conn.plesk_cli_runner.run(utility_name, command_arguments)
			elif (
				re.search(u'Domain with name ".*" already exists', unicode(e)) is not None
			):
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise MigrationNoRepeatError(messages.SUBSCRIPTION_WAS_ALREADY_CREATED)
			else:
				raise

		self._set_subscription_addon_plans(addon_plan_names, subscription)

	def enable_subscription_virtual_hosting(self, subscription, client):
		utility_name = 'subscription'
		cli_runner = self.conn.plesk_cli_runner

		if subscription.sysuser_login is not None:
			sysuser_login = subscription.sysuser_login
		else:
			sysuser_login = self.generate_random_login()

		try:
			command_arguments = self._update_subscription_command_args(
				client, subscription, sysuser_login
			)
			cli_runner.run(utility_name, command_arguments)
		except Exception as e:
			# If user with such login already exists - just try
			# to enable hosting for subscription subscription
			# with randomly generated login.
			# Then on further steps migrator will try to change it
			# to the source one and report issue to customer.
			if (
				(u'user %s already exists' % sysuser_login.lower()) in unicode(e).lower()
				and subscription.sysuser_login is not None
			):
				command_arguments = self._update_subscription_command_args(
					client, subscription, self.generate_random_login()
				)
				cli_runner.run(utility_name, command_arguments)
			else:
				raise

	def _update_subscription_command_args(self, client, subscription, sysuser_login):
		ips_list = join_nonempty_strs([subscription.web_ip, subscription.web_ipv6], ',')
		command_arguments = [
			'--update',
			subscription.name,
			'-hosting', 'true',
			'-hst_type', 'phys',
			'-login', sysuser_login,
			'-passwd', self._generate_subscription_password(subscription.name),
			'-ip', ips_list,
			'-mail-service-ip', ips_list,
			'-do-not-apply-skeleton',
		]
		if client.login is not None:
			command_arguments.extend([
				'-owner', client.login
			])
		return command_arguments

	def _create_subscription_command_args(
			self, client, subscription, sysuser_login, hosting_type='phosting'):
		ips_list = join_nonempty_strs([subscription.web_ip, subscription.web_ipv6], ',')
		if hosting_type != 'none':
			hosting_is_enabled = 'true'
		else:
			hosting_is_enabled = 'false'
		command_arguments = [
			'--create',
			subscription.name,
			'-hosting', hosting_is_enabled,
			'-hst_type', 'phys',
			'-login', sysuser_login,
			'-passwd', self._generate_subscription_password(subscription.name),
			'-ip', ips_list,
			'-mail-service-ip', ips_list,
			'-do-not-apply-skeleton',
			'-notify', 'false'
		]
		if client.login is not None:
			command_arguments.extend([
				'-owner', client.login
			])
		if subscription.plan_name is not None:
			command_arguments.extend([
				'-service_plan', subscription.plan_name,
				# force subscription creation even if there is a service in the plan that is not installed
				# or configured, for example FrontPage on Windows
				'-force'
			])
		return command_arguments

	@cached
	def _generate_subscription_password(self, subscription_name):
		"""Generate password for subscription, persistent for migration and multiple creation attempts"""
		unused(subscription_name)  # use subscription name just as cache key
		return generate_random_password()

	def _add_ips_to_reseller_pool(self, reseller, subscription):
		plesk_cli_runner = PleskCLIRunnerCLI(self.conn.plesk_server)

		ip_addresses = (
			(subscription.web_ip, subscription.web_ip_type),
			(subscription.web_ipv6, subscription.web_ipv6_type)
		)
		for ip_address, ip_type in ip_addresses:
			if ip_address is None:
				continue

			exit_code, stdout, stderr = plesk_cli_runner.run_unchecked(
				'ip_pool', [
					'--add', ip_address, '-type', ip_type, '-owner', reseller.login
				]
			)
			ip_already_in_pool = 2
			if exit_code not in (0, ip_already_in_pool):
				raise MigrationError(
					messages.FAILED_TO_ADD_IP_TO_RESELLER_POOL % (
						ip_address, reseller.login, stdout, stderr
					)
				)

	def sync_subscription(self, subscription_name):
		try:
			self.conn.plesk_api().send(
				plesk_ops.SubscriptionOperator.SyncSubscription(
					filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name])
				)
			).check()
		except PleskError as e:
			if e.code == 1023 and u'the component' in unicode(e).lower() and 'was not installed' in unicode(e).lower():
				# Force synchronization with CLI.
				# Note that we can't initially use subscription --sync-subscription -force
				# as it does not provide any information if synchronization was successful or not.
				self.conn.plesk_cli_runner.run(
					'subscription', [
						'--sync-subscription',
						subscription_name.encode('idna'),
						'-force'
					]
				)

				# Wrap ugly Plesk API message with instruction what to do
				raise SubscriptionCannotBeSyncedWithPlan(
					messages.SUBSCRIPTION_CANNOT_BE_SYNCED_WITH_PLAN_MISSING_COMPONENT
				)
			else:
				raise

	def create_service_template(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		HostingPlansImporter(self.conn).create_plan(owner_login, plan_name, plan_settings)

	def create_addon_service_template(self, owner_login, plan_name, plan_settings):
		"""
		:type owner_login: basestring | None
		:type plan_name: basestring
		:type plan_settings: parallels.plesk.models.target_data_model.HostingPlanSettings
		:rtype: None
		"""
		HostingPlansImporter(self.conn).create_addon_plan(owner_login, plan_name, plan_settings)

	def get_domain_dns_server_ips(self, target_server, domain_name):
		"""
		:type target_server: parallels.core.connections.target_servers.TargetServer
		"""
		# the only DNS server controlled by target Plesk is located on the same node as Plesk
		return [target_server.ip()]

	def suspend_customer(self, account_id):
		"""Suspend a single customer"""
		self.conn.plesk_api().send(
			plesk_ops.CustomerOperator.Set(
				filter=plesk_ops.CustomerOperator.FilterById([account_id]),
				customer_set_info=plesk_ops.CustomerSetInfo(
					status=plesk_ops.CustomerStatus.DISABLED_BY_ADMIN
				)

			)
		)

	@staticmethod
	def generate_random_login():
		random_digits = "".join(random.choice(string.digits) for _ in range(10))
		return "sub_%s" % (random_digits,)

	def _set_subscription_addon_plans(self, addon_plan_names, subscription):
		for addon_plan_name in addon_plan_names:
			self.conn.plesk_cli_runner.run(
				'subscription', ['--add-subscription', subscription.name, '-service-plan', addon_plan_name, '-force']
			)

	def create_auxuser(self, auxuser, owner, subscription_id):
		request = self._get_auxuser_request(auxuser, owner, subscription_id)
		result = self.conn.plesk_api().send(request)

		if self._has_api_error(result, PleskErrorCode.USER_ACCOUNT_ALREADY_EXISTS):
			self.logger.debug(
				messages.SKIPPED_CREATING_EXISTING_AUXILIARY_USER % auxuser.login)
			return
		elif self._has_api_error(result, PleskErrorCode.PASSWORD_IS_TOO_SHORT):
			# we need a password, that passes Plesk's 'Very strong' policy -
			# just to be sure
			chars_to_include = 'aFz25!%-'
			auxuser.password = generate_random_password(chars_to_include)
			description = (
				messages.USER_PASSWORD_DOES_NOT_MEET_PLESK_REQUIREMENTS % auxuser.password
			)
			self.logger.debug(description)
			request = self._get_auxuser_request(auxuser, owner, subscription_id)
			result = self.conn.plesk_api().send(request)
			if result.ok:
				return FailedObjectInfo(
					description, None, None, is_critical=False,
					severity=Problem.INFO)

		if not result.ok:
			raise MigrationError(result.message) 
	
	def _get_auxuser_request(self, auxuser, owner, subscription_id):
		status = 'enabled' if auxuser.is_active else 'disabled'
		contact_info = self._get_auxuser_contact_info(auxuser) 
		request = plesk_ops.UserOperator.Add(
			login=auxuser.login, passwd=auxuser.password,
			owner_guid=self.get_client_guid(owner), name=auxuser.name,
			roles=auxuser.roles, contact_info=contact_info,
			subscription_domain_id=subscription_id, status=status,
			email=auxuser.personal_info['email'])
		return request

	@staticmethod
	def _has_api_error(result, error_code):
		return not result.ok and result.code == error_code

	def create_auxuser_role(self, role, owner):
		"""Create an auxiliary user role."""
		request = plesk_ops.RoleOperator.Add(
			name=role.name, owner_guid=self.get_client_guid(owner),
			permissions=role.permissions)
		result = self.conn.plesk_api().send(request)
		if not result.ok:
			raise MigrationError(result.message) 

	@staticmethod
	def _get_auxuser_contact_info(auxuser):
		info = {}
		for name in plesk_ops.UserContactInfo._fields:
			if name in auxuser.personal_info:
				info[name] = auxuser.personal_info[name]
			else:
				info[name] = ''
		return plesk_ops.UserContactInfo(**info)

	def get_subscription_id(self, name):
		"""Get ID of the subscription created on target panel."""
		info = self.get_subscription(name)
		if info:
			return info.subscription_id
		else:
			return None

	def get_subscription(self, name):
		request = plesk_ops.SubscriptionOperator.Get(
			dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO],
			filter=plesk_ops.SubscriptionOperator.FilterByName([name]))
		response = self.conn.plesk_api().send(request).pop()
		if not response.ok: 
			return None
		else:
			subscription_id, subscription_info = response.data 
			gen_info = subscription_info.gen_info
			subscription = model.PleskSubscriptionInfo(
				subscription_id=subscription_id, owner_id=gen_info.owner_id,
				st_id=None, name=gen_info.name,
				is_active=bool(gen_info.status == 0))
			return subscription

	def get_client_guid(self, client):
		"""Retrieve client GUID from target panel."""
		if not client.login:
			# client is admin
			owner_guid = None
		else:
			client_request = plesk_ops.CustomerOperator.Get(
				filter=plesk_ops.CustomerOperator.FilterByLogin([client.login]),
				dataset=[plesk_ops.CustomerOperator.Dataset.GEN_INFO],
			)
			api = self.conn.plesk_api()
			client_data = api.send(client_request)
			owner_guid = client_data[0].data[1].guid
		return owner_guid
