"""mutate.py — apply or revert ONE purposeful-red arm on the gate tree, refusing unless every
site matches EXACTLY ONCE, and printing the landed region so the raw proves the mutation LANDED
(an unapplied mutation is a green that reads as a passing guard — memory 2026-08-2x).

usage: python .spt/mutate.py apply|revert A|B1|B2|C
Arms (W2 gate at f3c8495b):
  A  = emit.rs: the splice_typed_msg carve-out is dead      -> F14 cells red (predicted 3 of 4)
  B1 = lastmsg.rs: excerpt never unwraps any envelope        -> F16 red + T3 (ioedges :685) red
  B2 = lastmsg.rs: excerpt unwraps EVERY typed envelope       -> F16 red (other-envelopes arm), T3 green
  C  = livehost.rs: reaper runs OUTSIDE with_registry_write   -> EXPECT nothing red (gap g1, measured)
"""
import subprocess, sys
from pathlib import Path

# root = the gate tree the driver cd'd into, never __file__ (a frozen copy lives under .spt/frozen/<hash>/)
root = Path(subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=True).stdout.strip())
ARMS = {
    "A": [(
        "crates/spt-msg/src/emit.rs",
        "        if let Some(spliced) = splice_typed_msg(tracked_root, owner, body, warning) {\n"
        "            return (spliced, false);\n"
        "        }\n"
        "        return (body.to_string(), true);\n",
        "        if false {\n"
        "            if let Some(spliced) = splice_typed_msg(tracked_root, owner, body, warning) {\n"
        "                return (spliced, false);\n"
        "            }\n"
        "        }\n"
        "        return (body.to_string(), true);\n",
    )],
    "B1": [(
        "crates/spt-store/src/lastmsg.rs",
        "    let unwrapped = spt_proto::event::parse_event(body)\n"
        "        .filter(|parsed| parsed.event_type.as_deref() == Some(spt_proto::event::EVENT_TYPE_MSG))\n"
        "        .map(|parsed| parsed.body);\n",
        "    let unwrapped: Option<String> = None;\n",
    )],
    "B2": [(
        "crates/spt-store/src/lastmsg.rs",
        "    let unwrapped = spt_proto::event::parse_event(body)\n"
        "        .filter(|parsed| parsed.event_type.as_deref() == Some(spt_proto::event::EVENT_TYPE_MSG))\n"
        "        .map(|parsed| parsed.body);\n",
        "    let unwrapped = spt_proto::event::parse_event(body)\n"
        "        .map(|parsed| parsed.body);\n",
    )],
    "C": [
        (
            "crates/spt-daemon/src/livehost.rs",
            "    let held = crate::servehost::with_registry_write(|| {\n",
            "    let held: std::io::Result<()> = Ok((|| {\n",
        ),
        (
            "crates/spt-daemon/src/livehost.rs",
            "    });\n    if let Err(err) = held {\n",
            "    })());\n    if let Err(err) = held {\n",
        ),
    ],
}

# D = dispatch.rs: the F17 classify arm reverted (ServeFor -> Unknown again); OLD/NEW from env
#     D_OLD / D_NEW = the exact fixup line(s), filled when the fixup tip lands -> classify unit cell red
import os
if os.environ.get("D_OLD") and os.environ.get("D_NEW"):
    ARMS["D"] = [("crates/spt-daemon/src/dispatch.rs", os.environ["D_OLD"], os.environ["D_NEW"])]

def main():
    op, arm = sys.argv[1], sys.argv[2]
    sites = ARMS[arm]
    if op == "revert":
        files = sorted({f for f, _, _ in sites})
        subprocess.check_call(["git", "checkout", "--", *files], cwd=root)
        dirty = subprocess.run(["git", "status", "--short", "-uno"], cwd=root, capture_output=True, text=True).stdout
        print(f"REVERT {arm}: dirty_tracked={len(dirty.splitlines())}")
        if dirty.strip():
            print(dirty); sys.exit(5)
        return
    # apply: every site must match exactly once BEFORE any write; then write; then re-read.
    for f, old, _ in sites:
        text = (root / f).read_bytes().decode("utf-8")
        # count at ONE layer: pick the file's own terminator form, never both (a newline-free
        # site matched by both forms doubled to 2 and REFUSED -- measured 2026-09-07 arm D)
        o = old.replace("\n", "\r\n") if "\r\n" in text else old
        n = text.count(o)
        print(f"MATCH_COUNT {f}: {n}")
        if n != 1:
            print(f"REFUSE: arm {arm} site in {f} matched {n} times, need exactly 1"); sys.exit(6)
    for f, old, new in sites:
        p = root / f
        raw = p.read_bytes().decode("utf-8")
        crlf = "\r\n" in raw
        o = old.replace("\n", "\r\n") if crlf else old
        nw = new.replace("\n", "\r\n") if crlf else new
        assert raw.count(o) == 1
        raw = raw.replace(o, nw)
        p.write_bytes(raw.encode("utf-8"))
        landed = p.read_bytes().decode("utf-8")
        assert landed.count(nw) == 1 and landed.count(o) == 0, "mutation did not land"
        # print the landed region with line numbers
        lines = landed.split("\r\n" if crlf else "\n")
        first = nw.split("\n")[0].rstrip("\r")
        for i, ln in enumerate(lines, 1):
            if ln == first:
                lo, hi = max(1, i - 1), min(len(lines), i + len(nw.splitlines()))
                for j in range(lo, hi + 1):
                    print(f"  {f}:{j}: {lines[j-1]}")
                break
    print(f"MUTATION LANDED arm={arm}")

if __name__ == "__main__":
    main()
