from parallels.ppa_sn import messages
from collections import namedtuple, defaultdict

from parallels.plesk.utils.xml_rpc.plesk.operator.subscription import Ips
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 format_list, find_only
from parallels.core.utils.common import cached
from parallels.core.utils import migrator_utils
from .common import WebServiceType, HostingType

SubscriptionInfo = namedtuple('SubscriptionInfo', ('name', 'web_ips', 'mail_ips', 'hosting_type'))
SiteInfo = namedtuple('SiteInfo', ('name', 'hosting_type',))


class PPAData(object):
	def __init__(self, conn):
		self.conn = conn

	@cached
	def get_subscription_info(self, subscription_name):
		"""Get information about subscription in PPA

		Arguments:
		- subscription_name - name of subscription (webspace) as a string

		Returns SubscriptionInfo object if subscription exists in PPA,
		otherwise raises SubscriptionNotFoundException.

		The function is cached, so use carefull if some subscription's data
		changes during move.
		"""

		subscriptions = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Get(
				plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
				[
					plesk_ops.SubscriptionOperator.Dataset.GEN_INFO,
					plesk_ops.SubscriptionOperator.Dataset.HOSTING_BASIC,
					plesk_ops.SubscriptionOperator.Dataset.MAIL,
				]
			)
		)

		no_subscription_exception = SubscriptionNotFoundException(
			messages.SUBSCRIPTION_WITH_NAME_WAS_NOT_FOUND % (
				subscription_name
			)
		)

		if len(subscriptions) == 0:
			raise no_subscription_exception

		try:
			subscription = subscriptions[0].data[1]
		except PleskError as e:
			if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST:
				raise no_subscription_exception

		name = subscription.gen_info.name
		hosting = subscription.hosting
		mail = subscription.mail

		if isinstance(hosting, plesk_ops.SubscriptionHostingVirtual):
			hosting_type = HostingType.Virtual
		elif isinstance(hosting, plesk_ops.SubscriptionHostingStandardForwarding):
			hosting_type = HostingType.StandardForwarding
		elif isinstance(hosting, plesk_ops.SubscriptionHostingFrameForwarding):
			hosting_type = HostingType.FrameForwarding
		else:
			hosting_type = HostingType.NoHosting
		
		return SubscriptionInfo(
			name=name,
			web_ips=(
				hosting.ip_addresses if hosting_type in (
					HostingType.Virtual, HostingType.StandardForwarding, HostingType.FrameForwarding
				)
				else Ips(None, None)
			),
			mail_ips=mail.ip_addresses,
			hosting_type=hosting_type,
		)

	def get_subscription_sysuser_name(self, subscription_name):
		"""
		Return system user login for subscription by subscription name.
		If subscriptions does not have virtual hosting - return None.
		"""
		subscriptions = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Get(
				plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
				[
					plesk_ops.SubscriptionOperator.Dataset.GEN_INFO,
					plesk_ops.SubscriptionOperator.Dataset.HOSTING_BASIC,
				]
			)
		)
		if len(subscriptions) != 1:
			raise Exception(messages.FAILED_TO_GET_SYSTEM_USER % (subscription_name,))
		subscription = subscriptions[0].data[1]
		if subscription.gen_info.htype == HostingType.Virtual:
			return subscription.hosting.properties['ftp_login']
		else:
			return None

	def get_subscription_sites_info(self, subscription_name):
		return [
			SiteInfo(
				name=result.data[1].gen_info.name,
				hosting_type=self._get_hosting_type_from_str(result.data[1].gen_info.htype)
			)
			for result in self.conn.plesk_api().send(
				plesk_ops.SiteOperator.Get(
					filter=plesk_ops.SiteOperator.FilterByParentName([subscription_name]),
					dataset=[plesk_ops.SiteOperator.Dataset.GEN_INFO]
				)
			)
		]

	def get_subdomain_names(self, domain_names):
		return [
			result.data[1].name for result in self.conn.plesk_api().send(
				plesk_ops.SubdomainOperator.Get(
					filter=plesk_ops.SubdomainOperator.FilterByParentName(domain_names)
				)
			)
		]

	def get_domain_aliases(self, domain_name):
		return [result.data[1] for result in self.conn.plesk_api().send(
			plesk_ops.AliasOperator.Get(
				plesk_ops.AliasOperator.FilterBySiteName([domain_name]),
			)
		)]

	def get_domain_alias_dns_records(self, alias_id):
		return [result.data for result in self.conn.plesk_api().send(
			plesk_ops.DnsOperator.GetRec(filter=plesk_ops.DnsOperator.FilterBySiteAliasId([alias_id]), template=None)
		)]

	def delete_dns_record(self, record_id):
		result = self.conn.plesk_api().send(
			plesk_ops.DnsOperator.DelRec(filter=plesk_ops.DnsOperator.DelRec.FilterById([record_id]), template=None)
		)
		if len(result) != 1:
			raise Exception(messages.FAILED_TO_REMOVE_DNS_RECORD_EXPECTED_EXACTLY_ONE % (record_id,))
		if result[0].data.id != record_id:
			raise Exception(messages.FAILED_TO_REMOVE_DNS_RECORD_RESPONSE_ID_NOT_EQUAL % (record_id, result[0].id))

	def add_domain_alias_dns_record(self, alias_id, src, dst, opt, rec_type): 
		result = self.conn.plesk_api().send(
			plesk_ops.DnsOperator.AddRec(
				target=plesk_ops.DnsOperator.AddRec.TargetSiteAliasId(id=alias_id),
				src=src, dst=dst, opt=opt, rec_type=rec_type,
			)
		)
		if len(result) != 1:
			raise Exception(messages.FAILED_TO_ADD_DNS_RECORD_INVALID_RESPONSE)
		result[0].check()

	def get_webspace_id_by_name(self, webspace_name):
		result = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Get(
				dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO],
				filter=plesk_ops.SubscriptionOperator.FilterByName([webspace_name]),
			)
		)
		if len(result) != 1:
			raise Exception(messages.FAILED_TO_GET_WEBSPACE_ID_BY_NAME % (webspace_name,))

		return result[0].data[0]

	def get_db_server_id_by_host(self):
		return dict((result.data.host, result.data.id) for result in self.conn.plesk_api().send(
			plesk_ops.DbServerOperator.Get(
				filter=plesk_ops.DbServerOperator.FilterAll()
			)
		))

	def get_db_host_by_server_id(self):
		return dict((result.data.id, result.data.host) for result in self.conn.plesk_api().send(
			plesk_ops.DbServerOperator.Get(
				filter=plesk_ops.DbServerOperator.FilterAll()
			)
		))

	def get_mysql_db_server_hosts(self):
		return set([
			result.data.host for result in self.conn.plesk_api().send(
				plesk_ops.DbServerOperator.Get(
					filter=plesk_ops.DbServerOperator.FilterAll()
				)
			) if result.data.dbtype == 'mysql'
		])

	def get_db_servers_with_passwords(self):
		with self.conn.main_node_runner() as runner:
			with migrator_utils.plain_passwords_enabled(runner):  # temporary enable ability to fetch passwords via API
				return [result.data for result in self.conn.plesk_api().send(
					plesk_ops.DbServerOperator.Get(
						filter=plesk_ops.DbServerOperator.FilterAll()
					)
				)]

	def get_mysql_databases(self, subscription_name):
		return [result.data for result in self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.GetDb(
				filter=plesk_ops.DatabaseOperator.GetDb.FilterByWebspaceName([subscription_name]),
			)
		)]

	def create_mysql_database(self, webspace_id, db_server_id, db_name):
		result = self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.AddDb(
				webspace_id=webspace_id, name=db_name, type='mysql', db_server_id=db_server_id,
			)
		)
		result.check()

	def get_default_mysql_server_id(self, subscription_name):
		subscriptions = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Get(
				filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
				dataset=[plesk_ops.SubscriptionOperator.Dataset.GEN_INFO]
			)
		)
		
		if len(subscriptions) != 1:
			raise Exception(
				messages.FAILED_TO_GET_DEFAULT_MYSQL_SERVER_EXPECTED_EXACTLY_ONE_SUBSCRIPTION % (
					subscription_name, len(subscriptions)
				)
			)

		db_servers = {
			result.data[0]: result.data[1] for result in self.conn.plesk_api().send(
				plesk_ops.SubscriptionOperator.DbServers.List(
					plesk_ops.SubscriptionOperator.FilterAll()
				)
			)
		}
		subscription_id = subscriptions[0].data[0]
		return find_only(
			db_servers[subscription_id], 
			lambda db_server: db_server.server_type == 'mysql', 
			messages.FAILED_TO_GET_DEFAULT_MYSQL_SERVER_EXPECTED_EXACTLY_ONE_SERVER).server_id

	def get_db_admin_user_id(self, db_id):
		result = self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.GetDefaultUser(
				plesk_ops.DatabaseOperator.GetDefaultUser.FilterByDbIds([db_id])
			)
		)
		if len(result) != 1:
			raise Exception(messages.FAILED_TO_GET_DATABASE_ADMINISTRATOR_ID_MULTIPLE_RESULTS % (db_id,))
		try:
			response_db_id = result[0].data.db_id 
			if response_db_id != db_id:
				raise Exception(
					messages.FAILED_TO_GET_DATABASE_ADMIN_RESPONSE_ID_NOT_EQUAL % (db_id, response_db_id, db_id)
				)
			return result[0].data.db_user_id
		except PleskError as e:
			# XXX in case there is no default, Plesk API returns error,
			# and there is no reliable way to distinguish it from other errors, so we use error message to do this
			if e.code == PleskErrorCode.OBJECT_DOES_NOT_EXIST and e.message == 'default user does not exist':
				return None
			else:
				raise

	def set_db_admin_user_id(self, db_id, db_user_id):
		result = self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.SetDefaultUser(db_id, db_user_id)
		)
		result.check()

	def get_db_users_for_databases(self, db_ids):
		results = self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.GetDbUsers(
				plesk_ops.DatabaseOperator.GetDbUsers.FilterByDbIds(db_ids)
			)
		)
		db_users = defaultdict(list)
		for result in results:
			db_users[result.data.db_id].append(result.data)
		return db_users

	def delete_databases(self, db_ids):
		if len(db_ids) == 0: 
			# simply return, don't call Plesk API to avoid problem
			# while passing empty filter node "<filter/>" with no IDs
			# which is interpreted as "remove all databases" instead of "remove no databases" by Plesk API
			return

		results = self.conn.plesk_api().send(
			plesk_ops.DatabaseOperator.DelDb(
				plesk_ops.DatabaseOperator.DelDb.FilterByDbIds(db_ids),
			)
		)
		deleted_db_ids = [result.data.id for result in results]
		if set(db_ids) != set(deleted_db_ids):
			raise Exception(messages.NOT_ALL_DATABASES_DELETED, format_list(db_ids), format_list(deleted_db_ids))

	def get_database_limits(self, subscription_name):
		results = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Get(
				filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
				dataset=[plesk_ops.SubscriptionOperator.Dataset.LIMITS]
			)
		)
		if len(results) != 1:
			raise Exception(messages.FAILED_TO_GET_DATABASE_LIMIT % (subscription_name,))
		
		return {key: int(results[0].data[1].limits.limits[key]) for key in ('max_db', 'mysql_dbase_space')}

	def set_subscription_limits(self, subscription_name, limits):
		if len(limits) == 0:
			return

		results = self.conn.plesk_api().send(
			plesk_ops.SubscriptionOperator.Set(
				filter=plesk_ops.SubscriptionOperator.FilterByName([subscription_name]),
				hosting=None,
				limits=plesk_ops.LimitsInfo(overuse=None, limits=limits)
			)
		)
		if len(results) != 1:
			raise Exception(messages.FAILED_TO_SET_DATABASE_LIMIT_MULTIPLE_RESULTS % (subscription_name,))

		results[0].check()

	def get_web_type(self, domain_name, apache_resource_type_ids, iis_resource_type_ids):
		def get_resource_type_ids_by_webspace(domain_name):
			poa_api = self.conn.poa_api()
			return set(poa_api.get_webspace(
				poa_api.get_webspace_id_by_primary_domain(domain_name)
			).get('rt_ids', []))

		webspace_resource_type_ids = get_resource_type_ids_by_webspace(domain_name) 

		if len(webspace_resource_type_ids & apache_resource_type_ids) > 0:
			return WebServiceType.Apache
		elif len(webspace_resource_type_ids & iis_resource_type_ids) > 0:
			return WebServiceType.IIS
		else:
			return None

	def get_apache_resource_type_ids(self):
		return self._get_rt_ids_by_class_name('pleskweb_hosting')

	def get_iis_resource_type_ids(self):
		return self._get_rt_ids_by_class_name('pleskwebiis_hosting')

	def _get_rt_ids_by_class_name(self, class_name):
		poa_api = self.conn.poa_api()
		return set([i.get('resource_type_id') for i in poa_api.get_resource_types_by_class(class_name)])

	@staticmethod
	def _get_hosting_type_from_str(hosting_type_str):
		if hosting_type_str == 'vrt_hst':
			return HostingType.Virtual
		elif hosting_type_str == 'std_fwd':
			return HostingType.StandardForwarding
		elif hosting_type_str == 'frm_fwd':
			return HostingType.FrameForwarding
		elif hosting_type_str == 'none':
			return HostingType.NoHosting

		raise Exception(messages.UNKNOWN_HOSTING_TYPE_STRING % (hosting_type_str,))


class SubscriptionNotFoundException(Exception):
	pass
