#!/usr/bin/env bash
# gig-up — ONE command that says GO or NO-GO for the whole chain.
#
# Why this exists (2026-07-31, J-8 to OPAL)
# -----------------------------------------
# A fortnight of debugging produced a shelf of good checks — check-boot,
# check-mix, silent-eval, fix-mute-roles, pvlint, orphan-orbits — and every one
# of them is currently a command PLN would have to REMEMBER. On Sat 8 Aug at
# ~19:00 he is in a field, alone, with a soundcheck window and no assistant.
# A check you have to remember under pressure is a check you do not run.
#
# So this is not new logic. It is a running order and an exit code. Every check
# below already exists and already works; gig-up only decides which are
# GO-BLOCKING and prints the fix inline so the answer and the remedy arrive
# together.
#
# The one that earned its HARD FAIL
# ---------------------------------
# `check-mix.py` finding an orbit at -inf has now been caught three sessions in
# a row, with a DIFFERENT set of tracks each time:
#
#     2026-07-28   Tidal 05, 06, 08, 10 = -inf
#     2026-07-30   Tidal 06, 10, 12 = -inf
#     2026-07-31   Tidal 10 = -inf        (after the Ctrl+S that fixed 06 and 12)
#
# It recurs because the D-row faders are MIDI-learned to LCXL CC 77-84, so the
# saved session and the physical desk drift independently in BOTH directions.
# Three recurrences is not bad luck, it is a property of the setup — hence a
# hard gate rather than a warning. NB the remedy splits: a fader that is still
# -inf AFTER a save is genuinely down (raise it), not merely unpersisted (Ctrl+S).
#
# What it deliberately does NOT do
# --------------------------------
#   * make sound          — the audio gate is `--audio` (check-tracks.sh, ~45s
#                           per track). Default is cold, so this is safe to run
#                           mid-set, at 3am, or in a quiet backstage room.
#   * send any MIDI       — the surface check reads ALSA only. It never touches
#                           CC 77-84 (Ardour track gains; CC77 down is total
#                           silence) and never sweeps CC 93 (gPanic).
#   * touch a .tidal      — read-only, always. PLN rehearses on these files and
#                           Pulsar saves the BUFFER, not the disk.
#   * judge the corpus    — scope is the SETLIST. 14 corpus tracks do not compile
#                           (#112) and must never make the gig gate red.
#
# Usage
# -----
#     tools/gig-up.sh            # the cold gate — safe anywhere, ~1 min
#     tools/gig-up.sh --audio    # + check-tracks.sh (makes sound, ~10 min)
#     tools/gig-up.sh --live     # + assert a RIG exists, not just a set
#     tools/gig-up.sh --quiet    # only the verdict and any failures
#
# Exit 0 = GO. Non-zero = do not start; the reasons are printed first.
set -u
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 2

AUDIO=0
QUIET=0
LIVE=0
for a in "$@"; do
  case "$a" in
    --audio) AUDIO=1 ;;
    --live) LIVE=1 ;;
    --quiet|-q) QUIET=1 ;;
    -h|--help) sed -n '2,58p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; exit 0 ;;
    *) echo "gig-up: unknown option $a (try --help)" >&2; exit 2 ;;
  esac
done

if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; D=$'\033[2m'; Z=$'\033[0m'
else R=; G=; Y=; D=; Z=; fi

LOG="$(mktemp -t gig-up-XXXXXX.log)"
FAILURES=()   # one "name|fix command" per blocking failure, printed FIRST at the end
WARNINGS=()

# run <name> <fix-hint> <cmd...>
#   Blocking. Non-zero exit => the gig does not start.
run() {
  local name="$1" fix="$2"; shift 2
  printf '  %-22s' "$name"
  { echo "########## $name : $*"; "$@"; } >>"$LOG" 2>&1
  local rc=$?
  if [ $rc -eq 0 ]; then
    echo "${G}ok${Z}"
  else
    echo "${R}FAIL${Z}  ${D}(rc=$rc)${Z}"
    FAILURES+=("$name|$fix")
  fi
  return $rc
}

# soft <name> <hint> <cmd...>
#   Advisory. Reports, never blocks — for things that are known-open work or
#   depend on hardware that is legitimately absent at this moment.
soft() {
  local name="$1" hint="$2"; shift 2
  printf '  %-22s' "$name"
  { echo "########## $name : $*"; "$@"; } >>"$LOG" 2>&1
  if [ $? -eq 0 ]; then echo "${G}ok${Z}"
  else echo "${Y}warn${Z}"; WARNINGS+=("$name|$hint"); fi
}

# --- context: is the rig up? -------------------------------------------------
# Not a check, an interpretation key. Several results below mean different
# things cold vs live, and reading the report without knowing which is how you
# get a confidently wrong verdict.
rig_state() {
  local sc=no ard=no
  pgrep -x scsynth >/dev/null 2>&1 && sc=yes
  pgrep -x ardour >/dev/null 2>&1 || pgrep -x ardour8 >/dev/null 2>&1 \
    || pgrep -x ardour9 >/dev/null 2>&1 && ard=yes
  echo "$sc $ard"
}
read -r SC ARD <<<"$(rig_state)"

