from collections import namedtuple, defaultdict

from parallels.plesk_api.operator.subscription import Ips
from parallels.plesk_api import operator as plesk_ops 
from parallels.plesk_api.core import PleskError, PleskErrorCode
from parallels.utils import format_list, find_only
from parallels.utils import cached
from parallels.common.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(
			"Subscription with name '%s' was not found in PPA" % (
				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(u"Failed to get system user for subscription '%s': invalid response from Plesk API - expected exactly 1 result item" % (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(u"Failed to remove DNS record #%s: invalid response from Plesk API - expected exactly 1 result item" % (record_id,))
		if result[0].data.id != record_id:
			raise Exception(u"Failed to remove DNS record #%s: id '%s' in response is not equal to DNS record id" % (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(u"Failed to add DNS record: invalid response from Plesk API - expected exactly 1 result item")
		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(u"Failed to get webspace id by webspace name '%s': expected exactly 1 result item" % (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(u"Failed to get default MySQL server of subscription '%s': expected exactly 1 subscription in Plesk API response, got %s" % (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', 
			u"Failed to get default MySQL server of subscription '%s': expected to have exactly one default MySQL 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(u"Failed to get database administrator id for database #'%s': expected exactly 1 result item" % (db_id))
		try:
			response_db_id = result[0].data.db_id 
			if response_db_id != db_id:
				raise Exception(u"Failed to get database administrator id for database #'%s': database id '%s' in response is not equal to requested database id '%s'" % (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(u"Not all databases were deleted: requested %s, deleted %s", 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(u"Failed to get database limit for subscription '%s': expected exactly one result item" % (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(u"Failed to set database limit for subscription '%s': expected exactly one result item" % (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(u"Unknown hosting type string: '%s'" % (hosting_type_str,))

class SubscriptionNotFoundException(Exception):
	pass
