"""Ingest raw Spotify API responses (the JSON written by SpotifyWebAPI) into the
DB. Unions liked + top + playlist tracks, upserting by Spotify track id and
pulling the ISRC from external_ids — so Spotify tracks get a real identifier
for matching with no extra calls.
"""
import json
import logging
import os

from tqdm import tqdm

from Env import spotify_cache_dir
from src.db.database import session_scope
from src.db.models import IngestState, SOURCE_SPOTIFY, Track
from src.match.normalize import norm_key

log = logging.getLogger("musicindexer.spotify")

# Files written by SpotifyWebAPI.fetch_data(), each holding full track objects,
# mapped to the origin flag they set on a Track.
_SOURCES = {
    "tracks.json": "liked",
    "playlistTracks.json": "playlist",
    "top_tracks.json": "top",
}
_STATE_KEY = "spotify_cache_sig"

# origin name -> Track boolean column
_ORIGIN_FLAG = {"liked": "liked", "playlist": "from_playlist", "top": "from_top"}


def _cache_signature(spotify_dir):
    """Latest mtime across the cached payloads; changes whenever a fetch
    rewrites them, so an unchanged cache can be skipped."""
    mtimes = [os.path.getmtime(os.path.join(spotify_dir, f))
              for f in _SOURCES if os.path.isfile(os.path.join(spotify_dir, f))]
    return str(max(mtimes)) if mtimes else "0"


def prune_top_only(session, include_top):
    """Drop Spotify tracks that are *only* there because the top-tracks endpoint
    listed them — i.e. neither liked nor in any playlist. Keeps top tracks when
    the caller opted in. Safe only after a full pass that re-set the flags.
    Returns the number deleted."""
    keep = Track.liked.is_(True) | Track.from_playlist.is_(True)
    if include_top:
        keep = keep | Track.from_top.is_(True)
    doomed = (session.query(Track)
              .filter(Track.source == SOURCE_SPOTIFY)
              .filter(~keep).all())
    for track in doomed:
        session.delete(track)
    return len(doomed)


def spotify_track_fields(track_obj):
    """Map a Spotify full-track object to our Track columns."""
    names = [a.get("name") for a in track_obj.get("artists", []) if a.get("name")]
    primary = names[0] if names else None
    title = track_obj.get("name")
    return dict(
        title=title, artist=primary, artists=", ".join(names),
        album=(track_obj.get("album") or {}).get("name"),
        duration_ms=track_obj.get("duration_ms"),
        isrc=(track_obj.get("external_ids") or {}).get("isrc"),
        norm_key=norm_key(primary or "", title or ""),
    )


def upsert_spotify_track(session, existing, track_obj, counts, origin=None):
    """Upsert one Spotify track into `existing` (source_id -> Track), updating
    `counts`. Resets MBID enrichment only if the ISRC actually changed. `origin`
    (liked / playlist / top) sets the matching flag True and is never cleared,
    so a track seen from several sources accumulates all of them."""
    spotify_id = track_obj.get("id")
    if not spotify_id:
        return
    flag = _ORIGIN_FLAG.get(origin)
    fields = spotify_track_fields(track_obj)
    new_isrc = fields.pop("isrc")
    track = existing.get(spotify_id)
    if track is None:
        track = Track(source=SOURCE_SPOTIFY, source_id=spotify_id,
                      isrc=new_isrc, **fields)
        if flag:
            setattr(track, flag, True)
        session.add(track)
        existing[spotify_id] = track
        counts["new"] += 1
        return
    for key, value in fields.items():
        setattr(track, key, value)
    if flag:
        setattr(track, flag, True)
    if new_isrc and new_isrc != track.isrc:
        track.isrc, track.mbid, track.mbid_status, track.mbid_source = (
            new_isrc, None, None, None)
    elif new_isrc:
        track.isrc = new_isrc
    counts["updated"] += 1


def _track_objects(spotify_dir, include_top):
    """Yield (track_obj, origin) for every cached track. Top tracks are skipped
    unless include_top, so by default they don't pollute the library."""
    for fname, origin in _SOURCES.items():
        if origin == "top" and not include_top:
            continue
        path = os.path.join(spotify_dir, fname)
        if not os.path.isfile(path):
            continue
        with open(path, encoding="utf-8") as fh:
            data = json.load(fh)
        items = data if isinstance(data, list) else [v for lst in data.values() for v in lst]
        for item in items:
            track = item.get("track") if isinstance(item, dict) and "track" in item else item
            if track and track.get("id"):
                yield track, origin


def ingest_spotify(spotify_dir=None, force=False, include_top=False):
    spotify_dir = str(spotify_dir or spotify_cache_dir)
    if not os.path.isdir(spotify_dir):
        log.warning("no Spotify cache at %s — skipping", spotify_dir)
        return {"new": 0, "updated": 0, "skipped": True}

    signature = f"{_cache_signature(spotify_dir)}|top={int(include_top)}"
    with session_scope() as session:
        state = session.get(IngestState, _STATE_KEY)
        if state and state.value == signature and not force:
            log.info("Spotify cache unchanged — skipping")
            return {"new": 0, "updated": 0, "skipped": True}

    counts = {"new": 0, "updated": 0}
    with session_scope() as session:
        existing = {t.source_id: t for t in
                    session.query(Track).filter_by(source=SOURCE_SPOTIFY).all()}
        for track, origin in tqdm(list(_track_objects(spotify_dir, include_top)),
                                  desc="ingest spotify", unit="track"):
            upsert_spotify_track(session, existing, track, counts, origin=origin)

        # The cache is a complete snapshot, so it's safe to drop top-only rows.
        pruned = prune_top_only(session, include_top)

        state = session.get(IngestState, _STATE_KEY) or IngestState(key=_STATE_KEY)
        state.value = signature
        session.merge(state)

    log.info("spotify: %d new, %d updated, %d top-only pruned",
             counts["new"], counts["updated"], pruned)
    return counts


def fetch_spotify_live():
    """Run the real OAuth Spotify fetch into the cache dir (requires creds)."""
    from Env import client_id, client_secret, data_dir
    from src.backends.spotify.SpotifyWebAPI import fetch_all

    fetch_all(client_id, client_secret, data_dir)  # writes data_dir/spotify/*.json
