#!/usr/bin/env bash
#
# Make the device plugin notice a freshly-written leds.toml.
#
#     apply-leds.sh --strategy <none|signal|restart-plugin|restart-opendeck>
#     apply-leds.sh --probe        # find out which one your machine needs
#
# WHY THIS IS A SCRIPT AND NOT A LINE OF PYTHON. The akp05 device plugin reads
# leds.toml at startup, and nothing documents it re-reading. Nothing else can drive
# those LEDs either: the plugin holds the USB device open. So "the rings update live"
# comes down to one unknown — what makes that process pick the file up again — and the
# answer is a property of your installed build, not of this repo. The probe finds it
# in about two minutes with the dock in front of you; everything else here is already
# written and tested.
#
# The strategies, cheapest first:
#
#   none              write the file and stop. CORRECT IF the plugin already watches
#                     the file — which is the FIRST thing the probe tests, because if
#                     it does, the rings are live for free and every strategy below is
#                     a worse answer.
#   signal            SIGHUP the plugin process. Reload-on-SIGHUP is a common daemon
#                     convention, so it is worth one test — but be clear-eyed: the
#                     DEFAULT action for an unhandled SIGHUP is to terminate the
#                     process, so a build that does not implement it lands you in
#                     restart-plugin territory with extra steps. The probe checks
#                     whether the process survived and says so.
#   restart-plugin    kill the plugin and let OpenDeck respawn it. Re-initialises the
#                     device: expect a visible blink and the keys redrawing. Only
#                     tolerable behind apply_min_interval_seconds, never per detent.
#   restart-opendeck  restart the whole application. The last resort — it takes the
#                     whole dock away for a second or two.
#
# The real fix is upstream and small: make the device plugin watch leds.toml. See
# upstream-file-watch-request.md in this directory, which is written and ready to file.
set -uo pipefail

STRATEGY="${STREAM_DOCK_LED_APPLY_STRATEGY:-none}"
PLUGIN_PATTERN="${STREAM_DOCK_PLUGIN_PATTERN:-akp05}"
PROBE=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --strategy) STRATEGY="${2:-none}"; shift 2 ;;
    --probe) PROBE=true; shift ;;
    *) echo "usage: apply-leds.sh [--strategy <name>] [--probe]" >&2; exit 2 ;;
  esac
done

# The pattern is config-supplied, so it is never interpolated into a shell string:
# pgrep takes it as a separate argument, and -x is deliberately NOT used because the
# plugin's real process name is one of the things the probe is for.
#
# `pgrep -f` matches whole command lines, which includes THIS script's own and its
# parent shell's — a shell invoked as `STREAM_DOCK_PLUGIN_PATTERN=akp05 apply-leds.sh`
# carries the pattern in its command line and matches itself. Sending TERM to that is
# how a probe kills the terminal it is being run from, so every caller filters out its
# own process and its parent. Found the hard way, on the first test run.
# Every process between this script and init, so a match on an ancestor's command line
# can never be signalled. The parent alone is not enough: run the probe from a shell
# that was itself launched with the pattern on its command line and the match lands on
# the GRANDparent, which is exactly how the first test run took down the terminal.
ancestry() {
  local pid=$$ ppid
  while [[ -n "$pid" && "$pid" != "0" && "$pid" != "1" ]]; do
    echo "$pid"
    ppid="$(awk '{print $4}' "/proc/$pid/stat" 2>/dev/null)" || break
    [[ -n "$ppid" ]] || break
    pid="$ppid"
  done
}

exclude_self() { grep -v -x -F -f <(ancestry) || true; }

plugin_pids() { pgrep -f -- "$PLUGIN_PATTERN" 2>/dev/null | exclude_self; }

opendeck_pids() { pgrep -f -- "opendeck" 2>/dev/null | exclude_self; }

do_none() { return 0; }

do_signal() {
  local pids
  pids="$(plugin_pids || true)"
  [[ -n "$pids" ]] || { echo "apply-leds: no process matching '$PLUGIN_PATTERN'" >&2; return 1; }
  # shellcheck disable=SC2086
  kill -HUP $pids 2>/dev/null || return 1
  return 0
}

