#!/usr/bin/env bash
# beagle-doctor: check host environment, target readiness, AND prove the
# authoring loop is FUNCTIONALLY working (not merely alive). The compiler is the
# loop's oracle; vocabulary in docs/authoring-loops.md.
#
# Two halves:
#   environment  — host tools, daemon liveness, cache, target emitters.
#   authoring loop  — FUNCTIONAL canary pairs: a known-bad input that MUST be
#                  rejected + a known-good input that MUST pass. This is what
#                  catches SILENT DEGRADATION (a checker stuck "always-pass"
#                  or "always-fail"), which a version/liveness check cannot see.
#
# Use it two ways:
#   handshake  — run once before coding Beagle; green or don't trust feedback.
#   heartbeat  — run on a loop while coding; --revive self-heals a dead daemon.
#
# Usage:
#   beagle-doctor [--json] [--revive] [--quiet] [--deep] [DIR]
#     --json     machine-readable verdict
#     --revive   restart the daemon (watching DIR) if it is down, then re-check
#     --quiet    print nothing when healthy (silent heartbeat; loud on degrade)
#     --deep     also run the full suggestion->patch canary (beagle-repair)
#     DIR        daemon watch dir for --revive (default: cwd)
#
# Exit: 0 = healthy/warnings only, 1 = DEGRADED (a functional check or revive
# failed). The non-zero exit is what lets a handshake/heartbeat gate on it.

set -uo pipefail
source "$(dirname "$0")/_beagle-racket"
source "$(dirname "$0")/_beagle-python"
beagle_resolve_python || exit $?
source "$(dirname "$0")/_beagle-daemon-files"
BIN="$(cd "$(dirname "$0")" && pwd)"

JSON=0
REVIVE=0
QUIET=0
DEEP=0
WATCH_DIR="."
while [[ $# -gt 0 ]]; do
    case "$1" in
        --json)   JSON=1; shift ;;
        --revive) REVIVE=1; shift ;;
        --quiet)  QUIET=1; shift ;;
        --deep)   DEEP=1; shift ;;
        -h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *)        WATCH_DIR="$1"; shift ;;
    esac
done

STATUS="ok"          # whole-environment rollup (tools + targets) — informational
AUTHORING_STATUS="ok"   # authoring-loop health ONLY (daemon + functional canaries) — gates exit
ENVIRONMENT_TRUSTED=1 # package scope must resolve this checkout before USE is safe
CHECKS=()

check_tool() {
    local name="$1"
    local cmd="$2"
    local version_flag="${3:---version}"
    local result
    if result=$(eval "$cmd $version_flag" 2>&1 | head -1); then
        CHECKS+=("{\"name\":\"$name\",\"status\":\"ok\",\"message\":$(printf '%s' "$result" | "$BEAGLE_PYTHON" -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')}")
    else
        STATUS="error"
        CHECKS+=("{\"name\":\"$name\",\"status\":\"error\",\"message\":\"not found\"}")
    fi
}

check_tool "racket" "$RACKET" "--version"
# raco shares racket's version (same package) and `raco --version` is invalid
# (raco is a subcommand dispatcher), so probe liveness via `raco help`.
if "$RACO" help >/dev/null 2>&1; then
    CHECKS+=("{\"name\":\"raco\",\"status\":\"ok\",\"message\":\"available\"}")
else
    STATUS="error"
    CHECKS+=("{\"name\":\"raco\",\"status\":\"error\",\"message\":\"not found\"}")
fi

# Clojure CLI
if command -v clj &>/dev/null; then
    ver=$(clj --version 2>&1 | head -1)
    CHECKS+=("{\"name\":\"clojure\",\"status\":\"ok\",\"message\":$(printf '%s' "$ver" | "$BEAGLE_PYTHON" -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')}")
else
    CHECKS+=("{\"name\":\"clojure\",\"status\":\"warning\",\"message\":\"clj not found (needed for clj target)\"}")
    [[ "$STATUS" == "ok" ]] && STATUS="warning"
fi

# Node.js
if command -v node &>/dev/null; then
    ver=$(node --version 2>&1 | head -1)
    CHECKS+=("{\"name\":\"node\",\"status\":\"ok\",\"message\":$(printf '%s' "$ver" | "$BEAGLE_PYTHON" -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')}")
else
    CHECKS+=("{\"name\":\"node\",\"status\":\"warning\",\"message\":\"node not found (needed for js target)\"}")
    [[ "$STATUS" == "ok" ]] && STATUS="warning"
fi

