import os
import re
import dicognito
from rad.settings import REMOTE_URL
from django.db import models
from django.http.response import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone


from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.utils.html import format_html

from django.urls import reverse

from django.utils.translation import gettext_lazy as _
from django.utils.html import mark_safe

import string
from collections import defaultdict
from helpers.images import get_image_hash, image_as_base64

from django.contrib.contenttypes.fields import GenericRelation

from generic.models import (
    CidUser,
    CidUserGroup,
    ExamCollection,
    ExamUserStatus,
    QuestionBase,
    Site,
    # Condition,
    # Sign,
    ExamBase,
    QuestionNote,
    UserAnswerBase,
    UserUserGroup,
)

import reversion

import json

import hashlib

import pydicom
import pydicom.errors

from typing import List, TypedDict, Required, NotRequired


class QuestionData(TypedDict):
    images: Required[list[str]]
    annotations: Required[List[str]]
    type: Required[str]
    history: NotRequired[str]
    normal: Required[bool]
    answers: NotRequired[List[str]]
    feedback: NotRequired[str]
    feedback_images: NotRequired[List[str]]


image_storage = FileSystemStorage(
    # Physical file location ROOT
    location="{0}".format(settings.MEDIA_ROOT),
    # Url for file
    base_url="{0}".format(settings.MEDIA_URL),
)


def image_directory_path(instance, filename):
    # return u"{0}".format(filename)
    return "rapids/picture/{0}".format(filename)


def get_answer_compare(s):
    s = s.strip().translate(str.maketrans("", "", string.punctuation.replace("#", ""))).lower()
    s = " ".join(s.split())
    return s


