#!/usr/bin/env python3
"""
SHADES — Lane E screenshot capture. Python stdlib only, same house rule as
server.py: no pip, no venv, no build step.

WHY THIS EXISTS
---------------
There is no way to get a game screenshot onto disk. The canvas is WebGL, so
`toDataURL` hands back a blank buffer unless you render and read in the same
tick, and even then the only channel out of the page is text — moving one
900x506 JPEG as base64 costs ~60 KB of round-trip to save one picture.
DESIGN.md has wanted a shot of the assembled yard since Sprint 2 and it has been
carried four sprints waiting on a five-line fix in a file Lane E doesn't own.

So this serves the repo exactly like server.py, and additionally accepts
`POST /shot?name=<n>`, writing the raw body to docs/<n>.png. The browser posts a
Blob and the bytes go straight to disk — they never touch a text channel.

    python3 tools/yardshot/shot_server.py --port 8815

then, from the page:

    SHADES.render();                                  // same tick as the read!
    document.getElementById('c').toBlob(
      (b) => fetch('/shot?name=yard_day', { method: 'POST', body: b }));

LANE A: this is deliberately NOT an edit to server.py — that's your file, and
§6 says post the need rather than reach into it. I posted it twice; nobody had
the spare hands, which is fair. `do_POST` below is the whole fix: lift it
verbatim into server.py, delete this directory, and anyone can screenshot the
game forever after. Until then this stays a Lane E tool and touches nothing.

Dev tool, and it writes files, so: binds loopback only, the name is restricted
to a safe charset (no traversal), the body is size-capped, and it only ever
writes .png into docs/.
"""

from __future__ import annotations

import argparse
import http.server
import re
from pathlib import Path
from urllib.parse import parse_qs, urlparse

ROOT = Path(__file__).resolve().parents[2]
OUT_DIR = ROOT / "docs"
SAFE_NAME = re.compile(r"^[a-z0-9_\-]{1,64}$")
MAX_BYTES = 20 * 1024 * 1024


class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=str(ROOT), **kwargs)

    def do_POST(self):  # noqa: N802  (stdlib naming)
        url = urlparse(self.path)
        if url.path != "/shot":
            self.send_error(404, "only POST /shot")
            return

        name = (parse_qs(url.query).get("name") or ["shot"])[0]
        if not SAFE_NAME.match(name):
            self.send_error(400, "name must match [a-z0-9_-]{1,64}")
            return

        length = int(self.headers.get("Content-Length") or 0)
        if not 0 < length <= MAX_BYTES:
            self.send_error(413, "empty or too large")
            return

        data = self.rfile.read(length)
        # A blank WebGL read is the failure mode this tool exists to dodge, so
        # refuse to write one rather than quietly commit an empty picture.
        if data.startswith(b"\x89PNG"):
            ext = "png"
        elif data.startswith(b"\xff\xd8\xff"):
            ext = "jpg"
        else:
            self.send_error(415, "body is neither PNG nor JPEG")
            return

        OUT_DIR.mkdir(exist_ok=True)
        out = OUT_DIR / f"{name}.{ext}"
        out.write_bytes(data)
        print(f"  shot -> {out.relative_to(ROOT)} ({len(data) // 1024} KB)")

        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", "2")
        self.end_headers()
        self.wfile.write(b"ok")

    def log_message(self, fmt, *args):
        pass  # the shot line above is the only output worth having


def main():
    ap = argparse.ArgumentParser(description="Serve the repo + accept POST /shot")
    ap.add_argument("--port", type=int, default=8815)
    args = ap.parse_args()
    srv = http.server.ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
    print(f"serving {ROOT} on http://127.0.0.1:{args.port}/")
    print(f"  game     http://127.0.0.1:{args.port}/web/world/index.html")
    print(f"  shots -> {OUT_DIR}")
    srv.serve_forever()


if __name__ == "__main__":
    main()
