#!/bin/bash
# paliadin-shim — server-side RPC for paliad's remote-tmux turns.
#
# Invoked via mRiver's ~/.ssh/authorized_keys command= restriction. The
# client's requested command is exposed in $SSH_ORIGINAL_COMMAND; this
# script parses it and dispatches to a fixed verb set.
#
# Design: docs/design-paliadin-tailscale-ssh-2026-05-07.md §5.4.
#
# Verbs:
#   health                          -> "ok" iff tmux + claude reachable
#   bootstrap <prompt-base64>       -> ensure pane + send system prompt
#   run-turn  <uuid> <msg-base64>   -> send framed prompt, poll, return
#   reset                           -> /clear the conversation
#
# All multi-character payloads (prompts, messages) are base64-encoded by
# the Go caller so we never have to quote them through ssh's argv.
#
# Errors go to stderr with a non-zero exit. The Go side maps the exit
# status into a friendly error code.
set -euo pipefail
umask 077

readonly TMUX_SESSION="${PALIADIN_TMUX_SESSION:-paliad-paliadin}"
readonly RESPONSE_DIR="${PALIADIN_RESPONSE_DIR:-/tmp/paliadin}"
readonly TIMEOUT_S="${PALIADIN_TIMEOUT_S:-60}"
readonly PANE_READY_S=60      # max wait for claude pane to settle
readonly TURN_ID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'

mkdir -p "$RESPONSE_DIR"
chmod 700 "$RESPONSE_DIR"

# Parse $SSH_ORIGINAL_COMMAND into argv. Format: "<verb> <arg1> <arg2> …".
# We never `eval` this; `read -r -a` splits on $IFS without word-expansion.
read -r -a argv <<< "${SSH_ORIGINAL_COMMAND:-}"
verb="${argv[0]:-}"

# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

log_err() { printf 'paliadin-shim: %s\n' "$*" >&2; }

# ensure_pane creates the tmux session + claude window if missing, waits
# for the pane to become ready, and prints the target identifier
# ("session:window-idx") on stdout.
ensure_pane() {
  if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
    tmux new-session -d -s "$TMUX_SESSION"
  fi

  # Look for an existing window tagged with @paliadin-scope=chat.
  local target=""
  local idx scope
  while read -r idx; do
    [[ -z "$idx" ]] && continue
    scope=$(tmux show-window-option -t "$TMUX_SESSION:$idx" -v @paliadin-scope 2>/dev/null || true)
    if [[ "$scope" == "chat" ]]; then
      target="$TMUX_SESSION:$idx"
      break
    fi
  done < <(tmux list-windows -t "$TMUX_SESSION" -F '#{window_index}' 2>/dev/null || true)

  if [[ -z "$target" ]]; then
    if ! command -v claude >/dev/null 2>&1; then
      log_err "claude CLI not found in PATH"
      exit 3
    fi
    idx=$(tmux new-window -t "$TMUX_SESSION" -n claude-paliadin -P -F '#{window_index}' claude)
    target="$TMUX_SESSION:$idx"

    # Wait for claude to settle. Matches Go waitForPaneReady (paliadin.go:495).
    local deadline=$(( $(date +%s) + PANE_READY_S ))
    local pane=""
    while [[ $(date +%s) -lt $deadline ]]; do
      pane=$(tmux capture-pane -t "$target" -p 2>/dev/null || true)
      if [[ "$pane" == *"❯"* || "$pane" == *"│"* ]]; then
        break
      fi
      sleep 0.5
    done

    tmux set-window-option -t "$target" @paliadin-scope chat >/dev/null
    tmux set-window-option -t "$target" @fix-name claude-paliadin >/dev/null
  fi

  printf '%s' "$target"
}

# send_to_pane writes a literal string then Enter.
send_to_pane() {
  local target="$1" msg="$2"
  tmux send-keys -t "$target" -l -- "$msg"
  tmux send-keys -t "$target" Enter
}

# ---------------------------------------------------------------------------
# verb dispatch
# ---------------------------------------------------------------------------

case "$verb" in

  health)
    # Used by the Go side's healthGate to short-circuit when mRiver is
    # offline or tmux/claude is broken. Output is parsed verbatim.
    if ! command -v tmux >/dev/null 2>&1; then
      log_err "tmux not in PATH"; exit 1
    fi
    if ! command -v claude >/dev/null 2>&1; then
      log_err "claude not in PATH"; exit 1
    fi
    if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
      tmux new-session -d -s "$TMUX_SESSION"
    fi
    echo ok
    ;;

  bootstrap)
    # Inject the system prompt into a fresh claude pane. Idempotent —
    # the Go side may call this repeatedly; tmux send-keys is harmless
    # against a settled pane.
    if [[ -z "${argv[1]:-}" ]]; then
      log_err "bootstrap: missing prompt"; exit 2
    fi
    if ! prompt=$(printf '%s' "${argv[1]}" | base64 -d 2>/dev/null); then
      log_err "bootstrap: invalid base64 prompt"; exit 2
    fi
    target=$(ensure_pane)
    send_to_pane "$target" "$prompt"
    sleep 2  # let claude absorb before turns flow
    echo ok
    ;;

  run-turn)
    # $1 = turn_id (UUID), $2 = base64-encoded user message.
    turn_id="${argv[1]:-}"
    if [[ ! "$turn_id" =~ $TURN_ID_RE ]]; then
      log_err "run-turn: bad turn_id"; exit 2
    fi
    if [[ -z "${argv[2]:-}" ]]; then
      log_err "run-turn: missing message"; exit 2
    fi
    if ! msg=$(printf '%s' "${argv[2]}" | base64 -d 2>/dev/null); then
      log_err "run-turn: invalid base64 message"; exit 2
    fi
    target=$(ensure_pane)
    out="$RESPONSE_DIR/$turn_id.txt"
    rm -f "$out"

    # Envelope matches paliadin_prompt.go's `[PALIADIN:turn_id] <msg>` shape.
    send_to_pane "$target" "[PALIADIN:$turn_id] $msg"

    # Poll for the response file. Same shape as Go pollForResponse
    # (paliadin.go:530). Settle delay so we don't read mid-flush.
    deadline=$(( $(date +%s) + TIMEOUT_S ))
    while [[ $(date +%s) -lt $deadline ]]; do
      if [[ -s "$out" ]]; then
        sleep 0.05
        cat "$out"
        rm -f "$out"
        exit 0
      fi
      sleep 0.2
    done
    log_err "response timeout after ${TIMEOUT_S}s"
    exit 124
    ;;

  reset)
    # Send `/clear` so the next turn starts a fresh conversation.
    target=$(ensure_pane)
    send_to_pane "$target" "/clear"
    echo ok
    ;;

  '')
    log_err "no verb (set SSH_ORIGINAL_COMMAND via authorized_keys command=)"
    exit 2
    ;;

  *)
    log_err "unknown verb '$verb'"
    exit 2
    ;;
esac
