from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from genderbench.probing.attempt import Attempt
    from genderbench.probing.probe import Probe


class Evaluator(ABC):
    """`Evaluators` evaluate answers generated by attempts. The results of the
    evaluation can be anything that is appropriate for a probe.

    Warning:
        The output of `Evaluator` must be JSON-serializible.

    Args:
        probe (Probe): `Probe` object that uses this `Evaluator`.
        undetected (Any): Special value for `Attempts` where the evaluation was
            inconclusive or otherwise faulty. Example when this value might be
            used: We are trying to detect options such as "(a)", "(b)", and
            "(c)" in an asnwer to a multiple-choice question. If we fail to find
            any of those values or we find more than one, we will return
            `undetected` to indicate that the evaluation was not successful.
    """

    def __init__(self, probe: "Probe", undetected: Any = None):
        self.probe = probe
        self.undetected = undetected

    def evaluate(self, attempt: "Attempt") -> Any:
        """Perform the evaluation of `Attempt`. We first calculate the value
        with a calculation method :attr:`calculate_evaluation` and then validate
        it with a validation method :attr:`validate_evaluation`.

        Args:
            attempt (Attempt): `Attempt` that already has an `answer` generated.

        Raises:
            ValueError: If the evaluation does not return a valid value.

        Returns:
            Any: The result of the evaluation
        """
        result = self.calculate_evaluation(attempt)
        if result == self.undetected:
            attempt.evaluation_undetected = True
            return result
        if self.validate_evaluation(result):
            attempt.evaluation_undetected = False
            return result
        else:
            raise ValueError(f"The calculated evaluation value {result} is not valid.")

    @abstractmethod
    def calculate_evaluation(self, attempt: "Attempt") -> Any:
        """Perform the core evaluation routine.

        Args:
            attempt (Attempt): `Attempt` that already has an `answer` generated.

        Returns:
            Any: The result of the evaluation
        """
        raise NotImplementedError

    def validate_evaluation(self, evaluation: Any) -> bool:
        """Validate the value calculated by :attr:`calculate_evaluation`.

        Args:
            evaluation (Any): To-be-validated `evaluation` value.

        Returns:
            bool: Is `evaluation` valid.
        """
        return True

    def __call__(self, attempt: "Attempt") -> Any:
        return self.evaluate(attempt)


class ClosedSetEvaluator(Evaluator):
    """A subclass of `Evaluator` that only allows values from a predefined set
    of `options`.

    Inherits all the parameters from `Evaluator`.

    Args:
        options (list[Any]): A list of allowed values

    Attributes:
        options (list[Any]): See Parameters.
    """

    def __init__(self, probe: "Probe", options: list[Any], undetected: Any = None):
        super().__init__(probe, undetected)
        self.options = options

    def validate_evaluation(self, evaluation: Any) -> bool:
        return evaluation in self.options