(( QUIET )) || {
  echo
  echo "gig-up — proving the chain.  ${D}scsynth=$SC ardour=$ARD  $( [ "$SC" = no ] && echo '(rig DOWN — cold checks only are meaningful)')${Z}"
  echo
}

# --- 1. the helpers typecheck ------------------------------------------------
# First because one parse error in BootTidal.hs silences EVERY track at once —
# the worst failure this rig has, and invisible while a stale ghci still holds
# the old definitions.
run "boot helpers" \
    "read the ghc error: tools/check-boot.sh" \
    tools/check-boot.sh

# --- 2. the saved Ardour faders ----------------------------------------------
run "ardour faders" \
    "raise it on the desk, then Ctrl+S in Ardour, then re-run. Still -inf after a save = genuinely down." \
    python3 tools/check-mix.py --quiet

# --- 3. every setlist track compiles -----------------------------------------
# Cold, seeded, no rig required. Baseline 13/13 as of 2026-07-31.
run "setlist compiles" \
    "python3 tools/silent-eval.py --seeded --keep  # then read the ghc error for the named track" \
    python3 tools/silent-eval.py --seeded

# --- 4. the mute map has not drifted -----------------------------------------
# gM1 = the kick alone, gM2 = the other percs, gM3 = bass + melodics. The
# filters group differently on purpose (gF1 = the whole rhythm bloc), so this
# check exists precisely because the two maps LOOK like they should match.
run "mute map" \
    "python3 tools/fix-mute-roles.py --apply   # then re-read the diff before committing" \
    python3 tools/fix-mute-roles.py --check

# --- 5. lint, errors only ----------------------------------------------------
# pvlint's `--setlist` is a MODIFIER ("these paths are ordered"), not a mode —
# it still needs the paths. Rather than teach a fourth file how to parse the
# setlist (check-tracks.sh, orphan-orbits.py and set-coherence.py each already
# know), borrow the one parser that is importable. One parser per concept.
run "pvlint (setlist)" \
    "cd tools && python3 -m pvlint --setlist \$(...)   # --fix applies the safe ones" \
    bash -c 'cd tools && python3 -m pvlint --quiet --setlist $(python3 -c "
import importlib.util, pathlib
spec = importlib.util.spec_from_file_location(\"sc\", \"set-coherence.py\")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(\" \".join(str(p) for p in m.setlist_tracks()))")'

# --- 5b. the preload plan covers the set -------------------------------------
# Cold, and HARD. On 2026-08-01 preload.scd was 4 days old and 3 tracks short, so
# take_5_drops' banks were read from DISK on first play and PLN heard it as crackle
# mid-transition. Every other check was green throughout: the plan is GENERATED and
# GITIGNORED, so nothing kept it fresh and nothing compared it to the set. Its natural
# debut was a cold boot at the venue.
run "preload covers set" \
    "tools/check-preload.sh --fix   # then restart parvagues-sc to warm it" \
    tools/check-preload.sh

# --- 6. orbit ghosts across transitions --------------------------------------
# Advisory ON PURPOSE. Ghosting orbits are real (#77) and known-open; they make
# a transition messy, not the gig impossible. A gate that goes red for open work
# is a gate that gets ignored.
soft "transition ghosts" \
     "python3 tools/orphan-orbits.py --matrix   # #77 is the fix, not tonight" \
     python3 tools/orphan-orbits.py

# --- 7. the surface is plugged in --------------------------------------------
# READ-ONLY: asks ALSA who is on the bus. Sends nothing. Advisory because the
# desk is legitimately unplugged when this runs at a kitchen table — but at the
# venue, a warn here means the set has no hands.
soft "LCXL present" \
     "replug the LCXL (USB OUT endpoint stalls — LEDs dark but input alive), then tools/lcxl-init.py" \
     bash -c 'aseqdump -l 2>/dev/null | grep -qi "Launch Control XL"'