do_restart_plugin() {
  local pids
  pids="$(plugin_pids || true)"
  [[ -n "$pids" ]] || { echo "apply-leds: no process matching '$PLUGIN_PATTERN'" >&2; return 1; }
  # TERM, never KILL: the plugin owns a USB device, and giving it the chance to close
  # the handle is the difference between OpenDeck respawning cleanly and the next
  # instance finding the device busy.
  # shellcheck disable=SC2086
  kill -TERM $pids 2>/dev/null || return 1
  return 0
}

do_restart_opendeck() {
  # Flatpak first, since that is how OpenDeck is normally installed on Linux, then a
  # plain process. Deliberately no `systemctl --user restart` guess: OpenDeck ships no
  # user unit, and inventing a name here would fail silently every time.
  if command -v flatpak >/dev/null && flatpak ps --columns=application 2>/dev/null | grep -q opendeck; then
    flatpak kill me.amankhanna.opendeck >/dev/null 2>&1
    sleep 1
    setsid flatpak run me.amankhanna.opendeck >/dev/null 2>&1 &
    return 0
  fi
  local pids
  pids="$(opendeck_pids || true)"
  [[ -n "$pids" ]] || { echo "apply-leds: OpenDeck does not appear to be running" >&2; return 1; }
  # shellcheck disable=SC2086
  kill -TERM $pids 2>/dev/null || return 1
  echo "apply-leds: OpenDeck was asked to exit — it is NOT restarted automatically" >&2
  echo "apply-leds: outside a Flatpak install; start it again yourself" >&2
  return 1
}

run_strategy() {
  case "$1" in
    none)             do_none ;;
    signal)           do_signal ;;
    restart-plugin)   do_restart_plugin ;;
    restart-opendeck) do_restart_opendeck ;;
    *) echo "apply-leds: unknown strategy '$1'" >&2; return 2 ;;
  esac
}

# ---------------------------------------------------------------------------
# Probe
# ---------------------------------------------------------------------------
if ! $PROBE; then
  run_strategy "$STRATEGY"
  exit $?
fi

ENV_FILE="${STREAM_DOCK_ENV_FILE:-$HOME/.config/stream-dock/led-sync.env}"
LEDS_PATH="${STREAM_DOCK_LEDS_PATH:-}"
if [[ -z "$LEDS_PATH" && -r "$ENV_FILE" ]]; then
  LEDS_PATH="$(sed -n 's/^STREAM_DOCK_LEDS_PATH=//p' "$ENV_FILE" | head -1)"
fi
LEDS_PATH="${LEDS_PATH:-$HOME/.config/opendeck-akp05/leds.toml}"
LEDS_PATH="${LEDS_PATH/#\~/$HOME}"

cat <<EOF

Stream Dock ring-reload probe
=============================
LED file : ${LEDS_PATH}
Plugin   : processes matching '${PLUGIN_PATTERN}'

Have the dock plugged in and OpenDeck running, and be able to see the four knob
rings. Each test writes an obvious colour and asks whether the rings changed.

EOF

if [[ ! -e "$LEDS_PATH" ]]; then
  echo "warn: ${LEDS_PATH} does not exist yet — run stream-dock/setup-stream-dock.sh first." >&2
fi

echo "Processes that look like they could be the device plugin:"
ps -eo pid,comm,args 2>/dev/null | grep -iE "opendeck|akp05|streamdock|stream-dock" | grep -v grep \
  | sed 's/^/  /' || true
echo "  (if none of these match '${PLUGIN_PATTERN}', re-run with"
echo "   STREAM_DOCK_PLUGIN_PATTERN=<something from the list above>)"
echo

if systemctl --user is-active --quiet stream-dock-led-sync 2>/dev/null; then
  echo "Stopping stream-dock-led-sync for the duration of the probe, so it does not"
  echo "overwrite the test colours."
  systemctl --user stop stream-dock-led-sync
  RESTART_SYNC=true
else
  RESTART_SYNC=false
fi

restore() {
  if [[ "${RESTART_SYNC:-false}" == "true" ]]; then
    echo "Restarting stream-dock-led-sync."
    systemctl --user start stream-dock-led-sync
  fi
}
trap restore EXIT

