#!/usr/bin/env python3
"""trakt — Trakt.tv media discovery from the terminal.

Discover trending, anticipated, and popular movies and TV shows.
Calendar of upcoming releases. Uses Trakt.tv API with Client ID.
"""

import argparse, json, os, sys, warnings
from typing import Any, Dict, List, Optional, Tuple
warnings.simplefilter("ignore")
import requests

ENV_CLIENT_ID = os.getenv("TRAKT_CLIENT_ID", "")
API_BASE = "https://api.trakt.tv"
USER_AGENT = "agent-skills-trakt/1.0"

QUIET = False
GLOBAL_FLAGS: Dict[str, Any] = {"json": False, "dry_run": False, "quiet": False, "verbose": False}

# Canonical X-Pagination-* response headers mapped onto the stable JSON keys
# surfaced as the `pagination` object beside every discovery result.
PAGINATION_HEADERS: Tuple[Tuple[str, str], ...] = (
    ("X-Pagination-Page", "page"),
    ("X-Pagination-Limit", "limit"),
    ("X-Pagination-Page-Count", "page_count"),
    ("X-Pagination-Item-Count", "item_count"),
)

def log(m):    global QUIET; (not QUIET and not GLOBAL_FLAGS.get("json")) and print(m)
def warn(m):   print(f"Warning: {m}", file=sys.stderr)
def die(m, c=1): print(f"Error: {m}", file=sys.stderr); sys.exit(c)
def emit(h, d):
    if GLOBAL_FLAGS.get("json"): print(json.dumps(d, default=str))
    else: print(h)

def normalize_pagination(headers):
    """Map X-Pagination-* headers onto the stable pagination keys.

    Missing or non-numeric header values fall out of the mapping, so a
    response without pagination metadata degrades to an empty object
    instead of failing.
    """
    pagination: Dict[str, int] = {}
    for header, key in PAGINATION_HEADERS:
        raw = None
        for name in (header, header.lower()):
            if name in headers:
                raw = headers[name]
                break
        if raw is None: continue
        try: pagination[key] = int(str(raw).strip())
        except (TypeError, ValueError): continue
    return pagination

def _preparse(argv):
    BOOLS = {"--json","--dry-run","--quiet","--verbose"}
    f, fl = {}, [argv[0]]
    i = 1
    while i < len(argv):
        a = argv[i]
        if a in BOOLS: f[a.lstrip("-").replace("-","_")] = True; i += 1
        elif a in ("--help","-h"): return f, argv
        elif a == "--": fl.extend(argv[i:]); break
        else: fl.append(a); i += 1
    return f, fl

class TraktClient:
    def __init__(self, client_id="", dry_run=False):
        self.client_id = client_id or ENV_CLIENT_ID
        self.dry_run = dry_run
    def _get(self, path, params=None):
        """Fetch one page; returns (json data, normalized pagination dict)."""
        url = f"{API_BASE}{path}"
        if self.dry_run: return [{"dry_run":True, "url":url, "params":params}], {}
        if not self.client_id: die("TRAKT_CLIENT_ID not set. Get one from trakt.tv/oauth/applications.")
        try:
            r = requests.get(url, params=params, headers={
                "Content-Type": "application/json", "User-Agent": USER_AGENT,
                "trakt-api-version": "2", "trakt-api-key": self.client_id}, timeout=30)
        except ConnectionError as e: die(f"Cannot connect: {e}")
        if r.status_code == 401: die("Auth failed (401). Check TRAKT_CLIENT_ID.")
        if r.status_code >= 400:
            try: d = r.json()
            except: d = r.text[:200]
            die(f"API error ({r.status_code}): {d}")
        return r.json(), normalize_pagination(dict(r.headers))
    def movie_trending(self, page=1, limit=10):
        return self._get("/movies/trending", {"page":page, "limit":limit})
    def movie_anticipated(self, page=1, limit=10):
        return self._get("/movies/anticipated", {"page":page, "limit":limit})
    def movie_popular(self, page=1, limit=10):
        return self._get("/movies/popular", {"page":page, "limit":limit})
    def movie_calendar(self, start_date="", days=7):
        params = {"days":days}
        if start_date: params["start_date"] = start_date
        return self._get("/calendars/my/movies", params)
    def tv_trending(self, page=1, limit=10):
        return self._get("/shows/trending", {"page":page, "limit":limit})
    def tv_anticipated(self, page=1, limit=10):
        return self._get("/shows/anticipated", {"page":page, "limit":limit})
    def tv_popular(self, page=1, limit=10):
        return self._get("/shows/popular", {"page":page, "limit":limit})
    def tv_calendar(self, start_date="", days=7):
        params = {"days":days}
        if start_date: params["start_date"] = start_date
        return self._get("/calendars/my/shows", params)

