from contextlib import contextmanager
from collections import namedtuple, defaultdict
import logging

from parallels.common.logging_context import log_context
from parallels.common import run_command
from parallels.common.utils.steps_profiler import sleep
from parallels.common.checking import Problem

TargetModelStructure = namedtuple('TargetModelStructure', (
	'subscriptions', 'clients', 'resellers', 'plans', 'auxiliary_users', 'auxiliary_user_roles', 
	'general', # field for general errors, not related to any objects in target model
))

# 'is_critical' means, that migrator should stop migrating this object:
FailedObjectInfo = namedtuple(
	'FailedObjectInfo', (
		'error_message', 'solution', 'exception', 'is_critical', 'severity'))


class Safe:
	logger = logging.getLogger(__name__)

	def __init__(self, model):
		self.model = model
		self.failed_objects = self._create_target_model_structure()
		self.issues = self._create_target_model_structure()

	@staticmethod
	def _create_target_model_structure():
		return TargetModelStructure(
			subscriptions=defaultdict(list), clients=defaultdict(list), 
			resellers=defaultdict(list), plans=defaultdict(list),
			auxiliary_users=defaultdict(list),
			auxiliary_user_roles=defaultdict(list),
			general=[]
		)

	@contextmanager
	def try_subscription(self, name, error_message, solution=None, is_critical=True, use_log_context=True):
		try:
			if use_log_context:
				with log_context(name):
					yield
			else:
				yield
		except Exception as e:
			info = FailedObjectInfo(
					error_message, solution, e, is_critical, Problem.ERROR)
			self.failed_objects.subscriptions[name].append(info)
			self.logger.error(u"Failed to perform an action on subscription '%s': %s Exception message: %s", name, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)

	def try_subscription_with_rerun(
		self, func, name, error_message, solution=None,
		is_critical=True, repeat_error=None, repeat_count=3, sleep_time=10,
		use_log_context=True
	):
		with self.try_subscription(
			name, error_message, solution, is_critical,
			use_log_context=use_log_context
		):
			for i in range(1, repeat_count + 1):
				try:
					return func()
				except run_command.HclRunnerException:
					# We do not catch HclRunnerException because of it is catch on Hcl level
					raise
				except Exception as e:
					self.logger.debug(u"Exception: ", exc_info=e)
					if i == repeat_count:
						raise
					if repeat_error is not None:
						self.logger.error(repeat_error)
					sleep(sleep_time, 'Retry executing command')

	@contextmanager
	def try_subscriptions(self, subscriptions, is_critical=True):
		"""subscriptions is a dict with keys - subscription names and values - error messages"""
		try:
			yield
		except Exception as e:
			self.logger.debug(u"Exception:", exc_info=True)
			for name, error_message in subscriptions.iteritems():
				info = FailedObjectInfo(
						error_message, None, e, is_critical, Problem.ERROR)
				self.failed_objects.subscriptions[name].append(info)
				self.logger.error(u"Failed to perform an action on subscription '%s': %s Exception message: %s", name, error_message, e)

	def is_failed_subscription(self, subscription_name):
		"""Check if subscription is completely failed or not.

		For subscriptions marked as completely failed we do not continue migration.

		:param subscription_name: basestring
		:rtype: bool
		"""
		return not no_critical_errors(
			self.failed_objects.subscriptions, subscription_name
		)

	@contextmanager
	def try_plan(self, reseller_name, plan_name, error_message):
		try:
			with log_context(plan_name):
				yield
		except Exception as e:
			info = FailedObjectInfo(
					error_message, None, e, is_critical=True,
					severity=Problem.ERROR)
			self.failed_objects.plans[(reseller_name, plan_name)].append(info)
			owner_str = "'%s'" % (reseller_name,) if reseller_name is not None else 'admin'
			self.logger.error(u"Failed to perform an action on plan '%s' owned by %s: %s Exception message: %s", plan_name, owner_str, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)

			def fail_plan_subscriptions(clients):
				for client in clients:
					for subscription in client.subscriptions:
						if subscription.plan_name == plan_name:
							self._fail_subscription_plain(subscription.name, u"Failed to migrate service plan of this subscription.")

			if reseller_name is None: # admin
				fail_plan_subscriptions(self.model.clients.itervalues())
				for reseller in self.model.resellers.itervalues():
					if reseller.plan_name == plan_name:
						self._fail_reseller_plain(reseller.login, u"Failed to migrate service plan this reseller is assigned to.")
						for client in reseller.clients:
							self._fail_client_plain(client.login, u"Failed to migrate service plan that the reseller that owns this client is assigned to.")
							for subscription in client.subscriptions:
								self._fail_subscription_plain(subscription.name, u"Failed to migrate service plan that the reseller that owns client of this subscription is assigned to.")
						for plan in reseller.plans.itervalues():
							self._fail_plan_plain(reseller_name, plan.name, u"Failed to migrate service plan that the reseller of this plan is assigned to.")
			else:
				fail_plan_subscriptions(self.model.resellers[reseller_name].clients)

	@contextmanager
	def try_reseller(self, reseller_name, error_message):
		try:
			with log_context(reseller_name):
				yield
		except Exception as e:
			info = FailedObjectInfo(
					error_message, None, e, is_critical=True,
					severity=Problem.ERROR)
			self.failed_objects.resellers[reseller_name].append(info)
			self.logger.error(u"Failed to perform an action on reseller '%s': %s Exception message: %s", reseller_name, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)
			if self.model is not None:
				self._fail_reseller_with_subobj(reseller_name, error_message, e)

	@contextmanager
	def try_client(self, reseller_name, client_name, error_message):
		try:
			with log_context(client_name):
				yield
		except Exception as e:
			self.logger.error(u"Failed to perform an action on client '%s': %s Exception message: %s", client_name, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)
			self._fail_client_with_subobj(reseller_name, client_name, error_message, e)

	@contextmanager
	def try_auxiliary_user(self, client_name, auxiliary_user_name, error_message):
		try:
			with log_context(auxiliary_user_name):
				yield
		except Exception as e:
			self.logger.error(u"Failed to perform an action on auxiliary user '%s' of client '%s': %s Exception message: %s", auxiliary_user_name, client_name, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)
			info = FailedObjectInfo(
					error_message, None, e, is_critical=True,
					severity=Problem.ERROR)
			self.failed_objects.auxiliary_users[
					(client_name, auxiliary_user_name)].append(info)

	@contextmanager
	def try_auxiliary_user_role(self, client_name, auxiliary_user_role_name, error_message):
		try:
			with log_context(auxiliary_user_role_name):
				yield
		except Exception as e:
			self.logger.error(u"Failed to perform an action on auxiliary user role '%s' of client '%s': %s Exception message: %s", auxiliary_user_role_name, client_name, error_message, e)
			self.logger.debug(u"Exception:", exc_info=True)
			info = FailedObjectInfo(
					error_message, None, e, is_critical=True,
					severity=Problem.ERROR) 
			self.failed_objects.auxiliary_user_roles[
				(client_name, auxiliary_user_role_name)].append(info)

	@contextmanager
	def try_general(self, error_message, solution):
		try:
			yield
		except Exception as e:
			self.logger.debug(u"Exception:", exc_info=True)
			self.logger.error(error_message)
			info = FailedObjectInfo(
					error_message, solution, e, is_critical=False,
					severity=Problem.ERROR)
			self.failed_objects.general.append(info)

	def fail_auxiliary_user(self, user_name, client_name, error_message, solution=None, is_critical=True):
		self.logger.error(u"Failed to perform an action on auxiliary user '%s' of client '%s'", user_name, client_name)
		info = FailedObjectInfo(
				error_message, None, None, is_critical, Problem.ERROR)
		self.failed_objects.auxiliary_users[(client_name, user_name)].append(info)
		
	def fail_subscription(self, name, error_message, solution=None, is_critical=True, omit_logging=False):
		if not omit_logging:
			self.logger.error(u"Failed to perform an action on subscription '%s': %s", name, error_message)
		self._fail_subscription_plain(name, error_message, solution, is_critical)

	def add_issue_subscription(self, subscription_name, issue):
		self.issues.subscriptions[subscription_name].append(issue)

	def _fail_subscription_plain(self, name, error_message, solution=None, is_critical=True):
		info = FailedObjectInfo(
				error_message, solution, None, is_critical, Problem.ERROR)
		self.failed_objects.subscriptions[name].append(info)

	def fail_client(self, reseller_name, name, error_message):
		self.logger.error(u"Failed to perform an action on client '%s': %s", name, error_message)
		self._fail_client_with_subobj(reseller_name, name, error_message, None)

	def fail_reseller(self, name, error_message):
		self.logger.error(u"Failed to perform an action on reseller '%s': %s", name, error_message)
		self._fail_reseller_with_subobj(name, error_message, None)

	def _fail_client_with_subobj(self, reseller_name, name, error_message, exception):
		info = FailedObjectInfo(
				error_message, None, exception, is_critical=True,
				severity=Problem.ERROR)
		self.failed_objects.clients[name].append(info)

		if reseller_name is None: # admin
			client = self.model.clients[name]
		else:
			client = [reseller_client for reseller_client in self.model.resellers[reseller_name].clients if reseller_client.login == name][0]

		for subscription in client.subscriptions:
			self._fail_subscription_plain(subscription.name, u"Failed to migrate client of this subscription.")

	def _fail_reseller_with_subobj(self, name, error_message, exception):
		info = FailedObjectInfo(
				error_message, None, exception, is_critical=True,
				severity=Problem.ERROR)
		self.failed_objects.resellers[name].append(info)

		reseller = self.model.resellers[name]

		for client in reseller.clients:
			self._fail_client_plain(client.login, u"Failed to migrate reseller that owns this client.")
			for subscription in client.subscriptions:
				self._fail_subscription_plain(subscription.name, u"Failed to migrate reseller that owns client of this subscription.")

		for plan in reseller.plans.itervalues():
			self._fail_plan_plain(name, plan.name, u"Failed to migrate reseller of this plan.")

	def _fail_client_plain(self, name, error_message):
		info = FailedObjectInfo(
				error_message, None, None, is_critical=True,
				severity=Problem.ERROR)
		self.failed_objects.clients[name].append(info)

	def _fail_plan_plain(self, reseller_name, plan_name, error_message):
		info = FailedObjectInfo(
				error_message, None, None, is_critical=True,
				severity=Problem.ERROR)
		self.failed_objects.plans[(reseller_name, plan_name)].append(info)

	def _fail_reseller_plain(self, reseller_name, error_message):
		info = FailedObjectInfo(
				error_message, None, None, is_critical=True,
				severity=Problem.ERROR)
		self.failed_objects.resellers[reseller_name].append(info)
		
	def get_filtering_model(self):
		return FilteringModel(self.model, self.failed_objects)


