#!/usr/bin/env python3
"""What did this instrument kill? -- the re-open query for `docs/re/REFUTED.md`.

    tools/stale-instrument                     everything, grouped
    tools/stale-instrument render-vs-capture   what one instrument killed
    tools/stale-instrument --ours              only instruments that are ours
    tools/stale-instrument --check             self-test; exits non-zero if broken

Why this exists
---------------
Rule R1. A refutation whose instrument is one of our renderers is not a
refutation -- it is *"our renderer disagrees"*. The register carries that as a
🟡, but a colour alone does not re-open anything: the failure it is there to
prevent was a real disc field sitting dead for weeks because **nothing re-opens a
claim when the instrument that killed it improves.**

So this is the query you run *after* you fix a renderer, a reader or the harness.
It answers "what did I just invalidate?" in one command, instead of relying on
someone remembering which of 222 entries rested on the thing they changed.

The register is the only source. There is no second list to drift out of sync
with it -- the tags live at the end of each entry, in the file people actually
read, which is this corpus's own repeatedly-learned lesson about where a fact has
to live to exist.
"""

from __future__ import annotations

import os
import re
import sys
from collections import OrderedDict

REGISTER = os.environ.get("SYLPH_REGISTER", "docs/re/REFUTED.md")

# Ours, in the sense R1 means: an instrument whose defects we cannot rule out,
# because we built it. `our-tool` is ours too, but it is not disqualifying --
# see the note below and in the register's reading guide.
OURS = {"render-vs-capture", "screen-render", "our-reader", "harness", "our-tool"}

# Not a verdict. It means nobody recorded how the claim died.
UNRECORDED = "unrecorded"

TAG = re.compile(r"⟨([a-z][a-z-]*)⟩\s*$")


def entries(text: str):
    """Every bullet, with its instrument tag and whether it is already 🟡.

    An entry is a `* ` bullet plus its continuation lines, ending at a blank
    line or a heading -- the same span rule the register's own enumeration uses.
    """
    out, cur, start = [], None, 0
    for i, line in enumerate(text.split("\n"), 1):
        if line.startswith("* "):
            if cur is not None:
                out.append((start, cur))
            cur, start = [line], i
        elif line.startswith("#"):
            if cur is not None:
                out.append((start, cur))
            cur = None
        elif cur is not None:
            if not line.strip():
                out.append((start, cur))
                cur = None
            else:
                cur.append(line)
    if cur is not None:
        out.append((start, cur))

    for ln, body in out:
        joined = " ".join(x.strip() for x in body)
        m = TAG.search(body[-1].rstrip())
        # 🔴 THE VERDICT IS THE LEADING GLYPH, NOT ANY 🟡 IN THE ENTRY. Matching
        # anywhere counted four entries as re-opened that merely carry an inline
        # 🟡 caveat mid-sentence ("🟡 a third recorded run implies NEW GAME").
        # An entry whose verdict is ❌ and whose *reach* is qualified is not an
        # open claim, and reporting it as one inflates exactly the number this
        # tool exists to make trustworthy.
        yield {
            "line": ln,
            "instrument": m.group(1) if m else None,
            "open": body[0].startswith("* 🟡"),
            "settled": body[0].startswith("* ✅"),
            "text": re.sub(r"\s+", " ", joined),
        }


def summary(text: str) -> str:
    """The claim, short enough to scan a list of them."""
    t = re.sub(r"^\*\s*(🟡|❌|✅)?\s*", "", text)
    t = re.sub(r"[*`~]", "", t)
    return (t[:118] + "…") if len(t) > 118 else t


def die(msg: str):
    """A HARNESS fault, exit 2 -- distinct from 'no such instrument', exit 1.

    🔴 `sys.exit("text")` exits **1**, not 2. Every one of these sites used it,
    so three faults that this file's own docstring calls exit-2 were reporting
    the same code as an ordinary miss -- a real failure wearing a wrong
    diagnosis, the exact shape the register catalogues elsewhere. Its own
    `--check` caught it on the first run, which is the argument for having one.
    """
    print(msg, file=sys.stderr)
    raise SystemExit(2)


def load():
    if not os.path.exists(REGISTER):
        die(
            f"stale-instrument: {REGISTER} is not here.\n"
            "   Run from the repository root, or set SYLPH_REGISTER.\n"
            "   Exit 2: the harness is broken, not the corpus."
        )
    rows = list(entries(open(REGISTER, encoding="utf-8").read()))
    # 🔴 LIVENESS. A reader that parses nothing reports "no claims rest on that
    # instrument" -- which reads exactly like the good news you were hoping for.
    # A renamed file, a changed bullet shape or a lost tag all produce it. This
    # corpus has already produced one false zero from a reader invented minutes
    # earlier, so the empty case is a harness fault with its own exit code.
    if not rows:
        die("stale-instrument: parsed 0 entries from "
            f"{REGISTER} -- exit 2, the reader is broken.")
    if not any(r["instrument"] for r in rows):
        die(f"stale-instrument: {len(rows)} entries, none tagged ⟨…⟩ -- "
            "exit 2, the reader is broken or the register is untagged.")
    return rows


