#!/usr/bin/env python3
"""cheat-sheet — the one page PLN reads at the venue, generated not written.

WHY THIS EXISTS
---------------
#16, ranked "bonus, can be 80/20ed". The cheat-sheet IS the 80/20: at OPAL he
is alone in a field with a laptop, a desk, and no assistant, and the two things
he will actually need are "which knob does what" and "what do I press when it
goes wrong". Everything else in #16 (FOH signal chain, go-bag) he can hold in
his head; a 12-column control surface he cannot.

GENERATED, NOT AUTHORED — on purpose. A hand-written card is a copy of the
grid, and a copy drifts silently the moment the grid changes. This reads
`tools/lcxl_grid.py` (the one authored table, #97) and the setlist, so a card
that disagrees with the rig is impossible by construction. Same discipline as
the generated HUD map: one parser per concept, one author per fact.

It deliberately puts the EMERGENCY box first. The moment you need this page is
the moment something is wrong, and a card that opens with a reference table
makes you read past your own panic to reach the answer.

USAGE
    python3 tools/cheat-sheet.py               # print it
    python3 tools/cheat-sheet.py --md PATH     # write it (default docs/)
"""

from __future__ import annotations

import argparse
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))

import lcxl_grid as grid  # noqa: E402
from pvlint.core import Track  # noqa: E402

ROOT = pathlib.Path(__file__).resolve().parent.parent
DEFAULT_OUT = ROOT / "docs" / "GIG-CHEATSHEET.md"

W = 78  # printable width — fits one portrait page at a readable font size

def setlist():
    """(codename, path, sorted orbits) in play order. Reuses set-coherence's parser."""
    import importlib.util
    spec = importlib.util.spec_from_file_location(
        "sc", pathlib.Path(__file__).resolve().parent / "set-coherence.py")
    m = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(m)
    out = []
    for p in m.setlist_tracks():
        if not p.exists():
            continue
        orbits = sorted({o.number for o in
                         Track(path=str(p), text=p.read_text()).orbits()})
        out.append((p.stem, p, orbits))
    return out


