"""MusicBrainz client: the default free metadata source.

Deliberately small and polite:
* one global throttle so we never exceed ~1 request/second (MB's published limit);
* a descriptive User-Agent (MB rejects anonymous clients);
* retry/backoff on 503 and network errors;
* responses cached in the ``mb_cache`` table so completeness never hits the API
  during a normal page render.
"""

from __future__ import annotations

import json
import logging
import os
import threading
import time

import requests

from . import db

logger = logging.getLogger("homelabtoolkit.musicbrainz")

MB_BASE = "https://musicbrainz.org/ws/2"
USER_AGENT = os.environ.get(
    "MUSICBRAINZ_USER_AGENT",
    "HomelabToolkit/1.0 ( https://github.com/homelabtoolkit )",
)
MIN_INTERVAL_SECONDS = 1.1
CACHE_MAX_AGE_DAYS = 30

# Release-group filtering. Primary type must be Album; any of these secondary
# types excludes it (live albums, compilations, soundtracks, remixes, etc.).
# Kept as module constants so the filter can be relaxed later in one place.
INCLUDED_PRIMARY_TYPES = {"album"}
EXCLUDED_SECONDARY_TYPES = {
    "live",
    "compilation",
    "soundtrack",
    "remix",
    "dj-mix",
    "mixtape/street",
    "demo",
    "interview",
    "audiobook",
    "audio drama",
    "spokenword",
}

_throttle_lock = threading.Lock()
_last_request_at = 0.0


def _throttle() -> None:
    global _last_request_at
    with _throttle_lock:
        wait = MIN_INTERVAL_SECONDS - (time.monotonic() - _last_request_at)
        if wait > 0:
            time.sleep(wait)
        _last_request_at = time.monotonic()


def _cache_get(cache_key: str, max_age_days: int = CACHE_MAX_AGE_DAYS):
    cutoff = time.time() - max_age_days * 86400
    with db.connect() as conn:
        row = conn.execute(
            "SELECT payload, fetched_at FROM mb_cache WHERE cache_key=?", (cache_key,)
        ).fetchone()
    if not row:
        return None
    # fetched_at is ISO; treat anything older than the cutoff as a miss.
    try:
        import datetime as _dt

        fetched = _dt.datetime.fromisoformat((row["fetched_at"] or "").replace("Z", "+00:00"))
        if fetched.timestamp() < cutoff:
            return None
    except ValueError:
        pass
    try:
        return json.loads(row["payload"])
    except (TypeError, ValueError):
        return None


def _cache_put(cache_key: str, payload) -> None:
    with db.connect() as conn:
        conn.execute(
            "INSERT INTO mb_cache(cache_key, payload, fetched_at) VALUES(?,?,?) "
            "ON CONFLICT(cache_key) DO UPDATE SET payload=excluded.payload, fetched_at=excluded.fetched_at",
            (cache_key, json.dumps(payload), db.now_iso()),
        )


def _request(path: str, params: dict, cache_key: str, *, use_cache: bool = True):
    if use_cache:
        cached = _cache_get(cache_key)
        if cached is not None:
            return cached

    headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
    query = {**params, "fmt": "json"}
    for attempt in range(4):
        _throttle()
        try:
            resp = requests.get(f"{MB_BASE}/{path}", params=query, headers=headers, timeout=25)
            if resp.status_code == 503:
                time.sleep(2.0 * (attempt + 1))
                continue
            resp.raise_for_status()
            data = resp.json()
            _cache_put(cache_key, data)
            return data
        except requests.RequestException as exc:
            logger.warning("MusicBrainz request failed (%s attempt %d): %s", path, attempt + 1, exc)
            time.sleep(1.5 * (attempt + 1))
    return None


def search_artist(name: str) -> dict | None:
    """Best artist match for a name, with a 0–1 confidence score."""
    cache_key = f"artist_search::{name.lower()}"
    data = _request("artist", {"query": name, "limit": 5}, cache_key)
    if not data:
        return None
    artists = data.get("artists") or []
    if not artists:
        return None
    best = artists[0]
    return {
        "mbid": best.get("id"),
        "name": best.get("name") or "",
        "confidence": round(int(best.get("score", 0)) / 100.0, 3),
    }


def fetch_release_groups(artist_mbid: str) -> list[dict]:
    """All release groups for an artist (paged). Filtering happens later so the
    completeness filters can change without re-fetching."""
    results: list[dict] = []
    offset = 0
    limit = 100
    while True:
        cache_key = f"release_groups::{artist_mbid}::{offset}"
        data = _request(
            "release-group",
            {"artist": artist_mbid, "type": "album", "limit": limit, "offset": offset},
            cache_key,
        )
        if not data:
            break
        batch = data.get("release-groups") or []
        for rg in batch:
            results.append(
                {
                    "mbid": rg.get("id"),
                    "title": rg.get("title") or "",
                    "first_release_year": _year(rg.get("first-release-date")),
                    "primary_type": (rg.get("primary-type") or "").strip(),
                    "secondary_types": [s.strip() for s in (rg.get("secondary-types") or [])],
                }
            )
        total = int(data.get("release-group-count", len(results)))
        offset += limit
        if offset >= total or not batch:
            break
    return results


def is_official_album(primary_type: str | None, secondary_types: list[str] | None) -> bool:
    """Apply the default album filter (excludes EP/single/live/comp/etc.)."""
    if (primary_type or "").strip().lower() not in INCLUDED_PRIMARY_TYPES:
        return False
    for secondary in secondary_types or []:
        if secondary.strip().lower() in EXCLUDED_SECONDARY_TYPES:
            return False
    return True


def _year(date_str: str | None) -> int:
    if not date_str:
        return 0
    try:
        return int(str(date_str)[:4])
    except (ValueError, TypeError):
        return 0
