from collections import namedtuple, defaultdict
import logging
import urllib
from ssl import SSLError
import xmlrpclib

from parallels.utils import obj
from parallels.utils.logging import BleepingLogger
from parallels.utils.xmlrpc import LoggingTransport
from parallels.common.utils.steps_profiler import get_default_steps_profiler

ServiceTemplateInfo = namedtuple('ServiceTemplateInfo', ('st_id', 'name', 'description', 'active', 'owner_id'))
# XXX why ServiceTemplateDetails is missing 2 fields of ServiceTemplateInfo? how these fields are used?
ServiceTemplateDetails = namedtuple('ServiceTemplateDetails', ('st_id', 'name', 'owner_id', 'autoprovidable', 'resource_types'))
ResourceTypeDetails = namedtuple('ResourceTypeDetails', ('rt_id', 'name', 'resclass_name', 'limit', 'act_params'))
ActParamInfo = namedtuple('ActParamInfo', ('name', 'value'))

HostInfo = namedtuple('HostInfo', ('attributes', 'ready_to_provide', 'hostname', 'ip_addresses'))
ServiceNodeInfo = namedtuple('ServiceNodeInfo', ('host_id', 'attributes', 'is_ready_to_provide', 'license_status'))
IpAddressInfo = namedtuple('IpAddressInfo', ('type', 'address'))
SubscriptionInfo = namedtuple('SubscriptionInfo', ('subscription_id', 'owner_id', 'st_id', 'name', 'is_active', 'resources'))
ResourceDetails = namedtuple('ResourceDetails', ('rt_id', 'name', 'resclass_name', 'limit', 'act_params', 'usage'))

WebspaceInfo = namedtuple('WebspaceInfo', ('webspace_id', 'domain', 'sub_id', 'owner_id', 'status', 'rt_ids'))

AccountTypes = obj(ADMIN='A', RESELLER='R', CLIENT='C')
Identifiers = obj(OID_ADMIN=1)
RequestStatus = obj(SUCCESS=0, RUNNING=1, FAILED=2)

PersonInfo = namedtuple('PersonInfo', ('first_name', 'last_name', 'company_name'))
AddressInfo = namedtuple('AddressInfo', ('zipcode', 'city', 'country', 'state', 'street_name', 'address2'))
PhoneInfo = namedtuple('PhoneInfo', ('country_code', 'area_code', 'phone_num', 'ext_num'))
FaxInfo = namedtuple('FaxInfo', ('country_code', 'area_code', 'phone_num', 'ext_num'))
LocaleInfo = namedtuple('LocaleInfo', ('country_code', 'language_code', 'variant'))
AccountInfo = namedtuple('AccountInfo', ('account_id', 'account_type'))

AuthInfo = namedtuple('AuthInfo', ('login', 'password'))

AccountMemberIds = namedtuple('AccountMemberIds', ('account_id', 'user_id'))
UserFullInfo = namedtuple('UserFullInfo', ('account_id', 'user_id', 'auth_info', 'pers_contact'))
MemberFullInfo = namedtuple('MemberFullInfo', ('account_id', 'member_id', 'auth_info', 'pers_contact'))
PersContact = namedtuple('PersContact', ('first_name', 'last_name', 'email'))

# input types for createWebspace method
Param = namedtuple('Param', ('name', 'value'))
WebspaceResource = namedtuple('WebspaceResource', ('rt_id', 'params'))

ServiceInfo = namedtuple('ServiceInfo', ('host_id', 'service_name'))

profiler = get_default_steps_profiler()


class PoaApiError(Exception):
	def __init__(self, status, module_id, extype_id, error_code, error_message, properties):
		if error_code is not None:
			full_error_message = "[%s] %s" % (error_code, error_message)
		else:
			full_error_message = error_message
		super(PoaApiError, self).__init__(full_error_message)
		self.status = status
		self.module_id = module_id
		self.extype_id = extype_id
		self.error_code = error_code
		self.error_message = error_message
		self.properties = properties


