#!/usr/bin/env bash
# warm-conf.sh — a WARM, persistent conformance server for fast iteration.
#
# conformance.sh cold-loads all ~57 modules (datalog/acl/relations/persist/dream + host)
# on EVERY run — a fixed multi-minute tax, worst under box contention. This keeps a
# long-lived sx_server with the heavy dependency modules loaded ONCE, and per run reloads
# only the lib/host/* modules + the suite's test file (the things you actually edit),
# then evals the runner. Cross-run state is safe: each test file re-opens a fresh persist
# store at its top, and (since host/blog typing now reads direct KV edges, not lib/relations)
# the warm Datalog DB no longer feeds blog results, so stale facts can't pollute a re-run.
#
# Usage:
#   lib/host/warm-conf.sh start        # boot server, load the heavy dep modules once
#   lib/host/warm-conf.sh run blog     # reload host modules + tests/blog.sx, run the suite
#   lib/host/warm-conf.sh run          # run every suite
#   lib/host/warm-conf.sh stop         # kill the warm server
#   lib/host/warm-conf.sh restart      # stop + start
#
# It reads the MODULES + SUITES arrays straight from conformance.sh (no duplication, no
# drift). Heavy deps are everything NOT under lib/host/; those host modules + the test
# files are what `run` reloads.
set -u

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$HERE" || exit 1
CONF="lib/host/conformance.sh"

SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
[ -x "$SX_SERVER" ] || SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found" >&2; exit 1; fi

D="${WARM_CONF_DIR:-/tmp/warm-conf-host}"
FIFO="$D/in"; LOG="$D/out"; SPID="$D/server.pid"; HPID="$D/holder.pid"; EPF="$D/epoch"

