from parallels.ppa.source.hsphere import messages
import logging

from collections import namedtuple
from contextlib import contextmanager

from parallels.ppa.utils.xml_rpc.ppab import import_api, PPABAPIError
from parallels.ppa.source.hsphere.billing.utils import mask_credit_card_number
from parallels.ppa.source.hsphere.billing import data_model
from parallels.core.utils.common import list_get, cached

PPAData = namedtuple('PPAData', (
	'customers', # dictionary with key - customer login, value - customer POA ID
	'subscriptions', # dictionary with key - subscription name, value - subscription POA ID
))

# every public method creating a subscription returns its cost.
# every private method creating a subscription fills and returns the below structure, for public method to count the subscription's cost.
SubscriptionBillingInfo = namedtuple('SubscriptionBillingInfo', ('subscription_id', 'plan_id', 'period', 'period_type', 'next_bill_date', 'expiration_date'))


class BillingImporter(object):
	logger = logging.getLogger(__name__)

	def __init__(self, ppab_api):
		self.ppab_api = ppab_api

	def import_general_information(self, account, customer_id, customer_report):
		try:
			self.ppab_api.import_account(import_api.Account(
				vendor_account_id=1, # vendor is always administrator, as PPAB does not support resellers now
				account_id=customer_id,
				class_id=0,
				tax_reg_id="",
				balance=-account.balance,	# in PPA Billing, positive balance means debt, whereas in H-Sphere it means credit. Changing the sign here to match PPAB logic.
				tax_status=2,
			))

		except PPABAPIError as e:
			if "Account #%s already exists" % (customer_id,) in str(e):
				self.logger.debug(messages.CUSTOMER_ALREADY_IMPORTED_TO_BILLING, account.login)
			else:
				raise

		self.logger.debug("Import credit cards")
		for credit_card in account.credit_cards:
			try:
				self.ppab_api.import_credit_card(import_api.CreditCard(
					customer_id=customer_id,

					pay_system=credit_card.pay_system, card_number=credit_card.card_number, 
					card_holder_name=credit_card.card_holder_name, cvv=credit_card.cvv, 
					expiration_date=credit_card.expiration_date, start_date=credit_card.start_date, 
					issue_number=credit_card.issue_number,

					address1=credit_card.address1, address2=credit_card.address2, city=credit_card.city, state=credit_card.state, 
					zip_code=credit_card.zip_code, country=credit_card.country, email=credit_card.email,
					phone=import_api.PhoneNumber(
						country_code=credit_card.phone.country_code, area_code=credit_card.phone.area_code, 
						number=credit_card.phone.number, extension=credit_card.phone.extension
					),
					fax=import_api.PhoneNumber(
						country_code=credit_card.fax.country_code, area_code=credit_card.fax.area_code, 
						number=credit_card.fax.number, extension=credit_card.fax.extension
					),
					use_for_auto_payments=credit_card.use_for_auto_payments
				))
			except PPABAPIError as e:
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise Exception(messages.FAILED_TO_IMPORT_CREDIT_CARD % (
					mask_credit_card_number(credit_card.card_number), e.short_message
				))
			except Exception as e:
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise Exception(messages.FAILED_TO_IMPORT_CREDIT_CARD % (
					mask_credit_card_number(credit_card.card_number), str(e)
				))

	def import_hosting_subscription(self, subscription, customer_id, customer_report, ppa_data, migration_list_data):
		"""Import subscription, return its estimated cost.
		"""
		hs = self._import_hosting_subscription(subscription, customer_id, customer_report, ppa_data, migration_list_data, self.ppab_api)
		return self.ppab_api.get_next_order_amount(hs.subscription_id, hs.plan_id, hs.period, hs.period_type, hs.next_bill_date, hs.expiration_date)

	def _import_hosting_subscription(self, subscription, customer_id, customer_report, ppa_data, migration_list_data, ppab):
		billing_period_type_hsphere_to_ppab =  {
			data_model.BillingPeriodType.DAY: import_api.BillingPeriodType.DAY,
			data_model.BillingPeriodType.MONTH: import_api.BillingPeriodType.MONTH,
			data_model.BillingPeriodType.YEAR: import_api.BillingPeriodType.YEAR
		}

		plan_id = migration_list_data.billing_plan_ids.get(subscription.name)
		subscription_id = ppa_data.subscriptions[subscription.name]
		if plan_id is None:
			raise Exception(messages.NO_BILLING_SERVICE_PLAN_SPECIFIED % subscription.name)
		try:
			ppab.import_pem_subscription(import_api.PEMSubscription(
				subscription_id=subscription_id,
				plan_id=plan_id,
				expiration_date=subscription.next_bill_date, # H-Sphere does not have any expiration dates, in terms of PPAB subscriptions expires on the next bill date
				billing_period=import_api.BillingPeriod(
					billing_period_type_hsphere_to_ppab[subscription.billing_period.type],
					subscription.billing_period.size,
				),
				last_bill_date=subscription.last_bill_date,
				next_bill_date=subscription.next_bill_date,
			))
		except PPABAPIError as e:
			if messages.SUBSCRIPTION_ALREADY_EXISTS % (subscription_id,) in str(e):
				self.logger.debug(messages.HOSTING_SUBSCRIPTION_WAS_ALREADY_IMPORTED_TO_BILLING, subscription.name)
			else:
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise Exception(messages.FAILED_TO_IMPORT_HOSTING_SUBSCRIPTION % (subscription.name, e.short_message))
		except Exception as e:
			self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
			raise Exception(messages.FAILED_TO_IMPORT_HOSTING_SUBSCRIPTION % (subscription.name, str(e)))

		return SubscriptionBillingInfo(
			subscription_id=subscription_id,
			plan_id=plan_id,
			period=subscription.billing_period.size,
			period_type=billing_period_type_hsphere_to_ppab[subscription.billing_period.type],
			next_bill_date=subscription.next_bill_date,
			expiration_date=subscription.next_bill_date
		)

	def import_ssl_subscription(self, subscription, ssl_product_to_plan_id, customer_id, customer_report):
		"""Import subscription, return its estimated cost.
		"""
		ssl_s = self._import_ssl_subscription(subscription, ssl_product_to_plan_id, customer_id, customer_report, self.ppab_api)
		return self.ppab_api.get_next_order_amount(ssl_s.subscription_id, ssl_s.plan_id, ssl_s.period, ssl_s.period_type, ssl_s.next_bill_date, ssl_s.expiration_date)

	def _import_ssl_subscription(self, subscription, ssl_product_to_plan_id, customer_id, customer_report, ppab):
		def format_csr_information(csr_attrs):
			return """Make sure following information embedded into certificate request is valid:
Country Code: {C}
Common Name (Domain Name): {CN}
Organization Name: {O}
State or Province: {ST}
Locality (City): {L}
Email Address: {emailAddress}
Note: The value for the Common Name must exactly match the name of the server you plan to secure.""".format(**csr_attrs)

		def get_subscr_params(plan_id, subscription):
			"""Return list of (name, value) pairs of extra parameters required by SSL certificate providers.
			   Different SSL certificate providers require different extra parameters.
			   The SSL certificate provider is retrieved from PPAB by plan_id.
			"""
			cert_product = ppab.get_cert_product(plan_id)
			if cert_product.container_id == "CERTENOM" and cert_product.identity_code in [
				"ENOM_SSL_PRODUCT_CERTIFICATE_RAPIDSSL_RAPIDSSL",
				"ENOM_SSL_PRODUCT_CERTIFICATE_GEOTRUST_QUICKSSL",
				"ENOM_SSL_PRODUCT_CERTIFICATE_GEOTRUST_QUICKSSL_PREMIUM",
				"ENOM_SSL_PRODUCT_CERTIFICATE_GEOTRUST_TRUEBIZID",
				'ENOM_SSL_PRODUCT_CERTIFICATE_GEOTRUST_TRUEBIZID_EVENOM_SSL_PRODUCT_CERTIFICATE_GEOTRUST_TRUEBIZID_WILDCARD']:
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					('WEBSERVERTYPEID', '1'), # Apache+ModSSL
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
				]
			elif cert_product.container_id == "CERTENOM" and cert_product.identity_code in [
				"ENOM_SSL_PRODUCT_CERTIFICATE_COMODO_ESSENTIAL",
				"ENOM_SSL_PRODUCT_CERTIFICATE_COMODO_INSTANT",
				"ENOM_SSL_PRODUCT_CERTIFICATE_COMODO_PREMIUM",
				"ENOM_SSL_PRODUCT_CERTIFICATE_COMODO_EV",
				"ENOM_SSL_PRODUCT_CERTIFICATE_COMODO_EV_SGC"
			]:
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					('WEBSERVERTYPEID', '1'), # Apache+ModSSL
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
					('ADDRESS1ID', subscription.address1),
					('ADDRESS2ID', subscription.address2),	# optional for these products, but since H-Sphere has it, specify it
					# ADDRESS3 is optional for these products, and H-Sphere does not have it => omit it
					('POSTALCODEID', subscription.postal_code),
				]
			elif cert_product.container_id == "CERTOPENSRS" and cert_product.identity_code in [	# (product  Comodo)
				"opensrs_comodo_ev",
				"opensrs_comodo_instantssl",
				"opensrs_comodo_premiumssl",
				"opensrs_comodo_premiumssl_wildcard",
				"opensrs_comodo_ssl",
				"opensrs_comodo_wildcard",
				"opensrs_essentialssl",
				"opensrs_essentialssl_wildcard"
			]:
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					('WEBSERVERTYPEID', '510'), # Apache+ModSSL
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
				]
			elif cert_product.container_id == "CERTOPENSRS":	# (products GeoTrust, Thawte, Trustwave, Verisign)
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					('WEBSERVERTYPEID', '20'), # Apache+ModSSL
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
				]
			elif cert_product.container_id == "CERTSSLSTORE":
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					('WEBSERVERTYPEID', '20'), # Apache+ModSSL
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
				]
			elif cert_product.container_id == "CERTGLOBALSIGN":
				return [
					('CSRID', subscription.csr),
					('PKEYID', subscription.pkey),
					# WEBSERVERTYPEID is not needed
					('APPROVEREMAILID', subscription.approver_email),
					('JOBTITLE', 'Administrator'),
					('CSRINFORMATIONID', format_csr_information(subscription.csr_attrs)),
				]
			else:
				raise Exception(messages.SSL_CERTIFICATE_PROVIDER_OR_PRODUCT_NOT_SUPPORTED % (cert_product.container_id, cert_product.identity_code, plan_id))


		plan_id = ssl_product_to_plan_id.get(subscription.product)
		if plan_id is None:
			raise Exception(messages.NO_PPAB_SSL_PLAN_IDENTIFIER_FOR_PRODUCT_IN_CONFIG% (subscription.product, subscription.csr_attrs.get('CN'))
			)

		domain_name = subscription.csr_attrs.get('CN', 'N/A')
		try:
			period = self._get_plan_period_info(plan_id, subscription.period, import_api.BillingPeriodType.YEAR, ppab)
			plan_details = ppab.plan_details_get(plan_id)
			# H-Sphere is charging the account just before renewing the certificate,
			# and is renewing the certificate 30 days before its expiration.
			next_bill_date = subscription.expiration_date - 30 * 24 * 60 * 60
			subscription_id = ppab.find_ssl_subscription_id(domain_name)
			if subscription_id is None:
				subscr_params = get_subscr_params(plan_id, subscription)
				subscription_id = ppab.import_ssl_subscription(import_api.SSLSubscription(
					SubscriptionID=None,
					ParentID=None,
					SubscriptionName=messages.COMODO_SSL_CERTIFICATE_FROM_HSPHERE,
					Status=import_api.SubscriptionStatus.ACTIVE,	# TODO check if it should correlate with hosting subscription status
					startDate=subscription.start_date,		# extracted from cert
					ExpirationDate=subscription.expiration_date,	# extracted from cert
					AccountID=customer_id,
					PlanID=plan_id,

					Period=subscription.period,
					PeriodType=import_api.BillingPeriodType.YEAR,

					SetupFee=period.setup_fee,
					SubscriptionFee=period.subscription_fee,
					RenewalFee=period.renewal_fee,
					TransferFee=period.transfer_fee,
					NonRefundableAmt=period.non_refundable_amt,
					RefundPeriod=period.refund_period,

					BillingPeriod=plan_details.billing_period,
					BillingPeriodType=plan_details.billing_period_type, 
					LastBillDate=subscription.last_bill_date,
					NextBillDate=next_bill_date,

					DomainName=domain_name,
					Certificate=subscription.certificate,

					SubscrParamValue=subscr_params,
				))
			else:
				self.logger.debug(messages.SSL_CERTIFICATE_SUBSCRIPTION_ALREADY_IMPORTED % (domain_name, subscription_id))
			return SubscriptionBillingInfo(
				subscription_id=subscription_id,
				plan_id=plan_id,
				period=plan_details.billing_period,
				period_type=plan_details.billing_period_type,
				next_bill_date=next_bill_date,
				expiration_date=subscription.expiration_date
			)
		except PPABAPIError as e:
			self.logger.debug("Exception: ", exc_info=True)
			raise Exception(messages.FAILED_TO_IMPORT_SSL_SUBSCRIPTION % (domain_name, e.short_message))
		except Exception as e:
			self.logger.debug("Exception: ", exc_info=True)
			raise Exception(messages.FAILED_TO_IMPORT_SSL_SUBSCRIPTION_FOR_DOMAIN % (domain_name, str(e)))

	def import_domain_subscription(self, customer_id, subscription, domain_subscription_plans, customer_report):
		"""Import subscription, return its estimated cost.
		"""
		ds = self._import_domain_subscription(self.ppab_api, customer_id, subscription, domain_subscription_plans, customer_report)
		return self.ppab_api.get_next_order_amount(ds.subscription_id, ds.plan_id, ds.period, ds.period_type, ds.next_bill_date, ds.expiration_date)

	def _import_domain_subscription(self, ppab, customer_id, subscription, domain_subscription_plans, customer_report):
		class PPABUser(object):
			def __init__(self, user_id, user_entity):
				self.user_id = user_id
				self.first_name = user_entity.first_name.strip().lower()
				self.last_name = user_entity.last_name.strip().lower()
				self.email = user_entity.email.strip().lower()

			@staticmethod
			def are_contacts_equal(user1, user2):
				return user1.first_name == user2.first_name and user1.last_name == user2.last_name and user1.email == user2.email

		account_users = [] # list of PPABUser
		for user in ppab.get_users_list_for_account(customer_id):
			user_details = ppab.get_user_details(user.user_id)
			account_users.append(PPABUser(user_id=user_details.user_id, user_entity=user_details))

		def add_user(user):
			"""
			Add a user in a way, so if user with such first name, last name and email already
			exists in PPAB, then user is not used but rather reused.
			List of users is tracked in account_users variable.
			"""
			for existing_user in account_users:
				if PPABUser.are_contacts_equal(user, existing_user):
					return existing_user.user_id 

			# user with such contact data does not exist in PPAB, create it
			user_info = ppab.add_user(convert_user(user))
			account_users.append(PPABUser(user_id=user_info.user_id, user_entity=user))
			return user_info.user_id

		def convert_user(user):
			"""Convert user from backup format to import format"""
			return import_api.User(
				account_id=customer_id,
				login='',
				password='XXX', # special value for PPAB API
				first_name=user.first_name, 
				last_name=user.last_name,
				email=user.email,
				address1=user.address1,
				address2=user.address2,
				city=user.city,
				state=user.state,
				zip_code=user.zip_code,
				country=user.country,
				phone=import_api.PhoneNumber(
					country_code=user.phone.country_code, area_code=user.phone.area_code, 
					number=user.phone.number, extension=user.phone.extension
				),
				fax=import_api.PhoneNumber(
					country_code=user.fax.country_code, area_code=user.fax.area_code, 
					number=user.fax.number, extension=user.fax.extension
				),
			)

		def convert_domain_subscription_obj(subscription, default_user_id):
			"""Convert domain subscription from backup format to import format"""
			period = self._get_plan_period_info(plan_id, subscription.subscription_period, import_api.BillingPeriodType.YEAR, ppab)
			plan_details = ppab.plan_details_get(plan_id)

			return import_api.DomainSubscription(
				parent_id=None,
				status=import_api.SubscriptionStatus.ACTIVE,
				account_id=customer_id,
				plan_id=plan_id,
				subscription_period=subscription.subscription_period,
				start_date=subscription.start_date,
				expiration_date=subscription.next_bill_date,
				last_bill_date=subscription.last_bill_date,
				next_bill_date=subscription.next_bill_date,
				full_domain_name=subscription.name,
				nameservers=convert_nameservers(subscription.nameservers),
				users=import_api.DomainSubscriptionUsers(
					owner_id=default_user_id,
					admin_id=default_user_id,
					billing_id=default_user_id,
					tech_id=default_user_id,
				),
				# login and password are always empty as we have no login/password for domain in H-Sphere
				login='', password='',
				# billing period - take from PPAB
				billing_period_type=plan_details.billing_period_type, 
				billing_period=plan_details.billing_period,
				# prices - take from PPAB
				setup_fee=period.setup_fee,
				subscription_fee=period.subscription_fee,
				renewal_fee=period.renewal_fee,
				transfer_fee=period.transfer_fee,
				non_refundable_amt=period.non_refundable_amt,
				refund_period=period.refund_period,
			)

		def convert_nameservers(nameservers):
			return import_api.DomainSubscriptionNameServers(
				primary=list_get(nameservers, 0, ''),
				secondary=list_get(nameservers, 1, ''),
				third=list_get(nameservers, 2, ''),
				fourth=list_get(nameservers, 3, '')
			)


		@contextmanager
		def handle_generic_exception(subscription):
			"""Handle exceptions so if any issue arises, migrator will still continue to work and will try to import the next subscription"""
			try:
				yield
			except PPABAPIError as e:
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise Exception(messages.FAILED_TO_IMPORT_DOMAIN_SUBSCRIPTION % (subscription.name, e.short_message))
			except Exception as e:
				self.logger.debug(messages.LOG_EXCEPTION, exc_info=True)
				raise Exception(messages.FAILED_TO_IMPORT_DOMAIN_SUBSCRIPTION % (subscription.name, str(e)))

		with handle_generic_exception(subscription):
			zone = self.domain_zone(subscription.name, domain_subscription_plans.keys())
			plan_id = domain_subscription_plans.get(zone)
			if plan_id is None:
				raise Exception(
					(messages.NO_DOMAIN_SERVICE_PLAN_WAS_SET_FOR_ZONE) % (zone,)
				)
			
			default_user_id = self._get_default_user(ppab, customer_id)
			self.logger.debug(messages.DEBUG_DEFAULT_USER, customer_id, default_user_id)
			with handle_generic_exception(subscription):
				converted_domain_subscription = convert_domain_subscription_obj(subscription, default_user_id)
				try:
					subscription_info = ppab.add_domain_subscription(converted_domain_subscription)
					if subscription.registrar == 'enom':
						ppab.add_domain_ext_data(subscription_info.domain_id, subscription.extra)
						ppab.add_subscr_param_values(subscription_info.subscription_id, subscription.extra)
					elif subscription.registrar == 'opensrs':
						ppab.add_domain_ext_data(subscription_info.domain_id, subscription.extra)
				except Exception as e:
					if "Domain is already registered" in str(e):
						self.logger.debug(messages.DOMAIN_SUBSCRIPTION_WAS_ALREADY_IMPORTED_TO_BILLING, subscription.name)
						subscription_info = ppab.get_domain_subscription_info(subscription.name)
					else:
						raise
				self.logger.debug(messages.DOMAIN_SUBSCRIPTION_WAS_SUCCESSFULLY_IMPORTED_TO_BILLING, subscription.name)
				if ppab.init_users_from_registrar_api(subscription_info.domain_id):
					self.logger.debug(messages.INFO_ABOUT_DOMAIN_USERS_WAS_SUCCESSFULLY)
				else:
					self.logger.debug(messages.ADD_OR_REUSE_EXISTING_USERS, subscription.name)
					owner_id = add_user(subscription.users.owner)
					admin_id = add_user(subscription.users.admin)
					billing_id = add_user(subscription.users.billing)
					tech_id = add_user(subscription.users.tech)
					self.logger.debug(
						messages.DEBUG_SUBSCRIPTION_USERS, subscription.name, owner_id, admin_id, billing_id, tech_id
					)

					ppab.update_domain_contacts(subscription_info.domain_id, owner_id, admin_id, billing_id, tech_id)

				return SubscriptionBillingInfo(
					subscription_id=subscription_info.subscription_id,
					plan_id=plan_id,
					period=converted_domain_subscription.billing_period,
					period_type=converted_domain_subscription.billing_period_type,
					next_bill_date=converted_domain_subscription.next_bill_date,
					expiration_date=converted_domain_subscription.expiration_date
				)

	@staticmethod
	@cached
	def _get_plan_period_info(plan_id, required_period, required_period_type, ppab):
		periods_noun = {
			import_api.BillingPeriodType.DAY: "Day",
			import_api.BillingPeriodType.MONTH: "Month",
			import_api.BillingPeriodType.YEAR: "Year"
		}
			
		plan_periods = ppab.plan_period_list_get(plan_id)
		for plan_period in plan_periods:
			if plan_period.period == required_period and plan_period.period_type == required_period_type:
				return plan_period
		raise Exception(messages.PERIOD_DOES_NOT_EXIST % (
			required_period, periods_noun[required_period_type], 's' if required_period > 1 else '', plan_id
		))

	@staticmethod
	def domain_zone(domain_name, available_zones):
		"""
		Get the most apropriate domain zone by domain name and available zones list.
		For example for "example.ru.com", function will return "com" if zones list is ["com"], but
		will return "ru.com" if zones list is ["com", "ru.com"].
		Zone name is lowered. If no zones are matched function returns None.
		"""
		apropriate_zones = sorted([
			zone.lower() for zone in available_zones
			if domain_name.lower().endswith('.%s' % zone.lower())
		], key=len, reverse=True)
		if len(apropriate_zones) > 0:
			return apropriate_zones[0]
		else:
			return None

	def _get_default_user(self, ppab, account_id):
		result = None
		users = ppab.get_users_list_for_account(account_id)
		for user in users:
			if user.user_id > 1000000:
				return user.user_id

			if result is None:
				result = user.user_id

		return result

