from parallels.core import messages
import re
import codecs
from collections import defaultdict, namedtuple
from StringIO import StringIO
import os

from parallels.core.registry import Registry
from parallels.core.utils.common import if_not_none, group_by, sorted_dict, format_list, group_by_id, default
from parallels.core.utils.common.ip import is_ipv6, is_ipv4
from parallels.core.utils.entity import Entity

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 on target panel
	'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(Entity):
	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 ipv4(self):
		return self._ips.v4

	@property
	def ipv6(self):
		return self._ips.v6

	@property
	def ips(self):
		return self._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, dump_iterator,
		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
		- dump_iterator - 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(dump_iterator)
		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, dump_iterator, webspace_names,
		service_templates,
		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(
			dump_iterator, webspace_names, service_templates,
			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)

	@staticmethod
	def write_content(filename, content):
		with codecs.open(filename, "w", "utf-8") as f:
			f.write(content)
	
	@classmethod
	def write_selected_subscriptions(
		cls, filename, dump_iterator, webspace_names,
		service_templates, subscriptions_mapping,
		subscription_filter, pre_subscription_comments
	):
		initial_mapping_text = cls._generate_initial_content(
			dump_iterator, webspace_names, service_templates,
			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, dump_iterator, webspace_names, service_templates,
	):
		"""Read source dump and generate a list of objects to be migrated."""

		initial_mapping_text = cls._generate_initial_content(
			dump_iterator, webspace_names, service_templates,
		)
		return cls.read(StringIO(initial_mapping_text), dump_iterator)
	
	header = 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, dump_iterator, webspace_names, service_templates,
		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).
		"""
		if target_addon_plans is None:
			target_addon_plans = {}

		# list of SubscriptionSourceInfo
		subscriptions = cls._extract_subscriptions(
			dump_iterator, subscriptions_mapping, subscription_filter,
			include_addon_plans
		)
		# all plans, including ones without subscriptions on them, dictionary {reseller: [plan]}
		plans = cls._get_plans(
			dump_iterator, 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(
				dump_iterator, target_addon_plans,
				addon_plans=True, include_source_plans=include_source_plans
			)
		else:
			addon_plans = {} 
		# dictionary {login: contact}
		reseller_contacts = cls._get_reseller_contacts(dump_iterator)
		# dictionary {login: contact}
		customer_contacts = cls._get_customer_contacts(dump_iterator)
		reseller_plans = cls._get_reseller_plans(dump_iterator)

		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 webspace_names:
						subscription_comments.append(messages.ALREADY_EXISTS_IN_TARGET_PANEL)
				comment = u"# " if subscription.name in 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 u"    Customer: %s # %s" % (
					login, messages.AUTO_GENERATED_CUSTOMER_COMMENT % default(reseller_login, '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 not Registry.get_instance().get_context().target_panel_obj.has_custom_subscriptions_feature():
					plan_block_lines = [u"        %s" % 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 Registry.get_instance().get_context().target_panel_obj.has_admin_reseller_plan_feature():
				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(u"    # %s" % 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, dump_iterator, subscriptions_mapping=None,
		subscription_filter=None, include_addon_plans=False,
	):
		"""
		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 dump_iterator():
			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():
				admin_name = Registry.get_instance().get_context().target_panel_obj.get_default_admin_name()
				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 = {}

				reseller_name = Registry.get_instance().get_context().target_panel_obj.get_default_reseller_name(
					reseller
				)
				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, dump_iterator, 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 on target panel
		reseller_name is either a reseller login or None if reseller is admin
		"""
		plans = defaultdict(dict)
		if include_source_plans:
			for _, backup in dump_iterator():
				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(dump_iterator):
		"""Get dictionary with keys - reseller logins, values - plans they are assigned to

		:rtype: dict[basestring, basestring]
		"""
		reseller_plans = dict()
		for _, backup in dump_iterator():
			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, dump_iterator):
		reseller_contacts = {}
		for _, backup in dump_iterator():
			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, dump_iterator):
		customer_contacts = {}
		for _, backup in dump_iterator():
			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, dump_iterator):
		mylist = []
		for _, backup in dump_iterator():
			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:
			# Allow setting one plan for all customers to simplify
			# composing migration list for panels, 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(messages.CUSTOMER_ASSIGNED_TO_ANOTHER_RESELLER % (
				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(messages.SUBSCRIPTION_MUST_BE_ASSIGNED_TO_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,
				# create copy of the list so it is not modified for that subscription on the next unrelated addon plans
				addon_plans=list(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(messages.SUBSCRIPTION_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)
