#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/_beagle-racket"
source "$(dirname "$0")/_beagle-daemon-files"

PORTFILE="$BEAGLE_DAEMON_PORTFILE_PRIMARY"
PIDFILE="$BEAGLE_DAEMON_PIDFILE_PRIMARY"
IDENTITYFILE="$BEAGLE_DAEMON_IDENTITYFILE_PRIMARY"
export BEAGLE_DAEMON_PORTFILE="$PORTFILE"

# Mirror the runtime files into the repo (.beagle/run) so sandboxed processes
# that cannot read $XDG_RUNTIME_DIR can still discover the daemon.
mirror_runtime_files() {
    mkdir -p "$BEAGLE_DAEMON_RUN_LOCAL" 2>/dev/null || return 0
    cp -f "$PORTFILE" "$BEAGLE_DAEMON_PORTFILE_MIRROR" 2>/dev/null || true
    cp -f "$PIDFILE" "$BEAGLE_DAEMON_PIDFILE_MIRROR" 2>/dev/null || true
    cp -f "$IDENTITYFILE" "$BEAGLE_DAEMON_IDENTITYFILE_MIRROR" 2>/dev/null || true
}

clean_runtime_files() {
    rm -f "$PORTFILE" "$PIDFILE" "$IDENTITYFILE" \
          "$BEAGLE_DAEMON_PORTFILE_MIRROR" "$BEAGLE_DAEMON_PIDFILE_MIRROR" \
          "$BEAGLE_DAEMON_IDENTITYFILE_MIRROR"
}

usage() {
    cat >&2 <<'EOF'
usage: beagle-daemon <command> [args...]

Commands:
  start           Start daemon in background (TCP, ephemeral port)
  stop            Stop running daemon
  status          Check if daemon is running (JSON)
  query CMD ARGS  Send query to running daemon (falls back to one-shot)

Queries (same as CLI tools but returns JSON):
  sig <fn-name> <dir>
  fields <record> <dir>
  callers <fn-name> <dir>
  provides <file>
  impact <fn-name> <dir>
  check <dir>
  check-enriched <dir>        Full type check + enriched context
  check-result [file]         Latest pre-computed result from watcher
  latest-results              All results since last query (clears buffer)
  watch <dir>                 Start inotify watcher on directory
  unwatch                     Stop all watchers
  invalidate [file]
  ping

The daemon keeps parsed ASTs cached, eliminating Racket startup (0.33s)
and re-parse (1.6s) costs per tool call. ~60 queries/session → saves ~2min.

Environment:
  BEAGLE_DAEMON_PORTFILE  Port file (default: $XDG_RUNTIME_DIR/beagle-daemon.port)
  BEAGLE_DAEMON_PIDFILE   PID file (default: $XDG_RUNTIME_DIR/beagle-daemon.pid)
EOF
    exit 2
}

is_running() {
    # PID-signal liveness first; TCP ping as the sandbox-tolerant fallback
    # (a sandboxed caller may be unable to kill -0 an outside PID or read the
    # XDG pidfile, yet can still connect when network access is granted).
    if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
        return 0
    fi
    local port
    port=$(get_port)
    [[ -n "$port" ]] && (exec 3<>/dev/tcp/127.0.0.1/"$port") 2>/dev/null
}

get_port() {
    beagle_daemon_port
}

send_query() {
    local port response
    port="$(beagle_daemon_compatible_port || true)"
    if [[ -n "$port" ]]; then
        # A compatible port file can outlive its daemon. Bound the complete
        # request so a refused or wedged endpoint falls through to this
        # checkout's one-shot compiler instead of hanging the authoring loop.
        response="$(timeout --kill-after=1s 2s bash -c '
            exec 3<>/dev/tcp/127.0.0.1/"$1" 2>/dev/null || exit 1
            printf "%s\\n" "$2" >&3 || exit 1
            IFS= read -r reply <&3 || exit 1
            printf "%s\\n" "$reply"
        ' beagle-daemon-query "$port" "$*" 2>/dev/null)" || response=""
        if [[ -n "$response" ]]; then
            printf '%s\n' "$response"
            return
        fi
    fi

    # Watching is stateful: a one-shot compiler has no persistent owner for
    # the watcher threads.  Falling back here also makes the caller wait while
    # that disposable process recursively checks the entire tree.
    if [[ ${1:-} == "watch" ]]; then
        echo '{"ok":false,"schemaVersion":1,"error":"watch requires a running compatible daemon"}'
        return 1
    fi

    # Fallback: no compatible daemon, or its endpoint did not answer in time.
    # One command followed by EOF yields one response and exits. Appending a
    # second `quit` response then truncating with `head` races pipefail against
    # Racket's second write under load.
    printf '%s\n' "$*" | "$RACKET" -e '(require beagle/private/daemon) (run-daemon)' 2>/dev/null
}

