import json
import threading
import time
from threading import Lock
from contextlib import contextmanager

from parallels.core import messages, MigrationError, MigratorInterruptException
from parallels.core.registry import Registry
from parallels.core.utils.common import format_table, find_first, default, write_unicode_file, safe_string_repr
from parallels.core.utils.entity import Entity
from parallels.core.utils.yaml_utils import write_yaml
from parallels.core.utils.json_utils import encode_json, read_json, write_json
from parallels.core.utils.migrator_utils import is_locked


class MigrationProgress(object):

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

    # Do not update migration progress files too frequently to avoid performance issues:
    # this is a minimal time gap between two writes
    UPDATE_TIME_LIMIT = 1

    def __init__(self):
        self._is_started = False
        self._command = None
        self._status = None
        self._action = None
        self._task_id = None
        self._subscriptions = []
        self._file_write_lock = Lock()
        self._need_update = True

        self._is_report_to_file = False
        self._report_filename = None
        self._report_filename_yaml = None
        self._report_filename_json = None

        self._stop_progress_thread = False
        self.progress_thread = None

    def set_report_path(self, path):
        self._report_filename = path
        self._report_filename_yaml = '%s.yaml' % path
        self._report_filename_json = '%s.json' % path
        self._is_report_to_file = True

    def start_progress_thread(self):
        self.progress_thread = threading.Thread(
            target=self._progress_thread_main,
            name='ProgressThread'
        )
        # It is not good practice to have daemon threads, but that is required to force
        # interruption of a thread, as there is no safe way to do that (actually there is no way at all).
        self.progress_thread.daemon = True
        self.progress_thread.start()

    @contextmanager
    def report(self, command, task_id=None):
        if not Registry.get_instance().get_context().options.is_skip_progress_reporting:
            self._start(command, task_id=task_id)
        try:
            yield
        except MigratorInterruptException:
            self._stop()
            raise
        except MigrationError as e:
            self._stop(self.STATUS_FINISHED_ERRORS, safe_string_repr(e), e.error_id)
            raise
        else:
            self._stop()

    @contextmanager
    def overall_action(self, action_description):
        self._action = action_description
        self._set_need_update()
        try:
            yield
        finally:
            self._action = None
            self._set_need_update()

    def get_data(self):
        if not self._is_report_to_file:
            return None
        return read_json(self._report_filename_json)

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

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

    def update(self):
        if not self._is_report_to_file or is_locked():
            return
        data = self.get_data()
        if data is None:
            return
        if data['status'] == self.STATUS_IN_PROGRESS:
            # last command status still is in progress, but Plesk Migrator is not locked, so
            # there are no active migration processes working with current session;
            # assume that this 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 stop_progress_thread(self):
        self._stop_progress_thread = True

    def _start(self, command, task_id=None):
        """
        Start reporting about command execution
        :type command: str
        """
        self._is_started = True
        self._command = command
        self._status = self.STATUS_IN_PROGRESS
        self._need_update = True
        self._error_message = None
        self._error_id = None
        self._task_id = task_id

        with self._file_write_lock:
            self._write_to_file(messages.PER_SUBSCRIPTION_MIGRATION_WAS_NOT_STARTED_YET)

    def _stop(self, status=None, error_message=None, error_id=None):
        if not self._is_started:
            return

        if status is None:
            self._status = self.STATUS_FINISHED_OK
        else:
            self._status = status
        self._error_message = error_message
        self._error_id = error_id

        self._need_update = True

    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 += 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 += 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 += 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, messages.PROGRESS_DETAILS
            ])
            for s in self._subscriptions:
                subscriptions_table.append([
                    s.name, SubscriptionMigrationStatus.to_string(s.status),
                    default(s.action, ''), default(s.details, '')
                ])

            progress_file_text += format_table(subscriptions_table)
            self._write_to_file(progress_file_text)

    def _write_to_file(self, content):
        if not self._is_report_to_file:
            return
        write_unicode_file(self._report_filename, content)
        write_yaml(self._report_filename_yaml, self._get_report_data())
        write_json(
            self._report_filename_json, self._get_report_data(),
            SubscriptionMigrationProgressEncoder, pretty_print=True
        )
        if self._task_id is not None:
            write_json(
                "%s.%s.json" % (self._report_filename, self._task_id),
                self._get_report_data(), SubscriptionMigrationProgressEncoder, pretty_print=True
            )

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

    def _get_report_data(self):
        data = {
            'command': self._command,
            'status': self._status,
            'subscriptions': self._subscriptions,
            'action': self._action
        }
        if self._error_message is not None:
            data['error_message'] = self._error_message
        if self._error_id is not None:
            data['error_id'] = self._error_id
        if self._task_id is not None:
            data['task_id'] = self._task_id
        return data

    def _progress_thread_main(self):
        while not self._stop_progress_thread:
            time.sleep(self.UPDATE_TIME_LIMIT)
            if self._need_update:
                self._need_update = False
                self._write_status()
        self._write_status()

    def _set_need_update(self):
        self._need_update = True


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

    @classmethod
    def all(cls):
        """
        :rtype: list[str | unicode]
        """
        return [
            cls.NOT_STARTED,
            cls.IN_PROGRESS,
            cls.ON_HOLD,
            cls.FINISHED_OK,
            cls.FINISHED_WARNINGS,
            cls.FINISHED_ERRORS,
            cls.REVERTED,
            cls.CANCELLED,
        ]

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

    @classmethod
    def finished(cls):
        """
        :rtype: list[str | unicode]
        """
        return [
            cls.FINISHED_OK,
            cls.FINISHED_WARNINGS,
            cls.FINISHED_ERRORS,
            cls.REVERTED,
            cls.CANCELLED
        ]

    @classmethod
    def to_string(cls, state):
        """
        :type state: str | unicode
        :rtype: str | unicode
        """
        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,
            cls.REVERTED: messages.PROGRESS_STATUS_REVERTED,
            cls.CANCELLED: messages.PROGRESS_STATUS_CANCELLED
        }.get(state, state)


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

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

    @property
    def name(self):
        """
        :rtype: str | unicode
        """
        return self._name

    @property
    def status(self):
        """
        :rtype: str | unicode
        """
        return self._status

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

    @property
    def details(self):
        """
        :rtype: str | unicode
        """
        return self._details

    @details.setter
    def details(self, new_details):
        self._details = new_details
        self._on_change()

    @property
    def action(self):
        """
        :rtype: str | unicode
        """
        return self._action

    @action.setter
    def action(self, new_action):
        """
        :type new_action: str | unicode
        :rtype: None
        """
        self._action = new_action
        self._details = None
        self._on_change()


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