from parallels.common import messages
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, format_list, group_by_id
from parallels.common.target_panels import TargetPanels
from parallels.utils.ip import is_ipv6, is_ipv4

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


class SubscriptionIPMapping(object):
	AUTO = 'auto'  # map to the same IP type as on source panel
	SHARED = 'shared'  # map to default shared IP address
	DEDICATED = 'dedicated'  # map to free dedicated IP address
	NONE = 'none'  # do not use IP of that type

	def __init__(self, v4=None, v6=None):
		if v4 is not None:
			self._v4 = v4
		else:
			self._v4 = SubscriptionIPMapping.AUTO
		if v6 is not None:
			self._v6 = v6
		else:
			self._v6 = SubscriptionIPMapping.AUTO

	@property
	def v4(self):
		return self._v4

	@property
	def v6(self):
		return self._v6

	@classmethod
	def possible_ip_mapping_values(cls):
		return {cls.AUTO, cls.SHARED, cls.DEDICATED, cls.NONE}

	def __eq__(self, other):
		return self.v4 == other.v4 and self.v6 == other.v6


class SubscriptionMappingInfo(object):
	def __init__(self, plan=None, addon_plans=None, owner=None, ipv4=None, ipv6=None):
		self._plan = plan
		self._addon_plans = addon_plans
		self._owner = owner
		self._ips = SubscriptionIPMapping(ipv4, ipv6)

	@property
	def plan(self):
		return self._plan

	@property
	def addon_plans(self):
		return self._addon_plans

	@property
	def owner(self):
		return self._owner

	@property
	def ips(self):
		return self._ips

	def __eq__(self, other):
		return (
			self.plan == other.plan and
			self.addon_plans == other.addon_plans and
			self.owner == other.owner and
			self.ips == other.ips
		)


class MigrationListPlans(object):
	def __init__(self, plans=None, addon_plans=None):
		"""
		:param dict[basestring | None, list[basestring]] plans: Dictionary of hosting plan names
		:param dict[basestring | None, list[basestring]] addon_plans: Dictionary of addon hosting plan names
		"""
		if plans is not None:
			self._plans = plans
		else:
			self._plans = {}

		if addon_plans is not None:
			self._addon_plans = addon_plans
		else:
			self._addon_plans = {}

	@property
	def plans(self):
		"""Hosting plans - dictionary with reseller name as key (None for admin), list of plan names as value

		:rtype: dict[basestring | None, list[basestring]]
		"""
		return self._plans

	@property
	def addon_plans(self):
		"""Addon hosting plans - dictionary with reseller name as key (None for admin), list of plan names as value

		:rtype: dict[basestring | None, list[basestring]]
		"""
		return self._addon_plans

	def __eq__(self, other):
		return (
			isinstance(other, MigrationListPlans) and
			self.plans == other.plans and
			self.addon_plans == other.addon_plans
		)


