#!/usr/bin/env bash
# Resolve the correct Racket for beagle's compiled bytecode.
# Source this from bin/* scripts: source "$(dirname "$0")/_beagle-racket"
# Then use $RACKET instead of "racket" and $RACO instead of "raco".
#
# Also PREPENDS the pinned racket onto PATH (exported), so child scripts with a
# bare `#!/usr/bin/env racket` shebang (beagle-syntax, -fix, -lsp, -repl, ...)
# inherit it instead of an ambient racket. Without this, invoking beagle outside
# its direnv shell (dispatched agent / CI / cron / a non-direnv subshell) picks
# the system racket, where beagle-lib is unlinked -> "collection not found:
# beagle/private/*" -> the syntax + recompile gate fails SILENTLY. The resolved
# path is cached in _BEAGLE_RACKET so nested sources skip the costly direnv call.

BEAGLE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

RACKET="racket"
RACO="raco"

if [[ -z "${_BEAGLE_RACKET:-}" ]]; then
    # 1. This checkout's own direnv (the normal case).
    if [[ -d "$BEAGLE_ROOT/.direnv" ]]; then
        _direnv_racket="$(cd "$BEAGLE_ROOT" && direnv exec . which racket 2>/dev/null || true)"
    fi
    # 2. WORKTREE / un-allowed direnv: a `git worktree` has flake.nix but no
    #    allowed .direnv, so bare racket would be the SYSTEM version — which
    #    mismatches the flake-pinned bytecode and dies with "body of raco.rkt".
    #    Fall back to the canonical checkout's resolved racket (same flake.lock
    #    => same racket); internally consistent even for a worktree.
    if [[ -z "${_direnv_racket:-}" && -d "$HOME/code/beagle/main/.direnv" && "$BEAGLE_ROOT" != "$HOME/code/beagle/main" ]]; then
        _direnv_racket="$(cd "$HOME/code/beagle/main" && direnv exec . which racket 2>/dev/null || true)"
    fi
    # 3. Last resort: evaluate this flake directly (slow, but always correct).
    if [[ -z "${_direnv_racket:-}" && -f "$BEAGLE_ROOT/flake.nix" ]] && command -v nix >/dev/null 2>&1; then
        _direnv_racket="$(cd "$BEAGLE_ROOT" && nix develop --command which racket 2>/dev/null || true)"
    fi
    [[ -n "${_direnv_racket:-}" ]] && export _BEAGLE_RACKET="$_direnv_racket"
fi

if [[ -n "${_BEAGLE_RACKET:-}" ]]; then
    RACKET="$_BEAGLE_RACKET"
    RACO="$(dirname "$_BEAGLE_RACKET")/raco"
    case ":$PATH:" in
        *":$(dirname "$_BEAGLE_RACKET"):"*) ;;          # pinned racket already on PATH
        *) export PATH="$(dirname "$_BEAGLE_RACKET"):$PATH" ;;
    esac
fi

