#!/usr/bin/env bash
# scripts/nightly — LOCAL nightly full-build. Runs the whole suite in the dev
# container (via scripts/test) and records a dated green/red result, so
# `/whatneedsdoing` can surface "is main still green?" daily WITHOUT re-running
# the multi-minute suite. This is deliberately LOCAL — not GitHub Actions — so
# the signal lives on this machine next to the rest of the repo-dev tooling.
#
# Why a nightly at all: the ship gate already runs the full suite on every merge,
# so main is green at ship time. The only thing a nightly adds is catching
# UPSTREAM DEPENDENCY DRIFT — a transitive dep releases a breaking version and
# green main goes red with no code change. That's what a scheduled full run
# catches that per-ship gating can't.
#
# Contention filter: on this dark-factory machine the full suite routinely runs
# alongside sibling worktree suites sharing the local precis-test-db, which
# throws spurious collision ERRORs and load-starved FAILEDs. A raw RED is
# therefore NOT trusted — it is verified by re-running only the failed/errored
# ids in isolation (see the block below), so a recorded RED means a real,
# reproducible failure, not concurrency noise.
#
# Modes:
#   scripts/nightly            run the full suite now, append the result, print it.
#                              /whatneedsdoing delegates THIS to a background
#                              `test-runner` agent on DUE (off the main loop) —
#                              there's no launchd timer; the recurring driver is
#                              simply that /whatneedsdoing gets run regularly.
#   scripts/nightly --check    read-only: print the latest result + DUE if stale
#                              (>24h). Never runs the suite — this is what
#                              /whatneedsdoing calls inline.
#
# Status lives at <main-checkout>/.nightly-status.md (gitignored, machine-local,
# shared across worktrees). Advisory; always exits 0.
set -euo pipefail
cd "$(dirname "$0")/.."

MAIN_ROOT="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
STATUS="$MAIN_ROOT/.nightly-status.md"
STALE_HOURS=24

# portable "YYYY-MM-DD HH:MM" -> epoch seconds (BSD date on macOS, GNU on Linux)
epoch_of() { date -u -j -f "%Y-%m-%d %H:%M" "$1" +%s 2>/dev/null || date -u -d "$1" +%s 2>/dev/null; }

# --- --check: read-only cadence + status surface (what /whatneedsdoing runs) ---
if [[ "${1:-}" == "--check" ]]; then
    if [[ ! -f "$STATUS" ]]; then
        echo "nightly: DUE (never run locally) — run \`scripts/nightly\` to record a baseline"; exit 0
    fi
    top=$(grep -E '^\- \*\*[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}\*\*' "$STATUS" | head -1 || true)
    if [[ -z "$top" ]]; then
        echo "nightly: DUE (no dated result in $STATUS)"; exit 0
    fi
    ts=$(echo "$top" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}' | head -1)
    body=$(echo "$top" | sed -E 's/^\- \*\*[^*]+\*\* — //')
    e=$(epoch_of "$ts" || true)
    if [[ -z "$e" ]]; then echo "nightly: last result '$ts' unparseable — check $STATUS"; exit 0; fi
    hours=$(( ( $(date +%s) - e ) / 3600 ))
    if (( hours > STALE_HOURS )); then
        echo "nightly: DUE (last ${ts}, ${hours}h ago) — run \`scripts/nightly\`. Last: $body"
    else
        echo "nightly: $body (${ts}, ${hours}h ago)"
    fi
    exit 0
fi

# --- default: run the full suite locally, capture, record ---
# Preflight: scripts/test needs the dev container, so a dead Docker (Desktop /
# colima not up) would otherwise record a FALSE red. Record a neutral skip
# instead — --check still counts it stale, so the next /whatneedsdoing re-delegates.
if ! docker info >/dev/null 2>&1; then
    stamp="$(date -u +'%Y-%m-%d %H:%M')"
    line="- **$stamp** — ⚠ skipped — Docker unavailable (suite not run)"
    echo "nightly: ⚠ skipped — Docker unavailable; not running the suite"
    if [[ ! -f "$STATUS" ]]; then
        printf '# Nightly local build status\n\nGitignored, machine-local. Written by `scripts/nightly`, read by\n`scripts/nightly --check` (surfaced in `/whatneedsdoing`). Newest first.\n\n' > "$STATUS"
    fi
    tmp=$(mktemp)
    awk -v line="$line" 'BEGIN{done=0} /^- \*\*/ && !done {print line; done=1} {print} END{if(!done) print line}' "$STATUS" > "$tmp" && mv "$tmp" "$STATUS"
    exit 0
fi

echo "nightly: running full suite via scripts/test (this takes a few minutes)…"
run_log=$(mktemp)
# -rfE guarantees the per-test "FAILED …" / "ERROR …" short-summary lines the
# contention filter below parses (default -q output can omit the ERROR list).
if scripts/test "$@" -rfE >"$run_log" 2>&1; then result="✓ green"; else result="✗ RED"; fi