def main(argv: list[str]) -> int:
    if "--check" in argv:
        return selftest()

    rows = load()
    only_ours = "--ours" in argv
    wanted = [a for a in argv if not a.startswith("-")]

    by = OrderedDict()
    for r in rows:
        by.setdefault(r["instrument"] or UNRECORDED, []).append(r)

    if wanted:
        name = wanted[0]
        if name not in by:
            print(f"no entry names ⟨{name}⟩. Known instruments:")
            for k in sorted(by):
                print(f"  {k}")
            return 1
        group = by[name]
        mine = " -- OURS, so these are re-openable" if name in OURS else ""
        print(f"⟨{name}⟩ killed {len(group)} claim(s){mine}\n")
        for r in group:
            mark = "🟡" if r["open"] else ("✅" if r["settled"] else "❌")
            print(f"  {mark} L{r['line']:<4} {summary(r['text'])}")
        if name in OURS:
            print(f"\n  If you have improved {name}, every ❌ above is a claim that"
                  "\n  died to an instrument that no longer exists in that form.")
        return 0

    print(f"{len(rows)} entries in {REGISTER}\n")
    order = sorted(by, key=lambda k: (k not in OURS, k == UNRECORDED, -len(by[k])))
    for k in order:
        g = by[k]
        tag = "  ← OURS" if k in OURS else ("  ← nobody wrote it down"
                                            if k == UNRECORDED else "")
        opened = sum(1 for r in g if r["open"])
        note = f", {opened} already 🟡" if opened else ""
        print(f"  {k:20s} {len(g):3d} claim(s){note}{tag}")

    ours = sum(len(by[k]) for k in by if k in OURS)
    unrec = len(by.get(UNRECORDED, []))
    print(f"\n  {ours} claim(s) died to an instrument WE BUILT.")
    print(f"  {unrec} claim(s) record no instrument at all -- "
          "neither safe nor suspect,\n  just unauditable. That is the backfill queue.")
    print("\n  tools/stale-instrument <instrument>   to list one")
    return 0


def selftest() -> int:
    """Cases that must hold, executed -- not reasoned about.

    A control that does not run is not a control; this file's neighbours in
    `tools/` say so about other people's tools, so it is asserted here.
    """
    import subprocess
    import tempfile

    me = os.path.abspath(__file__)
    ok = 0

    def run(reg, args=()):
        env = dict(os.environ, SYLPH_REGISTER=reg)
        p = subprocess.run([sys.executable, me, *args], capture_output=True,
                           text=True, env=env)
        return p.returncode, p.stdout + p.stderr

    with tempfile.TemporaryDirectory() as d:
        good = os.path.join(d, "good.md")
        open(good, "w", encoding="utf-8").write(
            '* "a" → refuted. ⟨disc⟩\n\n* 🟡 "b" — our renderer disagrees. ⟨render-vs-capture⟩\n'
        )
        cases = [
            ("a tagged register parses", (good, ()), 0, "2 entries"),
            ("one instrument lists", (good, ("disc",)), 0, '"a"'),
            ("an OURS instrument says so", (good, ("render-vs-capture",)),
             0, "re-openable"),
            ("an unknown instrument fails", (good, ("nope",)), 1, "no entry names"),
        ]
        for name, (reg, args), want_rc, want_txt in cases:
            rc, out = run(reg, args)
            good_rc = rc == want_rc
            good_tx = want_txt in out
            if good_rc and good_tx:
                print(f"  {name:34s} exit {rc}  ✅")
            else:
                print(f"  {name:34s} exit {rc} (wanted {want_rc}), "
                      f"text {'found' if good_tx else 'MISSING'}  🔴")
                ok = 1

        # The two harness faults, which must be distinguishable from a clean run.
        empty = os.path.join(d, "empty.md")
        open(empty, "w").write("# nothing here\n")
        rc, out = run(empty)
        if rc == 2 and "reader is broken" in out:
            print(f"  {'an empty register is a fault':34s} exit 2  ✅")
        else:
            print(f"  {'an empty register is a fault':34s} exit {rc}, wanted 2  🔴")
            ok = 1

        untagged = os.path.join(d, "untagged.md")
        open(untagged, "w", encoding="utf-8").write('* "a" → refuted.\n')
        rc, out = run(untagged)
        if rc == 2 and "none tagged" in out:
            print(f"  {'an untagged register is a fault':34s} exit 2  ✅")
        else:
            print(f"  {'an untagged register is a fault':34s} exit {rc}, wanted 2  🔴")
            ok = 1

        rc, _ = run(os.path.join(d, "does-not-exist.md"))
        if rc == 2:
            print(f"  {'a missing register is a fault':34s} exit 2  ✅")
        else:
            print(f"  {'a missing register is a fault':34s} exit {rc}, wanted 2  🔴")
            ok = 1

    print()
    print("the query fails when it must, and says so distinctly" if not ok
          else "🔴 the query's own control is broken")
    return ok


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