class Abnormality(models.Model):
    name = models.CharField(
        max_length=200, unique=True, help_text="Primary abnormality on the film(s)"
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ("name",)


class Region(models.Model):
    name = models.CharField(
        max_length=200,
        unique=True,
        help_text="Region of the abnormality (not including lateralitiy)",
        blank=True,
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ("name",)


class Examination(models.Model):
    examination = models.CharField(max_length=200)

    def __str__(self):
        return self.examination

    class Meta:
        ordering = ("examination",)


class Answer(models.Model):
    question = models.ForeignKey(
        "Rapid", related_name="answers", on_delete=models.CASCADE
    )
    answer = models.TextField(max_length=500)
    answer_compare = models.TextField(max_length=500)

    class MarkOptions(models.TextChoices):
        UNMARKED = "", _("Unmarked")
        INCORRECT = "0", _("Incorrect")
        HALF_MARK = "1", _("Half mark")
        CORRECT = "2", _("Correct")

    status = models.CharField(
        max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED
    )

    proposed = models.BooleanField(default=False)

    def __str__(self):
        return self.answer

    def save(self, *args, **kwargs):
        self.clean()
        res = super(Answer, self).save(*args, **kwargs)
        # Update matching UserAnswer objects in real-time
        status = self.status
        callstate = UserAnswer.CallStateOptions.INCORRECTCALL
        if status == Answer.MarkOptions.CORRECT:
            callstate = UserAnswer.CallStateOptions.CORRECT
        elif status == Answer.MarkOptions.HALF_MARK:
            callstate = UserAnswer.CallStateOptions.PARTIALCALL

        UserAnswer.objects.filter(
            question=self.question,
            normal=False,
            answer_compare__iexact=self.answer_compare
        ).update(score=status, callstate=callstate)
        return res

    def delete(self, *args, **kwargs):
        # Update matching UserAnswer objects back to UNMARKED in real-time
        UserAnswer.objects.filter(
            question=self.question,
            normal=False,
            answer_compare__iexact=self.answer_compare
        ).update(score=Answer.MarkOptions.UNMARKED, callstate=None)
        return super(Answer, self).delete(*args, **kwargs)

    def clean(self):
        if self.answer:
            self.answer_compare = get_answer_compare(self.answer)


class Rapid(QuestionBase):
    """
    Django model that defines a rapid question.

    Images are help by a seperate model: RapidImage
    which has a foreign key to this model


    """

    question = models.TextField(null=True, blank=True)
    history = models.TextField(null=True, blank=True)

    NORMAL_CHOICES = (
        (True, "Normal"),
        (False, "Abnormal"), 
    )

    normal = models.BooleanField(default=False, help_text="Defines if a question is normal/abnormal", choices=NORMAL_CHOICES)

    NONE = "NONE"
    LEFT = "LEFT"
    RIGHT = "RIGHT"
    BILATERAL = "BILAT"

    LATERALITY_CHOICES = (
        (NONE, "None"),
        (LEFT, "Left"),
        (RIGHT, "Right"),
        (BILATERAL, "Bilateral"),
    )

    abnormality = models.ManyToManyField(
        Abnormality,
        blank=True,
        help_text="The abnormality (laterality and region independent). Used for categorisation but does not affect the answer",
    )
    region = models.ManyToManyField(
        Region,
        blank=True,
        help_text="Region of the abnormality (laterality independent)",
    )
    examination = models.ManyToManyField(
        Examination, help_text="Name of the (primary) examination"
    )
    laterality = models.CharField(
        max_length=20,
        choices=LATERALITY_CHOICES,
        default=NONE,
        help_text="Applies to the answer, not the examination",
    )
    # condition = tagulous.models.TagField(
    #    to=Condition,
    #    blank=True,
    #    help_text=
    #    "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
    # )
    # sign = tagulous.models.TagField(
    #    to=Sign, blank=True, help_text='Radiological signs in the question')

    DEFAULT_SITE_ID = 1
    # site = models.ManyToManyField(
    #    Site,
    #    related_name="site_rapid",
    #    blank=True,
    #    default=DEFAULT_SITE_ID,
    #    help_text=
    #    "If we know the source of the image")
    verified = models.BooleanField(default=False)

    author = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        help_text="Author of question",
        related_name="rapid_authored_questions",
    )

    scrapped = models.BooleanField(
        default=False, help_text="Question has been scrapped and will not be shown"
    )

    def get_app_name(self):
        return "rapids"

    def get_absolute_url(self):
        return reverse("rapids:question_detail", kwargs={"pk": self.pk})

    def get_regions(self):
        """Returns a comma seperated text list of regions"""
        regions = ", ".join([i.name for i in self.region.all()])
        return regions

    def get_abnormalities(self):
        """Returns a comma seperated text list of regions"""
        abnormalities = ", ".join([i.name for i in self.abnormality.all()])
        return abnormalities

    def get_examinations(self):
        """Returns a comma seperated text list of regions"""
        examinations = ", ".join([i.examination for i in self.examination.all()])
        return examinations

    def __str__(self):
        if self.normal:
            return "{}/normal".format(self.pk)
        else:
            return "{}".format(self.pk)

    def get_long_str(self):
        n = "Normal"
        if not self.normal:
            # n = self.answers.first()
            n = "{}/{} : {}".format(
                self.pk, self.abnormality.first(), self.region.first()
            )
        exams = self.get_examinations()
        lat = ""
        if self.laterality != "NONE":
            lat = self.laterality
        return "{}/{} : {} {}".format(self.pk, exams, lat, n)

    def get_primary_answer(self):
        if self.normal:
            return "Normal"
        else:
            if (
                hasattr(self, "prefetched_primary_answer")
                and self.prefetched_primary_answer
            ):
                ans = self.prefetched_primary_answer[0]
            else:
                ans = self.answers.filter(
                    proposed=False, status=Answer.MarkOptions.CORRECT
                ).first()

            if ans is None:
                return "None yet..."
            else:
                return ans.answer

    # def get_primary_answer(self):
    #    if self.normal:
    #        return "Normal"
    #    elif (
    #        self.answers.filter(
    #            proposed=False, status=Answer.MarkOptions.CORRECT
    #        ).count()
    #        > 0
    #    ):
    #        return (
    #            self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT)
    #            .first()
    #            .answer
    #        )
    #    else:
    #        return "None yet..."

    def get_exams(self):
        e = self.exams.all().values_list("name", flat=True)
        exams = ", ".join(e)
        return exams

    def get_unmarked_user_answer_string(self, exam_pk: int = None):
        unmarked_answers = self.get_unmarked_user_answers(exam_pk)

        if not unmarked_answers:
            return "No answers to mark"
        return format_html(
            "<span class='warn'>{} answer unmarked:</span> {}",
            len(unmarked_answers),
            ", ".join(unmarked_answers),
        )

    def get_unmarked_user_answers(self, exam_pk: int | None = None, marker=None):
        # If normal no answers to mark (they will be automarked)
        if self.normal:
            return []

        if exam_pk is None:
            user_answer_queryset = self.cid_user_answers.all()
        else:
            user_answer_queryset = self.cid_user_answers.filter(exam__id=exam_pk)

        marked_answers = self.get_marked_answers()

        unmarked_answers = set(
            [
                i
                for i in user_answer_queryset
                if i.normal == False and i.answer_compare not in marked_answers
            ]
        )

        unmarked_answers.discard("")

        return [i.answer_compare for i in unmarked_answers]

        # Marker code below is not used
        if marker is None:
            return [i.answer_compare for i in unmarked_answers]

        # If marker is specified we check for what they have marked
        marker_unmarked = []
        for answer in unmarked_answers:
            if answer.mark.filter(marker=marker).count() < 1:
                marker_unmarked.append(answer.answer_compare)

        return marker_unmarked

    def get_unmarked_user_answer_count(self, exam_pk=None, marker=None):
        if exam_pk is None:
            return self.cid_user_answers.all().count()

        return len(self.get_unmarked_user_answers(exam_pk, marker=marker))

    def get_user_answers(self, exam_pk=None, include_normal=True):
        if exam_pk is None:
            queryset = self.cid_user_answers.all()
        else:
            queryset = self.cid_user_answers.filter(exam__id=exam_pk)

        if not include_normal:
            queryset = queryset.filter(normal=False)

        user_answers = set([i.answer_compare for i in queryset])

        return user_answers

    def get_compare_answers(self):
        return set([i.answer_compare for i in self.answers.filter()])

    def get_marked_answers(self):
        return set(
            [
                i.answer_compare
                for i in self.answers.filter(proposed=False)
                if i.status != i.MarkOptions.UNMARKED
            ]
        )
        correct_answers = set(
            [i.answer.get_compare_string() for i in self.answers.all()]
        )
        half_mark_answers = set(
            [i.answer.get_compare_string() for i in self.half_mark_answers.all()]
        )
        incorrect_answers = set(
            [i.answer.get_compare_string() for i in self.incorrect_answers.all()]
        )

        marked_answers = correct_answers | half_mark_answers | incorrect_answers
        return marked_answers

    def get_correct_unstripped_answers(self):
        return set(
            [
                str(i)
                for i in self.answers.filter(proposed=False)
                if i.status == i.MarkOptions.CORRECT
            ]
        )

    def get_images(self, feedback=False):
        qs = self.images.all()

        if not feedback:
            images = [i.image for i in qs if not i.feedback_image]
        else:
            images = [i.image for i in qs]

        return images

    def get_image_urls(self):
        return ",".join([f"{REMOTE_URL}{i.url}" for i in self.get_images()])

    def get_image_url_array(self):
        return json.dumps([f"{REMOTE_URL}{i.url}" for i in self.get_images()])

    # def GetNonFeedbackQuestionImages(self):
    # return self.get_images()

    def get_image_annotations(self):
        return json.dumps(
            [i.image_annotations for i in self.images.all() if not i.feedback_image]
        )

    def get_laterality_string(self):
        if self.laterality == self.NONE:
            s = ""
        elif self.laterality == self.BILATERAL:
            s = "bilateral "
        elif self.laterality == self.RIGHT:
            s = "right "
        elif self.laterality == self.LEFT:
            s = "left "

        return s

    def get_suggested_answers(self):
        answers = []
        regions = self.region.all()
        laterality = self.get_laterality_string()
        if regions:
            for r in regions:
                for a in self.abnormality.all():
                    answers.append("{}{} {}".format(laterality, r.name, a.name))
                    answers.append("{} {}{}".format(a.name, laterality, r.name))
                    answers.append("{} {}".format(r.name, a.name))
                    answers.append("{} {}".format(a.name, r.name))
        else:
            for a in self.abnormality.all():
                laterality = self.get_laterality_string()
                answers.append("{} {}".format(laterality, a.name))
                answers.append("{} {}".format(a.name, laterality))
                answers.append("{}".format(a.name))

        compare_answers = self.get_compare_answers()

        new_answers = [
            a for a in answers if get_answer_compare(a) not in compare_answers
        ]

        return new_answers

    def get_question_json(
        self,
        based: bool = True,
        feedback: bool = False,
        answers: bool = True,
        history: bool = False,
        annotations: bool = False,
    ) -> QuestionData:
        """Returns a json representation of the question"""

        # Loop through rapidimage associations
        images = []
        annotations_list = []
        feedback_images = []
        i: RapidImage
        for i in self.images.all():
            # TODO: add anotations

            if i.feedback_image:
                if feedback:
                    annotations_list.append(i.image_annotations)
                    if based:
                        feedback_images.append(image_as_base64(i.image))
                    else:
                        feedback_images.append(
                            "{}/{}".format(settings.REMOTE_URL, i.image.url)
                        )
                    # feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))
            else:
                annotations_list.append(i.image_annotations)
                if based:
                    images.append(image_as_base64(i.image))
                else:
                    images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))

        json = {
            "images": images,
            # "feedback_image": [],
            "type": "rapid",
        }

        if annotations:
            json["annotations"] = annotations_list

        if history:
            json["history"] = self.history

        if answers:
            json["normal"] = self.normal
            json["answers"] = list(self.get_correct_unstripped_answers())

        if feedback:
            json["feedback_images"] = feedback_images
            json["feedback"] = self.feedback

        return json

    def anonymise_images(self):
        anonymizer = dicognito.anonymizer.Anonymizer()

        for image in self.images.all():
            file_path = os.path.join(settings.MEDIA_ROOT, image.image.name)

            try:
                with pydicom.dcmread(file_path) as dataset:
                    anonymizer.anonymize(dataset)
                    dataset.save_as(file_path)
            except pydicom.errors.InvalidDicomError:
                pass


