#!/usr/bin/env python3
"""rig_units — THE inventory of systemd --user units that ARE the rig.

WHY THIS FILE EXISTS
--------------------
2026-08-23. PLN launched the rig and found orbits 5-9 reaching no Ardour track:
`tidal-ardour-autoroute.service` was `disabled` + `inactive`. Orbits 1-4 and
10-12 were still wired as leftovers from an earlier session, so it read as
"half works" rather than "the service is off".

Two structural causes, both fixed here:

1. THE LIST LIVED IN THREE PLACES. `tools/bridge/rig.py` had a SERVICES table
   (five rows), `gig-up.sh` had a hardcoded loop (four names), and boot state
   lived in whatever `[Install]` section each unit happened to carry. When
   `lcxl-leds-watch` was missing from the Bridge table the panel showed a fully
   green rig while the board sat dark for a session — the #97 lesson (one grid,
   five copies, the HUD's went stale) repeating on a different noun.

   So: this module is the ONE authored copy. `gig-up.sh` reads `--list`, the
   Bridge imports `SERVICES`, and `parvagues-rig.target` is GENERATED by
   `--target`. Adding a unit to the rig = adding one row here.

2. `gig-up.sh` only ever ran `systemctl start`, never `enable`. A `start` fixes
   tonight and changes nothing about the next login, which is precisely how a
   unit stays disabled for weeks while every pre-gig check passes. `--ensure`
   does both, and reports which of the two it had to do.

BOOT POLICY, and why it is a per-unit field rather than a global
---------------------------------------------------------------
  "login"     enable at default.target. The reconcilers: they idle at ~0 CPU,
              hold no audio ports, make no sound, and their whole job is to
              re-assert a binding that something else keeps breaking. There is
              no such thing as "too early" for them.
  "on-demand" NOT enabled. `parvagues-sc` is the sound: it claims MIDI, opens
              the audio graph, and its own unit deliberately refuses
              Restart=always because audio gear must never flap. PLN launches
              the rig on purpose; a login should not start SuperDirt.

`is-enabled` returning **`linked`** means the unit file is a symlink and the
unit is NOT enabled — it reads like a healthy word and is not one. That is the
state `parvagues-sc` is in, correctly, and the state the watchdog was in by
accident. `--status` spells it out rather than printing systemd's word.
"""
from __future__ import annotations

import argparse
import subprocess
import sys

# unit, label, process that proves it really works (None = the unit is the whole
# story), boot policy, blurb
SERVICES = [
    ("parvagues-sc",           "SuperDirt",   "scsynth", "on-demand", "the sound"),
    ("parvagues-sc-watchdog",  "sc-watchdog", None,      "login",     "restarts SuperDirt if scsynth dies"),
    ("tidal-ardour-autoroute", "autoroute",   None,      "login",     "orbits → Ardour Tidal NN tracks"),
    ("midi-autoconnect",       "MIDI wiring", None,      "login",     "LCXL → Midi Through, return leg cut"),
    ("lcxl-leds-watch",        "LCXL LEDs",   None,      "login",     "the surface paint"),
    ("parvagues-bridge",       "Bridge",      None,      "login",     "dashboard + perf toolbar"),
    # Added 2026-08-29. Absent from this list until then, and that absence IS why
    # CosmicFest has no session log: the unit was installed 2026-07-29 and never
    # enabled, so it only ran when someone remembered to start it by hand. The
    # sessions that exist (through 2026-08-20) are exactly those manual runs.
    # Costs 0.4% of a core at 1 Hz, so there is no reason for it to ever be off.
    ("gig-log",                "gig-log",     None,      "login",     "1 Hz session recorder: thermals, xruns, gear, MIDI"),
]

TARGET = "parvagues-rig.target"

UNITS = [s[0] for s in SERVICES]
BOOT = {s[0]: s[3] for s in SERVICES}
BLURB = {s[0]: s[4] for s in SERVICES}
LABEL = {s[0]: s[1] for s in SERVICES}
PROOF = {s[0]: s[2] for s in SERVICES}


def _sc(*args: str) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["systemctl", "--user", *args], capture_output=True, text=True
    )