def build() -> str:
    L: list[str] = []
    def w(s: str = "") -> None:
        L.append(s)

    w("# OPAL 2026 — surface card")
    w()
    w("*Generated by `tools/cheat-sheet.py` from `tools/lcxl_grid.py` + the")
    w("setlist. Do not hand-edit — regenerate.*")
    w()

    # ---- emergency, first, always ------------------------------------------
    w("```")
    w("+" + "-" * (W - 2) + "+")
    w("|" + "  WHEN IT GOES WRONG".ljust(W - 2) + "|")
    w("+" + "-" * (W - 2) + "+")
    for line in [
        "",
        "  EVERYTHING OFF          hush        (or CC93 = gPanic, global kill)",
        "  ONE ORBIT OFF           dN $ silence",
        "  SOUND BUT NO CONTROL    the desk is not seeding -> sweep the row,",
        "                          each knob wakes on its FIRST move",
        "  LEDS DARK, KNOBS WORK   USB OUT endpoint stalled. REPLUG the LCXL.",
        "                          Input keeps working while output is dead,",
        "                          so 'it responds' does not mean it is fine.",
        "  A TRACK MAKES NO SOUND  it did not compile. One block, one error —",
        "                          check Pulsar's notification for EVAL ERROR.",
        "  AN ORBIT WILL NOT DIE   it is the PREVIOUS track's pattern still",
        "                          running (an orphan). dN $ silence, by hand.",
        "  MIXER SILENT, TIDAL OK  an Ardour fader is down. Faders are CC77-84",
        "                          and the desk wins on touch.",
        "",
    ]:
        w("|" + line.ljust(W - 2) + "|")
    w("+" + "-" * (W - 2) + "+")
    w("```")
    w()

    # ---- before you start ---------------------------------------------------
    w("## Before the first track")
    w()
    w("```")
    w("  tools/gig-up.sh          GO / NO-GO for the whole chain (cold, ~1 min)")
    w("  tools/gig-up.sh --audio  + boots every track and proves it sounds")
    w("```")
    w()
    w("A green `gig-up` proves the set **boots** correct. It does NOT prove the")
    w("mixer is correct — `check-mix` reads the SAVED Ardour session, and the")
    w("physical desk drifts from it in both directions. **Ctrl+S in Ardour**")
    w("before trusting a green fader line.")
    w()

    # ---- the two groupings --------------------------------------------------
    w("## The two groupings (they are NOT the same, on purpose)")
    w()
    fam_f: dict[int, list[int]] = {}
    fam_m: dict[int, list[int]] = {}
    for o in range(1, 13):
        fam_f.setdefault(grid.filter_family(o), []).append(o)
        fam_m.setdefault(grid.mute_family(o), []).append(o)
    w("```")
    w("  FILTERS group by BLOC              MUTES group by ROLE")
    w("  row C, knobs 1-3                   row F, buttons 1-3")
    w()
    # Stacked, not side-by-side: gF3/gM3 hold 7 and 8 orbits, which blew past a
    # two-column layout and silently misaligned the whole block.
    for tag, cc0, fam in (("gF", 48, fam_f), ("gM", 72, fam_m)):
        for i in (1, 2, 3):
            names = ",".join(f"d{o}" for o in fam.get(i, []))
            w(f"  {tag}{i} ^{cc0+i}   {names}")
        w()
    w("  DJ filter: 0.5 = BYPASS (centre). 0.05 is a ~26 Hz lowpass = silence.")
    w("  So a filter knob left at zero is not neutral, it is off.")
    w("```")
    w()
    w("The kick sits alone on **gM1** so it can drop by itself, but filters with")
    w("the whole rhythm bloc on **gF1**. That asymmetry is deliberate: `gM<N>`")
    w("and `gF<N>` carrying different numbers on the same orbit is CORRECT.")
    w()

    # ---- the surface --------------------------------------------------------
    w("## The surface, column by column")
    w()
    w("```")
    w("  col  1      2      3      4      5      6      7      8")
    w("  " + "-" * 60)
    rowmeaning = {
        "A": "A  knob top  d9-d12 level (1-4) | d9-d12 fx (5-8)",
        "B": "B  knob mid  per-orbit effect, column N = orbit N",
        "C": "C  knob bot  1-3 FAMILY FILTER | 4-8 per-orbit fx2",
        "D": "D  fader     orbit level  -- ARDOUR OWNS THESE (CC77-84)",
        "E": "E  button    per-orbit gate, column N = orbit N",
        "F": "F  button    1-3 FAMILY MUTE  | 4-8 per-orbit gate2",
    }
    for r in grid.PHYSICAL_ORDER:
        ccs = dict(grid._ROWS[r][1])
        line = "  " + r + "    " + " ".join(f"^{ccs[c]:<5}" for c in range(1, 9))
        w(line.rstrip())
    w("  " + "-" * 60)
    for r in grid.PHYSICAL_ORDER:
        w("  " + rowmeaning[r])
    w()
    w("  NEVER send CC 77-84 from Tidal (Ardour track gains; 77 down = silence).")
    w("  NEVER sweep CC 93 (gPanic).")
    w("```")
    w()

    # ---- the set ------------------------------------------------------------
    w("## The set, in play order")
    w()
    # A "roles" column was tried and dropped: every track carries kick+perc+
    # bass+lead, so it printed the same string 13 times and pushed the table
    # past the page. What is NOT declared is the useful fact — those are the
    # orbits that keep playing the previous track.
    w("```")
    w(f"  {'#':>2}  {'track':<32} {'declares':<27} ghost risk")
    w("  " + "-" * 74)
    for i, (stem, _p, orbits) in enumerate(setlist(), 1):
        obs = ",".join(str(o) for o in orbits)
        gaps = ",".join(str(o) for o in range(1, 13) if o not in orbits) or "-"
        w(f"  {i:>2}  {stem:<32} {obs:<27} {gaps}")
    w("```")
    w()
    w("An orbit a track does **not** declare is not silent by default — it keeps")
    w("playing whatever the previous track put there. When a transition sounds")
    w("wrong, that is the first thing to suspect.")
    w()
    return "\n".join(L) + "\n"


def main() -> int:
    ap = argparse.ArgumentParser(prog="cheat-sheet")
    ap.add_argument("--md", type=pathlib.Path, nargs="?", const=DEFAULT_OUT)
    a = ap.parse_args()
    text = build()
    if a.md:
        a.md.parent.mkdir(parents=True, exist_ok=True)
        a.md.write_text(text)
        print(f"wrote {a.md}")
    else:
        print(text, end="")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