# Nix
if command -v nix &>/dev/null; then
    ver=$(nix --version 2>&1 | head -1)
    CHECKS+=("{\"name\":\"nix\",\"status\":\"ok\",\"message\":$(printf '%s' "$ver" | "$BEAGLE_PYTHON" -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')}")
else
    CHECKS+=("{\"name\":\"nix\",\"status\":\"warning\",\"message\":\"nix not found (needed for nix target)\"}")
    [[ "$STATUS" == "ok" ]] && STATUS="warning"
fi

# Daemon — liveness plus compiler-closure identity, with --revive self-heal.
# A TCP endpoint from an older checkout is not a healthy authoring loop.
BEAGLE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DAEMON_REACHABLE=0
DAEMON_REVIVED=0
daemon_responding() {
    local port response
    port="$(beagle_daemon_compatible_port 2>/dev/null || true)"
    [[ -n "$port" ]] || return 1
    exec 3<>"/dev/tcp/127.0.0.1/$port" 2>/dev/null || return 1
    printf 'ping\n' >&3 || { exec 3>&-; return 1; }
    IFS= read -r -t 2 response <&3 || { exec 3>&-; return 1; }
    exec 3>&-
    [[ "$response" == *'"ok":true'* && "$response" == *'"status":"running"'* ]]
}
if daemon_responding; then
    DAEMON_REACHABLE=1
elif [[ "$REVIVE" == "1" ]]; then
    "$BIN/beagle-daemon" start --watch "$WATCH_DIR" >/dev/null 2>&1 || true
    for _ in 1 2 3 4 5; do daemon_responding && break; done
    if daemon_responding; then
        DAEMON_REACHABLE=1
        DAEMON_REVIVED=1
    else
        STATUS="error"; AUTHORING_STATUS="error"
    fi
else
    # daemon powers the watcher + the hook's fast feedback path — its absence
    # is a real authoring-loop degradation, not a benign warning.
    STATUS="error"; AUTHORING_STATUS="error"
fi

# Cache directory
CACHE_DIR="$BEAGLE_ROOT/.beagle"
if [[ -d "$CACHE_DIR" && -w "$CACHE_DIR" ]]; then
    CHECKS+=("{\"name\":\"cache\",\"status\":\"ok\",\"message\":\"$CACHE_DIR writable\"}")
elif [[ -d "$CACHE_DIR" ]]; then
    CHECKS+=("{\"name\":\"cache\",\"status\":\"warning\",\"message\":\"$CACHE_DIR not writable\"}")
    [[ "$STATUS" == "ok" ]] && STATUS="warning"
else
    CHECKS+=("{\"name\":\"cache\",\"status\":\"ok\",\"message\":\"$CACHE_DIR will be created on first use\"}")
fi

# ---------------------------------------------------------------------------
# Authoring loop — FUNCTIONAL canary pairs. A liveness check says the checker is
# running; these say it is still TELLING THE TRUTH. Each layer feeds a known
# input and asserts the known verdict, in BOTH directions (bad must fail, good
# must pass) so a checker stuck always-pass OR always-fail is caught.
# ---------------------------------------------------------------------------
AUTHORING_LOOP=()
al_ok()   { AUTHORING_LOOP+=("{\"name\":\"$1\",\"status\":\"ok\"}"); }
al_fail() { AUTHORING_LOOP+=("{\"name\":\"$1\",\"status\":\"error\",\"message\":$(printf '%s' "$2" | "$BEAGLE_PYTHON" -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')}"); STATUS="error"; AUTHORING_STATUS="error"; }

DOC_WORK="$(mktemp -d /tmp/beagle-doctor.XXXXXX)"
printf '#lang beagle/clj\n(ns doctor.canary)\n(defn f [] Nil (str "x"\n' > "$DOC_WORK/bad.bclj"
printf '#lang beagle/clj\n(ns doctor.canary)\n(defn f [] Nil nil)\n' > "$DOC_WORK/good.bclj"
printf '#lang beagle/clj\n(ns doctor.canary)\n(defn g [(n Int)] Nil nil)\n(defn f [] Nil (g "boom"))\n' > "$DOC_WORK/type-bad.bclj"

# syntax layer (canary pair)
if "$BIN/beagle-syntax" "$DOC_WORK/bad.bclj" &>/dev/null; then
    al_fail "syntax" "accepted a known-malformed file (stuck always-pass)"
elif ! "$BIN/beagle-syntax" "$DOC_WORK/good.bclj" &>/dev/null; then
    al_fail "syntax" "rejected a known-good file (stuck always-fail)"
else
    al_ok "syntax"
fi

