"""Convert TOKEN:-shaped eprintln! sites to the single-write emitter.

Per-site and verified, never a sweep: 200 of the 487 calls span multiple lines,
so a regex over the file would mangle 41% of the population. This edits ONE
macro token per named site, after re-finding it from the census line, and
refuses anything it cannot place exactly.

Three guards, each earned by a real miss:
  * STDOUT rows are skipped BY NAME and printed — the tear surface is the shared
    stderr, and a silent skip is how an exclusion becomes invisible.
  * PHANTOM rows are skipped BY NAME: the census attributes a macro by looking up
    to 4 lines back, and that window can reach an `eprintln!` the literal does not
    belong to (sealverb.rs:456 is a `format!` inside a `map_err`). Containment is
    re-checked here with a balanced-paren span, so a row that is not an emission
    is never edited.
  * ONE MACRO CALL IS CONVERTED ONCE even when it carries two census rows, and an
    already-converted call is recognised rather than refused.
"""
import re
import subprocess
import sys

ROOT = r"C:/Users/decid/Documents/projects/spt-core/.worktrees/emit-single-write"
SITES = r"C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/43af483b-6d22-46cb-9d99-8b7713a6ff46/scratchpad/cut-4d6007ac/census_sites.txt"
MACRO = re.compile(r"\b(eprintln!|eprint!)\s*\(")
ANY_MACRO_OPEN = re.compile(r"\b(eprintln!|println!|eprint!|print!)\s*\(")
# ANY emitter spelling counts as already-converted, not just the stderr-aimed pair:
# a consumer lane may have converted a site with the writer-taking spelling first
# (hertz did exactly that at autostart.rs), and double-converting it would be a
# silent wrong edit rather than a refusal.
ALREADY = re.compile(r"\b(spt_proto::)?emit_(line|block)(_err)?!\s*\(")

crates = sys.argv[1:]
if not crates:
    sys.exit("usage: convert.py <crate> [crate...]")

rows = [l.split("\t") for l in open(SITES, encoding="utf-8").read().splitlines()]
rows = [r for r in rows if r[0].split("/")[1] in crates]

skipped = [r for r in rows if r[2] in ("println!", "print!")]
for r in skipped:
    print(f"  SKIP (stdout, out of lane): {r[0]}  {r[1]}  {r[2]}")
rows = [r for r in rows if r[2] not in ("println!", "print!")]


def call_span(lines, start):
    """(first, last) line indices of the macro call opening at/after `start`."""
    text = "\n".join(lines[start:start + 80])
    m = ANY_MACRO_OPEN.search(text)
    if not m:
        return None
    i = m.end() - 1
    depth = 0
    instr = False
    esc = False
    for k in range(i, len(text)):
        c = text[k]
        if instr:
            if esc:
                esc = False
            elif c == chr(92):
                esc = True
            elif c == '"':
                instr = False
            continue
        if c == '"':
            instr = True
        elif c == "(":
            depth += 1
        elif c == ")":
            depth -= 1
            if depth == 0:
                return (start, start + text[:k].count("\n"))
    return None


by_file = {}
for loc, tok, mac in rows:
    path, ln = loc.rsplit(":", 1)
    by_file.setdefault(path, []).append((int(ln), tok, mac))

converted = 0
phantom = 0
already = 0
for path, sites in sorted(by_file.items()):
    full = ROOT + "/" + path
    lines = open(full, encoding="utf-8", newline="").read().split("\n")
    done_lines = set()
    for ln, tok, mac in sorted(sites, reverse=True):
        j = ln - 1
        start = None
        for k in range(j, max(-1, j - 5), -1):
            if MACRO.search(lines[k]) or ALREADY.search(lines[k]):
                start = k
                break
        if start is None:
            sys.exit(f"REFUSE: no macro token within 5 lines above {path}:{ln} ({tok})")
        span = call_span(lines, start) if MACRO.search(lines[start]) else None
        if span is not None and not (span[0] <= j <= span[1]):
            print(f"  SKIP (phantom, literal outside the call span): {path}:{ln}  {tok}")
            phantom += 1
            continue
        if ALREADY.search(lines[start]) or start in done_lines:
            already += 1
            continue
        new = MACRO.sub("spt_proto::emit_line_err!(", lines[start], count=1)
        if new == lines[start]:
            sys.exit(f"REFUSE: substitution changed nothing at {path}:{start+1}")
        lines[start] = new
        done_lines.add(start)
        converted += 1
    open(full, "w", encoding="utf-8", newline="").write("\n".join(lines))

print(
    f"converted {converted} calls across {len(by_file)} files in {crates}; "
    f"{already} rows shared an already-converted call, {phantom} phantom rows skipped, "
    f"{len(skipped)} stdout rows skipped"
)
