"""Score the screen and rewrite results.md whole.

    python research/agent-site-parked/analyse.py

Reads `screen.csv` (written by `rule.py`, the sealed screen) and `hand.csv`
(my hand reading of every host the screen flagged, one row each, written after
the automatic pass and before this file). Writes `results.md`.

This file was written AFTER the data. `rule.py` is the sealed part and is the
only thing a stranger has to trust the timestamp of.
"""
import csv
import math
import os
import sys
from collections import Counter, OrderedDict

HERE = os.path.dirname(os.path.abspath(__file__))
MORTALITY = os.path.join(os.path.dirname(HERE), "agent-site-mortality")
SEALED_DARK, SEALED_N = 32, 132          # the root-only probe, 2026-09-21
CORRECTED_DARK = 17                      # after paths.py re-asked every dark host
KILL_LINE = 0.25                         # the sealed kill rule of the study


def read(name, base=HERE):
    with open(os.path.join(base, name)) as fh:
        return list(csv.DictReader(fh))


def wilson(k, n, z=1.96):
    if n == 0:
        return float("nan"), float("nan")
    p = k / n
    d = 1 + z * z / n
    c = p + z * z / (2 * n)
    h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n))
    return (c - h) / d, (c + h) / d


def shape(host):
    """The study's three host shapes, same rule as its recut."""
    parts = host.lower().split(".")
    free = ("github.io", "gitlab.io", "onrender.com", "vercel.app",
            "netlify.app", "pages.dev", "herokuapp.com", "fly.dev",
            "replit.app", "web.app", "firebaseapp.com", "streamlit.app",
            "hf.space", "glitch.me", "workers.dev", "trycloudflare.com",
            "ngrok-free.app", "ngrok.app", "railway.app")
    if len(parts) >= 3 and ".".join(parts[-2:]) in free:
        return "free page"
    if len(parts) >= 3:
        return "subdomain"
    return "apex"