class MigrationList(object):
	"""Class for reading and writing migration list files"""

	allowed_labels = ['plan', 'addon plan', 'customer', 'reseller', 'billing plan id', 'ipv4', 'ipv6']

	@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)
		handler = FullMigrationListHandler(
			cls.allowed_labels, source_subscriptions,
			has_custom_subscriptions_feature,
			has_admin_subscriptions_feature,
			has_reseller_subscriptions_feature
		)
		cls._read(fileobj, handler)
		return handler.data, handler.errors

	@classmethod
	def read_resellers(cls, fileobj):
		"""Read resellers from migration list file

		Returns dictionary {reseller login: reseller plan}, if plan is not set and reseller
		subscription is custome, then reseller plan is None.

		:rtype: dict[basestring, basestring]
		"""
		handler = ResellersMigrationListHandler(cls.allowed_labels)
		cls._read(fileobj, handler)

		return handler.resellers, handler.errors

	@classmethod
	def read_plans(cls, fileobj):
		"""Read plans (service templates) from migration list file"""
		handler = PlansMigrationListHandler(cls.allowed_labels)
		cls._read(fileobj, handler)

		return MigrationListPlans(handler.plans, handler.addon_plans), handler.errors

	@staticmethod
	def _read(fileobj, handler):
		"""
		:type handler: BaseMigrationListLineHandler
		:rtype: None
		"""
		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=None,
		include_source_plans=True
	):
		if target_addon_plans is None:
			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,
			include_source_plans=include_source_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, comment_existing_subscriptions=False
		)
		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(messages.FILE_SPECIFIES_LIST_DOMAINS_AND_CLIENTS)

	# 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=None,
		comment_existing_subscriptions=True,
		include_source_plans=True
	):
		"""
		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
		"""
		if target_addon_plans is None:
			target_addon_plans = {}

		# 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, include_source_plans=include_source_plans
		)
		# 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, include_source_plans=include_source_plans
			)
		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)
		reseller_plans = cls._get_reseller_plans(plesk_backup_iter)

		lines = []

		def get_subscriptions_lines(login, subscriptions):
			subscription_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]:
						subscription_lines.append(u'# %s' % comment)
				subscription_comments = []
				if comment_existing_subscriptions:
					if subscription.name in ppa_webspace_names:
						subscription_comments.append(messages.ALREADY_EXISTS_IN_TARGET_PANEL)
				comment = u"# " if subscription.name in ppa_webspace_names else u""
				if len(subscription_comments) > 0:
					subscription_lines.append(u"            %s%s # %s" % (
						comment, subscription.name, u", ".join(subscription_comments))
					)
				else:
					subscription_lines.append(u"            %s" % subscription.name)
			return subscription_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 (
					messages.CUSTOMER_S_AUTOMATICALLY_GENERATED_SPECIAL_ACCOUNT % (
						login, reseller_login or 'admin'
					)
				)
			else:
				return u"    Customer: %s" % login

		def get_client_block(client_name, reseller, subscriptions):
			if client_name is not None:
				client_block_lines = [format_customer_section(client_name, reseller)]
			else:
				client_block_lines = []

			subscriptions_by_plan = group_by(subscriptions, lambda s: (s.plan, tuple(s.addon_plans)))
			client_block_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:
					client_block_lines += get_plan_block(client_name, plan, addons, subscriptions)
			return client_block_lines

		def get_plan_block(customer, plan_name, addon_plan_names, subscriptions):
			if plan_name is not None and include_source_plans:
				plan_block_lines = [cls._format_plan_section(plan_name)]
				for addon_plan_name in addon_plan_names:
					plan_block_lines.append(cls._format_addon_plan_section(addon_plan_name))
			elif plan_name is None and len(subscriptions) > 0 and include_source_plans:
				if target_panel == TargetPanels.PPA:
					plan_block_lines = [
						messages.CUSTOM_SUBSCRIPTIONS_MUST_ASSOCIATED_SERVICE_TEMPLATE]
				else:
					# Target Plesk supports custom subscriptions -
					# subscriptions not associated with any service plan
					plan_block_lines = []
			else:
				plan_block_lines = []

			for s in subscriptions:
				if s.plan == plan_name and tuple(s.addon_plans) == addon_plan_names:
					plan_block_lines += get_subscriptions_lines(customer, (s,))
			return plan_block_lines

		def get_reseller_block(login, reseller_subscriptions):
			reseller_block_lines = []

			if target_panel == TargetPanels.PLESK:
				plan = reseller_plans.get(login)
				if plan is not None:
					reseller_block_lines.append(cls._format_reseller_plan_section(plan))

			if login is not None:
				reseller_block_lines.append(cls._format_reseller_section(login, reseller_contacts))
			elif len(reseller_subscriptions) > 0:
				reseller_block_lines.append(messages.ADMIN_SUBSCRIPTIONS_AND_CUSTOMERS)

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

			subscriptions_by_plans = sorted_dict(
				group_by(reseller_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(cls._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(cls._format_addon_plan_section(plan_name))
				if unused_plans:
					if include_source_plans:
						reseller_block_lines.append(messages.EXISTING_HOSTING_PLANS_NOT_USED_BY)
					reseller_block_lines += unused_plans

			return reseller_block_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

	@staticmethod
	def _format_reseller_section(login, reseller_contacts):
		contact = reseller_contacts.get(login) or ''
		if contact != '':
			return u"Reseller: %s # %s" % (login, contact)
		else:
			return u"Reseller: %s" % (login,)

	@staticmethod
	def _format_plan_section(plan_name):
		return u"        Plan: %s" % (plan_name,)

	@staticmethod
	def _format_addon_plan_section(addon_plan_name):
		return u"        Addon Plan: %s" % (addon_plan_name,)

	@staticmethod
	def _format_reseller_plan_section(reseller_plan_name):
		return "Reseller Plan: %s" % reseller_plan_name

	@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 subs_plan_id: plans.get(subs_plan_id))
					if include_addon_plans:
						addon_plans = set([addon_plans[plan_id] for plan_id in subscription.addon_plan_ids])
					else:
						addon_plans = set()
				else:
					if subscriptions_mapping is not None:
						# subscription is mapped to SubscriptionMappingInfo(plan, owner),
						# and we want to display only the plan's name
						plan = subscriptions_mapping[subscription.name].plan
						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 subs_plan_id: plans[subs_plan_id])
						if include_addon_plans:
							addon_plans = set([addon_plans[plan_id] for plan_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, target_service_templates, addon_plans=False, include_source_plans=True):
		"""
		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)
		if include_source_plans:
			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 target_service_templates.iteritems():
			for template_name in template_names:
				plans[reseller_login][template_name] = True

		return plans

	@staticmethod
	def _get_reseller_plans(plesk_backup_iter):
		"""Get dictionary with keys - reseller logins, values - plans they are assigned to

		:rtype: dict[basestring, basestring]
		"""
		reseller_plans = dict()
		for _, backup in plesk_backup_iter():
			all_reseller_plans = group_by_id(
				backup.iter_admin_reseller_plans(),
				lambda p: p.id
			)

			for reseller in backup.iter_resellers():
				if reseller.login not in reseller_plans:
					reseller_plans[reseller.login] = if_not_none(
						all_reseller_plans.get(reseller.plan_id),
						lambda plan: plan.name
					)

		return reseller_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):
		mylist = []
		for _, backup in plesk_backup_iter():
			for subscription in backup.iter_all_subscriptions():
				mylist.append(subscription.name)
		return mylist


class BaseMigrationListLineHandler(object):
	def __init__(self, allowed_labels):
		"""
		:type allowed_labels: list[basestring]
		"""
		self.errors = []
		self.allowed_labels = allowed_labels

	def handle_reseller(self, reseller, line_number):
		"""Handle reseller label

		:type reseller: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_reseller_plan(self, plan, line_number):
		"""Handle reseller plan label

		:type plan: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_plan(self, plan, line_number):
		"""Handle hosting plan label

		:type plan: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_addon_plan(self, addon_plan, line_number):
		"""Handle hosting addon plan label

		:type addon_plan: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_customer(self, customer, line_number):
		"""Handle customer label

		:type customer: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_ipv4(self, ipv4, line_number):
		"""Handle IPv4 address label

		:type ipv4: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_ipv6(self, ipv6, line_number):
		"""Handle IPv6 address label

		:type ipv6: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_label(self, label, value, line_number):
		"""Handle label ("Key: value") in migration list file

		Return if some label matched (True) or not (False).

		:type label: basestring
		:type value: basestring
		:type line_number: int
		:rtype: bool
		"""
		label_handlers = {
			'customer': self.handle_customer,
			'reseller': self.handle_reseller,
			'plan': self.handle_plan,
			'reseller plan': self.handle_reseller_plan,
			'addon plan': self.handle_addon_plan,
			'ipv4': self.handle_ipv4,
			'ipv6': self.handle_ipv6
		}
		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):
		"""Handle subscription line in migration list file

		:type subscription: basestring
		:type line_number: int
		:rtype: None
		"""
		pass

	def handle_syntax_error(self, line, line_number):
		"""Handle syntax error in migration list file - when line is not a subscription and no label matches

		:type line: basestring
		:type line_number: int
		:rtype: None
		"""
		self.errors.append(u"Line %s with content '%s' is not valid" % (line_number, line))


class FullMigrationListHandler(BaseMigrationListLineHandler):
	def __init__(
		self, allowed_labels, source_subscriptions,
		has_custom_subscriptions_feature=False,
		has_admin_subscriptions_feature=False,
		has_reseller_subscriptions_feature=False
	):
		super(FullMigrationListHandler, self).__init__(allowed_labels)
		self.data = MigrationListData(
			subscriptions_mapping={}, resellers={}, customers_mapping={}, plans={None: set()}
		)
		self.source_subscriptions = source_subscriptions
		self.has_custom_subscriptions_feature = has_custom_subscriptions_feature
		self.has_admin_subscriptions_feature = has_admin_subscriptions_feature
		self.has_reseller_subscriptions_feature = has_reseller_subscriptions_feature
		self._reseller_plan = None
		self.current_reseller = None
		self.current_plan = None
		self.current_addon_plans = []
		self.current_customer = None
		self.current_ipv4 = None
		self.current_ipv6 = None

	def handle_reseller(self, reseller, line_number):
		self.current_reseller = reseller
		self.data.resellers[self.current_reseller] = self._reseller_plan
		self.data.plans[self.current_reseller] = set()
		self.current_plan = None
		self.current_addon_plans = []
		self.current_customer = None
		self.current_ipv4 = None
		self.current_ipv6 = None

	def handle_customer(self, customer, line_number):
		self.current_customer = customer
		if self.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_reseller_plan(self, plan, line_number):
		self._reseller_plan = 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
				self.has_admin_subscriptions_feature
			),
			# subscription is assigned to reseller
			(
				self.current_reseller is not None
				and
				self.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,
				ipv4=self.current_ipv4,
				ipv6=self.current_ipv6,
			)

	def handle_ipv4(self, ipv4, line_number):
		if ipv4 in SubscriptionIPMapping.possible_ip_mapping_values() or is_ipv4(ipv4):
			self.current_ipv4 = ipv4
		else:
			self.errors.append(
				messages.LINE_S_S_IS_NOT_VALID % (
					line_number, ipv4, format_list(SubscriptionIPMapping.possible_ip_mapping_values())
				)
			)

	def handle_ipv6(self, ipv6, line_number):
		if ipv6 in SubscriptionIPMapping.possible_ip_mapping_values() or is_ipv6(ipv6):
			self.current_ipv6 = ipv6
		else:
			self.errors.append(
				messages.LINE_S_S_IS_NOT_VALID_1 % (
					line_number, ipv6, format_list(SubscriptionIPMapping.possible_ip_mapping_values())
				)
			)

	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 self.source_subscriptions:
			self.errors.append(messages.LINE_S_SUBSCRIPTION_S_IS_NOT % (
				line_number, subscription
			))
			return False
		elif self.current_plan is None and not self.has_custom_subscriptions_feature:
			self.errors.append(
				messages.LINE_S_SUBSCRIPTION_S_IS_NOT_1 % (line_number, subscription)
			)
			return False
		else:
			return True


class ResellersMigrationListHandler(BaseMigrationListLineHandler):
	def __init__(self, allowed_labels):
		super(ResellersMigrationListHandler, self).__init__(allowed_labels)
		self._resellers = dict()
		self._reseller_plan = None

	def handle_reseller(self, reseller, line_number):
		self._resellers[reseller] = self._reseller_plan

	def handle_reseller_plan(self, plan, line_number):
		self._reseller_plan = plan

	@property
	def resellers(self):
		"""Get list of resellers read from migration list.
		Returns dictionary {reseller login: reseller plan}, if plan is not set and reseller
		subscription is custom, then reseller plan is None.

		:rtype: dict[basestring, basestring]
		"""
		return self._resellers


class PlansMigrationListHandler(BaseMigrationListLineHandler):
	def __init__(self, allowed_labels):
		super(PlansMigrationListHandler, self).__init__(allowed_labels)
		self.current_reseller = None
		self.plans = defaultdict(set)
		self.addon_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):
		self.addon_plans[self.current_reseller].add(addon_plan)
