"""Incremental live Spotify fetch.

Only fetches what changed since last run:
  - liked songs : returned newest-first, so we page until we reach the newest
    timestamp already stored, then stop.
  - playlists   : each carries a snapshot_id; a playlist's tracks are only
    re-fetched when its snapshot_id changed (new/removed playlists handled too).
  - cover art   : downloaded only when the image is missing or its URL changed.

Each track is tagged with its origin (liked / playlist / top). The top-tracks
endpoint is only consulted when include_top=True, and on a full resync the rows
that exist *only* because the top endpoint listed them are pruned.

Writes straight to the DB (upsert). Pass full=True to force a complete resync.
"""
import logging
import os

import requests

import src.backends.spotify.SpotifyWebAPI as sp
from Env import client_id, client_secret, spotify_cache_dir
from src.db.database import session_scope
from src.db.models import IngestState, SOURCE_SPOTIFY, SpotifyPlaylist, Track
from src.ingest.spotify_raw import prune_top_only, upsert_spotify_track

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

_LIKED_WATERMARK = "liked_newest_added_at"
_USER_COVER_URL = "spotify_user_cover_url"
_COVERS_DIR = "covers"
_TOP_RANGE = "long_term"


def _existing_tracks(session):
    return {t.source_id: t for t in
            session.query(Track).filter_by(source=SOURCE_SPOTIFY).all()}


def _get_state(session, key):
    row = session.get(IngestState, key)
    return row.value if row else None


def _set_state(session, key, value):
    row = session.get(IngestState, key) or IngestState(key=key)
    row.value = value
    session.merge(row)


def _cover_path(name):
    return os.path.join(str(spotify_cache_dir), _COVERS_DIR, f"{name}.jpg")


def _download_cover(name, url, prev_url):
    """Download only if the file is missing or the URL changed. Returns True
    if a download happened."""
    if not url:
        return False
    path = _cover_path(name)
    if url == prev_url and os.path.exists(path):
        return False
    try:
        resp = requests.get(url, timeout=30)
        resp.raise_for_status()
    except requests.RequestException as exc:
        log.warning("cover download failed for %s: %s", name, exc)
        return False
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "wb") as fh:
        fh.write(resp.content)
    return True


def _sync_liked(session, existing, full):
    watermark = None if full else _get_state(session, _LIKED_WATERMARK)
    counts = {"new": 0, "updated": 0}
    newest = watermark
    offset = 0
    stop = False
    while not stop:
        page = sp.fetch(f"v1/me/tracks?limit=50&offset={offset}")
        items = page.get("items", [])
        if not items:
            break
        for item in items:
            added = item.get("added_at")
            track = item.get("track")
            if watermark and added and added <= watermark:
                stop = True
                break
            if track:
                upsert_spotify_track(session, existing, track, counts, origin="liked")
                if newest is None or (added and added > newest):
                    newest = added
        if len(items) < 50:
            break
        offset += 50
    if newest:
        _set_state(session, _LIKED_WATERMARK, newest)
    log.info("liked songs: %d new, %d updated%s", counts["new"], counts["updated"],
             "" if watermark is None else " (incremental)")
    return counts


def _sync_playlists(session, existing, full):
    playlists = sp.fetch_paginated("v1/me/playlists?limit=50")
    states = {p.playlist_id: p for p in session.query(SpotifyPlaylist).all()}
    counts = {"new": 0, "updated": 0}
    changed = unchanged = covers = 0
    seen = set()

    for pl in playlists:
        pid = pl.get("id")
        if not pid:
            continue
        seen.add(pid)
        snapshot = pl.get("snapshot_id")
        cover_url = (pl.get("images") or [{}])[0].get("url")
        row = states.get(pid)
        if row is None:
            row = SpotifyPlaylist(playlist_id=pid)
            session.add(row)
            states[pid] = row

        if row.snapshot_id == snapshot and not full:
            unchanged += 1
        else:
            items = sp.fetch_paginated(f"v1/playlists/{pid}/tracks?limit=50")
            for item in items:
                track = item.get("track")
                if track and track.get("id"):
                    upsert_spotify_track(session, existing, track, counts, origin="playlist")
            changed += 1
            row.snapshot_id = snapshot
        row.name = pl.get("name")

        if _download_cover(pid, cover_url, row.cover_url):
            covers += 1
        row.cover_url = cover_url

    for pid, row in states.items():  # playlists deleted on Spotify
        if pid not in seen:
            session.delete(row)

    log.info("playlists: %d changed, %d unchanged; tracks %d new, %d updated; "
             "%d covers fetched", changed, unchanged, counts["new"], counts["updated"], covers)
    return counts


def _sync_top(session, existing):
    """Pull the top-tracks endpoint (full objects, so they carry an ISRC just
    like liked songs) and flag them. Opt-in; off by default."""
    items = sp.fetch_paginated(
        f"v1/me/top/tracks?limit=50&time_range={_TOP_RANGE}", desc="Fetching top tracks")
    counts = {"new": 0, "updated": 0}
    for track in items:
        if track and track.get("id"):
            upsert_spotify_track(session, existing, track, counts, origin="top")
    log.info("top tracks: %d new, %d updated", counts["new"], counts["updated"])
    return counts


def _sync_user_cover(session):
    profile = sp.fetch("v1/me")
    images = profile.get("images") or []
    url = images[-1]["url"] if images else None
    if _download_cover("user", url, _get_state(session, _USER_COVER_URL)):
        _set_state(session, _USER_COVER_URL, url)
        log.info("user cover updated")


def fetch_spotify_incremental(full=False, include_top=False):
    """Authenticate and sync liked songs, playlists and cover art incrementally.

    include_top also pulls the top-tracks endpoint. On a full resync the liked
    and playlist passes re-set every flag, so we then prune any Spotify rows
    that are neither liked nor in a playlist (top-only, unless include_top)."""
    sp.update_access_token(client_id, client_secret)
    with session_scope() as session:
        existing = _existing_tracks(session)
        _sync_liked(session, existing, full)
        _sync_playlists(session, existing, full)
        if include_top:
            _sync_top(session, existing)
        _sync_user_cover(session)
        if full:
            pruned = prune_top_only(session, include_top)
            if pruned:
                log.info("pruned %d top-only / orphaned Spotify tracks", pruned)
