"""
CorrectMap: A utility class to store and manage graded responses to CAPA questions.
Provides methods to track correctness, points, messages, hints, and queue state.
"""

# -----------------------------------------------------------------------------
# class used to store graded responses to CAPA questions
#
# Used by responsetypes and capa_problem


class CorrectMap:
    """
    Stores map between answer_id and response evaluation result for each question
    in a capa problem.  The response evaluation result for each answer_id includes
    (correctness, npoints, msg, hint, hintmode).

    - correctness : 'correct', 'incorrect', 'partially-correct', or 'incomplete'
    - npoints     : None, or integer specifying number of points awarded for this answer_id
    - msg         : string (may have HTML) giving extra message response
                    (displayed below textline or textbox)
    - hint        : string (may have HTML) giving optional hint
                    (displayed below textline or textbox, above msg)
    - hintmode    : one of (None,'on_request','always') criteria for displaying hint
    - queuestate  : Dict {key:'', time:''} where key is a secret string, and time is a string dump
                    of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued

    Behaves as a dict.
    """

    def __init__(self, *args, **kwargs):
        # start with empty dict
        self.cmap = {}
        self.items = self.cmap.items
        self.keys = self.cmap.keys
        self.overall_message = ""
        self.set(*args, **kwargs)

    def __getitem__(self, *args, **kwargs):
        return self.cmap.__getitem__(*args, **kwargs)

    def __iter__(self):
        return self.cmap.__iter__()

    # See the documentation for 'set_dict' for the use of kwargs
    def set(  # pylint: disable=too-many-positional-arguments,too-many-arguments
        self,
        answer_id=None,
        correctness=None,
        npoints=None,
        msg="",
        hint="",
        hintmode=None,
        queuestate=None,
        answervariable=None,
        **kwargs,  # pylint: disable=unused-argument
    ):
        """
        Set or update the stored evaluation result for a given answer_id.
        Unused kwargs are ignored for compatibility with older formats.
        """

        if answer_id is not None:
            self.cmap[answer_id] = {
                "correctness": correctness,
                "npoints": npoints,
                "msg": msg,
                "hint": hint,
                "hintmode": hintmode,
                "queuestate": queuestate,
                "answervariable": answervariable,
            }

    def __repr__(self):
        return repr(self.cmap)

    def get_dict(self):
        """
        return dict version of self
        """
        return self.cmap

    def set_dict(self, correct_map):
        """
        Set internal dict of CorrectMap to provided correct_map dict

        correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
        means that when the definition of CorrectMap (e.g. its properties) are altered,
        an existing correct_map dict will not coincide with the newest CorrectMap format as
        defined by self.set.

        For graceful migration, feed the contents of each correct map to self.set, rather than
        making a direct copy of the given correct_map dict. This way, the common keys between
        the incoming correct_map dict and the new CorrectMap instance will be written, while
        mismatched keys will be gracefully ignored.

        Special migration case:
            If correct_map is a one-level dict, then convert it to the new dict of dicts format.

        """
        # empty current dict
        self.__init__()  # pylint: disable=unnecessary-dunder-call

        if not correct_map:
            return

        # create new dict entries
        if not isinstance(list(correct_map.values())[0], dict):
            # special migration
            for k in correct_map:
                self.set(k, correctness=correct_map[k])
        else:
            for k in correct_map:
                self.set(k, **correct_map[k])

    def is_correct(self, answer_id):
        """
        Takes an answer_id
        Returns true if the problem is correct OR partially correct.
        """
        if answer_id in self.cmap:
            return self.cmap[answer_id]["correctness"] in ["correct", "partially-correct"]
        return None

    def is_partially_correct(self, answer_id):
        """
        Takes an answer_id
        Returns true if the problem is partially correct.
        """
        if answer_id in self.cmap:
            return self.cmap[answer_id]["correctness"] == "partially-correct"
        return None

    def is_queued(self, answer_id):
        """Return True if the answer has a non-empty queue state."""
        return answer_id in self.cmap and self.cmap[answer_id]["queuestate"] is not None

    def is_right_queuekey(self, answer_id, test_key):
        """Return True if the queued answer matches the provided queue key."""
        return self.is_queued(answer_id) and self.cmap[answer_id]["queuestate"]["key"] == test_key

    def get_queuetime_str(self, answer_id):
        """Return the stored queue timestamp string for the given answer."""
        if self.cmap[answer_id]["queuestate"]:
            return self.cmap[answer_id]["queuestate"]["time"]

        return None

    def get_npoints(self, answer_id):
        """Return the number of points for an answer, used for partial credit."""
        npoints = self.get_property(answer_id, "npoints")
        if npoints is not None:
            return npoints

        if self.is_correct(answer_id):
            return 1

        # if not correct and no points have been assigned, return 0
        return 0

    def set_property(self, answer_id, prop, value):
        """Set a specific property value for the given answer_id."""
        if answer_id in self.cmap:
            self.cmap[answer_id][prop] = value
        else:
            self.cmap[answer_id] = {prop: value}

    def get_property(self, answer_id, prop, default=None):
        """Return the specified property for an answer, or a default value."""
        if answer_id in self.cmap:
            return self.cmap[answer_id].get(prop, default)
        return default

    def get_correctness(self, answer_id):
        """Return the correctness value for the given answer."""
        return self.get_property(answer_id, "correctness")

    def get_msg(self, answer_id):
        """Return the feedback message for the given answer."""
        return self.get_property(answer_id, "msg", "")

    def get_hint(self, answer_id):
        """Return the hint text associated with the given answer."""
        return self.get_property(answer_id, "hint", "")

    def get_hintmode(self, answer_id):
        """Return the hint display mode for the given answer."""
        return self.get_property(answer_id, "hintmode", None)

    def set_hint_and_mode(self, answer_id, hint, hintmode):
        """
        - hint     : (string) HTML text for hint
        - hintmode : (string) mode for hint display ('always' or 'on_request')
        """
        self.set_property(answer_id, "hint", hint)
        self.set_property(answer_id, "hintmode", hintmode)

    def update(self, other_cmap):
        """
        Update this CorrectMap with the contents of another CorrectMap
        """
        if not isinstance(other_cmap, CorrectMap):
            raise Exception(  # pylint: disable=broad-exception-raised
                f"CorrectMap.update called with invalid argument {other_cmap}"
            )
        self.cmap.update(other_cmap.get_dict())
        self.set_overall_message(other_cmap.get_overall_message())

    def set_overall_message(self, message_str):
        """Set a message that applies to the question as a whole,
        rather than to individual inputs."""
        self.overall_message = str(message_str) if message_str else ""

    def get_overall_message(self):
        """Retrieve a message that applies to the question as a whole.
        If no message is available, returns the empty string"""
        return self.overall_message