def fmt_movie(d, idx=None):
    m = d.get("movie", d)
    t = m.get("title","?"); y = m.get("year","")
    ids = m.get("ids",{}); tmdb = ids.get("tmdb","?")
    tagline = m.get("tagline","")
    s = f"  {'%d. '%idx if idx else ''}{t:40} ({y})  tmdb={tmdb}"
    if tagline: s += f"\n  {'':4}{tagline[:80]}"
    return s

def fmt_show(d, idx=None):
    s = d.get("show", d)
    t = s.get("title","?"); y = s.get("year","")
    ids = s.get("ids",{}); tvdb = ids.get("tvdb","?")
    net = s.get("network",""); net_str = f" [{net}]" if net else ""
    s_out = f"  {'%d. '%idx if idx else ''}{t:35}{net_str} ({y})  tvdb={tvdb}"
    return s_out

def cmd_discovery(client, resource, endpoint, args):
    fmt = fmt_movie if resource == "movie" else fmt_show
    list_key = "movies" if resource == "movie" else "shows"
    plural = "movies" if resource == "movie" else "TV shows"
    label = "Movie" if resource == "movie" else "TV"
    p = argparse.ArgumentParser(prog=f"trakt {resource} {endpoint}")
    p.add_argument("--page", type=int, default=1,
                   help="1-based page number to fetch (default 1)")
    p.add_argument("--limit", type=int, default=10,
                   help="items per page (default 10)")
    parsed, _ = p.parse_known_args(args)
    if client.dry_run: return emit(f"[dry-run] {label} {endpoint}", {"dry_run":True})
    data, pagination = getattr(client, f"{resource}_{endpoint}")(page=parsed.page, limit=parsed.limit)
    data = data or []
    entries = data[:parsed.limit] if isinstance(data, list) else []
    payload: Dict[str, Any] = {list_key: entries, "pagination": pagination}
    if not entries: return emit(f"No {endpoint} {plural}.", payload)
    lines = [fmt(entry, i+1) for i, entry in enumerate(entries)]
    output = f"{endpoint.title()} {'movies' if resource == 'movie' else 'TV'}:\n"+"\n".join(lines)
    if pagination.get("page") is not None and pagination.get("page_count"):
        output += f"\n  Page {pagination['page']} of {pagination['page_count']}"
    emit(output, payload)

def main():
    global GLOBAL_FLAGS, QUIET
    GLOBAL_FLAGS, filtered_argv = _preparse(sys.argv)
    if GLOBAL_FLAGS.get("quiet"): QUIET = True
    if GLOBAL_FLAGS.get("json"): warnings.simplefilter("ignore")
    parser = argparse.ArgumentParser(prog="trakt", description="Trakt.tv media discovery.",
        epilog="Set TRAKT_CLIENT_ID. Get one at trakt.tv/oauth/applications.")
    sub = parser.add_subparsers(dest="resource")
    mp = sub.add_parser("movie", help="Movie discovery")
    ms = mp.add_subparsers(dest="action")
    for a in ["trending","anticipated","popular"]:
        ap = ms.add_parser(a, help=f"{a.title()} movies")
        ap.add_argument("--page", type=int, default=1, help="page number to fetch (default 1)")
        ap.add_argument("--limit", type=int, default=10, help="items per page (default 10)")
    tp = sub.add_parser("tv", help="TV discovery")
    ts = tp.add_subparsers(dest="action")
    for a in ["trending","anticipated","popular"]:
        ap = ts.add_parser(a, help=f"{a.title()} TV")
        ap.add_argument("--page", type=int, default=1, help="page number to fetch (default 1)")
        ap.add_argument("--limit", type=int, default=10, help="items per page (default 10)")
    args = parser.parse_args(filtered_argv[1:])
    if not args.resource: parser.print_help(); sys.exit(1)
    client = TraktClient(dry_run=GLOBAL_FLAGS.get("dry_run",False))
    remaining = filtered_argv[filtered_argv.index(args.resource)+1:]
    if args.resource in ("movie","tv"):
        if args.action: cmd_discovery(client, args.resource, args.action, remaining)
        elif args.resource == "movie": mp.print_help()
        else: tp.print_help()
    else: parser.print_help()

if __name__ == "__main__": main()