# reversion.register(Rapid, follow=["images"])
# @reversion.register
class RapidImage(models.Model):
    rapid = models.ForeignKey(Rapid, related_name="images", on_delete=models.CASCADE)
    image = models.FileField(upload_to=image_directory_path)

    image_annotations = models.TextField(
        blank=True,
        null=True,
        help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
    )

    feedback_image = models.BooleanField(default=False)

    filename = models.CharField(
        max_length=255,
        null=True,
        blank=True,
        help_text="Name of the original file when uploaded",
    )

    description = models.CharField(max_length=255, null=True, blank=True)

    image_md5_hash = models.CharField(max_length=32, null=True, blank=True)

    is_dicom = models.BooleanField(default=False)

    def image_tag(self):
        if self.image:
            return mark_safe(
                '<img src="{}" class="admin-rapid-image" /><span class="admin-rapid-image-info">Click and hold to zoom<span>'.format(
                    self.image.url
                )
            )
        else:
            return ""

    image_tag.short_description = "Image"

    def save(self, *args, **kwargs):
        """Override save method to add image hash"""
        if self.image:
            image_hash, is_dicom = get_image_hash(self.image, hash_type="md5", direct_pixel_data=False)
            self.is_dicom = is_dicom
            self.image_md5_hash = image_hash

        # Hack for tests
        if image_hash != "12345ABCD":
            super().save(*args, **kwargs)  # Call the "real" save() method.


