#!/usr/bin/env bash
#
# A microphone in another room, as a local audio source on this desktop.
#
#     remote-mic.sh start  <name>
#     remote-mic.sh stop   <name>
#     remote-mic.sh status <name>
#
# Called by the mic-follow agent's start_command/stop_command hooks; `<name>` is the
# room, and its parameters come from ~/.config/mic-follow/remote-<name>.env, generated
# from CoreSystemConfig.json.
#
# HOW IT WORKS, and why this shape:
#
#   panel:  pw-record  (its microphone)  ──ssh──>  desktop:  pw-cat --playback
#                                                             into a null sink
#                                                                  │
#                                    module-remap-source ──────────┘
#                                    presents it as a REAL source named <source_name>
#
# **SSH, not an audio protocol.** RTP and PipeWire's pulse-tunnel are both lower
# latency and both need a new listening service on the panel, an ACL, and config on two
# machines that has to agree. The panel already runs sshd as its documented admin path
# and already trusts this desktop's key, so the transport comes with authentication and
# encryption already solved, and nothing new listens on the network. For a smoke-break
# voice call, ~40 ms of extra buffering is not the constraint; a hot microphone in an
# empty room is.
#
# **Audio only exists while this runs.** No daemon, no always-on stream: `start` opens
# one SSH session, `stop` closes it, and the panel's microphone is not being read at
# any other time. That is a property of the transport rather than a promise in a
# config file, which is the reason to prefer it.
#
# **Why a null sink plus module-remap-source**, rather than just using the sink's
# monitor: a monitor is not a real source, applications treat it as "record what the
# desktop is playing", and mic-follow's own audio layer refuses to select one on
# purpose. remap-source turns it into an ordinary microphone with a name and a
# description, which is what Discord's device list needs to show.
#
# UNVERIFIED: this has never run against the real panel. The module names and the
# pw-record/pw-cat invocations are written from documentation. `status` exists to make
# checking it a one-liner.
set -uo pipefail

ACTION="${1:-}"
NAME="${2:-}"
[[ -n "$ACTION" && -n "$NAME" ]] || { echo "usage: remote-mic.sh <start|stop|status> <name>" >&2; exit 2; }

CONF="${MIC_FOLLOW_REMOTE_DIR:-$HOME/.config/mic-follow}/remote-${NAME}.env"
RUN_DIR="${XDG_RUNTIME_DIR:-/tmp}/mic-follow"
PID_FILE="${RUN_DIR}/${NAME}.pid"
MODULES_FILE="${RUN_DIR}/${NAME}.modules"

[[ -r "$CONF" ]] || { echo "remote-mic: no config at $CONF" >&2; exit 2; }
# shellcheck disable=SC1090
. "$CONF"

REMOTE_HOST="${REMOTE_HOST:-}"
REMOTE_USER="${REMOTE_USER:-kiosk}"
REMOTE_MIC="${REMOTE_MIC:-}"          # empty = whatever the panel's default input is
SOURCE_NAME="${SOURCE_NAME:-micfollow_${NAME}}"
SOURCE_DESCRIPTION="${SOURCE_DESCRIPTION:-${NAME} microphone}"
RATE="${RATE:-48000}"
CHANNELS="${CHANNELS:-1}"
LATENCY_MS="${LATENCY_MS:-40}"
SSH_KEY="${SSH_KEY:-}"

mkdir -p "$RUN_DIR"