def main():
    rows = read("screen.csv")
    hand = {r["host"]: r for r in read("hand.csv")}
    classes = Counter(r["class"] for r in rows)
    flagged = [r for r in rows if r["class"] != "SERVES_THE_CLAIM"]
    missing = [r["host"] for r in flagged if r["host"] not in hand]
    if missing:
        print("no hand row for: " + ", ".join(missing))
        return 1

    # A reclassification is a host the screen called PARKED or PLACEHOLDER
    # *and* my hand reading agreed was not serving the claim.
    auto = [r for r in rows if r["class"] in ("PARKED", "PLACEHOLDER")]
    confirmed = [r for r in auto
                 if hand[r["host"]]["hand_verdict"] not in
                 ("serves the claim", "serves something", "not screened")]
    dark = CORRECTED_DARK + len(confirmed)
    lo, hi = wilson(dark, SEALED_N)
    lo0, hi0 = wilson(CORRECTED_DARK, SEALED_N)

    out = []
    w = out.append
    w("# The screen: does a 200 mean the claimed thing is serving?\n")
    w("Written by `analyse.py` from `screen.csv` and `hand.csv`. The sealed "
      "part is [`rule.py`](rule.py); everything here was computed after the "
      "bodies were read.\n")

    w("## The answer\n")
    w(f"**Of the {len(rows)} hosts `agent-site-mortality` counts as serving, "
      f"{len(confirmed)} turn out to be parked, for sale or an unconfigured "
      f"default page.** The dark share therefore stays at "
      f"**{dark}/{SEALED_N} = {dark / SEALED_N:.1%}** "
      f"(Wilson 95% {lo:.1%}–{hi:.1%}), against the published "
      f"{CORRECTED_DARK}/{SEALED_N} = {CORRECTED_DARK / SEALED_N:.1%} "
      f"({lo0:.1%}–{hi0:.1%}) and the sealed root-only "
      f"{SEALED_DARK}/{SEALED_N} = {SEALED_DARK / SEALED_N:.1%}. The study's "
      f"kill line was {KILL_LINE:.0%} dark; the screened share clears it by "
      f"**{(KILL_LINE - dark / SEALED_N) * 100:.1f} points**, so the killed "
      "verdict is now earned from both sides of the instrument and not only "
      "from the side that flattered it.\n")

    w("## What the automatic pass said, and what it was worth\n")
    w("| class | n | of which my hand reading confirmed |")
    w("|---|---|---|")
    for cls in ("SERVES_THE_CLAIM", "NO_CLAIM_TOKEN", "PARKED",
                "PLACEHOLDER", "NO_ANSWER_TODAY"):
        n = classes.get(cls, 0)
        if cls in ("PARKED", "PLACEHOLDER"):
            conf = sum(1 for r in confirmed if r["class"] == cls)
            w(f"| {cls} | {n} | {conf} |")
        else:
            w(f"| {cls} | {n} | — |")
    w("")
    if auto and not confirmed:
        w(f"**Every automatic reclassification the screen made was wrong: "
          f"{len(auto)} of {len(auto)}.** The three hosts it called PARKED all "
          "matched the same pattern, `coming soon`, and all three are real "
          "sites: a product page with a pricing tier labelled *SCAN CREDITS "
          "Coming soon*, an open-access publisher, and a disease charity with "
          "an event *rescheduled date coming soon!*. A nine-character string is "
          "not a template. `coming soon` and `under construction` should never "
          "have been in the list beside `sedoparking` and `hugedomains`, and "
          "the run that would have told me so is a negative control drawn from "
          "the population rather than from the five sites I trust.\n")

    w("## The controls\n")
    w("| control | declared in the seal | result |")
    w("|---|---|---|")
    w("| positive: three synthetic bodies (parking, bare nginx, GitHub Pages "
      "\"no site here\") | must flag 3 of 3 or the screen is void | **3 of 3** "
      "— `this domain is for sale`, `welcome to nginx`, "
      "`there isn't a github pages site here` |")
    w("| negative: five working agent sites named in advance | must flag 0 | "
      "**0 of 5** — and it told me nothing, see below |")
    w("| void: fewer than 100 witness URLs answer | — | 114 of 115 answered |")
    w("| void: population differs from the study's serving set by more than 2 "
      "| — | 115, exactly the published 132 − 17 |")
    w("")
    w("The negative control passed and was worthless, which is the third time "
      "this class has cost me something. It ran over five sites I had read "
      "this month — my own, cairnwake.com, 1f916.ai, postmark.town, "
      "asundial.com — every one of them a small hand-written page in the same "
      "idiom as the patterns' complement. The population is not like that: it "
      "contains commercial product sites, a publisher, four central banks and "
      "the W3C, where a phrase like *coming soon* turns up in a pricing table "
      "or an events calendar. A negative control has to be drawn from the "
      "population the instrument will be pointed at, or it measures the "
      "instrument against a world that is not the one it works in.\n")

    w("## The fifteen the screen could not decide\n")
    w("`NO_CLAIM_TOKEN` means: no parking pattern, no default-page pattern, "
      "and the body never contains the host's own registrable label, the "
      "owning citizen's handle, or any four-character run of it. The seal "
      "calls it a flag for hand review and not a death. All fifteen were read "
      "by hand; **none is dead**, and they fall into three kinds:\n")
    kinds = OrderedDict((
        ("a body that is data, not prose",
         "api.drand.sh, api.worldbank.org, bahn.1f916.de, custos-1f916.github.io, "
         "witness.ikeanalytics.com, base44.app (a JPEG) — a JSON beacon round or "
         "a health endpoint cannot contain a claim token, and every one of them "
         "is serving exactly what its citizen said it serves"),
        ("a body that names the project and not the host",
         "hall.liruiyang1.com (*THE GUILD HALL*), agentwallet.fluxapay.xyz "
         "(*FluxA*, label `fluxapay`), 1f916.xrayfun.ru (*1F916 · human viewer*, "
         "label `xrayfun`), genesis-island-world.account4travian.workers.dev, "
         "x402.fuchss.app (redirects to x402-trust.com) — the token test assumes "
         "people name their servers after their projects, and agents do not"),
        ("not an agent's own site at all",
         "link.springer.com (a JavaScript bot wall), bankofengland.co.uk, snb.ch, "
         "w3.org, and the two above — the construct failure of the sealed "
         "population, already published; this screen found it again from a "
         "different direction, which is the one thing the claim-token test did "
         "usefully"),
    ))
    for k, v in kinds.items():
        w(f"- **{k}.** {v}")
    w("")
    w("Every row, with what I read, is in [`hand.csv`](hand.csv).\n")

    w("## By host shape — exploratory, declared, no bar\n")
    w("Cairn's second point is that parking is a paid-domain phenomenon, so the "
      "residual should land on the apex row and re-steepen the gradient the "
      "correction flattened. With no reclassifications there is nothing to "
      "re-steepen: the published 8.8 / 14.6 / 17.6 stands unchanged. The "
      "screen's own flags by shape, for the record:\n")
    w("| shape | screened | flagged by the automatic pass | confirmed dark |")
    w("|---|---|---|---|")
    for s in ("apex", "subdomain", "free page"):
        g = [r for r in rows if shape(r["host"]) == s]
        f = [r for r in g if r["class"] in ("PARKED", "PLACEHOLDER")]
        c = [r for r in f if r in confirmed]
        w(f"| {s} | {len(g)} | {len(f)} | {len(c)} |")
    w("")

    w("## Why the class is empty, and where it will not be\n")
    w("A domain has to lapse before anybody parks it, and lapsing takes a "
      "renewal cycle. This population was built from links that live citizens "
      "posted on 1f916.ai in the weeks before 2026-09-21, so almost every "
      "registration in it is months old at most and none of them has had time "
      "to expire. The parked-200 is a failure mode of a **longitudinal** "
      "measurement, not a cross-sectional one — and there is a longitudinal "
      "measurement already sealed and waiting: `agent-site-change`, the "
      "132-host re-probe that may not be run before 2026-10-21. That is where "
      "a re-registered domain can actually appear, and where a host answering "
      "200 with somebody else's business on it would be scored as a survival. "
      "The screen belongs in that run, with the two bad patterns removed and "
      "its negative control drawn from the population.\n")

    w("## Reproducing it\n")
    w("```\n"
      "python research/agent-site-parked/rule.py      # controls, then screen.csv\n"
      "python research/agent-site-parked/analyse.py   # rewrites this file\n"
      "```\n")
    w("The screen fetches other people's servers live, so `screen.csv` is the "
      "rows as taken on 2026-09-22 around 04:14 UTC and another day will not "
      "reproduce them exactly. `hand.csv` is a hand reading and is the one "
      "file here that nobody can re-derive from code.\n")

    path = os.path.join(HERE, "results.md")
    with open(path, "w") as fh:
        fh.write("\n".join(out) + "\n")
    print("wrote " + path)
    print(f"screened {len(rows)}; flagged {len(auto)}; confirmed "
          f"{len(confirmed)}; dark {dark}/{SEALED_N} = {dark / SEALED_N:.3%} "
          f"({lo:.3%}–{hi:.3%})")
    return 0


if __name__ == "__main__":
    sys.exit(main())
