#!/usr/bin/env python3
"""Local CLI for pulling Andromeda API data without curl."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urljoin, urlparse
from urllib.request import Request, urlopen


DEFAULT_BASE_URL = "https://andromeda1957.com"
USER_AGENT = "andromeda-local-api-client/1.0"


class ApiError(RuntimeError):
    pass


def fetch_url(url: str) -> tuple[bytes, str]:
    request = Request(url, headers={"User-Agent": USER_AGENT})
    try:
        with urlopen(request, timeout=30) as response:
            content_type = response.headers.get("Content-Type", "")
            return response.read(), content_type
    except HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise ApiError(f"HTTP {exc.code} for {url}\n{body}") from exc
    except URLError as exc:
        raise ApiError(f"Could not fetch {url}: {exc.reason}") from exc


def fetch_json(url: str) -> dict[str, Any]:
    body, _ = fetch_url(url)
    try:
        return json.loads(body.decode("utf-8"))
    except json.JSONDecodeError as exc:
        raise ApiError(f"Response was not JSON: {url}") from exc


def print_jq_json(data: Any) -> None:
    print(json.dumps(data, indent=2, ensure_ascii=False))


def print_public_players_table(players: list[dict[str, Any]]) -> None:
    for player in players:
        print(f'{player["slug"]}\t{player["game_count"]}\t{player["name"]}')


def normalized_base_url(value: str) -> str:
    return value.rstrip("/") + "/"


def api_url(base_url: str, path: str, params: dict[str, Any] | None = None) -> str:
    url = urljoin(normalized_base_url(base_url), path.lstrip("/"))
    cleaned = {
        key: value
        for key, value in (params or {}).items()
        if value is not None and value != ""
    }
    if cleaned:
        url = f"{url}?{urlencode(cleaned)}"
    return url


def extract_master_game_token(value: str) -> str:
    parsed = urlparse(value)
    if not parsed.scheme:
        return value.strip("/")

    parts = tuple(part for part in parsed.path.split("/") if part)
    for index, part in enumerate(parts):
        if part in {"games", "game-search"} and index + 1 < len(parts):
            return parts[index + 1]
    raise ApiError(f"Could not find a game token in URL: {value}")


def extract_public_game_token(value: str) -> tuple[str, str]:
    parsed = urlparse(value)
    raw_value = parsed.path if parsed.scheme else value
    parts = tuple(part for part in raw_value.strip("/").split("/") if part)

    if len(parts) == 2:
        return parts[0], parts[1]

    for index, part in enumerate(parts):
        if (
            part == "games"
            and index > 0
            and index + 1 < len(parts)
            and parts[index - 1] != "public"
        ):
            return parts[index - 1], parts[index + 1]

    for index in range(len(parts) - 3):
        if parts[index:index + 3] == ("api", "v1", "public") and parts[index + 3] == "games":
            if index + 5 < len(parts):
                return parts[index + 4], parts[index + 5]

    raise ApiError(f"Could not find a public game token in URL: {value}")


def write_or_print_text(text: str, output: str | None) -> None:
    if output:
        Path(output).write_text(text, encoding="utf-8")
        return
    print(text, end="" if text.endswith("\n") else "\n")


def build_game_query(args: argparse.Namespace) -> str:
    if args.query:
        query = args.query.strip()
    elif args.player and args.opponent:
        query = f"{args.player} {args.opponent}"
    elif args.players:
        query = " ".join(args.players)
    elif args.player:
        query = args.player
    elif args.eco:
        query = ""
    else:
        raise ApiError("games requires --query, --player, --players, or --eco.")

    if args.eco:
        query = f"{query} {args.eco}".strip()

    if args.year:
        query = f"{query} {args.year}"
    return query


def build_public_game_query(args: argparse.Namespace) -> str:
    if args.query:
        query = args.query.strip()
    elif args.player and args.opponent:
        query = f"{args.player} {args.opponent}"
    elif args.players:
        query = " ".join(args.players)
    elif args.player:
        query = args.player
    else:
        query = ""

    if args.eco:
        query = f"{query} {args.eco}".strip()

    if args.year:
        query = f"{query} {args.year}".strip()
    return query


def command_master_stats(args: argparse.Namespace) -> int:
    mode = args.mode or ("head_to_head" if args.opponent else "summary")
    if mode == "head_to_head" and not args.opponent:
        raise ApiError("stats --mode head_to_head requires --opponent.")
    if args.year:
        raise ApiError("The stats endpoint does not support --year; use games.")

    url = api_url(
        args.base_url,
        "/api/v1/stats/",
        {
            "mode": mode,
            "player": args.player,
            "opponent": args.opponent,
        },
    )
    print_jq_json(fetch_json(url))
    return 0


def command_master_game(args: argparse.Namespace) -> int:
    token = extract_master_game_token(args.token)
    url = api_url(args.base_url, f"/api/v1/games/{token}/")
    print_jq_json(fetch_json(url))
    return 0


def command_master_pgn(args: argparse.Namespace) -> int:
    token = extract_master_game_token(args.token)
    url = api_url(args.base_url, f"/api/v1/games/{token}/pgn/")
    body, _ = fetch_url(url)
    write_or_print_text(body.decode("utf-8"), args.output)
    return 0


def collect_master_game_pages(args: argparse.Namespace, query: str) -> dict[str, Any]:
    page_size = args.page_size
    requested_page = args.page
    payload = fetch_json(
        api_url(
            args.base_url,
            "/api/v1/games/",
            {"q": query, "page": requested_page, "page_size": page_size},
        )
    )
    if not args.all_pages:
        return payload

    results = list(payload.get("results", ()))
    page_count = int(payload.get("page_count") or 0)
    for page in range(requested_page + 1, page_count + 1):
        if args.limit and len(results) >= args.limit:
            break
        next_payload = fetch_json(
            api_url(
                args.base_url,
                "/api/v1/games/",
                {"q": query, "page": page, "page_size": page_size},
            )
        )
        results.extend(next_payload.get("results", ()))

    if args.limit:
        results = results[: args.limit]
    payload["results"] = results
    payload["page"] = requested_page
    payload["pages_fetched"] = page_count
    payload["next"] = None
    payload["previous"] = None
    return payload


def collect_public_game_pages(args: argparse.Namespace, query: str) -> dict[str, Any]:
    page_size = args.page_size
    requested_page = args.page
    payload = fetch_json(
        api_url(
            args.base_url,
            "/api/v1/public/games/",
            {
                "q": query,
                "archive_player": args.archive_player,
                "sort": args.sort,
                "page": requested_page,
                "page_size": page_size,
            },
        )
    )
    if not args.all_pages:
        return payload

    results = list(payload.get("results", ()))
    page_count = int(payload.get("page_count") or 0)
    for page in range(requested_page + 1, page_count + 1):
        if args.limit and len(results) >= args.limit:
            break
        next_payload = fetch_json(
            api_url(
                args.base_url,
                "/api/v1/public/games/",
                {
                    "q": query,
                    "archive_player": args.archive_player,
                    "sort": args.sort,
                    "page": page,
                    "page_size": page_size,
                },
            )
        )
        results.extend(next_payload.get("results", ()))

    if args.limit:
        results = results[: args.limit]
    payload["results"] = results
    payload["page"] = requested_page
    payload["pages_fetched"] = page_count
    payload["next"] = None
    payload["previous"] = None
    return payload


def pgn_text_for_games(base_url: str, games: list[dict[str, Any]]) -> str:
    pgns = []
    for game in games:
        path = game.get("urls", {}).get("api_pgn")
        if not path:
            raise ApiError(f"Game result has no PGN URL: {game!r}")
        body, _ = fetch_url(urljoin(normalized_base_url(base_url), path.lstrip("/")))
        pgns.append(body.decode("utf-8").strip())
    return "\n\n".join(pgns) + ("\n" if pgns else "")


def command_master_games(args: argparse.Namespace) -> int:
    query = build_game_query(args)
    payload = collect_master_game_pages(args, query)
    return write_games_payload(args, payload)


def command_public_games(args: argparse.Namespace) -> int:
    query = build_public_game_query(args)
    payload = collect_public_game_pages(args, query)
    return write_games_payload(args, payload)


def write_games_payload(args: argparse.Namespace, payload: dict[str, Any]) -> int:
    if args.limit and not args.all_pages:
        payload["results"] = payload.get("results", [])[: args.limit]

    if args.pgn:
        text = pgn_text_for_games(args.base_url, list(payload.get("results", ())))
        write_or_print_text(text, args.output)
        return 0

    print_jq_json(payload)
    return 0


def command_public_players(args: argparse.Namespace) -> int:
    payload = fetch_json(
        api_url(
            args.base_url,
            "/api/v1/public/players/",
            {"q": args.query},
        )
    )
    if args.plain:
        print_public_players_table(list(payload.get("results", ())))
        return 0
    print_jq_json(payload)
    return 0


def command_public_game(args: argparse.Namespace) -> int:
    player_slug, game_slug = extract_public_game_token(args.token)
    url = api_url(args.base_url, f"/api/v1/public/games/{player_slug}/{game_slug}/")
    print_jq_json(fetch_json(url))
    return 0


def command_public_pgn(args: argparse.Namespace) -> int:
    player_slug, game_slug = extract_public_game_token(args.token)
    url = api_url(args.base_url, f"/api/v1/public/games/{player_slug}/{game_slug}/pgn/")
    body, _ = fetch_url(url)
    write_or_print_text(body.decode("utf-8"), args.output)
    return 0


def add_games_args(parser: argparse.ArgumentParser, *, public: bool = False) -> None:
    parser.add_argument("-q", "--query", help="Raw game-search query.")
    parser.add_argument("--player", help="Player name query.")
    parser.add_argument("--opponent", help="Opponent name query.")
    parser.add_argument(
        "--players",
        nargs="+",
        help="One or more player-name fragments joined into the game query.",
    )
    parser.add_argument("--eco", help="Filter by ECO code or code fragment, such as B92.")
    parser.add_argument("--year", type=int, help="Append a year filter to the query.")
    if public:
        parser.add_argument(
            "--archive-player",
            help="Restrict to one public archive by study-player slug, name, or alias.",
        )
        parser.add_argument(
            "--sort",
            choices=("asc", "desc"),
            default="asc",
            help="Sort by public archive display date. Default: asc.",
        )
    parser.add_argument("--page", type=int, default=1, help="Result page. Default: 1.")
    parser.add_argument(
        "--page-size",
        type=int,
        default=50,
        help="Results per page, max 100 on the API. Default: 50.",
    )
    parser.add_argument("--all-pages", action="store_true", help="Fetch all pages.")
    parser.add_argument("--limit", type=int, help="Limit returned/fetched results.")
    parser.add_argument("--pgn", action="store_true", help="Fetch PGN for results.")
    parser.add_argument("--output", help="Write PGN output to this file.")


def add_master_commands(subparsers: argparse._SubParsersAction) -> None:
    master = subparsers.add_parser("master", help="Pull from the MasterDB stats/search API.")
    master_subparsers = master.add_subparsers(dest="master_command", required=True)

    stats = master_subparsers.add_parser("stats", help="Pull /api/v1/stats/ JSON.")
    stats.add_argument("--player", required=True, help="Player name query.")
    stats.add_argument("--opponent", help="Opponent name query for head-to-head.")
    stats.add_argument("--year", type=int, help="Not supported by stats API.")
    stats.add_argument(
        "--mode",
        choices=("summary", "head_to_head"),
        help="Stats mode. Defaults to head_to_head when --opponent is present.",
    )
    stats.set_defaults(func=command_master_stats)

    games = master_subparsers.add_parser("games", help="Pull /api/v1/games/ JSON or PGN.")
    add_games_args(games)
    games.set_defaults(func=command_master_games)

    game = master_subparsers.add_parser("game", help="Pull one MasterDB game detail JSON by token.")
    game.add_argument("token", help="MasterDB game token or game/detail URL.")
    game.set_defaults(func=command_master_game)

    pgn = master_subparsers.add_parser("pgn", help="Pull one raw MasterDB PGN by token.")
    pgn.add_argument("token", help="MasterDB game token or game/detail URL.")
    pgn.add_argument("--output", help="Write PGN output to this file.")
    pgn.set_defaults(func=command_master_pgn)


def add_public_commands(subparsers: argparse._SubParsersAction) -> None:
    public = subparsers.add_parser("public", help="Pull from the public study-player archive API.")
    public_subparsers = public.add_subparsers(dest="public_command", required=True)

    players = public_subparsers.add_parser("players", help="List public archive players.")
    players.add_argument("-q", "--query", help="Filter players by name, slug, or source alias.")
    players.add_argument(
        "--plain",
        action="store_true",
        help="Print tab-separated slug, game count, and name instead of JSON.",
    )
    players.set_defaults(func=command_public_players)

    games = public_subparsers.add_parser("games", help="Pull /api/v1/public/games/ JSON or PGN.")
    add_games_args(games, public=True)
    games.set_defaults(func=command_public_games)

    game = public_subparsers.add_parser("game", help="Pull one public archive game detail JSON.")
    game.add_argument("token", help="Public token, /players/... URL, or public API game URL.")
    game.set_defaults(func=command_public_game)

    pgn = public_subparsers.add_parser("pgn", help="Pull one raw public archive PGN.")
    pgn.add_argument("token", help="Public token, /players/... URL, or public API game URL.")
    pgn.add_argument("--output", help="Write PGN output to this file.")
    pgn.set_defaults(func=command_public_pgn)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Pull Andromeda API data from an explicit source.",
    )
    parser.add_argument(
        "--base-url",
        default=DEFAULT_BASE_URL,
        help=f"API base URL. Default: {DEFAULT_BASE_URL}",
    )
    subparsers = parser.add_subparsers(dest="source", required=True)
    add_master_commands(subparsers)
    add_public_commands(subparsers)

    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    try:
        return args.func(args)
    except ApiError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1


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