"""
Model Managers for Course Actions
"""
import traceback
from django.db import models, transaction


class CourseActionStateManager(models.Manager):
    """
    An abstract Model Manager class for Course Action State models.
    This abstract class expects child classes to define the ACTION (string) field.
    """
    class Meta:
        """Abstract manager class, with subclasses defining the ACTION (string) field."""
        abstract = True

    def find_all(self, exclude_args=None, **kwargs):
        """
        Finds and returns all entries for this action and the given field names-and-values in kwargs.
        The exclude_args dict allows excluding entries with the field names-and-values in exclude_args.
        """
        return self.filter(action=self.ACTION, **kwargs).exclude(**(exclude_args or {}))  # pylint: disable=no-member

    def find_first(self, exclude_args=None, **kwargs):
        """
        Returns the first entry for the this action and the given fields in kwargs, if found.
        The exclude_args dict allows excluding entries with the field names-and-values in exclude_args.

        Raises ItemNotFoundError if more than 1 entry is found.

        There may or may not be greater than one entry, depending on the usage pattern for this Action.
        """
        objects = self.find_all(exclude_args=exclude_args, **kwargs)
        if len(objects) == 0:
            raise CourseActionStateItemNotFoundError(
                "No entry found for action {action} with filter {filter}, excluding {exclude}".format(
                    action=self.ACTION,  # pylint: disable=no-member
                    filter=kwargs,
                    exclude=exclude_args,
                ))
        else:
            return objects[0]

    def delete(self, entry_id):
        """
        Deletes the entry with given id.
        """
        self.filter(id=entry_id).delete()


class CourseActionUIStateManager(CourseActionStateManager):
    """
    A Model Manager subclass of the CourseActionStateManager class that is aware of UI-related fields related
    to state management, including "should_display" and "message".
    """

    # add transaction protection to revert changes by get_or_create if an exception is raised before the final save.
    @transaction.commit_on_success
    def update_state(
            self, course_key, new_state, should_display=True, message="", user=None, allow_not_found=False, **kwargs
    ):
        """
        Updates the state of the given course for this Action with the given data.
        If allow_not_found is True, automatically creates an entry if it doesn't exist.
        Raises CourseActionStateException if allow_not_found is False and an entry for the given course
            for this Action doesn't exist.
        """
        state_object, created = self.get_or_create(course_key=course_key, action=self.ACTION)  # pylint: disable=no-member

        if created:
            if allow_not_found:
                state_object.created_user = user
            else:
                raise CourseActionStateItemNotFoundError(
                    "Cannot update non-existent entry for course_key {course_key} and action {action}".format(
                        action=self.ACTION,  # pylint: disable=no-member
                        course_key=course_key,
                    ))

        # some state changes may not be user-initiated so override the user field only when provided
        if user:
            state_object.updated_user = user

        state_object.state = new_state
        state_object.should_display = should_display
        state_object.message = message

        # update any additional fields in kwargs
        if kwargs:
            for key, value in kwargs.iteritems():
                setattr(state_object, key, value)

        state_object.save()
        return state_object

    def update_should_display(self, entry_id, user, should_display):
        """
        Updates the should_display field with the given value for the entry for the given id.
        """
        return self.update(id=entry_id, updated_user=user, should_display=should_display)


class CourseRerunUIStateManager(CourseActionUIStateManager):
    """
    A concrete model Manager for the Reruns Action.
    """
    ACTION = "rerun"

    class State(object):
        """
        An Enum class for maintaining the list of possible states for Reruns.
        """
        IN_PROGRESS = "in_progress"
        FAILED = "failed"
        SUCCEEDED = "succeeded"

    def initiated(self, source_course_key, destination_course_key, user, display_name):
        """
        To be called when a new rerun is initiated for the given course by the given user.
        """
        self.update_state(
            course_key=destination_course_key,
            new_state=self.State.IN_PROGRESS,
            user=user,
            allow_not_found=True,
            source_course_key=source_course_key,
            display_name=display_name,
        )

    def succeeded(self, course_key):
        """
        To be called when an existing rerun for the given course has successfully completed.
        """
        self.update_state(
            course_key=course_key,
            new_state=self.State.SUCCEEDED,
        )

    def failed(self, course_key):
        """
        To be called within an exception handler when an existing rerun for the given course has failed.
        """
        self.update_state(
            course_key=course_key,
            new_state=self.State.FAILED,
            message=traceback.format_exc(),
        )


class CourseActionStateItemNotFoundError(Exception):
    """An exception class for errors specific to Course Action states."""
    pass