# --- 7b. the live chain, opt-in (--live) -------------------------------------
# Everything above proves the SET is correct. None of it proves a RIG EXISTS.
# On 2026-08-01 scsynth had been dead for two days and this gate would still have
# printed GO: scsynth was read only to colour a banner and to skip --audio.
#
# Cold-by-default stays the right default — cold checks are safe to run mid-set, and
# a gate that demands hardware would be useless on a train. So this is a MODE.
#
# Severity is chosen per link, on purpose:
#   HARD  no audio server / no listener / no path to the DAW = there is no gig.
#   SOFT  Pulsar not open yet, desk unplugged, Ardour not launched, laptop on battery
#         — all normal ten minutes before doors, and a gate that reddens for them is a
#         gate PLN stops reading.
#
# Every check reads the thing that MAKES SOUND — a process, a socket, a real link —
# never `systemctl is-active`. That distinction is the whole reason this section
# exists: the unit reported active/running for two days while the rig was mute.
if (( LIVE )); then
  (( QUIET )) || { echo; echo "  ${D}live chain — is there a rig, not just a set?${Z}"; }

  run "scsynth alive" \
      "systemctl --user start parvagues-sc   # the watchdog should beat you to it" \
      pgrep -x scsynth

  run "sclang :57120" \
      "SuperDirt is not listening — systemctl --user restart parvagues-sc" \
      bash -c 'ss -lunp 2>/dev/null | grep -q ":57120"'

  # 12 orbits x 2 channels = 24 at an absolute minimum; a healthy rig shows ~52.
  # Zero here with scsynth alive means SuperCollider is running and going NOWHERE.
  run "SC -> Ardour" \
      "check the autoroute: systemctl --user restart tidal-ardour-autoroute" \
      bash -c 'n=$(pw-link -l 2>/dev/null | grep -ci supercollider); echo "supercollider links: $n"; [ "$n" -ge 24 ]'

  soft "Tidal :6010" \
       "open the set in Pulsar and boot Tidal (nothing is sending patterns yet)" \
       bash -c 'ss -lunp 2>/dev/null | grep -q ":6010"'

  soft "LCXL -> SC" \
       "replug the LCXL, then tools/lcxl-init.py — knobs will move nothing until this is up" \
       bash -c 'aconnect -l 2>/dev/null | grep -A3 "Launch Control XL" | grep -q "128:"'

  # Asks whether THIS instance warmed, by keying to the unit's own start time —
  # not "was there ever a PRELOAD line", which a previous boot would satisfy forever.
  #
  # NOTE the query. `journalctl --user -u parvagues-sc` is NOT reliable on this box:
  # it returned lines from a PREVIOUS instance (Jul 30) while the current one had
  # already logged "51/51 banks OK", so the first version of this check warned about a
  # rig that was perfectly warm. Filter the plain journal by SyslogIdentifier instead.
  soft "SuperDirt warmed" \
       "no PRELOAD line since this boot — systemctl --user restart parvagues-sc to warm the set's banks" \
       bash -c 't=$(systemctl --user show parvagues-sc.service -p ActiveEnterTimestamp --value); [ -n "$t" ] || exit 1; journalctl -t parvagues-sc --since "$t" --no-pager 2>/dev/null | grep -q "banks OK"'

  soft "perf mode" \
       "powerprofilesctl set performance   # and plug in AC: battery caps turbo" \
       bash -c 'powerprofilesctl get 2>/dev/null | grep -q performance'
fi

# --- verdict -----------------------------------------------------------------
# Failures print FIRST and carry their fix, because the read order in a field is
# "what is broken / what do I type", not "here is a report".
echo
if [ ${#FAILURES[@]} -gt 0 ]; then
  echo "${R}NO-GO — ${#FAILURES[@]} blocking failure(s):${Z}"
  for f in "${FAILURES[@]}"; do
    echo "  ${R}✗${Z} ${f%%|*}"
    echo "      fix: ${f#*|}"
  done
  echo
fi
if [ ${#WARNINGS[@]} -gt 0 ] && ! (( QUIET )); then
  for w in "${WARNINGS[@]}"; do
    echo "  ${Y}!${Z} ${w%%|*} — ${w#*|}"
  done
  echo
fi

# --- 8. the audio gate, opt-in ----------------------------------------------
# Last, and only on request: it makes sound and takes ~45s per track. Skipped if
# the cold gate is already red — booting 13 tracks to confirm what a typecheck
# just told you is 10 wasted minutes at a venue.
if (( AUDIO )); then
  if [ ${#FAILURES[@]} -gt 0 ]; then
    echo "${Y}--audio skipped: fix the cold failures first (they will only fail louder).${Z}"
  elif [ "$SC" = no ]; then
    echo "${Y}--audio skipped: scsynth is not running. Boot the rig, then re-run.${Z}"
  else
    echo "audio gate — every declared orbit must make sound (~45s/track):"
    tools/check-tracks.sh || FAILURES+=("audio gate|read the check-tracks log printed above")
    echo
  fi
fi

echo "${D}detail: $LOG${Z}"
if [ ${#FAILURES[@]} -gt 0 ]; then
  echo "${R}gig-up: NO-GO.${Z}"
  exit 1
fi
echo "${G}gig-up: GO.${Z}"
# State the limit every time, in these words. check-mix reads the SAVED session
# file, so a green fader line means "Ardour will BOOT with this fader up" — it
# says nothing about where the physical desk is right now, and the two drift
# independently (CC 77-84 are MIDI-learned; the desk wins on touch). A clean
# report otherwise reads as the stronger claim, which is how the -inf bug
# survived three sessions.
(( QUIET )) || {
  echo "${D}  Proves the set BOOTS correct. NOT that the mixer IS correct —${Z}"
  echo "${D}  check-mix reads the saved session, not the live desk. Ctrl+S in${Z}"
  echo "${D}  Ardour before trusting a green fader line.${Z}"
  [ "$AUDIO" = 0 ] && echo "${D}  Nothing here made sound: add --audio for the empirical gate.${Z}"
}
exit 0
