"""Reference-based evaluation judges."""

from __future__ import annotations

from difflib import SequenceMatcher

from agentbyte.cancellation_token import CancellationToken

from agentbyte.eval.base import BaseEvalJudge
from agentbyte.eval.types import AnswerStrategy, EvalScore, EvalTrajectory


def _failed_score(judge_name: str, trajectory: EvalTrajectory) -> EvalScore:
    reason = trajectory.error or "Execution failed"
    return EvalScore(
        overall=0.0,
        dimensions={"accuracy": 0.0},
        reasoning={"accuracy": reason},
        trajectory=trajectory,
        metadata={"judge": judge_name, "failed_execution": True},
    )


class ExactMatchJudge(BaseEvalJudge):
    """Score 10.0 for exact answer matches and 0.0 otherwise."""

    def __init__(
        self,
        *,
        name: str = "ExactMatch",
        answer_strategy: AnswerStrategy = "last_non_empty",
        source_filter: str | None = None,
        case_sensitive: bool = False,
        strip_whitespace: bool = True,
    ) -> None:
        super().__init__(name=name, answer_strategy=answer_strategy, source_filter=source_filter)
        self.case_sensitive = case_sensitive
        self.strip_whitespace = strip_whitespace

    async def score(
        self,
        trajectory: EvalTrajectory,
        criteria: list[str] | None = None,
        cancellation_token: CancellationToken | None = None,
    ) -> EvalScore:
        if trajectory.success is False:
            return _failed_score(self.name, trajectory)
        if trajectory.task.expected_output is None:
            raise ValueError("ExactMatchJudge requires task.expected_output")

        actual = self.extract_answer(trajectory)
        expected = trajectory.task.expected_output
        if self.strip_whitespace:
            actual = actual.strip()
            expected = expected.strip()
        if not self.case_sensitive:
            actual = actual.lower()
            expected = expected.lower()

        matched = actual == expected
        score = 10.0 if matched else 0.0
        reason = "Exact match" if matched else f"Expected {expected!r}, got {actual!r}"
        return EvalScore(
            overall=score,
            dimensions={"accuracy": score},
            reasoning={"accuracy": reason},
            trajectory=trajectory,
            metadata={"judge": self.name},
        )


class ContainsJudge(BaseEvalJudge):
    """Score 10.0 when the expected output appears in the answer."""

    def __init__(
        self,
        *,
        name: str = "Contains",
        answer_strategy: AnswerStrategy = "last_non_empty",
        source_filter: str | None = None,
        case_sensitive: bool = False,
    ) -> None:
        super().__init__(name=name, answer_strategy=answer_strategy, source_filter=source_filter)
        self.case_sensitive = case_sensitive

    async def score(
        self,
        trajectory: EvalTrajectory,
        criteria: list[str] | None = None,
        cancellation_token: CancellationToken | None = None,
    ) -> EvalScore:
        if trajectory.success is False:
            return _failed_score(self.name, trajectory)
        if trajectory.task.expected_output is None:
            raise ValueError("ContainsJudge requires task.expected_output")

        actual = self.extract_answer(trajectory)
        expected = trajectory.task.expected_output
        if not self.case_sensitive:
            actual = actual.lower()
            expected = expected.lower()

        matched = expected in actual
        score = 10.0 if matched else 0.0
        reason = (
            "Expected output found in response"
            if matched
            else f"Expected substring {expected!r} not found in {actual!r}"
        )
        return EvalScore(
            overall=score,
            dimensions={"accuracy": score},
            reasoning={"accuracy": reason},
            trajectory=trajectory,
            metadata={"judge": self.name},
        )


class FuzzyMatchJudge(BaseEvalJudge):
    """Score answers proportionally by fuzzy text similarity."""

    def __init__(
        self,
        *,
        name: str = "FuzzyMatch",
        answer_strategy: AnswerStrategy = "last_non_empty",
        source_filter: str | None = None,
        case_sensitive: bool = False,
        threshold: float = 0.8,
    ) -> None:
        if not 0 <= threshold <= 1:
            raise ValueError("threshold must be within [0, 1]")
        super().__init__(name=name, answer_strategy=answer_strategy, source_filter=source_filter)
        self.case_sensitive = case_sensitive
        self.threshold = threshold

    async def score(
        self,
        trajectory: EvalTrajectory,
        criteria: list[str] | None = None,
        cancellation_token: CancellationToken | None = None,
    ) -> EvalScore:
        if trajectory.success is False:
            return _failed_score(self.name, trajectory)
        if trajectory.task.expected_output is None:
            raise ValueError("FuzzyMatchJudge requires task.expected_output")

        actual = self.extract_answer(trajectory)
        expected = trajectory.task.expected_output
        if not self.case_sensitive:
            actual = actual.lower()
            expected = expected.lower()

        ratio = SequenceMatcher(None, actual, expected).ratio()
        if self.threshold == 0:
            score = 10.0
        else:
            score = min(max((ratio / self.threshold) * 10.0, 0.0), 10.0)
        reason = f"Similarity ratio={ratio:.3f}, threshold={self.threshold:.3f}"
        return EvalScore(
            overall=score,
            dimensions={"accuracy": score},
            reasoning={"accuracy": reason},
            trajectory=trajectory,
            metadata={
                "judge": self.name,
                "ratio": ratio,
                "threshold": self.threshold,
            },
        )


__all__ = ["ExactMatchJudge", "ContainsJudge", "FuzzyMatchJudge"]
