import re
import codecs
import textwrap
from collections import defaultdict, namedtuple
from StringIO import StringIO
import os

from parallels.utils import if_not_none, group_by, sorted_dict
from parallels.common.target_panels import TargetPanels

SubscriptionMappingInfo = namedtuple('SubscriptionMappingInfo', ('plan', 'addon_plans', 'owner'))

class BaseHandler(object):
	def __init__(self, allowed_labels):
		self.errors = []
		self.allowed_labels = allowed_labels

	def handle_reseller(self, reseller, line_number):
		pass

	def handle_plan(self, plan, line_number):
		pass 

	def handle_customer(self, customer, line_number):
		pass

	def handle_addon_plan(self, addon_plan, line_number):
		pass

	def handle_label(self, label, value, line_number):
		label_handlers = {
			'customer': self.handle_customer,
			'reseller': self.handle_reseller,
			'plan': self.handle_plan,
			'addon plan': self.handle_addon_plan,
		}
		if label in label_handlers:
			label_handlers[label](value, line_number)
			return True
		if label in self.allowed_labels:
			return True

		return False

	def handle_subscription(self, subscription, line_number):
		pass

	def handle_syntax_error(self, line, line_number):
		self.errors.append(u"Line %s with content '%s' is not valid" % (line_number, line))

