"""Pure normalization and conservative candidate matching."""

import re
import unicodedata
from dataclasses import dataclass
from difflib import SequenceMatcher

from app.domain.models import Track, TrackCandidate

_SUFFIX_PATTERN = re.compile(r"(?:\s*(?:\([^)]*\)|\[[^]]*\]))+\s*$")
_DASH_SUFFIX_PATTERN = re.compile(r"\s+-\s+[^-]+$")


@dataclass(frozen=True)
class MatchVerdict:
    """Match decision plus the two signals it was derived from.

    A rejection is attributable: either title_similarity fell below the
    caller's threshold, or is_artist_match is false, or both.
    """

    is_match: bool
    title_similarity: float
    is_artist_match: bool


def normalize_text(value: str) -> str:
    """Normalize names for stable comparisons without transport knowledge."""
    decomposed = unicodedata.normalize("NFKD", value.casefold())
    without_marks = "".join(
        character for character in decomposed if not unicodedata.combining(character)
    )
    without_suffix = _SUFFIX_PATTERN.sub("", without_marks)
    words = "".join(character if character.isalnum() else " " for character in without_suffix)
    return " ".join(words.split())


def title_similarity(candidate_title: str, track_title: str) -> float:
    """Return normalized title similarity, tolerating version dash suffixes."""
    normalized_candidate = normalize_text(candidate_title)
    full_similarity = _ratio(normalized_candidate, normalize_text(track_title))
    # Spotify appends version info as "Title - Remaster 2023"; some tracks
    # only exist in suffixed releases. The tiny penalty keeps an exact
    # original title ahead of a suffixed release at equal similarity.
    stripped_title = _DASH_SUFFIX_PATTERN.sub("", track_title)
    stripped_similarity = _ratio(normalized_candidate, normalize_text(stripped_title)) - 0.001
    return max(full_similarity, stripped_similarity)


def _ratio(left: str, right: str) -> float:
    return SequenceMatcher(None, left, right).ratio()


def artist_matches(candidate_artist: str, track_artists: tuple[str, ...]) -> bool:
    """Require one normalized Spotify artist to equal the proposed artist."""
    normalized_candidate = normalize_text(candidate_artist)
    return bool(normalized_candidate) and any(
        normalize_text(track_artist) == normalized_candidate for track_artist in track_artists
    )


def judge_candidate_match(
    candidate: TrackCandidate,
    track: Track,
    title_threshold: float,
) -> MatchVerdict:
    """Accept only a similar title paired with a near-exact artist."""
    similarity = title_similarity(candidate.title, track.title)
    is_artist_match = artist_matches(candidate.artist, track.artists)
    return MatchVerdict(
        is_match=similarity >= title_threshold and is_artist_match,
        title_similarity=similarity,
        is_artist_match=is_artist_match,
    )


def candidate_key(candidate: TrackCandidate) -> str:
    """Build the normalized cache key for a proposed title and artist."""
    return f"{normalize_text(candidate.title)}\x00{normalize_text(candidate.artist)}"


def track_key(track: Track) -> str:
    """Build the normalized title and primary-artist deduplication key."""
    primary_artist = track.artists[0] if track.artists else ""
    return f"{normalize_text(track.title)}\x00{normalize_text(primary_artist)}"