# --- Worktree-local package scope (NO global mutation) -----------------------
# The global `raco pkg` links for beagle/beagle-lib/beagle-test point at ONE
# checkout (canonically ~/code/beagle/main). With the pinned racket shared across
# checkouts, EVERY other checkout would silently resolve `(require beagle/...)`
# through those links to the canonical tree — a git worktree's tests would
# exercise the WRONG parse.rkt and doctor would still say healthy (observed on
# agent/js-census-019f791c). Fix at the seam every bin/* script already sources:
# prepend PLTCOLLECTS roots that route the `beagle` collection — contributed by
# BOTH beagle-lib (beagle/private/*, beagle/lang/*, …) and beagle-test
# (beagle/tests/*) — at THIS checkout. Racket merges a collection across collects
# roots, so each half resolves to this root. Idempotent symlinks under the
# gitignored .beagle/; touches no global pkg state; the trailing default keeps
# every OTHER collection (base, rackunit, cnf-*, …) resolving via the pkg links.
# An exact package link already has the right identity. Replacing it would mix
# setup-compiled package identities with symlink-scoped identities in one load.
#
# The probe below is a whole Racket VM (0.54s measured, 9.1 CS) that computes
# nothing but a fact already fixed by ($RACKET, this root, the pkg link table).
# It ran once per top-level bin/* invocation and was 52% of `beagle check` on a
# two-line file (1.255s cold -> 0.604s with the answer already in hand), so the
# verdict is stamped. Every recheck below is a pure bash test — no fork. A stamp
# that is unreadable, torn, or stale only costs one probe; it can never route a
# `require` at the wrong checkout, because any verdict that is not exactly
# `native` takes the scoping branch, which pins this root explicitly.
if [[ -d "$BEAGLE_ROOT/beagle-lib" && "${_BEAGLE_SCOPE_ROOT:-}" != "$BEAGLE_ROOT" ]]; then
    # Scope state must survive immutable checkout roots (notably /nix/store),
    # while remaining isolated per checkout and per resolved Racket.
    _beagle_scope_identity_root="$(realpath "$BEAGLE_ROOT" 2>/dev/null || printf '%s' "$BEAGLE_ROOT")"
    _beagle_scope_identity_racket="$(realpath "$RACKET" 2>/dev/null || command -v "$RACKET" 2>/dev/null || printf '%s' "$RACKET")"
    _beagle_scope_cache_root="${XDG_CACHE_HOME:-$HOME/.cache}/beagle/racket-scope"
    _beagle_scope_key="$(printf '%s\0%s' "$_beagle_scope_identity_root" \
        "$_beagle_scope_identity_racket" | sha256sum | cut -d' ' -f1)"
    _bscope_entry="$_beagle_scope_cache_root/$_beagle_scope_key"
    _bscope_stamp="$_bscope_entry/scope-stamp"
    _bscope_verdict=""
    if [[ -f "$_bscope_stamp" ]]; then
        _bscope_racket=""
        IFS=$'\t' read -r _bscope_racket _bscope_verdict < "$_bscope_stamp" || true
        # Only three things move this verdict, so only three are checked. The
        # racket is keyed in the stamp. The pkg link table decides where
        # `beagle` resolves, so any links.rktd newer than the stamp re-probes.
        # And the native branch requires BOTH of these paths to resolve, so
        # either one disappearing must fall back to scoping. Deliberately NOT
        # keyed on directory mtimes: `beagle-lib/` changes on every edit while
        # the verdict does not, and keying on it would re-probe all day.
        if [[ "$_bscope_racket" != "$RACKET" ]] ||
           [[ ! -e "$BEAGLE_ROOT/beagle-lib/private/parse.rkt" ]] ||
           [[ ! -e "$BEAGLE_ROOT/beagle-test/tests" ]]; then
            _bscope_verdict=""
        else
            for _bscope_dep in \
                "$HOME/.local/share/racket"/*/links.rktd \
                "${RACKET%/*}/../share/racket/links.rktd"; do
                if [[ -e "$_bscope_dep" && "$_bscope_dep" -nt "$_bscope_stamp" ]]; then
                    _bscope_verdict=""
                    break
                fi
            done
        fi
        unset _bscope_racket _bscope_dep
    fi

    if [[ -z "$_bscope_verdict" ]]; then
        _beagle_scope_probe="$("$RACKET" -e '
          (displayln (path->string (collection-file-path "parse.rkt" "beagle" "private")))
          (displayln (path->string (collection-path "beagle" "tests")))
        ' 2>/dev/null || true)"
        _beagle_scope_lib="${_beagle_scope_probe%%$'\n'*}"
        _beagle_scope_test="${_beagle_scope_probe#*$'\n'}"
        _beagle_scope_lib="$(realpath "$_beagle_scope_lib" 2>/dev/null || true)"
        _beagle_scope_test="$(realpath "$_beagle_scope_test" 2>/dev/null || true)"
        _beagle_expected_lib="$(realpath "$BEAGLE_ROOT/beagle-lib/private/parse.rkt" 2>/dev/null || true)"
        _beagle_expected_test="$(realpath "$BEAGLE_ROOT/beagle-test/tests" 2>/dev/null || true)"

        if [[ -n "$_beagle_expected_lib" && -n "$_beagle_expected_test" \
              && "$_beagle_scope_lib" == "$_beagle_expected_lib" \
              && "$_beagle_scope_test" == "$_beagle_expected_test" ]]; then
            _bscope_verdict="native"
        else
            _bscope_verdict="scoped"
        fi
        # Published by rename so a concurrent reader sees one whole verdict or
        # the previous one, never a half-written line.
        if mkdir -p "$_bscope_entry" 2>/dev/null &&
           printf '%s\t%s\n' "$RACKET" "$_bscope_verdict" \
               > "$_bscope_stamp.$$" 2>/dev/null; then
            mv -f "$_bscope_stamp.$$" "$_bscope_stamp" 2>/dev/null ||
                rm -f "$_bscope_stamp.$$" 2>/dev/null || true
        fi
        unset _beagle_scope_probe _beagle_scope_lib _beagle_scope_test
        unset _beagle_expected_lib _beagle_expected_test
    fi

    if [[ "$_bscope_verdict" == "native" ]]; then
        export _BEAGLE_SCOPE_ROOT="$BEAGLE_ROOT"
    else
        _bscope="$_bscope_entry/scope"
        if ! mkdir -p "$_bscope/lib" "$_bscope/test" 2>/dev/null; then
            echo "beagle: cannot create writable Racket scope cache: $_bscope_entry" >&2
            case "$-" in
                *i*) return 1 ;;
                *)   exit 1 ;;
            esac
        fi
        ln -sfn "$BEAGLE_ROOT/beagle-lib" "$_bscope/lib/beagle" 2>/dev/null || true
        [[ -d "$BEAGLE_ROOT/beagle-test" ]] && ln -sfn "$BEAGLE_ROOT/beagle-test" "$_bscope/test/beagle" 2>/dev/null || true
        if [[ -n "${PLTCOLLECTS:-}" ]]; then
            export PLTCOLLECTS="$_bscope/lib:$_bscope/test:$PLTCOLLECTS"
        else
            export PLTCOLLECTS="$_bscope/lib:$_bscope/test:"   # trailing : => append the default search
        fi
        export _BEAGLE_SCOPE_ROOT="$BEAGLE_ROOT"
        unset _bscope
    fi
    unset _beagle_scope_identity_root _beagle_scope_identity_racket
    unset _beagle_scope_cache_root _beagle_scope_key _bscope_entry
    unset _bscope_stamp _bscope_verdict
fi

# --- Compiled-closure freshness gate -----------------------------------------
# Racket's loader checks .zo freshness per file by timestamp and never
# transitively, so an unchanged dependent keeps loading bytecode built against
# an old dependency. `raco make` is the only transitive check (SHA-1 in
# compiled/*.dep), so it runs here, at the seam every bin/* entrypoint sources.
#
# Production check/build entrypoints cannot load beagle-test. Keep that closure
# on its own stamp so a fresh checkout does not compile the test suite before it
# can check one source. Direct sourcing, tests, and unknown entrypoints stay on
# the complete closure: an unclassified caller may never narrow freshness.
# The complete closure satisfies production freshness, never the reverse.
#
# The resolved racket path is part of each stamp: .zo is version-specific too.
# Escape hatch: BEAGLE_NO_ZO_GATE=1.
if [[ -n "${_BEAGLE_RACKET:-}" && -d "$BEAGLE_ROOT/beagle-lib" \
      && "${BEAGLE_NO_ZO_GATE:-0}" == 0 && "${_BEAGLE_ZO_GATE_PID:-}" != "$$" ]]; then
    # An exec chain (bin/beagle -> bin/beagle-check) keeps the SAME pid and
    # re-sources this file; a real subprocess never inherits its parent's pid,
    # so this dedupes the exec hop WITHOUT ever skipping a fresh invocation.
    export _BEAGLE_ZO_GATE_PID=$$

    _beagle_zo_profile=complete
    case "${0##*/}" in
        beagle)
            case "${1:-}" in
                check|build) _beagle_zo_profile=production ;;
            esac
            ;;
        beagle-check|beagle-check-all|beagle-build|beagle-build-all|beagle-build-core|beagle-ast|beagle-ast-bundle|beagle-project-session)
            _beagle_zo_profile=production
            ;;
    esac

    _zo_complete_stamp="$BEAGLE_ROOT/.beagle/zo-fresh"
    _zo_production_stamp="$BEAGLE_ROOT/.beagle/zo-fresh-production"
    if [[ "$_beagle_zo_profile" == production ]]; then
        _zo_stamp="$_zo_production_stamp"
    else
        _zo_stamp="$_zo_complete_stamp"
    fi
    _zo_lock="$BEAGLE_ROOT/.beagle/zo-gate.lock"
    # Every tree loadable by production entrypoints. A new production tree must
    # be added here. Not repo-wide: in-tree scratch output from a test run would
    # trip it constantly. Complete callers additionally own beagle-test.
    _zo_scan=()
    for _d in beagle-lib beagle contrib tools; do
        [[ -d "$BEAGLE_ROOT/$_d" ]] && _zo_scan+=("$BEAGLE_ROOT/$_d") || true
    done
    if [[ "$_beagle_zo_profile" == complete && -d "$BEAGLE_ROOT/beagle-test" ]]; then
        _zo_scan+=("$BEAGLE_ROOT/beagle-test")
    fi

    # Directory mtimes count too: a deleted .rkt leaves a still-loadable orphan
    # .zo. Wrong verdicts here can only cost an extra rebuild, never a skip.
    _beagle_zo_stale() {
        [[ -f "$_zo_stamp" ]] || return 0
        [[ "$(cat "$_zo_stamp" 2>/dev/null || true)" == "$_BEAGLE_RACKET" ]] || return 0
        [[ -n "$(find "${_zo_scan[@]}" -name compiled -prune -o \
                      \( -name '*.rkt' -o -type d \) -newer "$_zo_stamp" \
                      -print -quit 2>/dev/null)" ]]
    }

    _beagle_zo_rebuild() {
        _beagle_zo_stale || return 0          # a lock holder already fixed it
        local ref="$_zo_stamp.start" f
        local roots=()
        while IFS= read -r -d '' f; do
            [[ "$f" == bin/* ]] && continue              # shebang scripts, no .zo
            if [[ "$_beagle_zo_profile" == production ]]; then
                case "$f" in
                    beagle-lib/*|beagle/*|contrib/*|tools/*) ;;
                    *) continue ;;
                esac
            fi
            [[ -f "$BEAGLE_ROOT/$f" ]] && roots+=("$BEAGLE_ROOT/$f") || true
        done < <(git -C "$BEAGLE_ROOT" ls-files -z -- '*.rkt' 2>/dev/null)
        if [[ ${#roots[@]} -eq 0 ]]; then                # no git / not a checkout
            while IFS= read -r -d '' f; do roots+=("$f"); done \
                < <(find "${_zo_scan[@]}" -name compiled -prune -o -name '*.rkt' -print0)
        fi
        [[ ${#roots[@]} -gt 0 ]] || return 1
        # Creating a first compiled/ directory changes its source directory's
        # mtime. Do that before the reference point so our own build cannot
        # make the next invocation look stale.
        for f in "${roots[@]}"; do
            mkdir -p "$(dirname "$f")/compiled" || return 1
        done
        # The stamp inherits this file's mtime, so its precision is the
        # filesystem's own: a source touched from here on reads as newer.
        : > "$ref" || return 1
        local make_log=""
        if [[ "${BEAGLE_ZO_GATE_QUIET:-0}" == 1 ]]; then
            make_log="$(mktemp "$BEAGLE_ROOT/.beagle/zo-make.XXXXXX")" || return 1
            if ! "$RACO" make -j "$(nproc 2>/dev/null || echo 4)" "${roots[@]}" \
                >"$make_log" 2>&1; then
                cat "$make_log" >&2
                rm -f "$make_log"
                return 1
            fi
            rm -f "$make_log"
        else
            echo "beagle: source changed; recompiling bytecode (${#roots[@]} modules)…" >&2
            "$RACO" make -j "$(nproc 2>/dev/null || echo 4)" "${roots[@]}" >&2 || return 1
        fi
        printf '%s' "$_BEAGLE_RACKET" > "$_zo_stamp" || return 1
        touch -r "$ref" "$_zo_stamp" || return 1
        if [[ "$_beagle_zo_profile" == complete ]]; then
            printf '%s' "$_BEAGLE_RACKET" > "$_zo_production_stamp" || return 1
            touch -r "$ref" "$_zo_production_stamp" || return 1
        fi
        rm -f "$ref"
        if [[ "${BEAGLE_ZO_GATE_QUIET:-0}" != 1 ]]; then
            echo "beagle: bytecode current." >&2
        fi
    }

    _zo_ok=1
    if _beagle_zo_stale; then
        mkdir -p "$BEAGLE_ROOT/.beagle" 2>/dev/null || true
        # Concurrent bin/* invocations race constantly (hooks, daemon, agents)
        # and two raco makes on one compiled/ dir corrupt each other. The loser
        # WAITS: skipping would run it against the very bytecode the winner is
        # fixing. An unwritable lock degrades to building unlocked, not to
        # running stale.
        if : >>"$_zo_lock" 2>/dev/null; then
            ( flock 9 || true; _beagle_zo_rebuild ) 9>>"$_zo_lock" || _zo_ok=0
        else
            _beagle_zo_rebuild || _zo_ok=0
        fi
    fi

    unset -f _beagle_zo_stale _beagle_zo_rebuild
    unset _zo_stamp _zo_complete_stamp _zo_production_stamp _zo_lock _zo_scan _d
    unset _beagle_zo_profile
    if [[ "$_zo_ok" != 1 ]]; then
        unset _zo_ok
        # Never fall through to a broken or stale closure — that is the same
        # phantom-bug class this gate exists to end.
        echo "beagle: bytecode recompile FAILED (see the raco make errors above)." >&2
        echo "beagle: refusing to run against a stale/broken compiled closure." >&2
        echo "beagle: fix the compile error, or bypass with BEAGLE_NO_ZO_GATE=1." >&2
        case "$-" in
            *i*) return 1 ;;
            *)   exit 1 ;;
        esac
    fi
    unset _zo_ok
fi