# type-check layer (canary pair)
if "$BIN/beagle-check-all" --agent "$DOC_WORK/type-bad.bclj" &>/dev/null; then
    al_fail "check" "passed a known type error (stuck always-pass)"
elif ! "$BIN/beagle-check-all" --agent "$DOC_WORK/good.bclj" &>/dev/null; then
    al_fail "check" "failed a known-good file (stuck always-fail)"
else
    al_ok "check"
fi

# package-scope canary — the FALSE-GREEN backstop. Global `raco pkg` links for
# beagle/beagle-lib/beagle-test point at ONE checkout; a git worktree sharing the
# pinned racket would resolve `(require beagle/...)` to that SIBLING tree and
# silently test the wrong parse.rkt while every other canary stayed green
# (observed on agent/js-census-019f791c). Resolve the `beagle` collection the way
# tests do (in doctor's own env, which _beagle-racket has already scoped) and
# assert it lives under THIS checkout. Fail CLOSED with exact roots + repair on
# mismatch — never let doctor bless a cross-loaded checkout.
_scope_probe="$("$RACKET" -e '(require setup/collection-search)(with-handlers ([exn:fail? (lambda (e) (displayln "UNRESOLVED"))]) (displayln (path->string (collection-file-path "parse.rkt" "beagle" "private"))))' 2>/dev/null | head -1)"
_scope_root="$(cd "$BEAGLE_ROOT" && pwd -P)"
if [[ -z "$_scope_probe" || "$_scope_probe" == "UNRESOLVED" ]]; then
    ENVIRONMENT_TRUSTED=0
    al_fail "package-scope" "beagle collection unresolvable (no pkg link and no PLTCOLLECTS scope). Repair: run bin/beagle from a real checkout, or 'source bin/_beagle-racket' which auto-scopes PLTCOLLECTS at this root ($_scope_root)."
