"""Resolve a MusicBrainz recording MBID for each track WITHOUT touching files.

Strategy per the matching design:
  1. If the track has an ISRC (Spotify), resolve it directly via MB's ISRC
     endpoint — this is exact.
  2. Otherwise (iTunes / local), text-search MB by artist + title and accept
     the top hit only above a score threshold.

Incremental + cheap:
  - Only tracks with mbid_status NULL (or 'error', to retry) are processed.
  - Every distinct lookup (ISRC or normalized text) is cached in mb_cache,
    including negative results, so duplicates across sources cost no network
    and re-runs never repeat a query.
  - MB's ~1 req/s limit is respected by musicbrainzngs; commits happen
    periodically so an interrupted run resumes where it left off.
"""
import logging
from datetime import datetime

import musicbrainzngs as mb
from sqlalchemy import or_
from tqdm import tqdm

from Env import mb_contact
from src.db.database import session_scope
from src.db.models import MbCache, Track, MB_ERROR, MB_FOUND, MB_NOTFOUND
from src.match.normalize import normalize_artist, normalize_title

TEXT_SCORE_THRESHOLD = 90
_COMMIT_EVERY = 20
_agent_ready = False


def _ensure_agent():
    global _agent_ready
    if not _agent_ready:
        mb.set_useragent("MusicIndexer", "0.1", mb_contact)
        mb.set_rate_limit(1.0, 1)  # 1 request / second
        # musicbrainzngs logs a warning per unparsed XML element; quiet it.
        logging.getLogger("musicbrainzngs").setLevel(logging.ERROR)
        _agent_ready = True


def _score(rec):
    try:
        return int(rec.get("ext:score", rec.get("score", 0)))
    except (TypeError, ValueError):
        return 0


def _lookup_isrc(isrc):
    try:
        res = mb.get_recordings_by_isrc(isrc)
    except mb.ResponseError as exc:
        if getattr(getattr(exc, "cause", None), "code", None) == 404:
            return None, None, MB_NOTFOUND
        raise
    recs = res.get("isrc", {}).get("recording-list", [])
    return (recs[0]["id"], None, MB_FOUND) if recs else (None, None, MB_NOTFOUND)


def _lookup_text(artist, title):
    res = mb.search_recordings(artist=artist, recording=title, limit=5)
    recs = res.get("recording-list", [])
    if not recs:
        return None, None, MB_NOTFOUND
    best = max(recs, key=_score)
    score = _score(best)
    if score >= TEXT_SCORE_THRESHOLD:
        return best["id"], score, MB_FOUND
    return None, score, MB_NOTFOUND


def _cached(session, key, lookup):
    """Return (mbid, score, status) from cache or by calling `lookup` once."""
    row = session.get(MbCache, key)
    if row is not None:
        return row.mbid, row.score, row.status
    mbid, score, status = lookup()
    session.add(MbCache(query_key=key, mbid=mbid, score=score,
                        status=status, fetched_at=datetime.utcnow()))
    return mbid, score, status


def _resolve_one(session, track):
    """(mbid, score, source).

    ISRC first (exact recording), then text search on NORMALIZED artist/title
    (so the same song across sources shares one query + one cached lookup).
    Used to enrich Spotify / iTunes / untagged-local; tagged local files
    already carry their MBID from the file and skip this.
    """
    if track.isrc:
        mbid, score, status = _cached(
            session, f"isrc:{track.isrc}", lambda: _lookup_isrc(track.isrc)
        )
        if status == MB_FOUND:
            return mbid, score, MB_FOUND, "isrc"

    na, nt = normalize_artist(track.artist or ""), normalize_title(track.title or "")
    if na and nt:
        key = f"txt:{na}|{nt}"
        mbid, score, status = _cached(session, key, lambda: _lookup_text(na, nt))
        return mbid, score, status, "text"

    return None, None, MB_NOTFOUND, None


def resolve_mbids(limit=None, source=None, retry_errors=True):
    """Resolve MBIDs for unresolved (and optionally errored) tracks."""
    _ensure_agent()
    stats = {MB_FOUND: 0, MB_NOTFOUND: 0, MB_ERROR: 0}
    with session_scope() as session:
        status_filter = (
            or_(Track.mbid_status.is_(None), Track.mbid_status == MB_ERROR)
            if retry_errors else Track.mbid_status.is_(None)
        )
        query = session.query(Track).filter(status_filter)
        if source:
            query = query.filter(Track.source == source)
        tracks = query.order_by(Track.id).all()
        if limit is not None:
            tracks = tracks[:limit]

        for i, track in enumerate(tqdm(tracks, desc="resolve MBID", unit="track")):
            try:
                mbid, score, status, src = _resolve_one(session, track)
                track.mbid = mbid
                track.mbid_status = status
                track.mbid_source = src
                stats[status] = stats.get(status, 0) + 1
            except mb.WebServiceError:
                track.mbid_status = MB_ERROR
                stats[MB_ERROR] += 1
            if (i + 1) % _COMMIT_EVERY == 0:
                session.commit()
    return stats


def unresolved_count(source=None):
    with session_scope() as session:
        q = session.query(Track).filter(Track.mbid_status.is_(None))
        if source:
            q = q.filter(Track.source == source)
        return q.count()