class FilteringModel(object):
	"""Adapter over target model that filters out failed objects"""

	def __init__(self, model, failed_objects):
		self.model = model
		self.failed_objects = failed_objects 

	def __getattr__(self, name):
		return getattr(self.model, name)

	def iter_all_subscriptions(self):
		return [
			subscription for subscription in self.model.iter_all_subscriptions() 
			if no_critical_errors(self.failed_objects.subscriptions, subscription.name)
		]

	def iter_all_clients(self):
		return [
			FilteringClient(client, self.failed_objects) for client in self.model.iter_all_clients() 
			if client.login not in self.failed_objects.clients
		]

	@property
	def clients(self):
		return dict([
			(login, FilteringClient(client, self.failed_objects)) for login, client in self.model.clients.iteritems()
			if login not in self.failed_objects.clients
		])

	@property
	def resellers(self):
		return dict([
			(login, FilteringReseller(reseller, self.failed_objects)) for login, reseller in self.model.resellers.iteritems()
			if login not in self.failed_objects.resellers
		])


class FilteringReseller(object):
	"""Adapter over reseller target model object that filters out failed clients of reseller"""

	def __init__(self, reseller, failed_objects):
		self.reseller = reseller
		self.failed_objects = failed_objects 

	def __getattr__(self, name):
		return getattr(self.reseller, name)

	@property
	def clients(self):
		return [
			FilteringClient(client, self.failed_objects) for client in self.reseller.clients
			if client.login not in self.failed_objects.clients
		]


class FilteringClient(object):
	"""Adapter over client target model object that filters out failed subscriptions of client"""

	def __init__(self, client, failed_objects):
		self.client = client
		self.failed_objects = failed_objects

	def __getattr__(self, name):
		return getattr(self.client, name)

	def __setattr__(self, name, value):
		if name in ('client', 'failed_objects'):
			# skip internal properties of adapter
			super(FilteringClient, self).__setattr__(name, value)
		else:
			# pass set attribute request to client target model object
			return setattr(self.client, name, value)

	@property
	def subscriptions(self):
		return [
			subscription for subscription in self.client.subscriptions
			if no_critical_errors(self.failed_objects.subscriptions, subscription.name)
		]


def no_critical_errors(failed_objects_dict, key):
	return key not in failed_objects_dict or all(
		not failed_object_info.is_critical for failed_object_info in failed_objects_dict[key]
	)
