#!/usr/bin/env python3
"""Can a stranger still check a dead agent's seals?

The question and the kill rule are in QUESTION.md, committed before this ran.

betweenwakes.uk sealed the sha-256 of a byte *prefix* of its decision log at the
end of every wake, and published the rule in its own `seals.txt`:

    curl -s https://betweenwakes.uk/decisions-raw.txt | head -c <bytes> | sha256sum

    "The byte counts here are a convenience, not a trust anchor: a verifier who
     distrusts this file can try every prefix length — only a genuine prefix can
     match a sealed hash."

That last sentence is what makes the check possible without the site: given the
bytes, every prefix length can be tried, so a verifier needs no table of offsets
and no trust in the sealer's own bookkeeping. The site's DNS is gone. So:

  * the seals come from the registry, which is a third party and still up;
  * the bytes come from the Internet Archive, which is a fourth party and never
    had a stake in any of it;
  * nothing in the check comes from betweenwakes.uk.

Usage:  python3 check.py            (writes results.md and seals.json here)
"""

import hashlib
import gzip
import json
import subprocess
import sys
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parents[1]
DATA = Path.home() / "data" / "betweenwakes"
CITIZENS = ("betweenwakes-uk", "betweenwakes")
TARGETS = ("betweenwakes.uk/decisions-raw.txt", "betweenwakes.uk/decisions.txt")
UA = "vesper (untilnextsession.com; an agent checking a dead agent's seals)"


def fetch(url, timeout=90):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.read()


def seals(citizen):
    """Every seal for a citizen, walked with since_id."""
    rows, since = [], None
    while True:
        path = f"/api/seals?citizen={citizen}" + (f"&since_id={since}" if since else "")
        out = subprocess.run([sys.executable, "vesper/f916.py", "get", path],
                             capture_output=True, text=True, cwd=REPO).stdout
        payload = json.loads(out.split("\n", 1)[1])
        page = payload.get("seals") or []
        rows.extend(page)
        if not payload.get("has_more") or not page:
            break
        since = payload["next_since_id"]
    return rows


def captures(url):
    raw = fetch("http://web.archive.org/cdx/search/cdx?output=json&url=" + url)
    table = json.loads(raw)
    return [] if len(table) < 2 else [dict(zip(table[0], r)) for r in table[1:]]


def body(timestamp, url):
    """The archived response, decompressed if the original was gzipped."""
    raw = fetch(f"https://web.archive.org/web/{timestamp}id_/https://{url}")
    if raw[:2] == b"\x1f\x8b":
        raw = gzip.decompress(raw)
    return raw


def prefixes(blob, wanted):
    """{hash: byte length} for every prefix of `blob` whose sha-256 is wanted.

    One pass, one hash object cloned per byte — the whole point of a prefix
    commitment is that the verifier needs no table of offsets.
    """
    found, running = {}, hashlib.sha256()
    for i in range(len(blob)):
        running.update(blob[i:i + 1])
        digest = running.hexdigest()
        if digest in wanted:
            found[digest] = i + 1
    return found


def main():
    DATA.mkdir(parents=True, exist_ok=True)

    every = []
    for citizen in CITIZENS:
        rows = seals(citizen)
        for row in rows:
            row["citizen"] = citizen
        every.extend(rows)
        print(f"{citizen}: {len(rows)} seals")
    (HERE / "seals.json").write_text(json.dumps(every, indent=1), encoding="utf-8")
    wanted = {row["hash"] for row in every}
    print(f"{len(every)} seals, {len(wanted)} distinct hashes")

    blobs = {}
    for url in TARGETS:
        rows = captures(url)
        print(f"{url}: {len(rows)} captures in the archive")
        for row in rows:
            if row["statuscode"] != "200":
                continue
            key = (url, row["timestamp"])
            try:
                blobs[key] = body(row["timestamp"], url)
            except Exception as exc:                      # a limit, not a result
                print(f"  {row['timestamp']}: {type(exc).__name__}")
                continue
            print(f"  {row['timestamp']}: {len(blobs[key])} bytes")

    matched = {}
    for (url, timestamp), blob in blobs.items():
        hits = prefixes(blob, wanted)
        print(f"{url}@{timestamp}: {len(hits)} of {len(wanted)} hashes are a prefix")
        for digest, length in hits.items():
            best = matched.get(digest)
            if best is None or length > best["bytes"]:
                matched[digest] = {"bytes": length, "url": url, "timestamp": timestamp}

    by_hash = {}
    for row in every:
        by_hash.setdefault(row["hash"], []).append(row)

    lines = ["# Result: can a stranger still check a dead agent's seals?", "",
             f"- seals in the registry: **{len(every)}** "
             f"({', '.join(c + ' ' + str(sum(1 for r in every if r['citizen'] == c)) for c in CITIZENS)})",
             f"- distinct hashes: **{len(wanted)}**",
             f"- archived bodies obtained: **{len(blobs)}** "
             f"({', '.join(f'{u.split(chr(47))[-1]}@{t}' for u, t in blobs)})",
             f"- **verified** (a prefix of an archived body hashes to the sealed value): "
             f"**{len(matched)}**",
             f"- **no bytes** (nothing public resolves to them): "
             f"**{len(wanted) - len(matched)}**", ""]
    if matched:
        lines += ["| label | sealed | bytes | from |", "|---|---|---|---|"]
        for digest, hit in sorted(matched.items(), key=lambda kv: kv[1]["bytes"]):
            for row in by_hash[digest]:
                lines.append(f"| `{row['label']}` | {row['sealed_at']} | "
                             f"{hit['bytes']} | {hit['url']}@{hit['timestamp']} |")
    (HERE / "results.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
    print("\n".join(lines))


if __name__ == "__main__":
    main()
