"""Name normalisation and fuzzy matching for collection completeness.

Normalisation never mutates the stored original — callers keep both the original
and normalised values. The point is to make "Album (Deluxe Edition)" and
"Album - 2009 Remaster" collapse to the same comparable key without losing the
display name.
"""

from __future__ import annotations

import re
from difflib import SequenceMatcher

_BRACKET_RE = re.compile(r"[\(\[\{].*?[\)\]\}]")
_PUNCT_RE = re.compile(r"[^\w\s]", re.UNICODE)
_WS_RE = re.compile(r"\s+")

# Edition / remaster qualifiers stripped from album titles before comparison.
_EDITION_PATTERNS = [
    r"\bsuper deluxe( edition)?\b",
    r"\bdeluxe( edition| version)?\b",
    r"\bexpanded( edition| version)?\b",
    r"\bspecial edition\b",
    r"\bcollector'?s edition\b",
    r"\blegacy edition\b",
    r"\banniversary( edition)?\b",
    r"\b\d{1,3}(st|nd|rd|th) anniversary\b",
    r"\bremaster(ed)?\b",
    r"\bre-?master(ed)?\b",
    r"\breissue\b",
    r"\bbonus track(s)?( version)?\b",
    r"\bbonus edition\b",
    r"\b\d{4} remaster\b",
    r"\bexplicit( version)?\b",
    r"\bclean( version)?\b",
    r"\bmono\b",
    r"\bstereo\b",
    r"\bdisc \d+\b",
    r"\bcd\d+\b",
]
_EDITION_RE = re.compile("|".join(_EDITION_PATTERNS), re.IGNORECASE)

VARIOUS_ARTISTS = {
    "various artists",
    "various",
    "va",
    "v a",
    "soundtrack",
    "original soundtrack",
}


def _base_clean(text: str) -> str:
    text = text.lower()
    text = _BRACKET_RE.sub(" ", text)        # drop (...) [...] {...}
    text = _EDITION_RE.sub(" ", text)        # drop edition/remaster words
    text = text.replace("&", " and ")
    text = _PUNCT_RE.sub(" ", text)          # drop remaining punctuation
    return _WS_RE.sub(" ", text).strip()


def normalize_title(text: str | None) -> str:
    if not text:
        return ""
    return _base_clean(text)


def normalize_artist(text: str | None) -> str:
    if not text:
        return ""
    cleaned = _base_clean(text)
    if cleaned.startswith("the "):
        cleaned = cleaned[4:]
    return cleaned


def is_various_artists(name: str | None) -> bool:
    if not name:
        return False
    return normalize_artist(name) in VARIOUS_ARTISTS or _base_clean(name) in VARIOUS_ARTISTS


def similarity(a: str | None, b: str | None) -> float:
    na, nb = normalize_title(a), normalize_title(b)
    if not na or not nb:
        return 0.0
    if na == nb:
        return 1.0
    return SequenceMatcher(None, na, nb).ratio()


# ── Completeness classification ───────────────────────────────────────────────
# Statuses: owned | probably_owned | missing | uncertain  (ignored is manual).

OWNED = "owned"
PROBABLY_OWNED = "probably_owned"
UNCERTAIN = "uncertain"
MISSING = "missing"

FUZZY_PROBABLE = 0.88
FUZZY_UNCERTAIN = 0.60


def classify_release(
    ext_title: str,
    ext_year: int | None,
    ext_mbid: str | None,
    local_albums: list[dict],
) -> tuple[str, float, str, int | None]:
    """Decide a status for one external release group against local albums.

    ``local_albums`` items: ``{id, title, title_normalized, year, mbid}``.
    Match order: MusicBrainz id → normalised title (+year) → fuzzy title.
    Returns ``(status, confidence, reason, local_album_id|None)``.
    """
    ext_norm = normalize_title(ext_title)

    # 1) Exact MusicBrainz id match.
    if ext_mbid:
        for la in local_albums:
            if la.get("mbid") and la["mbid"] == ext_mbid:
                return (OWNED, 1.0, "MusicBrainz ID match", la["id"])

    # 2) Normalised title (with year corroboration).
    if ext_norm:
        for la in local_albums:
            if la.get("title_normalized") == ext_norm:
                ly, ey = la.get("year") or 0, ext_year or 0
                if ly and ey and abs(ly - ey) <= 1:
                    return (OWNED, 0.95, "Title and year match", la["id"])
                return (PROBABLY_OWNED, 0.85, "Normalised title match", la["id"])

    # 3) Fuzzy title.
    best_ratio, best = 0.0, None
    for la in local_albums:
        r = similarity(ext_title, la.get("title"))
        if r > best_ratio:
            best_ratio, best = r, la
    if best is not None:
        if best_ratio >= FUZZY_PROBABLE:
            return (PROBABLY_OWNED, round(best_ratio, 3), f"Fuzzy title match ({best_ratio:.2f})", best["id"])
        if best_ratio >= FUZZY_UNCERTAIN:
            return (UNCERTAIN, round(best_ratio, 3), f"Weak title match ({best_ratio:.2f})", best["id"])

    return (MISSING, 0.0, "No matching local album", None)