# --- Contention filter: verify a RED before trusting it -----------------------
# This dev machine routinely runs the full suite concurrently with sibling
# worktree suites that share the local precis-test-db. That collision throws
# spurious "database does not exist" / "in recovery mode" ERRORs and starves
# subprocess/timing FAILEDs — a shifting, load-dependent failure set, NOT code
# breakage (a real upstream-dep-drift break fails the SAME ids every run and
# survives isolation). So on RED, re-run ONLY the failed/errored ids in
# isolation for up to 3 rounds; a test that passes in ANY round was a flake, and
# whatever fails every round is real. Green is never retried. (Deeper root-cause
# fix — an isolated per-run test DB — is deferred; this is the low-risk filter.)
parse_ids() { grep -E '^(FAILED|ERROR) ' "$1" \
    | sed -E 's/^(FAILED|ERROR)[[:space:]]+//; s/[[:space:]]+-.*//' | sort -u || true; }
# bash-3.2-safe array load (macOS system bash lacks mapfile/readarray).
load_suspects() { suspects=(); local l
    while IFS= read -r l; do [[ -n "$l" ]] && suspects+=("$l"); done < <(parse_ids "$1"); }
summary_override=""
persistent=""
if [[ "$result" == "✗ RED" ]]; then
    load_suspects "$run_log"
    n0=${#suspects[@]}
    if (( n0 == 0 )); then
        : # RED with no parseable ids (collection/env crash) — trust it as-is
    elif (( n0 > 400 )); then
        result="⚠ inconclusive"
        summary_override="$n0 tests failed/errored under heavy concurrent load — too many to verify in isolation; re-run when the machine is quiet"
    else
        for round in 1 2 3; do
            if (( ${#suspects[@]} == 0 )); then break; fi
            echo "nightly: RED — verifying ${#suspects[@]} suspect(s) in isolation (round $round/3)…"
            vlog=$(mktemp)
            if scripts/test "${suspects[@]}" -rfE >"$vlog" 2>&1; then
                suspects=(); rm -f "$vlog"; break
            fi
            load_suspects "$vlog"
            rm -f "$vlog"
        done
        if (( ${#suspects[@]} == 0 )); then
            result="✓ green"
            summary_override="RED under concurrent load — $n0 flaky id(s), all passed on isolated retry (main green)"
        else
            persistent=$(printf '%s\n' "${suspects[@]}" | head -5 | paste -sd', ' - || true)
        fi
    fi
fi

# pytest's final summary line, e.g. "===== 7774 passed, 18 skipped in 128.4s ====="
# Take the LAST such line, then extract clean "<n> <word>" counts (+ duration).
sumline=$(grep -E '[0-9]+ (passed|failed|error)' "$run_log" | tail -1 || true)
# paste -d takes a *list* of delimiters used cyclically, so join with a single
# ',' then space it out — otherwise it alternates ',' and ' ' between fields.
summary=$(echo "$sumline" | grep -oE '[0-9]+ (passed|failed|errors?|skipped|xfailed|xpassed|deselected|warnings?)' \
    | paste -sd',' - | sed 's/,/, /g' || true)
dur=$(echo "$sumline" | grep -oE 'in [0-9]+(\.[0-9]+)?s' | tail -1 || true)
[[ -n "$summary" && -n "$dur" ]] && summary="$summary $dur"
[[ -z "$summary" ]] && summary="(no pytest summary — see run log; likely a collection/env error)"
# the contention filter may have reclassified the raw full-run counts
[[ -n "$summary_override" ]] && summary="$summary_override"

# name the tests that survived isolation (real), else the first few from the run
fails=""
if [[ -n "$persistent" ]]; then
    fails=" — persistent after isolated retry: $persistent"
elif [[ "$result" == "✗ RED" ]]; then
    fails=$(grep -E '^FAILED ' "$run_log" | sed -E 's/^FAILED //; s/ -.*//' | head -5 | paste -sd', ' - || true)
    [[ -n "$fails" ]] && fails=" — first fails: $fails"
fi

stamp="$(date -u +'%Y-%m-%d %H:%M')"
line="- **$stamp** — $result — $summary$fails"

# prepend under the header (newest-first); create the file on first run
if [[ ! -f "$STATUS" ]]; then
    {
        echo "# Nightly local build status"
        echo
        echo "Gitignored, machine-local. Written by \`scripts/nightly\`, read by"
        echo "\`scripts/nightly --check\` (surfaced in \`/whatneedsdoing\`). Newest first."
        echo
    } > "$STATUS"
fi
# insert the new line right after the first blank line following the header block
tmp=$(mktemp)
awk -v line="$line" 'BEGIN{done=0} /^- \*\*/ && !done {print line; done=1} {print} END{if(!done) print line}' "$STATUS" > "$tmp" && mv "$tmp" "$STATUS"

echo "nightly: $result — $summary$fails"
echo "nightly: recorded to $STATUS"
rm -f "$run_log"
exit 0
