#!/usr/bin/env bash
# gig-up — ordered 1-click gig launch.
#
# The problem it kills: at the start of a gig/reboot, whichever of SuperDirt or
# Ardour opens the LaunchControl XL first wins it; when Ardour wins, sclang can't
# get the controller and you're stuck doing the "disconnect MIDI / reconnect MIDI"
# dance mid-setup. So: bring SuperDirt up FIRST, WAIT until it has actually claimed
# MIDI, and only THEN start Ardour + Pulsar.
#
# Order:
#   1. sclang start_and_midi.scd  (SuperDirt on :57120 + LCXL CC->OSC bridge) in a terminal
#   2. wait for READY = scsynth alive + :57120 listening + a SuperCollider MIDI client
#   3. Ardour (the Tidal Multi session)   — now it can't beat SuperDirt to the LCXL
#   4. Pulsar (the Tidal folder)
#
# Everything is best-effort: a step that can't run warns and the rest still proceeds.

set -u
DIR="$(cd "$(dirname "$0")" && pwd)"                     # repo root (this script lives here)
ARDOUR_SESSION="$HOME/Work/Sound/Ardour/Tidal Live/Tidal Live.ardour"   # lean per-gig session (Tidal Multi = fat archive, retiring)
READY_TIMEOUT="${READY_TIMEOUT:-120}"                    # s to wait for SuperDirt (samples can be slow)
PRELOAD_TRACKS="${PRELOAD_TRACKS:-20}"                   # warm samples from the last N edited tracks (0 = off)

C_OK=$'\e[32m'; C_WARN=$'\e[33m'; C_INFO=$'\e[36m'; C_DIM=$'\e[2m'; C_RST=$'\e[0m'
ok(){ printf '%s✓ %s%s\n' "$C_OK" "$*" "$C_RST"; }
info(){ printf '%s• %s%s\n' "$C_INFO" "$*" "$C_RST"; }
warn(){ printf '%s! %s%s\n' "$C_WARN" "$*" "$C_RST"; }

# --- readiness probes (the three signals that mean "SuperDirt owns the rig") ---
scsynth_up(){ pgrep -x scsynth >/dev/null 2>&1; }
osc_up(){ ss -uln 2>/dev/null | grep -q ':57120'; }
# SuperCollider's ALSA-seq client appears once MIDIClient.init has run.
midi_claimed(){ aconnect -l 2>/dev/null | grep -qiE "client [0-9]+: 'SuperCollider'"; }
# GUI apps — so re-running gig-up converges the rig instead of spawning dupes
# (a 2nd Ardour on the same session = an annoying "already open/locked" dialog).
ardour_up(){ pgrep -f 'ardour-[0-9]' >/dev/null 2>&1; }
pulsar_up(){ pgrep -x pulsar >/dev/null 2>&1; }

# --- perf-aware gear: performing needs CPU HEADROOM. A capped CPU (low
# max_perf_pct, the quiet/cool desk regime) can't absorb a MIDI-sweep burst →
# audio thread starves → xrun/crack. So launching the rig ASSERTS Standard
# (uncapped, governor performance) and lets the Bridge watcher hold it. Revert
# to Cool in the tray when you're done. RT priority is already fine (rtprio 95).
# Skip with GIG_PERF=off. ---
ensure_perf(){
  [ "${GIG_PERF:-standard}" = off ] && { info "perf: auto-assert disabled (GIG_PERF=off)."; return; }
  local maxp gov
  maxp=$(cat /sys/devices/system/cpu/intel_pstate/max_perf_pct 2>/dev/null || echo 100)
  gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null)
  if [ "${maxp:-100}" -ge 100 ] 2>/dev/null && [ "$gov" = performance ]; then
    ok "perf: CPU uncapped + performance — headroom for MIDI bursts."; return
  fi
  info "perf: CPU capped at ${maxp}% ('$gov') → asserting Standard for the set…"
  # set_mode writes the desired-state file too, so the watcher won't revert it.
  if PYTHONPATH="$DIR/tools/bridge" python3 -c "import perf,sys; ok,m=perf.set_mode('standard'); print(m); sys.exit(0 if ok else 1)" 2>/dev/null; then
    ok "perf: Standard set + held by the watcher (fans may rise under load — fine on stage; tray → Cool to revert)."
  else
    warn "perf: auto-set failed — switch to STANDARD in the perf tray/Bridge yourself."
  fi
}

