"""Probe what THIS Spotify app is actually allowed to do.

Spotify's restrictions arrived in waves (Nov 2024, May 2025, Feb 2026), and each
wave grandfathered differently. Documentation and secondary reporting disagree
about what an older app retains. Rather than reason about it, ask the API.

    python3 scripts/spotify-probe.py

Reads SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET from ../.app.env (or the
environment). Uses the CLIENT CREDENTIALS flow — no user login, no browser, and
no user data is touched. Prints a capability map and nothing sensitive: the
credentials are never echoed, and neither is the token.

What it CANNOT tell you: how many users are on the app's allowlist. That is
dashboard-only — check it at
https://developer.spotify.com/dashboard -> your app -> User Management.
"""

from __future__ import annotations

import base64
import contextlib
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

TOKEN_URL = "https://accounts.spotify.com/api/token"
API = "https://api.spotify.com/v1"
TIMEOUT = 15

ENV_FILE = Path(__file__).resolve().parent.parent.parent / ".app.env"
PLACEHOLDER = "REPLACE_WITH_"


def load_credentials() -> tuple[str, str]:
    """Environment first, then .app.env. Values are never printed."""
    cid = os.environ.get("SPOTIFY_CLIENT_ID", "")
    secret = os.environ.get("SPOTIFY_CLIENT_SECRET", "")
    if not (cid and secret) and ENV_FILE.exists():
        for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("#") or "=" not in line:
                continue
            key, _, value = line.partition("=")
            if key == "SPOTIFY_CLIENT_ID" and not cid:
                cid = value.strip()
            elif key == "SPOTIFY_CLIENT_SECRET" and not secret:
                secret = value.strip()
    if not cid or not secret:
        sys.exit(f"No Spotify credentials found in the environment or {ENV_FILE}")
    if cid.startswith(PLACEHOLDER) or secret.startswith(PLACEHOLDER):
        sys.exit(
            f"{ENV_FILE} still holds placeholders — fill in the real client id "
            "and secret first."
        )
    return cid, secret


def get_token(cid: str, secret: str) -> str:
    basic = base64.b64encode(f"{cid}:{secret}".encode()).decode()
    req = urllib.request.Request(
        TOKEN_URL,
        data=urllib.parse.urlencode({"grant_type": "client_credentials"}).encode(),
        headers={
            "Authorization": f"Basic {basic}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
            return json.load(resp)["access_token"]
    except urllib.error.HTTPError as exc:
        sys.exit(
            f"Could not get a token ({exc.code}). The client id/secret are "
            "probably wrong, or the app was deleted."
        )


def call(token: str, path: str) -> tuple[int, dict | None]:
    req = urllib.request.Request(
        f"{API}{path}", headers={"Authorization": f"Bearer {token}"}
    )
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
            return resp.status, json.load(resp)
    except urllib.error.HTTPError as exc:
        body = None
        # An error body is a nicety, not a need — the status code is the answer.
        with contextlib.suppress(Exception):
            body = json.load(exc)
        return exc.code, body
    except urllib.error.URLError as exc:
        print(f"  network error: {exc}")
        return 0, None


def report(label: str, status: int, note: str = "") -> bool:
    ok = 200 <= status < 300
    mark = "AVAILABLE" if ok else "blocked  "
    detail = f" ({status})" if not ok else ""
    print(f"  {mark}  {label}{detail}{'  ' + note if note else ''}")
    return ok


def main() -> int:
    cid, secret = load_credentials()
    token = get_token(cid, secret)
    print(f"Probing Spotify app client_id={cid[:6]}… (rest not shown)\n")

    # A search gives a real track and artist id to probe with, so nothing here
    # depends on a hardcoded id that may vanish.
    status, data = call(token, "/search?q=daft%20punk&type=track&limit=1")
    if not report("search", status):
        print("\nSearch failed — nothing else can be probed reliably.")
        return 1
    items = (data or {}).get("tracks", {}).get("items", [])
    if not items:
        print("\nSearch returned nothing; cannot continue.")
        return 1
    track_id = items[0]["id"]
    artist_id = items[0]["artists"][0]["id"]
    album_id = items[0]["album"]["id"]

    # Feb 2026 capped Development Mode search at 10 results (was 50).
    status, data = call(token, "/search?q=rock&type=track&limit=50")
    if 200 <= status < 300:
        got = len((data or {}).get("tracks", {}).get("items", []))
        print(f"  note       search limit=50 returned {got} items", end="")
        print("  <- capped at 10 => Development Mode limits apply"
              if got <= 10 else "  <- NOT capped => extended access")
    else:
        print(f"  note       search with limit=50 rejected ({status})")

    print("\nDeprecated for NEW apps on 2026-11-27 — if these work, this app "
          "predates that cut and kept them:")
    kept_2024 = 0
    kept_2024 += report("audio-features", call(token, f"/audio-features/{track_id}")[0])
    kept_2024 += report("audio-analysis", call(token, f"/audio-analysis/{track_id}")[0])
    kept_2024 += report(
        "recommendations",
        call(token, f"/recommendations?limit=1&seed_tracks={track_id}")[0],
    )
    kept_2024 += report(
        "related-artists", call(token, f"/artists/{artist_id}/related-artists")[0]
    )
    kept_2024 += report(
        "featured-playlists", call(token, "/browse/featured-playlists?limit=1")[0]
    )

    print("\nRemoved from Development Mode in Feb 2026 — if these work, this app "
          "is not under the Feb 2026 restrictions:")
    kept_2026 = 0
    kept_2026 += report(
        "batch tracks (GET /tracks)", call(token, f"/tracks?ids={track_id}")[0]
    )
    kept_2026 += report(
        "batch albums (GET /albums)", call(token, f"/albums?ids={album_id}")[0]
    )
    kept_2026 += report(
        "artist (GET /artists/{id})", call(token, f"/artists/{artist_id}")[0]
    )
    kept_2026 += report(
        "new releases (browse)", call(token, "/browse/new-releases?limit=1")[0]
    )

    print("\nStill available to every app (the baseline this project builds on):")
    report("single track", call(token, f"/tracks/{track_id}")[0])
    report("single album", call(token, f"/albums/{album_id}")[0])

    print("\n" + "=" * 68)
    if kept_2024:
        print("NOTABLE: this app retains endpoints that new apps cannot get.")
        print("Audio features / recommendations are exactly what a playlist")
        print("builder wants. That is worth protecting — do not delete this app,")
        print("and do not assume a replacement app would behave the same.")
    else:
        print("This app has the same reduced surface as a brand-new app:")
        print("no audio-features, no recommendations. Track selection logic has")
        print("to be ours. Nothing is gained by keeping the old app.")
    if kept_2026:
        print("\nIt also retains endpoints removed from Development Mode in")
        print("Feb 2026 — check whether it is genuinely in Development Mode.")
    print("\nUser allowlist size is NOT visible here — check the dashboard:")
    print("https://developer.spotify.com/dashboard -> app -> User Management")
    print("Grandfathering preserved users you had ALREADY ADDED, not the old")
    print("25-user ceiling. Try adding a 6th user: if it is refused, the cap is 5.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
