#!/usr/bin/env python3
"""convert-973.py — mechanical #973 site conversion, driven by the frozen
denominator artifact (denominator-089615f.json), never by ad-hoc grepping.

For every row in the artifact it FIRST verifies the worktree line still says
what the artifact froze (exact match after whitespace strip, artifact-side
truncation as prefix match, or the normalized suite-summary template), and
aborts before touching anything on the first verification failure — a
conversion applied to a line the denominator did not measure would be the
umbrella defect wearing the converter's clothes.

Transforms (behaviour-preserving; verdict semantics unchanged on grep rc 0/1):
  E-count-capture   `grep -c ARGS`  -> `count_lines ARGS`  (helper adds -c)
  all other forms   `grep ARGS`     -> `has_match ARGS`    (drop-in)
  env prefixes (`LC_ALL=C grep`) are kept — the prefix reaches the grep child
  through the function (microtest C8).

The two multi-grep pipeline sites (digest-quarantine:560, install:420) convert
BOTH greps: each is measurement-bearing, and under pipefail an rc=2 in the
left element is masked by an rc=1 in the right — the same defect one pipe
deeper. They stay ONE denominator site each (one coordinate); the ledger
records helper calls, so those coordinates appear twice per execution and the
equality check compares SETS of coordinates.

Finally each suite gains three header lines directly after its SCRIPT_DIR
assignment (source + wake_assert_init + comment), shifting every site by +3
lines exactly; the validation harness maps base coordinates accordingly.
"""

import json
import re
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
WAKE = HERE.parent
ART = HERE / "denominator-089615f.json"

RX_GREP_TOKEN = re.compile(r"(^|[^A-Za-z0-9_.-])grep([^A-Za-z0-9_.-]|$)")

HEADER = [
    "# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness.\n",
    "# shellcheck disable=SC1091\n",
    '. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init\n',
]

MULTI_GREP_CONVERT_BOTH = {
    ("test-wake-digest-quarantine.sh", 560),
    ("test-wake-install.sh", 420),
}

SUMMARY_TEMPLATE_MARK = '$(grep -c . "$FAILFILE")'


def verify(row, actual):
    a = actual.strip()
    t = row["text"].strip()
    if a == t:
        return True
    if t and a.startswith(t):  # artifact-side truncation
        return True
    # normalized suite-summary template rows
    if t.startswith('echo "wake ') and SUMMARY_TEMPLATE_MARK in actual and a.startswith('echo "wake '):
        return True
    return False


def convert_line(row, line):
    key = (row["file"], row["line"])
    n_grep = len(RX_GREP_TOKEN.findall(line))
    if key in MULTI_GREP_CONVERT_BOTH:
        assert n_grep == 2, f"{key}: expected 2 grep tokens, found {n_grep}"
    else:
        assert n_grep == 1, f"{key}: expected 1 grep token, found {n_grep}: {line!r}"

    if row["form"].startswith("E"):
        assert line.count("grep -c ") == 1, f"{key}: E row without single 'grep -c ': {line!r}"
        return line.replace("grep -c ", "count_lines ", 1)

    def repl(m):
        return m.group(1) + "has_match" + m.group(2)

    count = 2 if key in MULTI_GREP_CONVERT_BOTH else 1
    return RX_GREP_TOKEN.sub(repl, line, count=count)


def main():
    art = json.loads(ART.read_text())
    rows = art["rows"]
    by_file = {}
    for r in rows:
        by_file.setdefault(r["file"], []).append(r)

    # pass 1: verify every row before touching any file
    bad = 0
    texts = {}
    for f, frs in by_file.items():
        lines = (WAKE / f).read_text().split("\n")
        texts[f] = lines
        for r in frs:
            if not verify(r, lines[r["line"] - 1]):
                bad += 1
                print(f"VERIFY-FAIL {f}:{r['line']}\n  artifact: {r['text']!r}\n  worktree: {lines[r['line'] - 1]!r}")
    if bad:
        sys.exit(f"ABORT: {bad} row(s) failed verification; nothing was modified.")

    # pass 2: convert + insert header
    total = {"has_match": 0, "count_lines": 0}
    for f, frs in sorted(by_file.items()):
        lines = texts[f]
        for r in frs:
            i = r["line"] - 1
            new = convert_line(r, lines[i])
            assert new != lines[i], f"{f}:{r['line']}: no-op conversion"
            lines[i] = new
            total["count_lines" if r["form"].startswith("E") else "has_match"] += 1
        # header insertion after the SCRIPT_DIR= line
        sd = [i for i, ln in enumerate(lines) if ln.startswith('SCRIPT_DIR="$(')]
        assert len(sd) == 1, f"{f}: expected exactly one SCRIPT_DIR line, found {len(sd)}"
        first_site = min(r["line"] for r in frs) - 1
        assert sd[0] < first_site, f"{f}: SCRIPT_DIR line {sd[0] + 1} not before first site {first_site + 1}"
        lines[sd[0] + 1 : sd[0] + 1] = [h.rstrip("\n") for h in HEADER]
        (WAKE / f).write_text("\n".join(lines))
        print(f"{f}: {len(frs)} sites converted, header at line {sd[0] + 2}")

    print(f"TOTAL: {total['has_match']} has_match + {total['count_lines']} count_lines = {sum(total.values())} sites in {len(by_file)} files")
    assert sum(total.values()) == art["total"] == 261, "site count mismatch vs artifact"


if __name__ == "__main__":
    main()