# --- generate the set-specific sample preload the boot will warm ---
gen_preload(){
  [ "${PRELOAD_TRACKS}" = 0 ] && { info "preload: disabled (PRELOAD_TRACKS=0)."; return; }

  # Prefer the explicit SETLIST over --last N. `--last` guesses the set from file
  # mtimes, and on 2026-07-28 that guess silently omitted a set track's banks — back
  # when a preload miss meant permanent silence rather than a slow first hit. Derive
  # the warm-up from what will be PLAYED. --last stays as the fallback for jamming.
  local SETLIST="${SETLIST:-$DIR/setlist_opal2026.txt}"
  local args desc
  if [ -f "$SETLIST" ]; then
    args="--setlist $SETLIST"; desc="setlist $(basename "$SETLIST")"
  else
    args="--last $PRELOAD_TRACKS"; desc="your last $PRELOAD_TRACKS edited tracks"
    warn "preload: no setlist at $SETLIST — falling back to mtime guessing."
  fi

  # Generate to a temp file and move it into place: `> preload.scd` truncates before
  # the generator runs, so a failure would otherwise leave an EMPTY preload.scd that
  # the boot happily sources as "nothing to warm".
  local tmp; tmp="$(mktemp)"
  if python3 "$DIR/tools/setlist_samples.py" $args --emit-sc > "$tmp" 2>"$tmp.err"; then
    mv "$tmp" "$DIR/preload.scd"
    ok "preload: $(grep -c '^\s*\[ \\' "$DIR/preload.scd" 2>/dev/null || echo 0) banks queued from $desc."
    [ -s "$tmp.err" ] && warn "preload: generator notes — $(head -3 "$tmp.err" | tr '\n' ' ')"
  else
    warn "preload: generation failed — samples will lazy-load on demand (not fatal)."
    [ -s "$tmp.err" ] && warn "preload: $(head -3 "$tmp.err" | tr '\n' ' ')"
    rm -f "$tmp" "$DIR/preload.scd"
  fi
  rm -f "$tmp.err"
}

# --- terminal for the sclang post window ---
pick_term(){
  if [ -n "${TERMINAL:-}" ] && command -v "$TERMINAL" >/dev/null 2>&1; then echo "$TERMINAL"; return; fi
  for t in konsole kitty alacritty wezterm gnome-terminal xterm; do
    command -v "$t" >/dev/null 2>&1 && { echo "$t"; return; }
  done
  echo ""
}

open_term(){  # open a new terminal running the headless sclang gig-boot
  local term; term="$(pick_term)"
  if [ -z "$term" ]; then
    warn "no terminal emulator found — running SuperDirt in the background (post → $DIR/gig-sclang.log)"
    ( cd "$DIR" && QT_QPA_PLATFORM=offscreen setsid sclang start_and_midi.scd >gig-sclang.log 2>&1 & )
    return
  fi
  # konsole/gnome-terminal want `-e`; most others accept it too.
  setsid "$term" -e bash -lc "cd '$DIR'; QT_QPA_PLATFORM=offscreen sclang start_and_midi.scd; exec bash" \
    >/dev/null 2>&1 &
}

launch_bin(){  # launch_bin "<label>" <binary names...> — first found, detached, with LAUNCH_ARGS
  local label="$1"; shift
  local bin
  for bin in "$@"; do
    if command -v "$bin" >/dev/null 2>&1; then
      info "launching $label ($bin)…"
      setsid "$bin" "${LAUNCH_ARGS[@]}" >/dev/null 2>&1 &
      return 0
    fi
  done
  warn "$label not found (tried: $*) — start it by hand."
  return 1
}

# ---------------------------------------------------------------------------
echo
info "gig-up — ordered launch from $DIR"
ensure_perf

# 1) SuperDirt (skip if already running)
if scsynth_up; then
  ok "SuperDirt already running — skipping boot (preload only applies to a fresh boot)."
else
  gen_preload   # write preload.scd so start_and_midi.scd warms the set's samples
  info "starting SuperDirt (sclang start_and_midi.scd, headless)…"
  open_term
fi

# 2) wait for the rig to own MIDI
info "waiting for SuperDirt to boot & claim the LaunchControl (timeout ${READY_TIMEOUT}s)…"
deadline=$(( $(date +%s) + READY_TIMEOUT ))
while :; do
  if scsynth_up && osc_up && midi_claimed; then
    sleep 1.5   # settle: let MIDIIn.connectAll + CC bridge attach
    ok "SuperDirt ready — scsynth up, :57120 listening, MIDI claimed."
    break
  fi
  if [ "$(date +%s)" -ge "$deadline" ]; then
    warn "timed out — proceeding anyway. Check the sclang window; if MIDI isn't claimed,"
    warn "you may still need the disconnect/reconnect dance for the LaunchControl."
    printf '%s  (scsynth=%s  :57120=%s  midi=%s)%s\n' "$C_DIM" \
      "$(scsynth_up && echo y || echo n)" "$(osc_up && echo y || echo n)" \
      "$(midi_claimed && echo y || echo n)" "$C_RST"
    break
  fi
  sleep 1