class RapidCreationDefault(models.Model):
    author = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name="rapid_default", on_delete=models.CASCADE
    )

    # site = models.ManyToManyField(
    #    Site,
    #    related_name="site_rapid_creation_default",
    #    blank=True,
    #    default=1,
    #    help_text="Default site to use when creating a new rapid")

    def get_absolute_url(self):
        return reverse("rapids:question_create")

class ExamQuestionDetail(models.Model):
    exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
    rapid = models.ForeignKey(Rapid, on_delete=models.CASCADE)

    sort_order = models.IntegerField(default=1000)

    class Meta:
        ordering = ("sort_order",)

# @reversion.register
class Exam(ExamBase):
    app_name = "rapids"

    exam_questions = models.ManyToManyField(Rapid, through=ExamQuestionDetail, related_name="exams")

    time_limit = models.IntegerField(
        help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)",
        default=35 * 60,
    )

    author = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        help_text="Author of exam",
        related_name="rapid_exam_author",
    )

    markers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        help_text="Authorised markers for the exam",
        related_name="rapid_exam_markers",
    )

    valid_cid_users = models.ManyToManyField(
        CidUser, blank=True, related_name="rapid_exams"
    )

    valid_user_users = models.ManyToManyField(
        settings.AUTH_USER_MODEL, blank=True, related_name="user_rapid_exams"
    )

    cid_user_groups = models.ManyToManyField(
        CidUserGroup,
        blank=True,
        help_text="These groups define which candidates are able to be added to the exams/collection.",
        related_name="rapid_cid_user_groups",
    )

    user_user_groups = models.ManyToManyField(
        UserUserGroup,
        blank=True,
        help_text="These groups define which candidates are able to be added to the exams/collection.",
        related_name="rapid_user_user_groups",
    )

    exam_user_status = GenericRelation(ExamUserStatus, related_query_name="rapids_exams")
    cid_user_exam = GenericRelation("generic.CidUserExam", related_query_name="rapids_exams")

    examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="rapids_exams")


    def get_app_name(self):
        return "rapids"

    def get_normal_abnormal_breakdown(self):
        # Inefficient but more extendible
        questions = self.exam_questions.all()

        normal = []
        abnormal = []
        for q in questions:
            if q.normal:
                normal.append(q)
            else:
                abnormal.append(q)

        return len(normal)

    def get_exam_json(self, based=True):
        questions = self.get_questions()

        exam_questions = defaultdict(dict)

        exam_order = []

        q: Rapid
        for q in questions:
            exam_order.append(q.id)

            # Loop through rapidimage associations
            images = []
            feedback_images = []
            image_titles = []
            for i in q.images.all():
                if i.feedback_image == True:
                    if based:
                        feedback_images.append(image_as_base64(i.image))
                    else:
                        feedback_images.append(
                            "{}/{}".format(settings.REMOTE_URL, i.image.url)
                        )
                else:
                    if based:
                        images.append(image_as_base64(i.image))
                    else:
                        images.append("{}/{}".format(settings.REMOTE_URL, i.image.url))

                if i.description:
                    image_titles.append(i.description)
                else:
                    image_titles.append("")

            exam_questions[q.id] = {
                "images": images,
                # "feedback_image": [],
                # "annotations": [str(q.image_annotations)],
                "type": "rapid",
            }

            if not self.exam_mode:
                exam_questions[q.id]["normal"] = q.normal
                exam_questions[q.id]["feedback_images"] = feedback_images
                exam_questions[q.id]["answers"] = list(
                    q.get_correct_unstripped_answers()
                )

            if self.include_history and q.history:
                exam_questions[q.id]["history"] = q.history

            if any(image_titles):
                exam_questions[q.id]["image_titles"] = image_titles

            # if feedback_images:
            #    exam_questions[q.id]["feedback_image"] = feedback_images

        exam_json = {
            "eid": "rapid/{}".format(self.id),
            "cached": False,
            "exam_type": "rapid",
            "exam_name": self.name,
            "exam_mode": self.exam_mode,
            "exam_order": exam_order,
            "questions": exam_questions,
        }

        if self.time_limit:
            exam_json["exam_time"] = self.time_limit

        return exam_json

    def get_absolute_url(self):
        return reverse("rapids:exam_overview", kwargs={"pk": self.pk})