class MigrationList(object):
	allowed_labels = ['plan', 'addon plan', 'customer', 'reseller', 'billing plan id']

	@classmethod
	def read(
		cls, fileobj, plesk_backup_iter, 
		has_custom_subscriptions_feature=False, 
		has_admin_subscriptions_feature=False,
		has_reseller_subscriptions_feature=False
	):
		"""Read migration list from file

		Returns: tuple(MigrationListData, list of error messages)

		Parameters:
		- fileobj - file-like object to read from
		- plesk_backup_iter - iterator (list) which provides Plesk backup
		  objects, so we can check if subscription/customer/reseller
		  specified in the list actually exists on the source system
		- has_custom_subscriptions_feature - if subscriptions that are not assigned
		  to any service template are allowed 
		- has_admin_subscriptions_feature - if subscriptions that are assigned
		  directly to admin are allowed
		- has_reseller_subscriptions_feature - if subscriptions that are assigned
		  directly to reseller are allowed
		"""
		
		source_subscriptions = cls._get_source_subscriptions(plesk_backup_iter)

		class Handler(BaseHandler):
			def __init__(self, allowed_labels):
				super(Handler, self).__init__(allowed_labels)
				self.data = MigrationListData(subscriptions_mapping={}, resellers=set(), customers_mapping={}, plans={None: set()})
				self.current_reseller = None
				self.current_plan = None
				self.current_addon_plans = []
				self.current_customer = None

			def handle_reseller(self, reseller, line_number):
				self.current_reseller = reseller
				self.data.resellers.add(self.current_reseller)
				self.data.plans[self.current_reseller] = set()
				self.current_plan = None
				self.current_addon_plans = []
				self.current_customer = None
		
			def handle_customer(self, customer, line_number):
				self.current_customer = customer
				if has_custom_subscriptions_feature:
					# If custom subscriptions are allowed - reset plan name, to
					# allow customer to have custom subscription even if the
					# previous customer or admin had some plan assigned to
					# subscriptions above
					self.current_plan = None
					self.current_addon_plans = []
				else:
					# Still allow setting one plan for all customers to save
					# backward compatibility for PPA migrations and simplify
					# composing migration list, where custom subscriptions are
					# not allowed
					pass
				if self.current_customer in self.data.customers_mapping and self.data.customers_mapping[self.current_customer] != self.current_reseller:
					self.errors.append(u"Line %s: customer '%s' was already assigned to another reseller '%s'" % (line_number, self.current_customer, self.data.customers_mapping[self.current_customer]))
				self.data.customers_mapping[self.current_customer] = self.current_reseller
		
			def handle_plan(self, plan, line_number):
				self.current_plan = plan
				self.current_addon_plans = []
				self.data.plans[self.current_reseller].add(self.current_plan)

			def handle_addon_plan(self, addon_plan, line_number):
				self.current_addon_plans.append(addon_plan)
				# put addon plans in the same list as regular plans
				self.data.plans[self.current_reseller].add(addon_plan)

			def handle_subscription(self, subscription, line_number):
				subscription_assigned = any([
					# subscription is assigned to customer
					self.current_customer is not None,
					# subscription is assigned to admin
					(
						self.current_reseller is None 
						and 
						has_admin_subscriptions_feature
					),
					# subscription is assigned to reseller
					(
						self.current_reseller is not None 
						and 
						has_reseller_subscriptions_feature
					)
				])

				if not subscription_assigned:
					self.errors.append(u"Line %s: subscription '%s' must be assigned to a customer" % (line_number, subscription))
					return
				if self._check_subscription(line_number, subscription):
					if self.current_customer is not None:
						owner = self.current_customer
					elif self.current_reseller is not None:
						owner = self.current_reseller
					else:
						owner = None # special value for admin

					self.data.subscriptions_mapping[subscription] = SubscriptionMappingInfo(
						plan=self.current_plan,
						addon_plans=self.current_addon_plans,
						owner=owner
					)
	
			def _check_subscription(self, line_number, subscription):
				if subscription in self.data.subscriptions_mapping:
					self.errors.append(u"Line %s: subscription '%s' is already defined" % (line_number, subscription))
					return False
				elif subscription not in source_subscriptions:
					self.errors.append(u"Line %s: subscription '%s' is not presented on source Plesks" % (line_number, subscription))
					return False
				elif self.current_plan is None and not has_custom_subscriptions_feature:
					self.errors.append(u"Line %s: subscription '%s' is not assigned to a plan. Custom subscriptions are not supported" % (line_number, subscription))
					return False
				else:
					return True
	
		handler = Handler(cls.allowed_labels)
		cls._read(fileobj, handler)
		return (handler.data, handler.errors)

	@classmethod
	def read_resellers(cls, fileobj):
		class Handler(BaseHandler):
			def __init__(self, allowed_labels):
				super(Handler, self).__init__(allowed_labels)
				self.resellers = set()

			def handle_reseller(self, reseller, line_number):
				self.resellers.add(reseller)

		handler = Handler(cls.allowed_labels)
		cls._read(fileobj, handler)

		return (handler.resellers, handler.errors)

	@classmethod
	def read_plans(cls, fileobj):
		class Handler(BaseHandler):
			def __init__(self, allowed_labels):
				super(Handler, self).__init__(allowed_labels)
				self.current_reseller = None
				self.plans = defaultdict(set)

			def handle_reseller(self, reseller, line_number):
				self.current_reseller = reseller

			def handle_plan(self, plan, line_number):
				self.plans[self.current_reseller].add(plan)

			def handle_addon_plan(self, addon_plan, line_number):
				# put addon plans in the same list as regular plans
				self.plans[self.current_reseller].add(addon_plan) 

		handler = Handler(cls.allowed_labels)
		cls._read(fileobj, handler)

		return (dict(handler.plans), handler.errors)

	@staticmethod
	def _read(fileobj, handler):
		for line_number, line in enumerate(fileobj, 1):
			line = line.strip('\r\n')
	
			line_matched = False
			if re.match("^\s*(#.*)?$", line, re.IGNORECASE) is not None:
				line_matched = True
			else:
				match = re.match("^\s*([^#]*):\s*([^#]*)\s*(#.*)?$", line, re.IGNORECASE)
				if match is not None:
					label = match.group(1).strip().lower()
					value = match.group(2).strip()
					line_matched = handler.handle_label(label, value, line_number)

				else:
					match = re.match("^\s*([^#\s]*)\s*(#.*)?$", line, re.IGNORECASE)
					if match is not None:
						handler.handle_subscription(match.group(1).strip(), line_number)
						line_matched = True
	
			if not line_matched:
				handler.handle_syntax_error(line, line_number)
	
	@classmethod
	def write_initial(
			cls, filename, plesk_backup_iter, ppa_webspace_names,
			ppa_service_templates, target_panel, include_addon_plans=False,
			target_addon_plans={}):
		initial_mapping_text = cls._generate_initial_content(
				plesk_backup_iter, ppa_webspace_names, ppa_service_templates,
				target_panel, include_addon_plans=include_addon_plans,
				target_addon_plans=target_addon_plans
		)
		with codecs.open(filename, "w", "utf-8") as f:
			f.write(initial_mapping_text)
	
	@classmethod
	def write_selected_subscriptions(
			cls, filename, plesk_backup_iter, ppa_webspace_names,
			ppa_service_templates, target_panel, subscriptions_mapping,
			subscription_filter, pre_subscription_comments):
		initial_mapping_text = cls._generate_initial_content(
				plesk_backup_iter, ppa_webspace_names, ppa_service_templates,
				target_panel, subscriptions_mapping, subscription_filter,
				pre_subscription_comments
		)
		with codecs.open(filename, "w", "utf-8") as f:
			f.write(initial_mapping_text)

	@classmethod
	def generate_migration_list(
			cls, plesk_backup_iter, ppa_webspace_names, ppa_service_templates,
			target_panel):
		"""Read source dump and generate a list of objects to be migrated."""

		initial_mapping_text = cls._generate_initial_content(
				plesk_backup_iter, ppa_webspace_names, ppa_service_templates,
				target_panel)
		return cls.read(StringIO(initial_mapping_text), plesk_backup_iter)
	
	header = textwrap.dedent("""\
		# This file specifies a list of domains and clients to be migrated.
		# For each domain, you should specify a service plan, that is to be
		# used when creating subscription for this domain. You should also
		# specify a client, that owns the domain. Domains that are not assigned
		# to a customer, will be automatically assigned to a reseller or admin
		# account, depending on what you've specified earlier in this file.
		#
		# For each client, you should specify a reseller, that will be a vendor
		# of the corresponding customer. If you do not specify a reseller,
		# corresponding customer will be created under admin. For the examples
		# of how to specify that, consult migrator documentation.""")

	# Resellers may have zero or more clients.
	# Client may have zero or more plans used by his domains.
	# Each plan may have zero or more domains subscribed to it.
	# Clients may have domains that do not belong to a plan.
	# Resellers may have plans not used by any client

	SubscriptionSourceInfo = namedtuple(
		'SubscriptionSourceInfo', (
			'name', 'plan', 'addon_plans', 'reseller', 'customer'))
	
	@classmethod
	def _generate_initial_content(
		cls, plesk_backup_iter, ppa_webspace_names, ppa_service_templates,
		target_panel, subscriptions_mapping=None, subscription_filter=None,
		pre_subscription_comments=None, include_addon_plans=False,
		target_addon_plans={}):
		"""
		Parameters:
		- include_addon_plans if False, do not add addon plans to migration list. It can be useful
			for target panels that don't have addon plans (addon service templates), for example PPA
		"""
		# list of SubscriptionSourceInfo
		subscriptions = cls._extract_subscriptions(
			plesk_backup_iter, subscriptions_mapping, subscription_filter,
			include_addon_plans, target_panel) 
		# all plans, including ones without subscriptions on them, dictionary {reseller: [plan]}
		plans = cls._get_plans(
			plesk_backup_iter, ppa_service_templates, addon_plans=False
		)
		# all addon plans, including ones without subscriptions on them, dictionary {reseller: [plan]}
		if include_addon_plans:
			addon_plans = cls._get_plans(
				plesk_backup_iter, target_addon_plans, addon_plans=True
			)
		else:
			addon_plans = {} 
		# dictionary {login: contact}
		reseller_contacts = cls._get_reseller_contacts(plesk_backup_iter) 
		# dictionary {login: contact}
		customer_contacts = cls._get_customer_contacts(plesk_backup_iter)

		lines = []
	
		def get_subscriptions_lines(login, subscriptions):
			lines = []
			for subscription in subscriptions:
				if subscription.customer != login:
					continue
				if pre_subscription_comments is not None and subscription.name in pre_subscription_comments:
					for comment in pre_subscription_comments[subscription.name]:
						lines.append(u'# %s' % (comment))
				subscription_comments = []
				if subscription.name in ppa_webspace_names:
					subscription_comments.append(u'already exists in the target panel')
				comment = u"# " if subscription.name in ppa_webspace_names else u""
				if len(subscription_comments) > 0:
					lines.append(u"            %s%s# %s" % (comment, subscription.name, u", ".join(subscription_comments)))
				else:
					lines.append(u"            %s" % subscription.name)
			return lines

		def format_customer_section(login, reseller_login=None):
			contact = customer_contacts.get(login)
			if contact is not None and contact != '':
				return u"    Customer: %s # %s" % (login, contact)
			elif contact is None:
				return u"    Customer: %s # automatically generated special account which will receive %s's subscriptions" % (login, reseller_login or 'admin')
			else:
				return u"    Customer: %s" % login

		def format_plan_section(plan_name):
			return u"        Plan: %s" % (plan_name,)

		def format_addon_plan_section(addon_plan_name):
			return u"        Addon Plan: %s" % (addon_plan_name,)
	
		def get_client_block(client_name, reseller, subscriptions):
			if client_name is not None:
				lines = [ format_customer_section(client_name, reseller) ]
			else:
				lines = []

			subscriptions_by_plan = group_by(subscriptions, lambda s: (s.plan, tuple(s.addon_plans)))
			lines += get_plan_block(client_name, None, (), subscriptions_by_plan.get((None, ()), []))
			for (plan, addons), subscriptions in subscriptions_by_plan.iteritems():
				if plan is not None:
					lines += get_plan_block(client_name, plan, addons, subscriptions)
			return lines
	
		def get_plan_block(customer, plan_name, addon_plan_names, subscriptions):
			if plan_name is not None:
				lines = [ format_plan_section(plan_name) ]
				for addon_plan_name in addon_plan_names:
					lines.append(format_addon_plan_section(addon_plan_name))
			elif plan_name is None and len(subscriptions) > 0:
				if target_panel == TargetPanels.PPA: 
					lines = [
						u"        # Custom subscriptions, must be associated with a"
						u" service template to be transferred" 
					]
				else:
					# Target Plesk supports custom subscriptions -
					# subscriptions not associated with any service plan
					lines = []
			else:
				lines = []

			for s in subscriptions:
				if s.plan == plan_name and tuple(s.addon_plans) == addon_plan_names:
					lines += get_subscriptions_lines(customer, (s,))
			return lines
	
		def format_reseller_section(login):
			contact = reseller_contacts.get(login) or ''
			if contact != '':
				return u"Reseller: %s # %s" % (login, contact)
			else:
				return u"Reseller: %s" % (login,)
	
		def get_reseller_block(login, subscriptions):
			if login is not None:
				lines = [ format_reseller_section(login) ]
			elif len(subscriptions) > 0:
				lines = [ u"# Admin subscriptions" ]
			else:
				lines = []

			subscriptions_by_client = sorted_dict(group_by(subscriptions, lambda s: s.customer))
			lines += get_client_block(None, login, subscriptions_by_client.get(None, []))
			used_plans = []
			for client, subscriptions in subscriptions_by_client.iteritems():
				used_plans.extend([sub.plan for sub in subscriptions])
				if client is not None:
					lines += get_client_block(client, login, subscriptions)

			subscriptions_by_plans = sorted_dict(group_by(subscriptions, lambda s: (s.plan, tuple(s.addon_plans))))
			used_addon_plans = set([
				addon_plan_name
				for (plan_name, addon_plan_names) in subscriptions_by_plans.keys()
				for addon_plan_name in addon_plan_names
			])
			if subscription_filter is None:
				unused_plans = []
				# iterate over plans with no subscriptions assigned, but still exist on target or source servers
				for plan_name in sorted(set(plans[login]) - set(used_plans)):
					unused_plans.append(format_plan_section(plan_name))
				# iterate over addon plans with no subscriptions assigned, but still exist on target or source servers
				for plan_name in sorted(set(addon_plans.get(login, [])) - used_addon_plans):
					unused_plans.append(format_addon_plan_section(plan_name))
				if unused_plans:
					lines.append("    # Existing hosting plans not used by any customer")
					lines += unused_plans

			return lines
	
		lines.append(cls.header)
		lines.append(u"")
	
		subscriptions_by_reseller = group_by(subscriptions, lambda s: s.reseller)
		lines += get_reseller_block(None, subscriptions_by_reseller.get(None, []))
		for reseller, subscriptions in subscriptions_by_reseller.iteritems():
			if reseller is not None:
				lines += get_reseller_block(reseller, subscriptions)
	
		return os.linesep.join(lines) + os.linesep
	
	@classmethod
	def _extract_subscriptions(
			cls, plesk_backup_iter, subscriptions_mapping=None,
			subscription_filter=None, include_addon_plans=False,
			target_panel=None):
		"""
		Returns list of SubscriptionSourceInfo
		"""
		processed_subscriptions = set()
		subscriptions = []
	
		def add_subscription(reseller, client, plans, addon_plans, subscription):
			if subscription.name not in processed_subscriptions and (subscription_filter is None or subscription.name in subscription_filter):
				if subscription_filter is None:
					plan = if_not_none(subscription.plan_id, lambda id: plans[id])
					if include_addon_plans:
						addon_plans = set([addon_plans[id] for id in subscription.addon_plan_ids])
					else:
						addon_plans = set()
				else:
					if subscriptions_mapping is not None:
						plan = subscriptions_mapping[subscription.name].plan	# subscription is mapped to SubscriptionMappingInfo(plan, owner), and we want to display only the plan's name
						if include_addon_plans:
							addon_plans = subscriptions_mapping[subscription.name].addon_plans
						else:
							addon_plans = set()
					else:
						plan = if_not_none(subscription.plan_id, lambda id: plans[id])
						if include_addon_plans:
							addon_plans = set([addon_plans[id] for id in subscription.addon_plan_ids])
						else:
							addon_plans = set()
				subscriptions.append(cls.SubscriptionSourceInfo(
					name=subscription.name, plan=plan, addon_plans=addon_plans, 
					reseller=reseller, customer=client
				))
				processed_subscriptions.add(subscription.name)
	
		for _, backup in plesk_backup_iter():
			admin_plans = dict((plan.id, plan.name) for plan in backup.get_plans())
			if include_addon_plans:
				admin_addon_plans = {plan.id: plan.name for plan in backup.get_addon_plans()}
			else:
				admin_addon_plans = {}
			for subscription in backup.iter_admin_subscriptions():
				if target_panel == TargetPanels.PPA: 
					admin_name = 'ppa-admin'
				else:
					admin_name = None
				add_subscription(None, admin_name, admin_plans, admin_addon_plans, subscription) 
	
			for client in backup.iter_clients():
				for subscription in client.subscriptions:
					add_subscription(None, client.login, admin_plans, admin_addon_plans, subscription)
	
			for reseller in backup.iter_resellers():
				reseller_plans = dict((plan.id, plan.name) for plan in backup.iter_reseller_plans(reseller.login))
				if include_addon_plans:
					reseller_addon_plans = {plan.id: plan.name for plan in backup.iter_reseller_addon_plans(reseller.login)}
				else:
					reseller_addon_plans = {}
	
				if target_panel == TargetPanels.PPA: 
					reseller_name = u"ppa-%s" % reseller.login
				else:
					reseller_name = None
				for subscription in reseller.subscriptions:
					add_subscription(reseller.login, reseller_name, reseller_plans, reseller_addon_plans, subscription)
	
				for client in reseller.clients:
					for subscription in client.subscriptions:
						add_subscription(reseller.login, client.login, reseller_plans, reseller_addon_plans, subscription)
	
		return subscriptions
	
	@classmethod
	def _get_plans(cls, plesk_backup_iter, ppa_service_templates, addon_plans=False):
		"""
		Parameters:
		- addon_plans: if False, get regular (non-addon) plans, if True, get addon plans
		Return the following structure:
		plans[reseller_name][plan_name] = bool: whether this plan exists in PPA
		  reseller_name is either a reseller login or None if reseller is admin
		"""
		plans = defaultdict(dict)
		for _, backup in plesk_backup_iter():
			if addon_plans:
				admin_plans = backup.get_addon_plans()
			else:
				admin_plans = backup.get_plans()

			for plan in admin_plans:
				plans[None][plan.name] = False
			for reseller in backup.iter_resellers():
				if addon_plans:
					reseller_plans = backup.iter_reseller_addon_plans(reseller.login)
				else:
					reseller_plans = backup.iter_reseller_plans(reseller.login)
				for plan in reseller_plans:
					plans[reseller.login][plan.name] = False

		for reseller_login, template_names in ppa_service_templates.iteritems():
			for template_name in template_names:
				plans[reseller_login][template_name] = True

		return plans

	@classmethod
	def _get_reseller_contacts(cls, plesk_backup_iter):
		reseller_contacts = {}
		for _, backup in plesk_backup_iter():
			for reseller in backup.iter_resellers():
				if reseller.login not in reseller_contacts:
					reseller_contacts[reseller.login] = reseller.contact
		return reseller_contacts
	
	@classmethod
	def _get_customer_contacts(cls, plesk_backup_iter):
		customer_contacts = {}
		for _, backup in plesk_backup_iter():
			for customer in backup.iter_all_clients():
				if customer.login not in customer_contacts:
					customer_contacts[customer.login] = customer.contact
		return customer_contacts
	
	@classmethod
	def _get_source_subscriptions(cls, plesk_backup_iter):
		return [
				subscription.name
				for _, backup in plesk_backup_iter()
				for subscription in backup.iter_all_subscriptions()
		       ]

MigrationListData = namedtuple('MigrationListData', (
	'subscriptions_mapping', # dict with keys - subscription names, values - plan names a subscription is assigned to
	'resellers', # set consisting of names of resellers that should be migrated
	'customers_mapping', # dict with keys - names of customers that should be migrated, values - resellers under which to create the customer in PPA
	'plans' # dict with keys - reseller names (None for admin), values - sets consisting of names of plans that should be migrated
))
