from parallels.core import messages
import logging
from collections import defaultdict
from contextlib import closing
from itertools import chain
import itertools

from parallels.core import MigrationError
from parallels.core import target_data_model
from parallels.core.utils.common import default
from parallels.core.utils.entity import Entity

logger = logging.getLogger(__name__)


class ConvertedPlanInfo(Entity):
	def __init__(self, plan, issues=None):
		self._plan = plan
		self._issues = default(issues, [])

	@property
	def plan(self):
		"""
		:rtype: parallels.core.target_data_model.Plan
		"""
		return self._plan

	@property
	def issues(self):
		"""
		:rtype: list[parallels.core.checking.Issue]
		"""
		return self._issues


class PlansConverter(object):
	"""Generate list of plans ready to import to target panel.
	
	Plans converter takes the following information:
	- Plesk backup or other source of information 
	about resellers and plans on source servers
	- Import API which provides information about 
	resellers and plans that already exist on target panel
	- Migration list, which contains list of plans that 
	will be used during migration

	Plans converter:
	- Converts each plan we need to migrate to 
	format ready to import to target panel.
	- Performs conflict resolution, if plan exists on multiple
	source panel, or already exists on target panel.

	Result is a list of plans ready to import to target panel.
	"""
	def convert_plans(self, source_panels, import_api, plan_migration_list, backup_plan_converter):
		"""Convert plans from source and target panel information to target model

		plan_migration_list is a dictionary with key - reseller login, value - list of service templates to create
			for example: {None: ['Plan 1', 'Plan 2'], 'res1': ['Plan 3']} means that we need to have
			'Plan 1' and 'Plan 2' plans for administrator and 'Plan 3' plan for reseller 'res1'

		:type source_panels: list[parallels.core.global_context.SourceInfo]
		:type import_api: parallels.core.import_api.import_api.ImportAPI
		:type plan_migration_list: parallels.core.migration_list.MigrationListPlans
		:type backup_plan_converter: BackupToTargetModelPlanConverter

		:rtype: dict[(basestring|None, basestring, bool), ConvertedPlanInfo]
		"""
		logger.info(messages.LOG_CONVERT_PLANS)

		all_plans = defaultdict(list)
		self._convert_target_admin_plans(all_plans, import_api)
		self._convert_target_admin_addon_plans(all_plans, import_api)
		self._convert_target_reseller_all_plans(all_plans, import_api, plan_migration_list)
		self._convert_source_panel_plans(all_plans, backup_plan_converter, source_panels)

		merged_plans = dict()
		self._merge_plans(all_plans, merged_plans, plan_migration_list)
		self._merge_addon_plans(all_plans, merged_plans, plan_migration_list)

		return merged_plans

	@staticmethod
	def _convert_target_admin_plans(all_plans, import_api):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type import_api: parallels.core.import_api.import_api.ImportAPI
		"""
		target_admin_plans = import_api.get_service_template_list(None)
		for plan in target_admin_plans:
			all_plans[(None, plan.name, False)].append(ConvertedPlanInfo(
				plan=target_data_model.Plan(name=plan.name, source='target', is_addon=False, settings=None)
			))

	@staticmethod
	def _convert_target_admin_addon_plans(all_plans, import_api):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type import_api: parallels.core.import_api.import_api.ImportAPI
		"""
		target_admin_addon_plans = import_api.get_addon_service_template_list(None)
		for plan in target_admin_addon_plans:
			all_plans[(None, plan.name, True)].append(ConvertedPlanInfo(
				plan=target_data_model.Plan(name=plan.name, source='target', is_addon=True, settings=None)
			))

	@staticmethod
	def _convert_target_reseller_all_plans(all_plans, import_api, plan_migration_list):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type import_api: parallels.core.import_api.import_api.ImportAPI
		:type plan_migration_list: parallels.core.migration_list.MigrationListPlans
		"""
		reseller_logins = [
			reseller
			for reseller in itertools.chain(
				plan_migration_list.plans.iterkeys(),
				plan_migration_list.addon_plans.iterkeys()
			)
			if reseller is not None
		]
		target_resellers = import_api.list_resellers(reseller_logins)
		for reseller in target_resellers:
			target_reseller_plans = import_api.get_service_template_list(reseller.id)
			for plan in target_reseller_plans:
				all_plans[(reseller.contact.username, plan.name, False)].append(ConvertedPlanInfo(
					plan=target_data_model.Plan(name=plan.name, source='target', is_addon=False, settings=None)
				))
			target_reseller_addon_plans = import_api.get_addon_service_template_list(reseller.id)
			for plan in target_reseller_addon_plans:
				all_plans[(reseller.contact.username, plan.name, True)].append(ConvertedPlanInfo(
					plan=target_data_model.Plan(name=plan.name, source='target', is_addon=True, settings=None)
				))

	@staticmethod
	def _convert_source_panel_plans(all_plans, backup_plan_converter, source_panels):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type backup_plan_converter: BackupToTargetModelPlanConverter
		:type source_panels: list[parallels.core.global_context.SourceInfo]
		"""
		for source_panel in source_panels:
			with closing(source_panel.load_raw_dump()) as backup:
				backup_all_admin_plans = chain(
					backup.get_plans(),
					backup.get_addon_plans()
				)
				for plan in backup_all_admin_plans:
					settings, issues = backup_plan_converter.convert(plan)
					all_plans[(None, plan.name, plan.is_addon)].append(ConvertedPlanInfo(
						plan=target_data_model.Plan(
							name=plan.name,
							source=source_panel.id,
							is_addon=plan.is_addon,
							settings=settings
						),
						issues=issues
					))
				for reseller in backup.iter_resellers():
					backup_all_reseller_plans = chain(
						backup.iter_reseller_plans(reseller.login),
						backup.iter_reseller_addon_plans(reseller.login)
					)
					for plan in backup_all_reseller_plans:
						settings, issues = backup_plan_converter.convert(plan)
						all_plans[(reseller.login, plan.name, plan.is_addon)].append(ConvertedPlanInfo(
							plan=target_data_model.Plan(
								name=plan.name,
								source=source_panel.id,
								is_addon=plan.is_addon,
								settings=settings
							),
							issues=issues
						))

	@staticmethod
	def _merge_addon_plans(all_plans, merged_plans, plan_migration_list):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type merged_plans: dict[(basestring | None, basestring, bool), ConvertedPlanInfo]
		:type plan_migration_list: parallels.core.migration_list.MigrationListPlans
		"""
		for reseller, plans in plan_migration_list.addon_plans.iteritems():
			for plan in plans:
				converted_plans = all_plans.get((reseller, plan, True), [])
				if len(converted_plans) == 0:
					if reseller is None:
						raise MigrationError(messages.ADMIN_ADDON_PLAN_S_SPECIFIED_IN_MIGRATION.format(
							plan_name=plan
						))
					else:
						raise MigrationError(messages.ADDON_PLAN_S_SPECIFIED_IN_MIGRATION.format(
							reseller_name=reseller,
							plan_name=plan
						))
				else:
					first_converted_plan_info = converted_plans[0]
					if len(converted_plans) > 1:
						logger.debug(
							messages.ADDON_PLAN_S_EXISTS_MULTIPLE_PANELS,
							plan, first_converted_plan_info.plan.source
						)
					merged_plans[(reseller, plan, True)] = first_converted_plan_info

	@staticmethod
	def _merge_plans(all_plans, merged_plans, plan_migration_list):
		"""
		:type all_plans: dict[(basestring | None, basestring, bool), list[ConvertedPlanInfo]]
		:type merged_plans: dict[(basestring | None, basestring, bool), ConvertedPlanInfo]
		:type plan_migration_list: parallels.core.migration_list.MigrationListPlans
		"""
		for reseller, plans in plan_migration_list.plans.iteritems():
			for plan in plans:
				converted_plans = all_plans.get((reseller, plan, False), [])
				if len(converted_plans) == 0:
					if reseller is None:
						raise MigrationError(messages.ADMIN_PLAN_S_SPECIFIED_IN_MIGRATION_LIST.format(
							plan_name=plan
						))
					else:
						raise MigrationError(messages.PLAN_S_SPECIFIED_IN_MIGRATION_LIST.format(
							reseller_name=reseller,
							plan_name=plan
						))
				else:
					first_converted_plan_info = converted_plans[0]
					if len(converted_plans) > 1:
						logger.debug(
							messages.PLAN_S_EXISTS_MULTIPLE_PANELS_PLAN,
							plan, first_converted_plan_info.plan.source
						)
					merged_plans[(reseller, plan, False)] = first_converted_plan_info


class BackupToTargetModelPlanConverter(object):
	def convert(self, backup_plan):
		"""Convert plan settings from Plesk backup format to format ready to import with target panel API

		:type backup_plan: parallels.core.dump.data_model.HostingPlan
		"""
		raise NotImplementedError()
