import logging
from contextlib import contextmanager

from parallels.common.actions.base.common_action \
	import CommonAction
from parallels.common.actions.base.subscription_action \
	import SubscriptionAction
from parallels.common.actions.base.compound_action \
	import CompoundAction

from parallels.common.utils.migrator_utils import trace_step
from parallels.common.utils.steps_profiler import get_default_steps_profiler

logger = logging.getLogger(__name__)


class ActionRunner(object):
	"""Executes workflow actions and entry points

	Considers their sequence, handles exceptions so non-critical failures do
	not break migration
	"""

	def __init__(self, global_context):
		self._global_context = global_context
		self._executed_actions = set()

	def run_entry_point(self, entry_point):
		try:
			self.run(entry_point)
		finally:
			shutdown_paths = set()
			all_shutdown_actions = entry_point.get_shutdown_actions()
			for path in self._executed_actions:
				if path in all_shutdown_actions:
					shutdown_paths.update(all_shutdown_actions[path])

			for path in shutdown_paths - self._executed_actions:
				self.run(entry_point.get_path(path))

	def run(self, action, action_id=None, path=None):
		if path is None:
			path = []

		if isinstance(action, CompoundAction):
			with self._profile_action(action_id, action):
				with self._trace_action(action_id, action):
					for child_action_id, child_action in action.get_all_actions():
						self.run(child_action, child_action_id, path + [child_action_id])
		elif isinstance(action, SubscriptionAction):
			self._run_subscription_action(action_id, action)
		elif isinstance(action, CommonAction):
			self._run_action(action_id, action)
		else:
			raise Exception(
				"Invalid action '%s': %s is not a subclass of "
				"CompoundAction, SubscriptionAction or BaseAction" % (
					action_id,
					action.__class__.__name__
			))

		self._executed_actions.add('/'.join(path))

	def _run_action(self, action_id, action):
		with self._profile_action(action_id, action):
			with self._trace_action(action_id, action):
				action.run(self._global_context)

	def _run_subscription_action(self, action_id, action):
		"""Run specified action on all migrated subscriptions

		This function also considers isolated safe execution for each
		subscription and logging.

		Arguments:
		- action - subscription action, instance of
		  parallels.common.utils.subscription_action.SubscriptionAction
		"""
		with self._profile_action(action_id, action):
			logger.debug("Run subscription action class %s", action.__class__.__name__)
			logger.debug(action.get_description())
			subscriptions = filter(
				lambda s: action.filter_subscription(self._global_context, s),
				self._global_context.iter_all_subscriptions()
			)

			if len(subscriptions) > 0:
				with self._trace_action(action_id, action):
					for n, subscription in enumerate(subscriptions, start=1):
						if action.get_logging_properties().info_log:
							logging_function = logger.info
						else:
							logging_function = logger.debug

						logging_function(
							"Process subscription '%s' (#%s out of %s)", 
							subscription.name, n, len(subscriptions)
						)
						safe = self._global_context.safe
						safe.try_subscription_with_rerun(
							lambda: action.run(self._global_context, subscription),
							subscription.name,
							error_message=action.get_failure_message(
								self._global_context, subscription
							),
							repeat_error="%s. Try to repeat operation once more." % (
								action.get_failure_message(
									self._global_context, subscription
								)
							)
						)

	@contextmanager
	def _trace_action(self, action_id, action):
		if action.get_description() is not None:
			if action.get_logging_properties().info_log:
				log_level = 'info'
			else:
				log_level = 'debug'

			with trace_step(
				action_id, action.get_description(),
				log_level=log_level,
				compound=action.get_logging_properties().compound
			):
				yield
		else:
			yield

	@staticmethod
	@contextmanager
	def _profile_action(action_id, action):
		if action.get_description() is not None:
			with get_default_steps_profiler().measure_time(
				action_id, action.get_description()
			):
				yield
		else:
			# 1) Compound actions that have no descriptions are profiled by all child actions.
			# 2) Other actions that have no descriptions are measured as "Other" action
			yield