from parallels.core import messages
from contextlib import contextmanager
from collections import namedtuple, defaultdict
import logging

from parallels.core.logging_context import log_context, subscription_context
from parallels.core import run_command, MigrationError, MigrationNoRepeatError
from parallels.core.utils.steps_profiler import sleep
from parallels.core.checking import Problem
from parallels.core.utils.common import all_equal

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 subscription_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
	):
		exceptions = []

		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:
					exceptions.append(e)
					self.logger.debug(u"Exception: ", exc_info=e)
					if i == repeat_count or isinstance(e, MigrationNoRepeatError):
						raise MultipleAttemptsMigrationError(exceptions)
					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, messages.FAILED_MIGRATE_SERVICE_PLAN_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, messages.FAILED_MIGRATE_SERVICE_PLAN_RESELLER_IS)
						for client in reseller.clients:
							self._fail_client_plain(client.login, messages.FAILED_MIGRATE_SERVICE_PLAN_THAT_RESELLER)
							for subscription in client.subscriptions:
								self._fail_subscription_plain(subscription.name, messages.FAILED_MIGRATE_SERVICE_PLAN_THAT_RESELLER_1)
						for plan in reseller.plans.itervalues():
							self._fail_plan_plain(reseller_name, plan.name, messages.FAILED_MIGRATE_SERVICE_PLAN_THAT_RESELLER_2)
			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(messages.FAILED_PERFORM_AN_ACTION_AUXILIARY_USER, 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, messages.FAILED_MIGRATE_CLIENT_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, messages.FAILED_MIGRATE_RESELLER_THAT_OWNS_CLIENT)
			for subscription in client.subscriptions:
				self._fail_subscription_plain(subscription.name, messages.FAILED_MIGRATE_RESELLER_THAT_OWNS_CLIENT_1)

		for plan in reseller.plans.itervalues():
			self._fail_plan_plain(name, plan.name, messages.FAILED_MIGRATE_RESELLER_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]
	)



class MultipleAttemptsMigrationError(MigrationError):
	def __init__(self, exceptions):
		if not all_equal([unicode(e) for e in exceptions]):
			message = ("\n%s\n" % ('-' * 76,)).join(
				[messages.MIGRATION_TOOLS_TRIED_PERFORM_OPERATION_IN % len(exceptions)] +
				[
					"Attempt #%s failed with the following error:\n%s" % (i, unicode(e))
					for i, e in enumerate(exceptions, start=1)
				]
			)
		else:
			if len(exceptions) > 0:
				message = "Migration tools tried to perform operation in %s attempts: %s" % (
					len(exceptions), unicode(exceptions[0])
				)
			else:
				assert False, "No attempts were performed to perform operation"

		super(MultipleAttemptsMigrationError, self).__init__(message)
