import json
from collections import defaultdict
from threading import Lock
from contextlib import contextmanager

from parallels.core import messages
from parallels.core.registry import Registry
from parallels.core.utils.common import unused
from parallels.core.utils.file_lock import FileLock, FileLockException
from parallels.core.utils.entity import Entity
from parallels.core.utils.yaml_utils import write_yaml
from parallels.core.utils.json_utils import get_json, read_json, write_json
from parallels.core.utils.common import find_first, default, write_unicode_file
from parallels.core import ParallelExecutionError


class MigrationProgress(object):

	STATUS_IN_PROGRESS = 'in-progress'
	STATUS_FINISHED_OK = 'finished-ok'
	STATUS_FINISHED_ERRORS = 'finished-errors'

	def __init__(self):
		report_filename = Registry.get_instance().get_context().session_files.get_path_to_progress()
		self._is_started = False
		self._command = None
		self._status = None
		self._subscriptions = []
		self._report_filename = report_filename
		self._lock_filename = '%s.lock' % report_filename
		self._report_filename_yaml = '%s.yaml' % report_filename
		self._report_filename_json = '%s.json' % report_filename
		self._file_write_lock = Lock()
		self._execution_lock = FileLock(self._lock_filename)

	@contextmanager
	def report(self, command):
		if not Registry.get_instance().get_context().options.is_skip_reporting:
			self._start(command)
		yield
		self._stop()

	def get_data(self):
		return read_json(self._report_filename_json)

	def get_serialized_data(self):
		return get_json(self.get_data())

	def get_subscription(self, name):
		"""
		:type name: basestring
		:rtype: parallels.core.utils.migration_progress.SubscriptionMigrationProgress
		"""
		s = find_first(self._subscriptions, lambda s: s.name == name)
		if s is None:
			s = SubscriptionMigrationProgress(
				name=name, status=SubscriptionMigrationStatus.NOT_STARTED, action=None,
				on_change=self._write_status
			)
			self._subscriptions.append(s)
		return s

	def update(self):
		is_completed = False
		try:
			self._execution_lock.acquire()
			is_completed = True
			self._execution_lock.release()
		except FileLockException as e:
			# assume that command is still in progress
			unused(e)
		data = self.get_data()
		if data is None:
			return
		if is_completed and data['status'] == self.STATUS_IN_PROGRESS:
			# last command status still is in progress, but lock was aquired, so
			# there are no active Panel Migrator processes working with current session;
			# assume that last command was finished with error and failed to update status,
			# so make update it manually
			data['status'] = self.STATUS_FINISHED_ERRORS
			write_json(self._report_filename_json, data, SubscriptionMigrationProgressEncoder)

	def _start(self, command):
		"""
		Start reporting about command execution
		:type command: str
		"""
		self._is_started = True
		self._command = command
		self._status = self.STATUS_IN_PROGRESS

		try:
			self._execution_lock.acquire()
		except FileLockException as e:
			unused(e)
			raise ParallelExecutionError()

		with self._file_write_lock:
			write_unicode_file(self._report_filename, messages.PERSUBSCRIPTION_MIGRATION_WAS_NOT_STARTED_YET)
			write_yaml(self._report_filename_yaml, self._get_report_data())
			write_json(self._report_filename_json, self._get_report_data(), SubscriptionMigrationProgressEncoder)

	def _stop(self):
		if not self._is_started:
			return
		data = read_json(self._report_filename_json)
		data['status'] = self.STATUS_FINISHED_OK
		write_yaml(self._report_filename_yaml, data)
		write_json(self._report_filename_json, data, SubscriptionMigrationProgressEncoder)
		self._execution_lock.release()

	def _write_status(self):
		"""
		:rtype: None
		"""
		if not self._is_started:
			return
		with self._file_write_lock:
			progress_file_text = ''
			# collect stats
			statuses = {}
			for status in SubscriptionMigrationStatus.all():
				statuses[status] = len([s for s in self._subscriptions if s.status == status])
			total = len(self._subscriptions)
			finished = len([s for s in self._subscriptions if s.status in SubscriptionMigrationStatus.finished()])
			not_finished = total - finished

			progress_file_text += '%s:\n' % messages.MESSAGE_SUMMARY
			summary_table = list()
			summary_table.append([
				messages.PROGRESS_STATUS_TOTAL, total,
				messages.PROGRESS_STATUS_FINISHED, self._format_percentage(finished, total),
				messages.PROGRESS_STATUS_NOT_FINISHED, self._format_percentage(not_finished, total)
			])
			progress_file_text += self._format_table(summary_table)

			progress_file_text += '%s:\n' % messages.PROGRESS_STATUS_NOT_FINISHED
			not_finished = sum([
				[
					SubscriptionMigrationStatus.to_string(status),
					self._format_percentage(statuses[status], total)
				]
				for status in SubscriptionMigrationStatus.not_finished()
			], [])
			progress_file_text += self._format_table([not_finished])

			progress_file_text += '%s:\n' % messages.PROGRESS_STATUS_FINISHED
			finished_table = sum([
				[
					SubscriptionMigrationStatus.to_string(status),
					self._format_percentage(statuses[status], total)
				]
				for status in SubscriptionMigrationStatus.finished()
			], [])
			progress_file_text += self._format_table([finished_table]) + '\n'

			progress_file_text += messages.SUBSCRIPTION_MIGRATION_STATUS + '\n'
			subscriptions_table = list()
			subscriptions_table.append([
				messages.SUBSCRIPTION_TITLE, messages.PROGRESS_STATUS, messages.PROGRESS_ACTION
			])
			for s in self._subscriptions:
				subscriptions_table.append([
					s.name, SubscriptionMigrationStatus.to_string(s.status), default(s.action, '')
				])

			progress_file_text += self._format_table(subscriptions_table)

			write_unicode_file(self._report_filename, progress_file_text)
			write_yaml(self._report_filename_yaml, self._get_report_data())
			write_json(self._report_filename_json, self._get_report_data(), SubscriptionMigrationProgressEncoder)

	@staticmethod
	def _format_percentage(numerator, denominator):
		"""
		:type numerator: int
		:type denominator: int
		:rtype: basestring
		"""
		if denominator is None or denominator == 0:
			return 'N/A%'
		else:
			percentage = (float(numerator) / float(denominator)) * 100.0
			return "%s (%.0f%%)" % (numerator, percentage)

	@staticmethod
	def _format_table(rows, h_padding=2):
		"""
		:type rows: list[list]
		:type h_padding: int
		:rtype: basestring
		"""
		if len(rows) == 0:
			return ''

		cols = defaultdict(list)

		for row in rows:
			for col_num, val in enumerate(row):
				cols[col_num].append(val)

		max_col_length = {
			col_num: max(len(unicode(val)) for val in col)
			for col_num, col in cols.iteritems()
		}

		border_row = "+%s+\n" % (
			'+'.join([
				'-' * (max_len + h_padding)
				for max_len in max_col_length.values()
			])
		)

		result = ''
		result += border_row

		row_strings = []
		for row in rows:
			row_strings.append("|%s|\n" % (
				'|'.join([
					unicode(value).center(max_col_length[col_num] + h_padding)
					for col_num, value in enumerate(row)
				])
			))

		result += border_row.join(row_strings)

		result += border_row

		return result

	def _get_report_data(self):
		return {'command': self._command, 'status': self._status, 'subscriptions': self._subscriptions}