write_test_file() {
  mkdir -p "$(dirname "$LEDS_PATH")"
  cat > "${LEDS_PATH}.tmp" <<EOF
# Written by apply-leds.sh --probe
brightness = 100

[mode.Static]
colors = [[$1], [$1], [$1], [$1]]
EOF
  mv "${LEDS_PATH}.tmp" "$LEDS_PATH"
}

ask() {
  local answer
  read -r -p "$1 [y/N] " answer
  [[ "$answer" == "y" || "$answer" == "Y" ]]
}

WINNER=""

echo
echo "--- Test 1 of 4: does the plugin already watch the file? (strategy: none) ---"
write_test_file "0, 255, 0"
echo "Wrote all four rings GREEN. Waiting 3 seconds."
sleep 3
if ask "Did the rings turn green?"; then
  WINNER="none"
else
  echo
  echo "--- Test 2 of 4: SIGHUP the plugin (strategy: signal) ---"
  BEFORE="$(plugin_pids | tr '\n' ' ')"
  write_test_file "0, 0, 255"
  if do_signal; then
    sleep 3
    AFTER="$(plugin_pids | tr '\n' ' ')"
    if [[ -z "$AFTER" ]]; then
      echo "note: the plugin process is GONE — this build does not handle SIGHUP, and the"
      echo "      default action terminated it. If the rings changed anyway, OpenDeck"
      echo "      respawned it, which is the restart-plugin strategy in disguise."
    elif [[ "$BEFORE" != "$AFTER" ]]; then
      echo "note: the process id changed — it was restarted, not reloaded."
    fi
    if ask "Did the rings turn blue?"; then
      [[ -n "$AFTER" && "$BEFORE" == "$AFTER" ]] && WINNER="signal" || WINNER="restart-plugin"
    fi
  else
    echo "signal could not be sent — skipping."
  fi
fi

if [[ -z "$WINNER" ]]; then
  echo
  echo "--- Test 3 of 4: restart the plugin (strategy: restart-plugin) ---"
  write_test_file "255, 0, 0"
  if do_restart_plugin; then
    echo "Asked the plugin to exit. Waiting 5 seconds for OpenDeck to respawn it."
    sleep 5
    if [[ -z "$(plugin_pids)" ]]; then
      echo "note: nothing came back — OpenDeck does not respawn this plugin on its own,"
      echo "      so this strategy is not usable. The dock may need OpenDeck restarted."
    fi
    ask "Did the rings turn red?" && WINNER="restart-plugin"
  else
    echo "could not signal the plugin — skipping."
  fi
fi

if [[ -z "$WINNER" ]]; then
  echo
  echo "--- Test 4 of 4: restart OpenDeck (strategy: restart-opendeck) ---"
  echo "This takes the whole dock away for a moment."
  if ask "Try it?"; then
    write_test_file "255, 0, 255"
    if do_restart_opendeck; then
      echo "Waiting 10 seconds for OpenDeck to come back."
      sleep 10
      ask "Did the rings turn magenta?" && WINNER="restart-opendeck"
    fi
  fi
fi

echo
if [[ -n "$WINNER" ]]; then
  cat <<EOF
=== Result: '${WINNER}' works ===

Set it in CoreSystemConfig.json:

    "stream_dock": { "knob_leds": { "apply_strategy": "${WINNER}" } }

EOF
  if [[ "$WINNER" == "none" ]]; then
    cat <<'EOF'
That is the good outcome: the plugin picks the file up on its own, so the rings are
live with no reload hack at all. You can also set apply_min_interval_seconds to 0 —
there is nothing expensive to pace.
EOF
  else
    cat <<EOF
Keep apply_min_interval_seconds at 2 or more: '${WINNER}' re-initialises the device,
and doing that on every detent of a spun dial is how a dock ends up blinking instead
of lighting.
EOF
  fi
  echo
  echo "Then re-run:  stream-dock/setup-stream-dock.sh"
else
  cat <<'EOF'
=== Result: nothing tested here makes the plugin re-read the file ===

That is a real answer, not a failure of the probe. The rings will keep showing
whatever they had at plugin start, and the fix is upstream: see
stream-dock/upstream-file-watch-request.md, which is written and ready to file
against the device plugin. Everything on this side is already done — the moment the
plugin watches the file, the rings go live with no change here.
EOF
fi