# All module load paths from conformance.sh's MODULES=( ... ) array (in order).
mapfile -t ALL_MODULES < <(awk '/^MODULES=\(/{f=1;next} f&&/^\)/{f=0} f' "$CONF" | grep -oE '"[^"]+\.sx"' | tr -d '"')
# Heavy deps = everything that is NOT a lib/host module (loaded once, kept warm).
DEPS=(); HOSTMODS=()
for m in "${ALL_MODULES[@]}"; do
  case "$m" in lib/host/*) HOSTMODS+=("$m") ;; *) DEPS+=("$m") ;; esac
done
# Suites: "NAME RUNNER FILE" lines from conformance.sh's SUITES=( ... ) array.
mapfile -t SUITES < <(awk '/^SUITES=\(/{f=1;next} f&&/^\)/{f=0} f' "$CONF" | grep -oE '"[^"]+"' | tr -d '"')

_running() { [ -f "$SPID" ] && kill -0 "$(cat "$SPID")" 2>/dev/null; }

_send() { printf '%s\n' "$1" > "$FIFO"; }

# wait until a line matching $1 appears in the log AFTER byte-offset $2, or $3 seconds pass.
_wait_for() {
  local pat="$1" from="$2" timeout="${3:-1200}" waited=0
  while true; do
    if tail -c +"$((from+1))" "$LOG" | grep -qE "$pat"; then return 0; fi
    if tail -c +"$((from+1))" "$LOG" | grep -qE 'Undefined symbol|Unhandled exception|: error |expected list, got'; then
      echo "  ! error in server output:" >&2
      tail -c +"$((from+1))" "$LOG" | grep -nE 'Undefined symbol|Unhandled exception|: error |expected list, got' | head -5 >&2
      return 2
    fi
    sleep 1; waited=$((waited+1))
    [ "$waited" -ge "$timeout" ] && { echo "  ! timeout after ${timeout}s waiting for /$pat/" >&2; return 1; }
  done
}

_emit_loads() { # $@ = module paths; uses + bumps the epoch counter in $EPF
  local e; e="$(cat "$EPF")"
  { for m in "$@"; do e=$((e+1)); printf '(epoch %d)\n(load "%s")\n' "$e" "$m"; done; } > "$FIFO"
  echo "$e" > "$EPF"; echo "$e"   # echo the last epoch used
}

cmd_start() {
  cmd_stop >/dev/null 2>&1
  mkdir -p "$D"; : > "$LOG"; echo 0 > "$EPF"
  rm -f "$FIFO"; mkfifo "$FIFO"
  "$SX_SERVER" < "$FIFO" > "$LOG" 2>&1 &
  echo $! > "$SPID"
  sleep infinity > "$FIFO" &          # holder: keeps the write end open so the server never EOFs
  echo $! > "$HPID"
  echo "warm: loading ${#DEPS[@]} dependency modules (once)..."
  local last; last="$(_emit_loads "${DEPS[@]}")"
  if _wait_for "^\(ok $last " 0 900; then
    echo "warm: ready — ${#DEPS[@]} deps loaded, server pid $(cat "$SPID")"
  else
    echo "warm: FAILED to load deps" >&2; return 1
  fi
}

cmd_stop() {
  [ -f "$HPID" ] && kill "$(cat "$HPID")" 2>/dev/null
  [ -f "$SPID" ] && kill "$(cat "$SPID")" 2>/dev/null
  rm -f "$FIFO" "$SPID" "$HPID" "$EPF"
  echo "warm: stopped"
}

cmd_run() {
  if ! _running; then echo "warm: server not running — starting it first"; cmd_start || return 1; fi
  local filter="${1:-}" any=0 totp=0 totf=0
  for s in "${SUITES[@]}"; do
    read -r name runner file <<< "$s"
    [ -n "$filter" ] && [ "$name" != "$filter" ] && continue
    any=1
    # reload the host modules (what changes) + this suite's test file, then eval the runner.
    local off; off="$(wc -c < "$LOG")"
    _emit_loads "${HOSTMODS[@]}" "$file" >/dev/null
    local e; e="$(cat "$EPF")"; e=$((e+1))
    _send "(epoch $e)"; _send "(eval \"($runner)\")"
    echo "$e" > "$EPF"
    if ! _wait_for '^\{:' "$off" 1800; then echo "X $name — no result"; continue; fi
    local dict; dict="$(tail -c +"$((off+1))" "$LOG" | grep -E '^\{:' | tail -1)"
    local p f; p="$(echo "$dict" | grep -oE ':passed [0-9]+' | awk '{print $2}')"; f="$(echo "$dict" | grep -oE ':failed [0-9]+' | awk '{print $2}')"
    p="${p:-0}"; f="${f:-0}"; totp=$((totp+p)); totf=$((totf+f))
    if [ "$f" -gt 0 ]; then
      printf 'X %-12s %d/%d\n' "$name" "$p" "$((p+f))"
      echo "$dict" | grep -oE ':name "[^"]*"' | sed 's/:name /    fail: /'
    else
      printf 'ok %-12s %d passed\n' "$name" "$p"
    fi
  done
  [ "$any" = 0 ] && { echo "no suite matched '$filter'"; return 1; }
  if [ "$totf" -eq 0 ]; then echo "ok $totp passed (warm)"; else echo "FAIL $totp passed, $totf failed (warm)"; return 1; fi
}

# profiling: eval an SX expression against the warm image, report round-trip time. The
# epoch protocol only accepts COMMANDS, so the expr is wrapped in (eval "<source>") with
# quotes/backslashes escaped; errors come back as (error N …), success as (ok N …).
cmd_eval() {
  if ! _running; then echo "warm: not running"; return 1; fi
  local expr="$1" esc off e t0 t1
  esc="${expr//\\/\\\\}"; esc="${esc//\"/\\\"}"
  off="$(wc -c < "$LOG")"; e="$(cat "$EPF")"; e=$((e+1)); echo "$e" > "$EPF"
  t0=$(date +%s.%N)
  { printf '(epoch %d)\n(eval "%s")\n' "$e" "$esc"; } > "$FIFO"
  # an (eval …) acks as (ok-len N C) with the result printed on its own line(s); an error
  # acks as (error N …). Wait for either, then show the result line (the non-ack output).
  _wait_for "^\((ok-len|error) $e " "$off" 600 || { echo "  (eval timeout)"; return 1; }
  t1=$(date +%s.%N)
  printf '  [%6.2fs] %s\n' "$(echo "$t1 - $t0" | bc -l)" "$(tail -c +"$((off+1))" "$LOG" | grep -vE '^\((ok|ok-len) ' | tail -1)"
}
# reload one or more module files into the warm image (e.g. after editing blog.sx).
cmd_reload() { if ! _running; then echo "warm: not running"; return 1; fi; shift; local last; last="$(_emit_loads "$@")"; _wait_for "^\(ok $last " 0 300 && echo "warm: reloaded $* (epoch $last)"; }

case "${1:-}" in
  start)   cmd_start ;;
  stop)    cmd_stop ;;
  restart) cmd_stop; cmd_start ;;
  run)     shift; cmd_run "${1:-}" ;;
  eval)    cmd_eval "${2:-}" ;;
  reload)  cmd_reload "$@" ;;
  *) echo "usage: $0 {start|run [suite]|stop|restart|eval <expr>|reload <files...>}" >&2; exit 1 ;;
esac