def states() -> dict[str, dict[str, str]]:
    """{unit: {"active":…, "enabled":…}} in TWO forks, not two per unit.

    Parsed by KEY (`Id=`), never by position: `systemctl show` emits properties
    in systemd's own order, not the order you asked for — pairing them
    positionally is what silently swapped LoadState and ActiveState in the
    Bridge's copy of this.
    """
    out: dict[str, dict[str, str]] = {u: {"active": "?", "enabled": "?"} for u in UNITS}
    r = _sc("show", "--property=Id", "--property=ActiveState", *[f"{u}.service" for u in UNITS])
    unit = None
    for line in r.stdout.splitlines():
        k, _, v = line.partition("=")
        if k == "Id":
            unit = v.removesuffix(".service")
        elif k == "ActiveState" and unit in out:
            out[unit]["active"] = v
    # is-enabled has no --property form; one fork for all of them.
    r = _sc("is-enabled", *[f"{u}.service" for u in UNITS])
    for u, line in zip(UNITS, r.stdout.splitlines()):
        out[u]["enabled"] = line.strip()
    return out


def gen_target() -> str:
    wants = "\n".join(f"Wants={u}.service" for u in UNITS)
    return f"""[Unit]
# GENERATED by tools/rig_units.py --target. Do not hand-edit: add a row to
# SERVICES in that module and regenerate, or the list drifts out of the one
# place that is supposed to own it.
#
# The single handle for "bring the whole rig up":
#     systemctl --user start {TARGET}
#
# This target is deliberately NOT enabled at boot. It Wants parvagues-sc, and a
# login must not start SuperDirt (see the boot policy note in rig_units.py).
# The reconcilers are enabled individually, by `rig_units.py --ensure`.
Description=The ParVagues rig (SuperDirt + every reconciler)
After=pipewire.service
Wants=pipewire.service
{wants}
"""


def ensure(apply: bool) -> int:
    """Enable per policy AND start. The half gig-up.sh never did."""
    st = states()
    todo = []
    for u in UNITS:
        s = st[u]
        # "linked" is NOT enabled, and neither is "disabled".
        if BOOT[u] == "login" and s["enabled"] not in ("enabled", "enabled-runtime"):
            todo.append(("enable", u, s["enabled"]))
        if s["active"] != "active":
            todo.append(("start", u, s["active"]))
    if not todo:
        print(f"rig: all {len(UNITS)} units enabled-per-policy and active — nothing to do")
        return 0
    for verb, u, was in todo:
        if apply:
            r = _sc(verb, f"{u}.service")
            ok = "ok" if r.returncode == 0 else f"FAILED: {r.stderr.strip()}"
            print(f"rig: {verb:<6} {u:<24} (was {was}) — {ok}")
        else:
            print(f"rig: would {verb:<6} {u:<24} (is {was})")
    if not apply:
        print("\nrig: dry run — pass --apply to act")
    return 0


def status() -> int:
    st = states()
    bad = 0
    print(f"{'unit':<24} {'boot':<10} {'enabled':<10} {'active':<10} note")
    for u in UNITS:
        s = st[u]
        want_enabled = BOOT[u] == "login"
        note = []
        if want_enabled and s["enabled"] not in ("enabled", "enabled-runtime"):
            note.append("NOT enabled at boot")
            bad += 1
        if s["active"] != "active":
            note.append("NOT running")
            bad += 1
        if s["enabled"] == "linked" and not want_enabled:
            note.append("on-demand, as designed")
        print(f"{u:<24} {BOOT[u]:<10} {s['enabled']:<10} {s['active']:<10} {'; '.join(note) or BLURB[u]}")
    print(f"\nrig: {len(UNITS)} unit(s), {bad} problem(s)")
    return 1 if bad else 0


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    g = p.add_mutually_exclusive_group(required=True)
    g.add_argument("--list", action="store_true", help="unit names, one per line (for shell consumers)")
    g.add_argument("--status", action="store_true", help="enabled/active table; exit 1 if anything is off")
    g.add_argument("--ensure", action="store_true", help="enable per boot policy AND start")
    g.add_argument("--target", action="store_true", help=f"print the generated {TARGET}")
    p.add_argument("--apply", action="store_true", help="with --ensure: actually act")
    a = p.parse_args()
    if a.list:
        print("\n".join(UNITS))
        return 0
    if a.target:
        print(gen_target(), end="")
        return 0
    if a.ensure:
        return ensure(a.apply)
    return status()


if __name__ == "__main__":
    sys.exit(main())
