"""Exact match metric — does the answer match the expected answer exactly?"""

from __future__ import annotations

from evalkit.judges.base import BaseJudge
from evalkit.metrics._text import normalize_text
from evalkit.metrics.base import BaseMetric
from evalkit.models import MetricScore, TestCase


class ExactMatchMetric(BaseMetric):
    """Binary check: does the answer match the expected answer?

    When ``normalize=True`` (default), both strings are lowercased, stripped
    of punctuation, and collapsed to single spaces before comparison.

    Score is 1.0 (match) or 0.0 (no match).  Deterministic — no judge call.
    """

    name: str = "exact_match"

    def __init__(self, threshold: float = 0.5, *, normalize: bool = True) -> None:
        self.threshold = threshold
        self.normalize = normalize

    def _build_prompt(self, test_case: TestCase) -> str:
        return ""

    def _compute(self, test_case: TestCase) -> MetricScore:
        if test_case.expected_answer is None:
            return MetricScore(
                metric_name=self.name,
                score=0.0,
                reason="Exact match requires expected_answer",
                passed=False,
            )
        answer = test_case.answer or ""
        expected = test_case.expected_answer
        if self.normalize:
            answer = normalize_text(answer)
            expected = normalize_text(expected)
        match = answer == expected
        return MetricScore(
            metric_name=self.name,
            score=1.0 if match else 0.0,
            reason="Exact match" if match else f"Expected: {test_case.expected_answer!r}",
            passed=match,
        )

    def score(self, test_case: TestCase, judge: BaseJudge | None) -> MetricScore:
        return self._compute(test_case)

    async def score_async(self, test_case: TestCase, judge: BaseJudge | None) -> MetricScore:
        return self._compute(test_case)
