import logging

from collections import namedtuple
from contextlib import contextmanager

from parallels.ppab_api import import_api, PPABAPIError
from parallels.hsphere_migrator.billing.utils import mask_credit_card_number
from parallels.hsphere_migrator.billing import data_model
from parallels.utils 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(u"Customer '%s' was already imported to the billing, skip import", 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("Exception: ", exc_info=True)
				raise Exception(u"Failed to import the credit card data for the card '%s': %s" % (mask_credit_card_number(credit_card.card_number), e.short_message))
			except Exception as e:
				self.logger.debug("Exception: ", exc_info=True)
				raise Exception(u"Failed to import the credit card data for the card '%s': %s" % (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("No billing service plan is specified for the subscription '%s' in the migration list file. The subscription will not be imported to PPAB." % 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 "Subscription #%s already exists" % (subscription_id,) in str(e):
				self.logger.debug(u"Hosting subscription '%s' was already imported to the billing, skip import", subscription.name)
			else:
				self.logger.debug("Exception: ", exc_info=True)
				raise Exception(u"Failed to import the hosting subscription '%s': %s" % (subscription.name, e.short_message))
		except Exception as e:
			self.logger.debug("Exception: ", exc_info=True)
			raise Exception(u"Failed to import the hosting subscription '%s': %s" % (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_EV"
				"ENOM_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("The SSL certificate provider (code %s) or product (code %s) specified in the PPA Billing plan #%s are not supported by the migration tool.\nEdit the plan and choose one of these certificate providers: ENOM, OpenSRS, SSLStore, GlobalSign." % (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(u"No PPAB SSL plan identifier for the product '%s' was set in the migration tool's configuration file. The SSL subscription for the domain %s will not be imported to PPAB."
				% (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)
			next_bill_date = subscription.expiration_date - 30 * 24 * 60 * 60	# H-Sphere is charging the account just before renewing the certificate, and is renewing the certificate 30 days before its expiration.
			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='Comodo SSL certificate from H-Sphere',
					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("An SSL certificate subscription for domain %s was already imported, its identifier is: %s" % (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(u"Failed to import the SSL subscription for the domain '%s': %s" % (domain_name, e.short_message))
		except Exception as e:
			self.logger.debug("Exception: ", exc_info=True)
			raise Exception(u"Failed to import the SSL subscription for the domain '%s': %s" % (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("Exception: ", exc_info=True)
				raise Exception(u"Failed to import the domain subscription %s: %s" % (subscription.name, e.short_message))
			except Exception as e:
				self.logger.debug("Exception: ", exc_info=True)
				raise Exception(u"Failed to import the domain subscription %s: %s" % (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(
					(u"No domain service plan was set for the zone %s.\n"
					u"Please specify a correct domain service plan for the zone in the migration tool's configuration file, in the billing-domain-plans section.\n"
					u"Otherwise the domain subscription will not be transferred.") % (zone,)
				)
			
			default_user_id = self._get_default_user(ppab, customer_id)
			self.logger.debug(u"Default user id for PPAB account '%s': '%s'", 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(u"Domain subscription '%s' was already imported to the billing, skip import", subscription.name)
						subscription_info = ppab.get_domain_subscription_info(subscription.name)
					else:
						raise
				self.logger.debug(u"Domain subscription '%s' was successfully imported to the billing", subscription.name)
				if ppab.init_users_from_registrar_api(subscription_info.domain_id):
					self.logger.debug(u"Information about domain users was successfully imported from registrars")
				else:
					self.logger.debug(u"Add or reuse existing users for domain subscription '%s'", 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(u"User IDs for domain subscription '%s': owner '%s', admin '%s', billing '%s', tech '%s'", 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(u"The period of %s %s%s does not exist for the service plan %s. Please define the period in service plan settings." % (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 == None:
				result = user.user_id

		return result

