"""TUI fuzzy matching — subsequence matching with scoring."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, TypeVar

T = TypeVar("T")


@dataclass
class FuzzyMatch:
    """Result of a fuzzy match."""

    matches: bool
    score: float


def fuzzy_match(query: str, text: str) -> FuzzyMatch:
    """Fuzzy-match query against text.

    Returns whether all characters in query appear in order in text,
    plus a score (lower = better match).

    Scoring:
    - Consecutive matches: -5 per consecutive character
    - Word boundary matches: -10 bonus
    - Gap penalty: +2 per gap character
    - Position penalty: +0.1 per position in text
    """
    if not query:
        return FuzzyMatch(matches=True, score=0)

    q = query.lower()
    t = text.lower()

    qi = 0  # Query index
    score = 0.0
    last_match_pos = -2  # Track consecutive matches

    for ti in range(len(t)):
        if qi >= len(q):
            break
        if t[ti] == q[qi]:
            # Position penalty
            score += ti * 0.1

            # Consecutive bonus
            if ti == last_match_pos + 1:
                score -= 5

            # Word boundary bonus
            if ti == 0 or t[ti - 1] in " _-./\\":
                score -= 10

            # Gap penalty
            if last_match_pos >= 0:
                gap = ti - last_match_pos - 1
                score += gap * 2

            last_match_pos = ti
            qi += 1

    if qi < len(q):
        return FuzzyMatch(matches=False, score=float("inf"))

    return FuzzyMatch(matches=True, score=score)


def fuzzy_filter(
    items: list[T],
    query: str,
    get_text: Callable[[T], str] | None = None,
) -> list[T]:
    """Filter and sort items by fuzzy match quality.

    Handles space-separated tokens (all must match).
    Returns items sorted by total score (lower first = best).
    """
    if not query.strip():
        return list(items)

    if get_text is None:
        get_text = str  # type: ignore[assignment]

    tokens = query.strip().split()

    scored: list[tuple[float, int, T]] = []
    for idx, item in enumerate(items):
        text = get_text(item)  # type: ignore[misc]
        total_score = 0.0
        all_match = True

        for token in tokens:
            result = fuzzy_match(token, text)
            if not result.matches:
                all_match = False
                break
            total_score += result.score

        if all_match:
            scored.append((total_score, idx, item))

    scored.sort(key=lambda x: (x[0], x[1]))
    return [item for _, _, item in scored]
