#!/usr/bin/env python3
"""Serve the production web client and append completed games to JSONL."""

from __future__ import annotations

import argparse
import json
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any

MAX_RECORD_BYTES = 6_000_000

# v2 adds the opponent's identity (codename + hash) and the match layer -- three
# deals, the coin flips, and which round each move belongs to. v1 is still
# accepted because 111 games were recorded under it; they are all altair, which
# data/models.json records since v1 has nowhere to say so.
SUPPORTED_FORMATS = frozenset({"lost-cities-web-game-v1", "lost-cities-web-game-v2"})


def parse_record(body: bytes) -> dict[str, Any]:
    if len(body) > MAX_RECORD_BYTES:
        raise ValueError("record is too large")
    value = json.loads(body)
    if not isinstance(value, dict) or value.get("format") not in SUPPORTED_FORMATS:
        raise ValueError("unsupported game record")
    if not isinstance(value.get("gameId"), str) or not isinstance(value.get("moves"), list):
        raise ValueError("invalid game record")
    return value


def make_handler(dist: Path, output: Path):
    seen_ids: set[str] = set()
    if output.exists():
        for line in output.read_text(encoding="utf-8").splitlines():
            try:
                game_id = json.loads(line).get("gameId")
                if isinstance(game_id, str):
                    seen_ids.add(game_id)
            except (json.JSONDecodeError, AttributeError):
                continue

    class Handler(SimpleHTTPRequestHandler):
        def __init__(self, *args: Any, **kwargs: Any) -> None:
            super().__init__(*args, directory=str(dist), **kwargs)

        def do_POST(self) -> None:  # noqa: N802
            if self.path != "/api/game-records":
                self.send_error(HTTPStatus.NOT_FOUND)
                return
            try:
                length = int(self.headers.get("content-length", "0"))
                record = parse_record(self.rfile.read(length))
            except (ValueError, json.JSONDecodeError) as error:
                self.send_error(HTTPStatus.BAD_REQUEST, str(error))
                return
            game_id = record["gameId"]
            if game_id not in seen_ids:
                output.parent.mkdir(parents=True, exist_ok=True)
                with output.open("a", encoding="utf-8") as stream:
                    stream.write(
                        json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n"
                    )
                seen_ids.add(game_id)
            self.send_response(HTTPStatus.NO_CONTENT)
            self.end_headers()

    return Handler


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", default="0.0.0.0")
    parser.add_argument("--port", type=int, default=5173)
    parser.add_argument("--dist", type=Path, default=Path("web/dist"))
    parser.add_argument("--output", type=Path, default=Path("data/human-play/game-records.jsonl"))
    args = parser.parse_args()
    server = ThreadingHTTPServer((args.host, args.port), make_handler(args.dist, args.output))
    print(f"Serving {args.dist} on http://{args.host}:{args.port}", flush=True)
    print(f"Writing game records to {args.output}", flush=True)
    server.serve_forever()


if __name__ == "__main__":
    main()