is_running() {
  [[ -f "$PID_FILE" ]] || return 1
  local pid; pid="$(cat "$PID_FILE" 2>/dev/null || true)"
  [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}

case "$ACTION" in
  status)
    if is_running; then
      echo "running (pid $(cat "$PID_FILE"))"
      pactl list short sources 2>/dev/null | grep -F "$SOURCE_NAME" || echo "  WARNING: the source is not registered"
      exit 0
    fi
    echo "stopped"
    exit 1
    ;;

  start)
    [[ -n "$REMOTE_HOST" ]] || { echo "remote-mic: REMOTE_HOST is not set in $CONF" >&2; exit 2; }
    if is_running; then
      echo "remote-mic: ${NAME} is already running"
      exit 0
    fi
    command -v pactl >/dev/null || { echo "remote-mic: pactl is not installed" >&2; exit 2; }
    command -v pw-cat >/dev/null || { echo "remote-mic: pw-cat is not installed (pipewire-bin)" >&2; exit 2; }

    # The sink and the source it is remapped into. Recorded so `stop` unloads exactly
    # what `start` loaded, rather than pattern-matching modules somebody else may own.
    : > "$MODULES_FILE"
    SINK_MODULE="$(pactl load-module module-null-sink \
        sink_name="${SOURCE_NAME}_sink" \
        sink_properties="device.description='${SOURCE_DESCRIPTION} (transport)'" 2>/dev/null)" || {
      echo "remote-mic: could not create the null sink" >&2; exit 1; }
    echo "$SINK_MODULE" >> "$MODULES_FILE"

    SOURCE_MODULE="$(pactl load-module module-remap-source \
        master="${SOURCE_NAME}_sink.monitor" \
        source_name="${SOURCE_NAME}" \
        source_properties="device.description='${SOURCE_DESCRIPTION}'" 2>/dev/null)" || {
      echo "remote-mic: could not create the remapped source" >&2
      pactl unload-module "$SINK_MODULE" 2>/dev/null
      exit 1; }
    echo "$SOURCE_MODULE" >> "$MODULES_FILE"

    SSH_ARGS=(-o BatchMode=yes -o ConnectTimeout=5 -o ServerAliveInterval=5
              -o ServerAliveCountMax=2 -o StrictHostKeyChecking=accept-new)
    [[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")

    # The panel end. --target is left off entirely when REMOTE_MIC is empty, so the
    # panel's own default input is used and nothing here has to know its device names.
    REMOTE_CMD="pw-record --rate ${RATE} --channels ${CHANNELS} --format s16 --latency ${LATENCY_MS}ms"
    [[ -n "$REMOTE_MIC" ]] && REMOTE_CMD="${REMOTE_CMD} --target '${REMOTE_MIC}'"
    REMOTE_CMD="${REMOTE_CMD} -"

    # setsid so the pipe survives the hook's own shell exiting, and so `stop` can kill
    # the whole thing by process group rather than chasing two processes.
    setsid bash -c "ssh ${SSH_ARGS[*]} '${REMOTE_USER}@${REMOTE_HOST}' \"${REMOTE_CMD}\" \
        | pw-cat --playback --rate ${RATE} --channels ${CHANNELS} --format s16 \
                 --latency ${LATENCY_MS}ms --target '${SOURCE_NAME}_sink' -" \
        </dev/null >/dev/null 2>&1 &
    echo $! > "$PID_FILE"
    sleep 0.5
    if ! is_running; then
      echo "remote-mic: the stream died immediately — check: ssh ${REMOTE_USER}@${REMOTE_HOST} pw-record --help" >&2
      "$0" stop "$NAME" >/dev/null 2>&1
      exit 1
    fi
    echo "remote-mic: ${NAME} up as source '${SOURCE_NAME}'"
    ;;

  stop)
    # Kill the transport FIRST, then tear down the local plumbing: the order that
    # guarantees the panel stops being recorded even if unloading a module fails.
    if [[ -f "$PID_FILE" ]]; then
      pid="$(cat "$PID_FILE" 2>/dev/null || true)"
      if [[ -n "$pid" ]]; then
        kill -TERM -- "-${pid}" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
        sleep 0.3
        kill -KILL -- "-${pid}" 2>/dev/null || true
      fi
      rm -f "$PID_FILE"
    fi
    if [[ -f "$MODULES_FILE" ]]; then
      # Reverse order: the remapped source depends on the sink.
      tac "$MODULES_FILE" | while read -r module; do
        [[ -n "$module" ]] && pactl unload-module "$module" 2>/dev/null
      done
      rm -f "$MODULES_FILE"
    fi
    echo "remote-mic: ${NAME} stopped"
    ;;

  *)
    echo "usage: remote-mic.sh <start|stop|status> <name>" >&2
    exit 2
    ;;
esac