stop_running_daemon() {
    local pid port
    pid="$(cat "$PIDFILE" 2>/dev/null || true)"
    port="$(get_port)"
    if [[ -n "$port" ]]; then
        (exec 3<>/dev/tcp/127.0.0.1/"$port" && echo "quit" >&3 && exec 3>&-) 2>/dev/null || true
        sleep 0.2
    fi
    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
        kill "$pid" 2>/dev/null || true
    fi
    clean_runtime_files
}

cmd_start() {
    local compatible_port identity
    compatible_port="$(beagle_daemon_compatible_port || true)"
    if [[ -n "$compatible_port" ]] && is_running; then
        echo "Daemon already running (pid $(cat "$PIDFILE"), port $(get_port))" >&2
        exit 0
    fi

    if is_running; then
        echo "Replacing daemon from a different compiler closure." >&2
        stop_running_daemon
    fi

    clean_runtime_files
    identity="$(beagle_compiler_identity)" || {
        echo "Cannot identify this Beagle compiler closure." >&2
        exit 1
    }

    "$RACKET" -e '(require beagle/private/daemon) (run-daemon-tcp)' &
    local pid=$!
    echo "$pid" > "$PIDFILE"
    chmod 0600 "$PIDFILE"

    # Wait for port file to appear. Cold-start cost is dominated by
    # `(require beagle/private/daemon)` expansion: ~0.5s with compiled
    # bytecode (compiled/daemon_rkt.zo present), ~8-10s without. 30s
    # covers a clean checkout, a stale-bytecode rebuild, and slower
    # machines without ever masking a real failure.
    local timeout_tenths="${BEAGLE_DAEMON_START_TIMEOUT_TENTHS:-300}"
    local waited=0
    while [[ ! -f "$PORTFILE" ]] && [[ $waited -lt $timeout_tenths ]]; do
        if ! kill -0 "$pid" 2>/dev/null; then
            echo "Daemon process died during startup (pid $pid)." >&2
            echo "Try: $RACKET -e '(require beagle/private/daemon) (run-daemon-tcp)' to see the error." >&2
            rm -f "$PIDFILE"
            exit 1
        fi
        sleep 0.1
        waited=$((waited + 1))
    done

    if [[ ! -f "$PORTFILE" ]]; then
        echo "Daemon failed to start (no port file after $((timeout_tenths / 10))s)." >&2
        echo "Hint: precompile bytecode with: raco make beagle-lib/private/daemon.rkt" >&2
        kill "$pid" 2>/dev/null || true
        rm -f "$PIDFILE"
        exit 1
    fi

    local port
    port=$(get_port)
    printf '%s\n' "$identity" > "$IDENTITYFILE"
    chmod 0600 "$IDENTITYFILE"
    mirror_runtime_files
    echo "Daemon started (pid $pid, port $port)" >&2
}

cmd_stop() {
    if ! is_running; then
        echo "Daemon not running" >&2
        clean_runtime_files
        exit 0
    fi

    local pid
    pid="$(cat "$PIDFILE" 2>/dev/null || true)"
    stop_running_daemon
    echo "Daemon stopped (pid $pid)" >&2
}

cmd_status() {
    if [[ -n "$(beagle_daemon_compatible_port || true)" ]] && is_running; then
        send_query "ping"
    elif is_running; then
        echo '{"ok":false,"status":"stale","message":"daemon compiler closure does not match this checkout"}'
    else
        echo '{"ok":false,"status":"stopped"}'
        # Only clean when we can actually SEE the primary location: a sandboxed
        # caller with a masked $XDG_RUNTIME_DIR and no network must not delete
        # the mirrors of a daemon it merely cannot reach.
        if [[ -w "$(dirname "$PORTFILE")" ]]; then
            clean_runtime_files
        fi
    fi
}

if [[ $# -lt 1 ]]; then
    usage
fi

case "$1" in
    start)
        shift
        cmd_start
        # If --watch <dir> was passed, start watching after daemon is up
        if [[ ${1:-} == "--watch" ]] && [[ -n ${2:-} ]]; then
            send_query "watch" "$2"
        fi
        ;;
    stop)    cmd_stop ;;
    status)  cmd_status ;;
    query)   shift; send_query "$@" ;;
    -h|--help) usage ;;
    *)       usage ;;
esac
