#!/usr/bin/env python3
"""Lane D shot sink — write canvas frames from a dev page straight into docs/shots/laneD/.

Why: the eyeball law wants in-engine PNGs committed as evidence every round, but `DBG.shot()`
relies on an <a download> click, and in a headless/embedded browser pane those land as unnamed
half-written temp blobs in ~/Downloads. Naming evidence by guessing at file sizes is not
evidence. So: the page POSTs the canvas here and the file lands with the right name, first try.

    python3 pipeline/shot_sink.py &            # listens on :8141, writes docs/shots/laneD/
    python3 pipeline/shot_sink.py --dir docs/shots/laneD --port 8141

From the page (any origin — CORS is wide open, it's a localhost dev tool):

    canvas.toBlob(b => fetch('http://localhost:8141/shot/my_name.png', {method:'POST', body:b}));

Stdlib only. Refuses anything that isn't a PNG, and sanitises the name to a bare basename so a
page can't post its way out of the shots directory.
"""
import argparse, os, re, sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
OUTDIR = os.path.join(ROOT, "docs", "shots", "laneD")


class Sink(BaseHTTPRequestHandler):
    def _cors(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "*")

    def do_OPTIONS(self):
        self.send_response(204)
        self._cors()
        self.end_headers()

    def _read_body(self):
        """Content-Length when present, else chunked. fetch(body: Blob) normally sends a
        Content-Length, but some embedded browsers stream it chunked instead — and
        read(Content-Length or 0) silently returns b"" for those, which looks exactly like
        "not a PNG". Handle both rather than rejecting a perfectly good frame."""
        cl = self.headers.get("Content-Length")
        if cl is not None:
            return self.rfile.read(int(cl))
        if (self.headers.get("Transfer-Encoding") or "").lower() != "chunked":
            return b""
        buf = bytearray()
        while True:
            n = int(self.rfile.readline().split(b";")[0] or b"0", 16)
            if n == 0:
                self.rfile.readline()               # trailing CRLF
                return bytes(buf)
            buf += self.rfile.read(n)
            self.rfile.readline()                   # CRLF after each chunk

    def do_POST(self):
        m = re.match(r"^/shot/([\w.-]+)$", self.path)
        if not m:
            self.send_response(404); self._cors(); self.end_headers(); return
        name = os.path.basename(m.group(1))
        if not name.endswith(".png"):
            name += ".png"
        body = self._read_body()
        if not body.startswith(PNG_MAGIC):          # only ever write real PNGs
            print(f"  reject {name}: {len(body)}B, head={body[:8]!r}, "
                  f"cl={self.headers.get('Content-Length')!r}, "
                  f"te={self.headers.get('Transfer-Encoding')!r}", flush=True)
            self.send_response(415); self._cors(); self.end_headers()
            self.wfile.write(b"not a png"); return
        path = os.path.join(OUTDIR, name)
        with open(path, "wb") as f:
            f.write(body)
        print(f"  shot {name}  {len(body)//1024}KB -> {os.path.relpath(path, ROOT)}", flush=True)
        self.send_response(200); self._cors(); self.end_headers()
        self.wfile.write(b"ok")

    def log_message(self, *a):                      # the prints above are the only log we want
        pass


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8141)
    ap.add_argument("--dir", default=OUTDIR)
    a = ap.parse_args()
    OUTDIR = os.path.abspath(a.dir)
    os.makedirs(OUTDIR, exist_ok=True)
    print(f"shot sink: POST http://localhost:{a.port}/shot/<name>.png -> {OUTDIR}", flush=True)
    ThreadingHTTPServer(("127.0.0.1", a.port), Sink).serve_forever()