done

# 2b) LIGHT THE SURFACE. The rig owns MIDI now, so paint the LCXL immediately —
# before Ardour and Pulsar even start. Previously the only painter lived in the
# Pulsar HUD, so the board stayed DARK until the editor was up and its frame
# pipeline healthy (#57 latches an early empty frame), and a dark board reads as
# broken hardware. The no-track paint is the channel CONVENTION, so there is always
# something to look at; tidal-remote repaints per-track on each boot.
#
# The touch-reactive daemon — which keeps a model of every control and repaints from
# it, so colours PERSIST after you let go of a knob — is NOT started from here any
# more (#85). It is a systemd --user unit, `lcxl-leds-watch.service`, enabled and
# started at boot. That matters because this script used to `setsid` its own copy:
# with a unit already running you got TWO watchers writing SysEx to the same board,
# fighting over every LED, and neither one wrong enough to look broken. One owner.
#
# The daemon is SysEx-out only and reads input read-only via aseqdump, so it never
# steals MIDI from SuperDirt and never sends a CC at anything.
leds(){
  [ "${GIG_LEDS:-paint}" = off ] && { info "leds: disabled (GIG_LEDS=off)."; return; }
  if ! "$DIR/tools/lcxl-leds.py" --map -q 2>/tmp/gig-leds.err; then
    warn "leds: paint failed — $(head -2 /tmp/gig-leds.err | tr '\n' ' ')"
    warn "leds: if input works but the board is dark, that's a USB OUT stall — REPLUG the LCXL."
    return
  fi
  ok "leds: surface painted (convention colours). LOOK AT IT — a clean exit is not a lit LED."
  # Restart, not start: idempotent, and it guarantees exactly one watcher owns the
  # board even if a stale one survived a crash. Fresh boot also re-reads the track.
  if systemctl --user restart lcxl-leds-watch.service 2>/tmp/gig-leds-unit.err; then
    ok "leds: watcher unit running (lcxl-leds-watch.service — persists across reboot)."
  else
    warn "leds: watcher unit failed — $(head -2 /tmp/gig-leds-unit.err | tr '\n' ' ')"
    warn "leds: colours will not persist after you let go of a knob. journalctl --user -u lcxl-leds-watch"
  fi
}
leds

# 3) Ardour — now safe to open (SuperDirt already holds the controller)
if ardour_up; then
  ok "Ardour already running — skipping."
elif [ -f "$ARDOUR_SESSION" ]; then
  LAUNCH_ARGS=("$ARDOUR_SESSION"); launch_bin "Ardour" ardour ardour9 ardour8 Ardour
else
  warn "Ardour session not found at: $ARDOUR_SESSION"
fi

# 4) Pulsar — the Tidal folder
if pulsar_up; then
  ok "Pulsar already running — skipping."
else
  LAUNCH_ARGS=("$DIR"); launch_bin "Pulsar" pulsar
fi

# 5) READINESS GATE. Not a report — a gate, with a verdict you can act on.
#
# Everything above proves processes STARTED. Nothing above proves the rig will make
# a sound, because the two things that most reliably swallow it are STATE, not
# processes: a global mute/filter/gate parked somewhere on the surface, and an
# Ardour fader parked at -inf. On 2026-07-29 gMask sat armed at 127 for 74 minutes
# while every static check was green, and PLN found it by ear. The ear is not
# supposed to be the smoke detector.
#
# Neither check is fatal to the launch — you may be starting up precisely to fix
# them — so they report and never abort.
echo
if command -v python3 >/dev/null; then
  info "readiness: surface state (globals that could be swallowing your sound)"
  python3 "$DIR/tools/gig-log.py" preflight 2>&1 | sed 's/^/    /' || true
  info "readiness: Ardour faders (reads the last SAVED session — save first)"
  python3 "$DIR/tools/check-mix.py" 2>&1 | tail -4 | sed 's/^/    /' || true
  # Pulsar saves the BUFFER, not the file, and Window:Reload restores the cached
  # buffer rather than disk — so a tab left open across an edit can silently write
  # three-day-old text back over committed work. This catches that in seconds.
  info "readiness: track drift (did a stale Pulsar buffer overwrite committed work?)"
  bash "$DIR/tools/check-drift.sh" 2>&1 | tail -8 | sed 's/^/    /' || true
fi

echo
ok "gig-up done. SuperDirt owns the LaunchControl; Ardour + Pulsar are coming up."
