from datetime import datetime

import pytz

from opaque_keys.edx.keys import CourseKey, UsageKey
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type

from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
from .constants import ScoreDatabaseTableEnum
from .events import SUBSECTION_OVERRIDE_EVENT_TYPE
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED


def _get_key(key_or_id, key_cls):
    """
    Helper method to get a course/usage key either from a string or a key_cls,
    where the key_cls (CourseKey or UsageKey) will simply be returned.
    """
    return (
        key_cls.from_string(key_or_id)
        if isinstance(key_or_id, basestring)
        else key_or_id
    )


class GradesService(object):
    """
    Course grade service

    Provides various functions related to getting, setting, and overriding user grades.
    """

    def get_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
        """
        Finds and returns the earned subsection grade for user
        """
        course_key = _get_key(course_key_or_id, CourseKey)
        usage_key = _get_key(usage_key_or_id, UsageKey)

        return PersistentSubsectionGrade.objects.get(
            user_id=user_id,
            course_id=course_key,
            usage_key=usage_key
        )

    def get_subsection_grade_override(self, user_id, course_key_or_id, usage_key_or_id):
        """
        Finds the subsection grade for user and returns the override for that grade if it exists

        If override does not exist, returns None. If subsection grade does not exist, will raise an exception.
        """
        course_key = _get_key(course_key_or_id, CourseKey)
        usage_key = _get_key(usage_key_or_id, UsageKey)

        grade = self.get_subsection_grade(user_id, course_key, usage_key)

        try:
            return PersistentSubsectionGradeOverride.objects.get(
                grade=grade
            )
        except PersistentSubsectionGradeOverride.DoesNotExist:
            return None

    def override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id, earned_all=None,
                                  earned_graded=None):
        """
        Override subsection grade (the PersistentSubsectionGrade model must already exist)

        Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. Will not
        override earned_all or earned_graded value if they are None. Both default to None.
        """
        course_key = _get_key(course_key_or_id, CourseKey)
        usage_key = _get_key(usage_key_or_id, UsageKey)

        grade = PersistentSubsectionGrade.objects.get(
            user_id=user_id,
            course_id=course_key,
            usage_key=usage_key
        )

        # Create override that will prevent any future updates to grade
        override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
            grade=grade,
            earned_all_override=earned_all,
            earned_graded_override=earned_graded
        )

        # Cache a new event id and event type which the signal handler will use to emit a tracking log event.
        create_new_event_transaction_id()
        set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)

        # Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
        # which will use the above override to update the grade before writing to the table.
        SUBSECTION_OVERRIDE_CHANGED.send(
            sender=None,
            user_id=user_id,
            course_id=unicode(course_key),
            usage_id=unicode(usage_key),
            only_if_higher=False,
            modified=override.modified,
            score_deleted=False,
            score_db_table=ScoreDatabaseTableEnum.overrides
        )

    def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
        """
        Delete the override subsection grade row (the PersistentSubsectionGrade model must already exist)

        Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the
        override does not exist, no error is raised, it just triggers the recalculation.
        """
        course_key = _get_key(course_key_or_id, CourseKey)
        usage_key = _get_key(usage_key_or_id, UsageKey)

        override = self.get_subsection_grade_override(user_id, course_key, usage_key)
        # Older rejected exam attempts that transition to verified might not have an override created
        if override is not None:
            override.delete()

        # Cache a new event id and event type which the signal handler will use to emit a tracking log event.
        create_new_event_transaction_id()
        set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)

        # Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
        # which will no longer use the above deleted override, and instead return the grade to the original score from
        # the actual problem responses before writing to the table.
        SUBSECTION_OVERRIDE_CHANGED.send(
            sender=None,
            user_id=user_id,
            course_id=unicode(course_key),
            usage_id=unicode(usage_key),
            only_if_higher=False,
            modified=datetime.now().replace(tzinfo=pytz.UTC),  # Not used when score_deleted=True
            score_deleted=True,
            score_db_table=ScoreDatabaseTableEnum.overrides
        )

    def should_override_grade_on_rejected_exam(self, course_key_or_id):
        """Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
        course_key = _get_key(course_key_or_id, CourseKey)
        return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