else
    _scope_probe_real="$(cd "$(dirname "$_scope_probe")" 2>/dev/null && pwd -P || echo "$_scope_probe")"
    if [[ "$_scope_probe_real" != "$_scope_root" && "$_scope_probe_real" != "$_scope_root"/* ]]; then
        ENVIRONMENT_TRUSTED=0
        al_fail "package-scope" "beagle collection resolves to $_scope_probe (root $_scope_probe_real) but THIS checkout is $_scope_root — tests would load a SIBLING parse.rkt (false green). Repair: invoke via bin/* (they 'source bin/_beagle-racket', which scopes PLTCOLLECTS at this root with NO global pkg mutation); never rely on the global 'raco pkg' links inside a worktree."
    else
        al_ok "package-scope"
    fi
fi

# A reachable endpoint and an unusable checkout are distinct diagnoses. Do not
# turn package-scope distrust into a false "not running" daemon report.
if [[ "$DAEMON_REACHABLE" == "1" ]]; then
    if [[ "$ENVIRONMENT_TRUSTED" == "0" ]]; then
        CHECKS+=("{\"name\":\"daemon\",\"status\":\"warning\",\"message\":\"reachable, environment untrusted\"}")
    elif [[ "$DAEMON_REVIVED" == "1" ]]; then
        CHECKS+=("{\"name\":\"daemon\",\"status\":\"ok\",\"message\":\"revived (watch $WATCH_DIR)\"}")
    else
        CHECKS+=("{\"name\":\"daemon\",\"status\":\"ok\",\"message\":\"running\"}")
    fi
else
    CHECKS+=("{\"name\":\"daemon\",\"status\":\"error\",\"message\":\"not running (re-run with --revive, or: beagle-daemon start --watch $WATCH_DIR)\"}")
fi

# full machinery (--deep): parse -> diagnostic -> suggestion -> producer ->
# consumer -> patch. A bare (assert) in a .bnix must round-trip to a
# nix/assert rename via beagle-repair --emit-patch.
if [[ "$DEEP" == "1" ]]; then
    CN="$DOC_WORK/repair"; mkdir -p "$CN"
    printf '#lang beagle/nix\n(assert true 1)\n' > "$CN/m.bnix"
    printf '#!/usr/bin/env bash\ntrue\n' > "$CN/verify.sh"; chmod +x "$CN/verify.sh"
    PATCH="$("$BIN/beagle-repair" "$CN" "$CN/verify.sh" --emit-patch 2>/dev/null || true)"
    if grep -q '^+(nix/assert' <<<"$PATCH"; then
        al_ok "repair-suggestion"
    else
        al_fail "repair-suggestion" "suggestion->patch did not emit the nix/assert rename"
    fi
fi
rm -rf "$DOC_WORK"

# Target emitters — the roster AND each target's declared status come from the
# canonical table (beagle-lib/private/targets.rkt, projected into
# share/targets.sh); this script classifies only what it can OBSERVE, namely
# whether the declared emitter module is actually present. A declared target
# with no emitter on disk is a soft warning, never an authoring-loop failure.
TARGETS=()
source "$BEAGLE_ROOT/share/targets.sh"
for target in "${BEAGLE_TARGET_IDS[@]}"; do
    declared="${BEAGLE_TARGET_STATUS[$target]}"
    pipeline="${BEAGLE_TARGET_PIPELINE[$target]}"
    if [[ "$pipeline" == "native-program" && -x "$BEAGLE_ROOT/bin/beagle-build-core" ]]; then
        TARGETS+=("{\"name\":\"$target\",\"status\":\"$declared\"}")
    elif [[ "$pipeline" == "hosted-emitter" && -f "$BEAGLE_ROOT/beagle-lib/private/emit-${target}.rkt" ]]; then
        TARGETS+=("{\"name\":\"$target\",\"status\":\"$declared\"}")
    else
        TARGETS+=("{\"name\":\"$target\",\"status\":\"not-built\"}")
        [[ "$STATUS" == "ok" ]] && STATUS="warning"
    fi
done

# --quiet: a healthy heartbeat says nothing. It speaks only when the REPAIR
# LOOP degrades — not for a missing unused target emitter or a stale inventory.
if [[ "$QUIET" == "1" && "$AUTHORING_STATUS" != "error" && "$JSON" == "0" ]]; then
    exit 0
fi

if [[ "$JSON" == "1" ]]; then
    CHECKS_JSON=$(IFS=,; echo "${CHECKS[*]}")
    AUTHORING_JSON=$(IFS=,; echo "${AUTHORING_LOOP[*]}")
    TARGETS_JSON=$(IFS=,; echo "${TARGETS[*]}")
    echo "{\"schemaVersion\":1,\"status\":\"$STATUS\",\"authoring_status\":\"$AUTHORING_STATUS\",\"checks\":[$CHECKS_JSON],\"authoring_loop\":[$AUTHORING_JSON],\"targets\":[$TARGETS_JSON]}"
else
    echo "beagle-doctor"
    echo "============="
    echo ""
    if [[ "$AUTHORING_STATUS" == "error" ]]; then
        echo "Authoring loop: DEGRADED  ← do not trust silent green while degraded"
    else
        echo "Authoring loop: ok  (daemon + functional canaries healthy)"
    fi
    echo "Environment: $STATUS"
    echo ""
    echo "Environment:"
    for check in "${CHECKS[@]}"; do
        name=$(echo "$check" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["name"])')
        status=$(echo "$check" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["status"])')
        msg=$(echo "$check" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["message"])')
        if [[ "$status" == "ok" ]]; then
            echo "  [ok]      $name: $msg"
        elif [[ "$status" == "warning" ]]; then
            echo "  [warn]    $name: $msg"
        else
            echo "  [ERROR]   $name: $msg"
        fi
    done
    echo ""
    echo "Authoring loop (functional):"
    for entry in "${AUTHORING_LOOP[@]}"; do
        name=$(echo "$entry" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["name"])')
        status=$(echo "$entry" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["status"])')
        if [[ "$status" == "ok" ]]; then
            echo "  [ok]      $name canary"
        else
            msg=$(echo "$entry" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d.get("message",""))')
            echo "  [ERROR]   $name canary: $msg"
        fi
    done
    echo ""
    echo "Target emitters:"
    for target in "${TARGETS[@]}"; do
        name=$(echo "$target" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["name"])')
        status=$(echo "$target" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d["status"])')
        if [[ "$status" == "ok" || "$status" == "live" || "$status" == "experimental" ]]; then
            echo "  [$status]  $name"
        elif [[ "$status" == "not-built" ]]; then
            echo "  [warn]    $name: in the target table but no emitter module built"
        else
            msg=$(echo "$target" | "$BEAGLE_PYTHON" -c 'import json,sys; d=json.load(sys.stdin); print(d.get("message",""))')
            echo "  [ERROR]   $name: $msg"
        fi
    done
fi

# Exit gates on the AUTHORING LOOP only (daemon + functional canaries), NOT on the
# environment rollup. A stale target inventory or a missing emitter for an
# unused target must not trip the handshake/heartbeat — only a genuinely
# degraded authoring loop does.
[[ "$AUTHORING_STATUS" == "error" ]] && exit 1
exit 0