class SubscriptionMigrationStatus(object):
	NOT_STARTED = 'not-started'
	IN_PROGRESS = 'in-progress'
	ON_HOLD = 'on-hold'
	FINISHED_OK = 'finished-ok'
	FINISHED_WARNINGS = 'finished-warnings'
	FINISHED_ERRORS = 'finished-errors'

	@classmethod
	def all(cls):
		"""
		:rtype: list[basestring]
		"""
		return [
			cls.NOT_STARTED,
			cls.IN_PROGRESS,
			cls.ON_HOLD,
			cls.FINISHED_OK,
			cls.FINISHED_WARNINGS,
			cls.FINISHED_ERRORS,
		]

	@classmethod
	def not_finished(cls):
		"""
		:rtype: list[basestring]
		"""
		return [
			cls.NOT_STARTED,
			cls.IN_PROGRESS,
			cls.ON_HOLD,
		]

	@classmethod
	def finished(cls):
		"""
		:rtype: list[basestring]
		"""
		return [
			cls.FINISHED_OK,
			cls.FINISHED_WARNINGS,
			cls.FINISHED_ERRORS,
		]

	@classmethod
	def to_string(cls, state):
		"""
		:type state: basestring
		:rtype: basestring
		"""
		return {
			cls.NOT_STARTED: messages.PROGRESS_STATUS_NOT_STARTED,
			cls.IN_PROGRESS: messages.PROGRESS_STATUS_IN_PROGRESS,
			cls.ON_HOLD: messages.PROGRESS_STATUS_ON_HOLD,
			cls.FINISHED_OK: messages.PROGRESS_STATUS_FINISHED_OK,
			cls.FINISHED_WARNINGS: messages.PROGRESS_STATUS_FINISHED_WARNINGS,
			cls.FINISHED_ERRORS: messages.PROGRESS_STATUS_FAILED,
		}.get(state, state)


class SubscriptionMigrationProgress(Entity):
	def __init__(self, name, status, action, on_change):
		"""
		:type name: basestring
		:type status: basestring
		:type action: basestring | None
		:type on_change: () -> None
		:return:
		"""
		self._name = name
		self._status = status
		self._action = action
		self._on_change = on_change

	@property
	def properties_list(self):
		return super(SubscriptionMigrationProgress, self).properties_list - {'on_change'}

	@property
	def name(self):
		"""
		:rtype: basestring
		"""
		return self._name

	@property
	def status(self):
		"""
		:rtype: basestring
		"""
		return self._status

	@status.setter
	def status(self, new_status):
		"""
		:type new_status: basestring
		:rtype: None
		"""
		self._status = new_status
		self._on_change()

	@property
	def action(self):
		"""
		:rtype: basestring
		"""
		return self._action

	@action.setter
	def action(self, new_action):
		"""
		:type new_action: basestring
		:rtype: None
		"""
		self._action = new_action
		self._on_change()


class SubscriptionMigrationProgressEncoder(json.JSONEncoder):
	def default(self, obj):
		if isinstance(obj, SubscriptionMigrationProgress):
			return {obj.name: {'status': obj.status, 'action': obj.action}}
		# Let the base class default method raise the TypeError
		return json.JSONEncoder.default(self, obj)
