"""String similarity metric — normalized Levenshtein distance."""

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


def _levenshtein_distance(s1: str, s2: str) -> int:
    """Compute the Levenshtein edit distance between two strings."""
    if len(s1) < len(s2):
        return _levenshtein_distance(s2, s1)
    if len(s2) == 0:
        return len(s1)
    prev = list(range(len(s2) + 1))
    for i, c1 in enumerate(s1):
        curr = [i + 1]
        for j, c2 in enumerate(s2):
            curr.append(
                min(
                    prev[j + 1] + 1,  # deletion
                    curr[j] + 1,  # insertion
                    prev[j] + (c1 != c2),  # substitution
                )
            )
        prev = curr
    return prev[-1]


class StringSimilarityMetric(BaseMetric):
    """Normalized Levenshtein similarity between answer and expected answer.

    Score = 1 - (edit_distance / max_length).  A score of 1.0 means the
    strings are identical; 0.0 means they share nothing in common.

    Both strings are normalized (lowercased, stripped of punctuation) before
    comparison.  Deterministic — no judge call, no external dependencies.
    """

    name: str = "string_similarity"

    def __init__(self, threshold: float = 0.5) -> None:
        self.threshold = threshold

    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="String similarity requires expected_answer",
                passed=False,
            )
        answer = normalize_text(test_case.answer or "")
        expected = normalize_text(test_case.expected_answer)
        max_len = max(len(answer), len(expected))
        if max_len == 0:
            score = 1.0
        else:
            dist = _levenshtein_distance(answer, expected)
            score = 1.0 - dist / max_len
        passed = score >= self.threshold
        return MetricScore(
            metric_name=self.name,
            score=round(score, 4),
            reason=f"Levenshtein similarity: {score:.2%}",
            passed=passed,
        )

    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)
