#!/usr/bin/env python3
"""dump_sink.py — receive structural dumps from the page and write them straight to disk.

Usage: dump_sink.py <run-dir> [--port 8765]

Listens on 127.0.0.1 only. The page posts its outline with
  fetch("http://127.0.0.1:8765/dump?id=<state-id>", {method: "POST", body: window.__OUTLINE})
and the sink writes <run-dir>/evidence/a11y/<state-id>.txt, so a dump never passes through the model's
context and no clipboard is involved. A state id must match <nn>-<slug>[-<state>]; anything else is refused.
GET /ping answers "ok"; GET /capture.js serves this skill's capture script, so a page installs it with one line:
  (()=>{const x=new XMLHttpRequest();x.open("GET","http://127.0.0.1:8765/capture.js",false);x.send();(0,eval)(x.responseText);return typeof window.__dsc})() Stop it with Ctrl-C or by killing the process when capture closes.
"""
import os, re, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs

RUN = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else None
PORT = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 8765
ID = re.compile(r"^\d{2,3}-[a-z0-9][a-z0-9-]{0,80}$")

class H(BaseHTTPRequestHandler):
    def _send(self, code, text):
        self.send_response(code)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(text.encode())
    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, GET")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()
    def do_GET(self):
        if urlparse(self.path).path == "/capture.js":
            return self._send(200, open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "capture.js")).read())
        self._send(200, "ok")
    def do_POST(self):
        q = parse_qs(urlparse(self.path).query)
        sid = (q.get("id") or [""])[0]
        if not ID.match(sid):
            return self._send(400, "bad state id")
        n = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(min(n, 5_000_000)).decode("utf-8", "replace")
        out = os.path.join(RUN, "evidence", "a11y", sid + ".txt")
        os.makedirs(os.path.dirname(out), exist_ok=True)
        with open(out, "w") as f:
            f.write(body)
        self._send(200, f"saved {sid} {len(body)}")
    def log_message(self, *a):
        pass

if __name__ == "__main__":
    if not RUN or not os.path.isdir(RUN):
        print(__doc__); sys.exit(2)
    print(f"dump sink on 127.0.0.1:{PORT} → {RUN}/evidence/a11y", flush=True)
    HTTPServer(("127.0.0.1", PORT), H).serve_forever()