class PoaApiSettingsExceptionTransport(object):
	"""Transport wrapper class to handle common mistake when you have migration/moving tools running on another host than PPA management node, 
	but have not configured PPA Public API properly. It just catches all SSLError exceptions, and throws an exception with comprehensive instructions what to do. 
	"""
	def __init__(self, transport, logger):
		self.transport = transport
		self.logger = logger

	def request(self, host, handler, request_body, verbose=0):
		try:
			# parse method name in quick way for profiling
			method_name = 'unknown'
			method_name_start = request_body.find('<methodName>')
			if method_name_start != -1:
				method_name_end = request_body.find('</methodName')
				if method_name_end != -1:
					method_name = request_body[
						method_name_start + len('<methodName>'):method_name_end
					]

			with profiler.measure_api_call(host, 'POA API', method_name):
				return self.transport.request(host, handler, request_body, verbose)
		except SSLError as e:
			self.logger.debug("Exception: ", exc_info=True)
			raise Exception("""Failed to call PPA Public API method: %s. 
Most probably the issue is caused by incorrect PPA Public API configuration. Please verify settings at: "System" - "Settings" - "Public API":
1. "SSL" should be enabled.
2. "HTTP Authentication" should be enabled.
3. "Accept connections" should have one of the values:
   1) "Only from allowed networks". In that case you should add host where you run migration/moving tools to allowed hosts at "Allowed Networks" tab.
   2) "From everywhere". No additional configuration is required in that case, still this option is not recommended due to security considerations.""" % (e,))

	def close(self):
		self.transport.close()


