from logging import getLogger

from inspect_ai.scorer import (
    CORRECT,
    INCORRECT,
    NOANSWER,
    PARTIAL,
    Metric,
    SampleScore,
    Value,
    ValueToFloat,
    metric,
    value_to_float,
)

logger = getLogger(__name__)


def precision_value_to_float(
    correct: Value = CORRECT,
    incorrect: Value = INCORRECT,
    partial: Value = PARTIAL,
    noanswer: Value = NOANSWER,
) -> ValueToFloat:
    """Create a ValueToFloat function.

    Create a ValueToFloat function that maps scalar values of
    different types into floats, specifically handling noanswer
    by treating it specially. For strings, common boolean
    representations (e.g. 'yes', 'no', 'true', 'false') are
    mapped to 1 and 0. In addition, the specified correct,
    incorrect, partial, and noanswer values (by default "C"
    "I", "P", and "N") are mapped to 1, 0, 0.5, and -1. Note that
    those are the default literal values, but they can be
    customized. Strings with only numbers are converted, and
    numeric values are cast to float. Arrays and dictionaries
    give a warning and return 0.

    Args:
       correct (Value): Value that represents a correct answer (1)
       incorrect (Value): Value that represents an incorrect answer (0)
       partial (Value): Value to assign partial credit for (0.5)
       noanswer (Value): Value for refusals to answer (-1)

    Returns:
        ValueToFloat function.
    """

    def to_float(value: Value) -> float:
        if isinstance(value, int | float | bool):
            return float(value)
        elif value == noanswer:
            return -1
        else:
            return value_to_float(
                correct=correct, incorrect=incorrect, partial=partial, noanswer=noanswer
            )(value)

    return to_float


@metric
def precision(to_float: ValueToFloat = precision_value_to_float()) -> Metric:
    r"""Compute proportion of total answers which are correct to total questions answered.

    Args:
       to_float: Function for mapping `Value` to float for computing
          metrics. The default `value_to_float()` maps CORRECT ("C") to 1.0,
          INCORRECT ("I") to 0, PARTIAL ("P") to 0.5, and NOANSWER ("N") to -1,
          casts numeric values to float directly, and prints a warning and returns
          0 if the Value is a complex object (list or dict).

          Note that this value_to_float must return -1 for NOANSWER in order for
          the precision metric to be computed correctly.

    Returns:
       Precision metric
    """

    def metric(scores: list[SampleScore]) -> float:
        answered = [item for item in scores if to_float(item.score.value) != -1]
        if len(answered) == 0:
            return 0.0

        total = 0.0
        for item in answered:
            total += to_float(item.score.value)
        return total / float(len(answered))

    return metric


@metric
def coverage(to_float: ValueToFloat = precision_value_to_float()) -> Metric:
    r"""Compute proportion of answered questions to total questions.

    Args:
       to_float: Function for mapping `Value` to float for computing
          metrics. The default `value_to_float()` maps CORRECT ("C") to 1.0,
          INCORRECT ("I") to 0, PARTIAL ("P") to 0.5, and NOANSWER ("N") to -1,
          casts numeric values to float directly, and prints a warning and returns
          0 if the Value is a complex object (list or dict).

          Note that this value_to_float must return -1 for NOANSWER in order for
          the precision metric to be computed correctly.

    Returns:
       Coverage metric
    """

    def metric(scores: list[SampleScore]) -> float:
        if len(scores) == 0:
            return 0.0
        # Filter to only answered questions
        answered = [item for item in scores if to_float(item.score.value) != -1]
        return float(len(answered)) / float(len(scores))

    return metric
