"""Manual library match service.

Lets users explicitly link a source track (wishlist/sync-history candidate) to
an existing library track so SoulSync stops trying to re-download it.
"""

from __future__ import annotations

import json
from typing import Any, Optional

from utils.logging_config import get_logger

logger = get_logger("library.manual_library_match")


def save_match(
    db,
    profile_id: int,
    source: str,
    source_track_id: str,
    library_track_id: int,
    **meta,
) -> bool:
    """Save (insert or replace) a manual match."""
    return db.save_manual_library_match(
        profile_id, source, source_track_id, library_track_id, **meta
    )


def get_match(
    db,
    profile_id: int,
    source: str,
    source_track_id: str,
    server_source: str = "",
) -> Optional[dict]:
    """Return match row dict or None if not found."""
    getter = getattr(db, "get_manual_library_match", None)
    if getter is None:
        return None
    return getter(profile_id, source, source_track_id, server_source)


def _first_artist_name(track: dict[str, Any]) -> str:
    artists = track.get("artists") or []
    if isinstance(artists, list) and artists:
        first = artists[0]
        if isinstance(first, dict):
            return (first.get("name") or "").strip()
        return str(first).strip()
    return (track.get("artist") or track.get("artist_name") or "").strip()


def _track_source_candidates(track: dict[str, Any], default_source: str = "") -> list[str]:
    candidates = [
        track.get("provider"),
        track.get("source"),
        default_source,
        "spotify",
    ]
    out = []
    for source in candidates:
        source = (source or "").strip()
        if source and source not in out:
            out.append(source)
    return out


def _track_id_candidates(track: dict[str, Any]) -> list[str]:
    candidates = [
        track.get("source_track_id"),
        track.get("spotify_track_id"),
        track.get("track_id"),
        track.get("id"),
        track.get("musicbrainz_recording_id"),
        track.get("deezer_id") or track.get("deezer_track_id"),
        track.get("itunes_track_id"),
        track.get("tidal_id") or track.get("tidal_track_id"),
        track.get("qobuz_id") or track.get("qobuz_track_id"),
        track.get("amazon_id") or track.get("amazon_track_id"),
    ]
    out = []
    for value in candidates:
        value = str(value).strip() if value is not None else ""
        if value and value not in out:
            out.append(value)
    return out


def get_match_for_track(
    db,
    profile_id: int,
    track: dict[str, Any],
    *,
    default_source: str = "",
    server_source: str = "",
) -> Optional[dict]:
    """Return a manual match for a wishlist/sync track.

    Exact source+ID matches are preferred, but source labels can legitimately
    change between UI surfaces (for example ``mirrored`` in sync history versus
    ``wishlist`` in the wishlist batch). Fall back to track ID and finally
    title/artist so saved manual matches are honored consistently.
    """
    if not isinstance(track, dict):
        return None

    sources = _track_source_candidates(track, default_source)
    track_ids = _track_id_candidates(track)
    for track_id in track_ids:
        for source in sources:
            match = get_match(db, profile_id, source, track_id, server_source)
            if match:
                return match

    id_getter = getattr(db, "find_manual_library_match_by_source_track_id", None)
    if id_getter is not None:
        for track_id in track_ids:
            match = id_getter(profile_id, track_id, server_source)
            if match:
                return match

    title = (track.get("name") or track.get("title") or track.get("track_name") or "").strip()
    artist = _first_artist_name(track)
    metadata_getter = getattr(db, "find_manual_library_match_by_metadata", None)
    if metadata_getter is not None and title and artist:
        return metadata_getter(profile_id, title, artist, server_source)
    return None


def delete_match(db, match_id: int, profile_id: int) -> bool:
    """Delete match by PK id, scoped to profile."""
    return db.delete_manual_library_match(match_id, profile_id)


def list_matches(db, profile_id: int, limit: int = 100) -> list[dict]:
    """Return all matches for profile, most-recently-updated first."""
    rows = db.list_manual_library_matches(profile_id, limit)
    return [_enrich_match(row, db) for row in rows]