@reversion.register
class UserAnswer(UserAnswerBase):
    """User answers by candidate"""

    app_name = "rapids"

    question = models.ForeignKey(
        Rapid, related_name="cid_user_answers", on_delete=models.CASCADE
    )

    # For rapids the answer can be normal in which case the below field is true
    normal = models.BooleanField(default=False)
    answer = models.TextField(max_length=500, blank=True)

    answer_compare = models.TextField(max_length=500, blank=True)

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name="user_rapid_user_answers",
    )

    # Each user answer is associated with a particular exam
    exam = models.ForeignKey(
        Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
    )

    score = models.CharField(
        max_length=1,
        choices=Answer.MarkOptions.choices,
        default=Answer.MarkOptions.UNMARKED,
        blank=True,
    )

    class CallStateOptions(models.TextChoices):
        CORRECT = "C", _("Correct Call")
        OVERCALL = "O", _("Overcall")
        UNDERCALL = "U", _("Undercall")
        INCORRECTCALL = "W", _("Incorrect Call")
        PARTIALCALL = "P", _("Partial Call")

    callstate = models.CharField(
        max_length=1, choices=CallStateOptions.choices, blank=True, null=True
    )

    def __str__(self):
        name = self.get_candidate_name()

        try:
            exam = self.exam
        except (Exam.DoesNotExist, KeyError) as e:
            exam = "None"
        return "{}/{}/{}: {}".format(
            exam, name, self.question.get_primary_answer(), self.answer
        )

    def save(self, *args, **kwargs):
        self.clean()

        # Auto-mark on save based on existing template answers
        q = self.question
        if q.normal != self.normal:
            self.callstate = UserAnswer.CallStateOptions.OVERCALL if q.normal else UserAnswer.CallStateOptions.UNDERCALL
            self.score = Answer.MarkOptions.INCORRECT
        elif q.normal and self.normal:
            self.callstate = UserAnswer.CallStateOptions.CORRECT
            self.score = Answer.MarkOptions.CORRECT
        else:
            ans = self.answer_compare
            if ans == "":
                self.score = Answer.MarkOptions.INCORRECT
                self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
            else:
                marked_ans = Answer.objects.filter(question_id=self.question_id, answer_compare__iexact=ans).first()
                if marked_ans is not None:
                    self.score = marked_ans.status
                    if marked_ans.status == Answer.MarkOptions.CORRECT:
                        self.callstate = UserAnswer.CallStateOptions.CORRECT
                    elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
                        self.callstate = UserAnswer.CallStateOptions.PARTIALCALL
                    elif marked_ans.status == Answer.MarkOptions.INCORRECT:
                        self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
                else:
                    self.score = Answer.MarkOptions.UNMARKED
                    self.callstate = None
        return super(UserAnswer, self).save(*args, **kwargs)

    def clean(self):
        if not self.score:
            self.score = Answer.MarkOptions.UNMARKED
        if self.answer:
            self.answer = self.answer.strip()
            self.answer_compare = get_answer_compare(self.answer)
        else:
            self.answer = ""
            self.answer_compare = ""

    # def get_compare_string(self):
    #     # strip here should be unneccasry (providing clean is now working)
    #     s = self.answer.lower().strip()
    #     s = s.translate(str.maketrans('', '', string.punctuation))
    #     return s

    def get_answer(self):
        return self.get_answer_string()

    def get_answer_string(self):
        if self.normal:
            return "Normal"
        else:
            return self.answer

    def get_answer_callstate(self):
        if not self.callstate:
            return ""
        return self.callstate

    def get_answer_score(self, cached=True):
        if cached and self.score and self.score != Answer.MarkOptions.UNMARKED:
            if self.score == Answer.MarkOptions.CORRECT:
                return 2
            elif self.score == Answer.MarkOptions.HALF_MARK:
                return 1
            elif self.score == Answer.MarkOptions.INCORRECT:
                return 0

        q = self.question

        # First step we check that the normal/abnormal states match
        if q.normal != self.normal:
            new_callstate = UserAnswer.CallStateOptions.OVERCALL if q.normal else UserAnswer.CallStateOptions.UNDERCALL
            if self.score != Answer.MarkOptions.INCORRECT or self.callstate != new_callstate:
                self.score = Answer.MarkOptions.INCORRECT
                self.callstate = new_callstate
                self.save()
            return 0
        elif q.normal and self.normal:
            if self.score != Answer.MarkOptions.CORRECT or self.callstate != UserAnswer.CallStateOptions.CORRECT:
                self.score = Answer.MarkOptions.CORRECT
                self.callstate = UserAnswer.CallStateOptions.CORRECT
                self.save()
            return 2

        ans = self.answer_compare

        if ans == "":
            if self.score != Answer.MarkOptions.INCORRECT or self.callstate != UserAnswer.CallStateOptions.INCORRECTCALL:
                self.score = Answer.MarkOptions.INCORRECT
                self.callstate = UserAnswer.CallStateOptions.INCORRECTCALL
                self.save()
            return 0

        marked_ans = None
        for a in q.answers.all():
            if a.answer_compare.lower() == ans.lower():
                marked_ans = a
                break

        mark = "unmarked"
        if marked_ans is not None:
            new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
            if marked_ans.status == Answer.MarkOptions.CORRECT:
                new_callstate = UserAnswer.CallStateOptions.CORRECT
                mark = 2
            elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
                new_callstate = UserAnswer.CallStateOptions.PARTIALCALL
                mark = 1
            elif marked_ans.status == Answer.MarkOptions.INCORRECT:
                new_callstate = UserAnswer.CallStateOptions.INCORRECTCALL
                mark = 0

            if self.score != marked_ans.status or self.callstate != new_callstate:
                self.score = marked_ans.status
                self.callstate = new_callstate
                self.save()
        else:
            if self.score != Answer.MarkOptions.UNMARKED or self.callstate is not None:
                self.score = Answer.MarkOptions.UNMARKED
                self.callstate = None
                self.save()

        return mark