class Client:
	logger = logging.getLogger(__name__)

	def __init__(self, url):
		proto, _ = urllib.splittype(url)

		if proto == 'https':
			transport = xmlrpclib.SafeTransport(use_datetime=0)
		elif proto == 'http':
			transport = xmlrpclib.Transport(use_datetime=0)
		else:
			assert False, u"Only http and https protocols are supported by xmlrpclib"

		logger = BleepingLogger(self.logger)
		transport = LoggingTransport(transport, logger)
		transport = PoaApiSettingsExceptionTransport(transport, self.logger)

		self.proxy = xmlrpclib.ServerProxy(url, transport=transport)

	def _makeAccountInfoArgs(self, person, address, phone, email, fax, locale):
		args = {
			'person': dict(person._asdict()),
			'address': dict(address._asdict()),
			'phone': dict(phone._asdict()),
			'email': email
		}
		if fax is not None:
			args.update({'fax': dict(fax._asdict())})
		if locale is not None:
			args.update({'locale': dict(locale._asdict())})
		return args

	def addAccount(self, account_type, parent_account_id, person, address, phone, email, fax=None, locale=None, txn_id=None):
		args = {
			'account_type': account_type,
			'parent_account_id': parent_account_id,
		}
		args.update(self._makeAccountInfoArgs(person, address, phone, email, fax, locale))
		if txn_id is not None:
			args['txn_id'] = txn_id

		response = self.proxy.pem.addAccount(args)

		result = self._parse_response(response)
		return result['account_id']
	
	def addAccountMember(self, account_id, auth, person, address, phone, email, fax=None, txn_id=None):
		args = {
			'account_id': account_id,
			'auth': {
				'login': auth.login,
				'password': auth.password
			}
		}
		args.update(self._makeAccountInfoArgs(person, address, phone, email, fax, locale=None))
		if txn_id is not None:
			args['txn_id'] = txn_id

		response = self.proxy.pem.addAccountMember(args)

		result = self._parse_response(response)
		return result['user_id']
	
	def addSubscription(self, account_id, service_template_id, act_params=None, subscription_name=None):
		args = {
			'account_id': account_id,
			'service_template_id': service_template_id,
		}

		if act_params is not None:
			args['parameters'] = act_params
		if subscription_name is not None:
			args['subscription_name'] = subscription_name

		response = self.proxy.pem.addSubscription(args)
		result = self._parse_response(response)
		if type(result) is int:
			# PPA >= 11.6
			return result
		else:
			# PPA <= 11.5
			return result['subscription_id']

	def createWebspace(self, sub_id, domain, resources, txn_id=None):
		"""Create webspace with specified domain name and resources in specified subscription.
		   Return webspace identifier.
		"""
		#resources = {rt_id: [(name, value), (name, value), ...], rt_id: []}

		r_array = []	# [ { } ]
		for r in resources:
			r_serialized = {
				'rt_id': r.rt_id
			}
			p_array = []	# [ { } ]
			for p in r.params:
				p_array.append({
					'name': p.name,
					'value': p.value
				})
			if len(r.params) > 0:
				r_serialized['params'] = p_array
			r_array.append(r_serialized)

		args = {
			'new_webspace': {
				'sub_id': sub_id,
				'domain': domain,
				'resources': r_array,
			}
		}

		if txn_id is not None:
			args['txn_id'] = txn_id
		response = self.proxy.pem.pleskintegration.createWebspace(args)
		result = self._parse_response(response)
		return result['webspace_id']

	def getServiceTemplateList(self, owner_id, active=None):
		"""Get list of service templates that belong to specified owner, with their information
		"""
		args = dict(owner_id = owner_id)
		if active is not None:
			args['active'] = active
		response = self.proxy.pem.getServiceTemplateList(args)
		result = self._parse_response(response)
		return [ServiceTemplateInfo(i['st_id'], i['name'], i['description'], i['active'], i['owner_id']) for i in result]

	def getServiceTemplate(self, st_id, get_resources):
		"""Get details about specified service template
		"""
		args = {
			'st_id': st_id,
			'get_resources': get_resources,
			'get_full_info': get_resources	# resources' activation parameters
		}

		response = self.proxy.pem.getServiceTemplate(args)
		result = self._parse_response(response)

		if get_resources:
			rts = self._parse_resource_types(result)
		else:
			rts = None

		return ServiceTemplateDetails(st_id=result['st_id'], name=result['name'], owner_id=result['owner_id'], autoprovidable=result['autoprovidable'], resource_types=rts)

	def _parse_resource_types(self, result):
		rts = []
		for rt_item in result['resource_types']:
			act_params_dict = {}
			act_params = []
			if 'act_params' in rt_item:
				for rtap_item in rt_item['act_params']:
					act_params_dict[rtap_item['var_name']] = rtap_item['var_value']
			if 'st_act_params' in rt_item:
				for rtap_item in rt_item['st_act_params']:
					act_params_dict[rtap_item['var_name']] = rtap_item['var_value']
			act_params = [ActParamInfo(name, value) for name, value in act_params_dict.iteritems()]
			rts.append(ResourceTypeDetails(rt_id=rt_item['resource_type_id'], name=rt_item['name'], resclass_name=rt_item['resclass_name'], limit=int(rt_item['limit']), act_params=act_params))
		return rts

	def getAccountInfo(self, account_id):
		args = {'account_id': account_id}
		response = self.proxy.pem.getAccountInfo(args)
		result = self._parse_response(response)
		return AccountInfo(account_id=account_id, account_type=result['account_type'])

	def getMemberFullInfo(self, member_id):
		"""Get information about specified member (user with type 'member')
		"""
		args = {'member_id': member_id}
		response = self.proxy.pem.getMemberFullInfo(args)
		result = self._parse_response(response)
		return MemberFullInfo(
			account_id=result['account_id'], member_id=member_id,
			auth_info=AuthInfo(login=result['auth_info']['login'], password=result['auth_info']['password']),
			pers_contact=PersContact(
				email=result['pers_contact']['email'],
				first_name=result['pers_contact']['first_name'],
				last_name=result['pers_contact']['last_name']
			)
		)

	def getAccountSubscriptions(self, account_id):
		"""Get identifiers of subscriptions of specified account
		"""
		response = self.proxy.pem.getAccountSubscriptions({'account_id': account_id})
		return self._parse_response(response)	# result consists of array of subscription identifiers, no need in conversion, return it as is

	def getSubscription(self, subscription_id, get_resources=False):
		"""Get information about specified subscription
		"""
		response = self.proxy.pem.getSubscription({
			'subscription_id': subscription_id,
			'get_resources': get_resources
		})
		result = self._parse_response(response)

		if get_resources:
			resources = self._parse_resources(result)
		else:
			resources = None

		return SubscriptionInfo(
			subscription_id=result['subscription_id'], owner_id=result['owner_id'], st_id=result['st_id'], name=result['name'], is_active=result['is_active'], resources=resources
		)

	def _parse_resources(self, result):
		resources = []
		for rt_item in result['resource_types']:
			act_params = []
			if 'act_params' in rt_item:
				for rtap_item in rt_item['act_params']:
					act_params.append(ActParamInfo(name=rtap_item['var_name'], value=rtap_item['var_value']))
			resources.append(ResourceDetails(rt_id=rt_item['resource_type_id'], name=rt_item['name'], resclass_name=rt_item['resclass_name'], limit=int(rt_item['limit']), act_params=act_params, usage=int(rt_item['usage'])))
		return resources

	def getWebspace(self, webspace_id):
		response = self.proxy.pem.pleskintegration.getWebspace({'webspace_id': webspace_id})
		result = self._parse_response(response)
		return WebspaceInfo(
			webspace_id=result['webspace_id'], domain=result['domain'], sub_id=result['sub_id'],
			owner_id=result['owner_id'], status=result['status'], rt_ids=result['rt_ids']
		)

	def getAccountMemberByLogin(self, login):
		"""Get information (identifiers) about account member by its login
		"""
		response = self.proxy.pem.getAccountMemberByLogin({'login': login})
		result = self._parse_response(response)
		return AccountMemberIds(account_id=result['account_id'], user_id=result['user_id'])

	def disableAccount(self, account_id):
		response = self.proxy.pem.disableAccount({'account_id': account_id})
		self._parse_response(response)

	def disableSubscription(self, subscription_id):
		response = self.proxy.pem.disableSubscription({'subscription_id': subscription_id})
		self._parse_response(response)

	def getHost(self, host_id, txn_id=None):
		args = {'host_id': host_id}
		if txn_id is not None:
			args['txn_id'] = txn_id
		response = self.proxy.pem.getHost(args)
		result = self._parse_response(response)
		return HostInfo(
			attributes=result['attributes'],
			ready_to_provide=result['ready_to_provide'],
			hostname=result['hostname'],
			ip_addresses=[
				IpAddressInfo(type=ip_address['ip_type'], address=ip_address['ip_address'])
				for ip_address in result['ip_addresses']
			]
		)

	def findHost(self, hostname=None, ip_address=None, txn_id=None):
		assert hostname is not None or ip_address is not None
		args = dict()
		if hostname is not None:
			args['hostname'] = hostname
		if ip_address is not None:
			args['ip_address'] = ip_address
		if txn_id is not None:
			args['txn_id'] = txn_id
		response = self.proxy.pem.findHost(args)

		try:
			result = self._parse_response(response)
		except PoaApiError as e:
			# POA API does not provide more reliable way to detect error type:
			# - there is no error code
			# - 'extype_id' for this error intersects with other errors) 
			if e.error_message == "Host not found": 
				return None
			else:
				raise

		return result['host_id']
		
	def register_ipv6_address(self, host_id, address, interface, mask):
		self._parse_response(self.proxy.pem.pleskintegration.registerIPv6Address(dict(
			host_id = host_id,
			ip_address = address,
			network_interface = interface,
			prefix = mask,
		)))

	def _parse_response(self, response):
		if response['status'] != 0:
			raise PoaApiError(
				error_code = response.get('error_code', None),
				**(dict((name, response.get(name)) for name in ('status', 'module_id', 'extype_id', 'error_message', 'properties')))
			)
		return response.get('result')

	def upload_additional_license(self, body):
		args = {'license_content': body}
		response = self.proxy.pem.msp_license_manager.uploadAdditionalLicense(args)
		result = self._parse_response(response)
		return result['license_id']

	def assign_license(self, host_id, license_id):
		args = {'host_id': host_id, 'license_id': license_id}
		response = self.proxy.pem.msp_license_manager.assignNodeLicense(args)
		self._parse_response(response)

	def get_ppa_license_status(self):
		response = self.proxy.pem.msp_license_manager.getPPALicenseStatus()
		result = self._parse_response(response)
		return result['license_status']

	def get_service_nodes_info(self):
		response = self.proxy.pem.msp_license_manager.getServiceNodesInfo()
		result = self._parse_response(response)
		return [ ServiceNodeInfo(
				host_id=item['host_id'],
				attributes=item['attributes'],
				is_ready_to_provide=item['is_ready_to_provide'],
				license_status=item['license_status']
			) for item in result
		]
	
	def getRequestStatus(self, request_id):
		"""Get request status.
		Return tuple(request_status, status_messages).
		"""
		response = self.proxy.pem.getRequestStatus({'request_id': str(request_id)})
		result = self._parse_response(response)
		return (result['request_status'], result['status_messages'])

	def txnBegin(self, request_id):
		"""Begin OpenAPI transaction.
		Return transaction id, to be used with txnCommit.
		"""
		response = self.proxy.txn.Begin({'request_id': str(request_id)})
		return self._parse_response(response)['txn_id']

	def txnCommit(self, txn_id):
		"""Commit OpenAPI transaction.
		"""
		response = self.proxy.txn.Commit({'txn_id': txn_id})
		self._parse_response(response)

	def get_domain_name_servers(self, domain_name):
		response = self.proxy.pem.getDomainNameServers({'domain_name': domain_name})
		return self._parse_response(response)

	def msp_license_manager_get_sip_data(self):
		return self._parse_response(self.proxy.pem.msp_license_manager.getSipData())

	def pleskweb_allocate_ips_for_webspace(self, webspace_id, host_id):
		args = {'request': {'webspace_id': webspace_id, 'host_id': host_id}}
		return self._parse_response(self.proxy.pem.pleskweb.allocateIPsForWebspace(args))

	def pleskwebiis_allocate_ips_for_webspace(self, webspace_id, host_id):
		args = {'request': {'webspace_id': webspace_id, 'host_id': host_id}}
		return self._parse_response(self.proxy.pem.pleskwebiis.allocateIPsForWebspace(args))
	
	def pleskmail_allocate_ips_for_webspace(self, webspace_id, host_id):
		args = {'request': {'webspace_id': webspace_id, 'host_id': host_id}}
		return self._parse_response(self.proxy.pem.pleskmail.allocateIPsForWebspace(args))

	def pleskweb_change_webspace_ips(self, webspace_id, main_ip):
		args = {'new_ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskweb.changeWebspaceIPs(args))

	def pleskwebiis_change_webspace_ips(self, webspace_id, main_ip):
		args = {'new_ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskwebiis.changeWebspaceIPs(args))

	def pleskmail_change_webspace_ips(self, webspace_id, main_ip):
		args = {'new_ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskmail.changeWebspaceIPs(args))

	def pleskweb_deallocate_ips_for_webspace(self, webspace_id, main_ip):
		args = {'ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskweb.deallocateIPsForWebspace(args))

	def pleskwebiis_deallocate_ips_for_webspace(self, webspace_id, main_ip):
		args = {'ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskwebiis.deallocateIPsForWebspace(args))

	def pleskmail_deallocate_ips_for_webspace(self, webspace_id, main_ip):
		args = {'ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskmail.deallocateIPsForWebspace(args))

	def pleskdb_allocate_mysql_ips_for_webspace(self, webspace_id, host_id):
		args = {'request': {'webspace_id': webspace_id, 'host_id': host_id}}
		return self._parse_response(self.proxy.pem.pleskdb.allocateMySQLIPsForWebspace(args))

	def pleskdb_change_webspace_mysql_ips(self, webspace_id, main_ip):
		args = {'new_ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskdb.changeWebspaceMySQLIPs(args))

	def pleskdb_deallocate_mysql_ips_for_webspace(self, webspace_id, main_ip):
		args = {'ips': {'webspace_id': webspace_id, 'ips': {'main_ip': main_ip}}}
		return self._parse_response(self.proxy.pem.pleskdb.deallocateMySQLIPsForWebspace(args))

	def set_resource_type_limits(self, subscription_id, limits):
		"""
		limits - list of dicts with 'resource_type_id' and 'limit' keys
		"""
		args = {'subscription_id': subscription_id, 'resource_limits': limits}
		return self._parse_response(self.proxy.pem.setResourceTypeLimits(args))

	def get_webspace_id_by_primary_domain(self, domain_name):
		args = {'domain': domain_name}
		return self._parse_response(self.proxy.pem.pleskintegration.getWebspaceIDByPrimaryDomain(args))['webspace_id']

	def get_webspace(self, webspace_id):
		args = {'webspace_id': webspace_id}
		return self._parse_response(self.proxy.pem.pleskintegration.getWebspace(args))

	def get_resource_types_by_class(self, resclass_name):
		args = {'resclass_name': resclass_name}
		return self._parse_response(self.proxy.pem.getResourceTypesByClass(args))

	def getWebspaceIDByPrimaryDomain(self, domain_name):
		args = {'domain': domain_name}
		response = self.proxy.pem.pleskintegration.getWebspaceIDByPrimaryDomain(args)
		result = self._parse_response(response)
		return int(result['webspace_id'])

	def remove_webspace(self, webspace_id):
		args = {'webspace_id': webspace_id}
		response = self.proxy.pem.pleskintegration.removeWebspace(args)
		self._parse_response(response)

	def listServices(self):
		args = {}
		response = self.proxy.pem.packaging.listServices(args)
		result = self._parse_response(response)
		return [ ServiceInfo(r['host_id'], r['service_name']) for r in result ]

	def get_bm_xml_rpc_address(self):
		args = {}
		response = self.proxy.pem.billing.getBMXmlRpcAddress(args)
		return self._parse_response(response)

	def activate_subscription(self, account_id, service_template_id, act_params=None, subscription_name=None, resource_limits=None, txn_id=None):
		args = {
			'account_id': account_id,
			'service_template_id': service_template_id,
		}

		if act_params is not None:
			args['parameters'] = act_params
		if subscription_name is not None:
			args['subscription_name'] = subscription_name
		if resource_limits:
			args['resource_limits'] = resource_limits
		if txn_id is not None:
			args['txn_id'] = txn_id
		response = self.proxy.pem.activateSubscription(args)
		result = self._parse_response(response)
		return result['subscription_id']

	def get_subscription_app_instances(self, subscription_id):
		args = {
			'subscription_id' : subscription_id
		}
		response = self.proxy.pem.APS.getSubscriptionApplicationInstances(args)
		result = self._parse_response(response)
		return result[0]['application_instance_id']

	def get_app_instance_settings(self, app_instance_id):
		args = {
			'application_instance_id': app_instance_id
		}
		response = self.proxy.pem.APS.getApplicationInstanceSettings(args)
		result = defaultdict(list)
		for item in self._parse_response(response):
			result[item['name']].append(item['value'])

		return result

	def get_subscription_aps_token(self, subscription_id):
		args = {
			'subscription_id': subscription_id
		}
		response = self.proxy.pem.APS.getSubscriptionToken(args)
		result = self._parse_response(response)
		return result
	
	def account_added_to_pba(self, account_id):
		self.proxy.pem.billing.accountAddedToPBA({'account_id': account_id})