def search_source_candidates(db, query: str, profile_id: int, limit: int = 15) -> list[dict]:
    """Search wishlist + sync history for source track candidates matching query."""
    if not query or not query.strip():
        return []

    q = query.strip()
    like = f"%{q}%"
    results: dict[tuple, dict] = {}

    # 1) Wishlist tracks
    try:
        with db._get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT
                    json_extract(spotify_data, '$.id')   AS track_id,
                    json_extract(spotify_data, '$.name') AS title,
                    json_extract(spotify_data, '$.artists[0].name') AS artist,
                    json_extract(spotify_data, '$.album.name')      AS album,
                    date_added AS added_at
                FROM wishlist_tracks
                WHERE profile_id = ?
                  AND (
                      json_extract(spotify_data, '$.name') LIKE ?
                      OR json_extract(spotify_data, '$.artists[0].name') LIKE ?
                  )
                ORDER BY date_added DESC
                LIMIT ?
            """, (profile_id, like, like, limit * 2))
            for row in cursor.fetchall():
                r = dict(row)
                if not r.get("track_id"):
                    continue
                key = ("spotify", r["track_id"])
                if key not in results:
                    results[key] = {
                        "source": "spotify",
                        "source_track_id": r["track_id"],
                        "title": r["title"] or "",
                        "artist": r["artist"] or "",
                        "album": r["album"] or "",
                        "context": "Wishlist",
                        "added_at": r["added_at"] or "",
                    }
    except Exception as exc:
        logger.debug("source_candidates wishlist query failed: %s", exc)

    # 2) Sync history — scan tracks_json blobs
    try:
        with db._get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT playlist_name, source, tracks_json, started_at
                FROM sync_history
                ORDER BY started_at DESC
                LIMIT 50
            """)
            for row in cursor.fetchall():
                sh = dict(row)
                try:
                    tracks = json.loads(sh["tracks_json"] or "[]")
                except Exception:
                    continue
                for t in tracks:
                    title = t.get("name", "")
                    artist = ""
                    artists = t.get("artists", [])
                    if artists:
                        first = artists[0]
                        artist = first.get("name", "") if isinstance(first, dict) else str(first)
                    if q.lower() not in title.lower() and q.lower() not in artist.lower():
                        continue
                    src = sh["source"] or "spotify"
                    tid = t.get("id") or t.get("spotify_track_id") or ""
                    if not tid:
                        continue
                    key = (src, tid)
                    if key not in results:
                        album = ""
                        alb = t.get("album")
                        if isinstance(alb, dict):
                            album = alb.get("name", "")
                        elif isinstance(alb, str):
                            album = alb
                        results[key] = {
                            "source": src,
                            "source_track_id": tid,
                            "title": title,
                            "artist": artist,
                            "album": album,
                            "context": sh["playlist_name"] or "",
                            "added_at": sh["started_at"] or "",
                        }
                    if len(results) >= limit * 3:
                        break
    except Exception as exc:
        logger.debug("source_candidates sync_history query failed: %s", exc)

    # Sort by recency and cap
    sorted_results = sorted(results.values(), key=lambda r: r.get("added_at", ""), reverse=True)
    return sorted_results[:limit]


def search_library_candidates(db, query: str, limit: int = 15) -> list[dict[str, Any]]:
    """Search library tracks using the existing api_search_tracks method."""
    if not query or not query.strip():
        return []
    q = query.strip()
    # Pass the full query as title (covers most single-field searches).
    # Also try as artist in parallel and merge, deduped by track id.
    title_rows = db.api_search_tracks(title=q, limit=limit)
    artist_rows = db.api_search_tracks(artist=q, limit=limit)
    seen: set[int] = set()
    merged = []
    for row in title_rows + artist_rows:
        rid = row.get('id')
        if rid not in seen:
            seen.add(rid)
            merged.append(row)
        if len(merged) >= limit:
            break
    return merged


def _enrich_match(match_row: dict, db) -> dict:
    """Add library track details to a match row."""
    out = dict(match_row)
    lib_id = match_row.get("library_track_id")
    if lib_id:
        try:
            tracks = db.api_get_tracks_by_ids([lib_id])
            if tracks:
                t = tracks[0]
                out["library_title"] = t.get("title", "")
                out["library_artist"] = t.get("artist_name", "")
                out["library_album"] = t.get("album_title", "")
                out["library_file_path"] = t.get("file_path", "")
                out["library_bitrate"] = t.get("bitrate")
        except Exception as exc:
            logger.debug("enrich_match track lookup failed for id=%s: %s", lib_id, exc)
    return out
