#!/usr/bin/env bash
# scripts/live-build.sh — rebuild the frontend bundle on a box that serves its
# working tree directly, WITHOUT taking the site down.
#
# Why this exists: Vite EMPTIES its output directory before it writes, so a plain
# `npm run build` into public/build deletes manifest.json for the whole build and
# every request 500s (ViteManifestNotFoundException). The old workaround was to
# wrap the build in `php artisan down` / `up` — which merely turns the 500 into a
# 503: the whole site still shows the maintenance splash for every build, and on
# a day of small edits that was 19 outages (2026-08-30). scripts/deploy-update.sh
# already solved this with a staging directory + rename swap; this script is that
# same swap, packaged for the dev loop.
#
# What it does:
#   1. Takes a lock, so concurrent sessions queue instead of building on top of
#      each other (two builds at once take longer than two in a row, and can race
#      on the swap).
#   2. Skips entirely when nothing under resources/js|css, vite.config.js or
#      package.json is newer than the live manifest (--force overrides).
#   3. Refuses when MemAvailable is below --min-mem (default 3500 MB) — the client
#      build peaks at ~2.85 GB RSS (measured 2026-09-02), and a build has
#      OOM-killed this box before (docs/petav3-dev-incident-summary.md). The old
#      1200 MB floor predated the measurement and was under half of what a build
#      actually needs.
#   4. Builds the CLIENT bundle into public/build.new (vite.config.js honours
#      VITE_BUILD_OUT_DIR), with NO pipe on the build command so a killed build
#      cannot masquerade as a success.
#   5. Carries the outgoing build's hashed chunks into the new directory (open
#      tabs keep lazy-loading fine), then swaps with two renames — the gap where
#      public/build does not exist is microseconds, and a failed build leaves the
#      live site untouched.
#   6. Prunes carried-over chunks older than 7 days.
#
# The SSR bundle is NOT built unless --ssr is passed: it is a second ~30 s build
# whose output only matters where the inertia SSR daemon runs, and that service
# does not exist on the dev box. deploy-update.sh still builds both.
#
# Usage:
#   bash scripts/live-build.sh            # build if stale
#   bash scripts/live-build.sh --force    # build regardless
#   bash scripts/live-build.sh --ssr      # also rebuild bootstrap/ssr
#   bash scripts/live-build.sh --min-mem 800
#
# Exit codes: 0 built (or fresh, nothing to do) · 1 build failed (site untouched)
#             2 refused: not enough memory · 3 bad invocation

set -euo pipefail

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

FORCE=false
WITH_SSR=false
MIN_MEM_MB=3500
LOG="${LIVE_BUILD_LOG:-/tmp/live-build.log}"
LOCK="${LIVE_BUILD_LOCK:-/tmp/petav3-live-build.lock}"

while [[ $# -gt 0 ]]; do
    case "$1" in
        --force) FORCE=true ;;
        --ssr) WITH_SSR=true ;;
        --min-mem) shift; MIN_MEM_MB="${1:-}" ;;
        -h|--help) sed -n '2,45p' "$0"; exit 0 ;;
        *) echo "unknown option: $1" >&2; exit 3 ;;
    esac
    shift
done
[[ "$MIN_MEM_MB" =~ ^[0-9]+$ ]] || { echo "--min-mem needs a number of MB" >&2; exit 3; }

BUILD_DIR="${APP_DIR}/public/build"
BUILD_NEW="${APP_DIR}/public/build.new"
BUILD_OLD="${APP_DIR}/public/build.old"
MANIFEST="${BUILD_DIR}/manifest.json"

say() { echo "==> $*"; }

# ---------------------------------------------------------------------------
# 1. One build at a time. flock waits; say so, so a session that sees nothing
#    happen for a minute knows why.
# ---------------------------------------------------------------------------
exec 9>"$LOCK"
if ! flock -n 9; then
    say "another live-build is running — waiting for it to finish"
    flock 9
fi

# ---------------------------------------------------------------------------
# 2. Anything to build?
# ---------------------------------------------------------------------------
if [[ "$FORCE" != "true" && -f "$MANIFEST" ]]; then
    STALE_FILE="$(find resources/js resources/css vite.config.js package.json \
        -newer "$MANIFEST" -print -quit 2>/dev/null || true)"
    if [[ -z "$STALE_FILE" ]]; then
        say "FRESH — nothing under resources/js|css, vite.config.js or package.json is newer than $(date -r "$MANIFEST" '+%H:%M:%S'); skipping (use --force to build anyway)"
        exit 0
    fi
    say "STALE — e.g. ${STALE_FILE} is newer than the live manifest"
fi

# ---------------------------------------------------------------------------
# 3. Memory guard.
# ---------------------------------------------------------------------------
AVAIL_MB="$(awk '/^MemAvailable:/{print int($2/1024)}' /proc/meminfo)"
if (( AVAIL_MB < MIN_MEM_MB )); then
    echo "REFUSED: only ${AVAIL_MB} MB available, need ${MIN_MEM_MB} MB (other sessions eat RAM; retry when quieter, or --min-mem N)" >&2
    exit 2
fi

# ---------------------------------------------------------------------------
# 4. Build the client bundle into the staging directory. The live public/build
#    is not touched. No pipe: a `| tail` would hide the exit code of a build the
#    kernel OOM-killed.
# ---------------------------------------------------------------------------
rm -rf "$BUILD_NEW" "$BUILD_OLD"
START="$(date +%s)"
say "building client bundle → public/build.new (${AVAIL_MB} MB free; log: ${LOG})"
# Node sizes its heap from TOTAL machine RAM (~a quarter of it), so the same
# commit gets 4144 MB here and 2083 MB on the 8 GB production box — and the build
# peaks at ~2.85 GB. Pin it rather than inherit it, so a build that passes here
# is evidence the deploy's build will pass too. See scripts/deploy-update.sh.
export NODE_OPTIONS="--max-old-space-size=4096"
set +e
VITE_BUILD_OUT_DIR="public/build.new" npx vite build > "$LOG" 2>&1
BUILD_EXIT=$?
set -e

if (( BUILD_EXIT != 0 )) || [[ ! -f "${BUILD_NEW}/manifest.json" ]]; then
    rm -rf "$BUILD_NEW"
    echo "BUILD FAILED (exit ${BUILD_EXIT}) — live site untouched, still serving the previous bundle." >&2
    if grep -q "Killed" "$LOG" 2>/dev/null || (( BUILD_EXIT == 137 )); then
        echo "  looks OOM-killed by the KERNEL — the box ran out of RAM. Check MemAvailable and other sessions' builds." >&2
    elif (( BUILD_EXIT == 134 )) || grep -q "heap out of memory" "$LOG" 2>/dev/null; then
        echo "  V8 hit its HEAP CAP (not the kernel — the box may have had RAM to spare)." >&2
        echo "  Raise --max-old-space-size above ${NODE_OPTIONS##*=} in this script, if MemAvailable allows it." >&2
    fi
    echo "  last lines of ${LOG}:" >&2
    tail -n 15 "$LOG" >&2
    exit 1
fi

# ---------------------------------------------------------------------------
# 5. Swap. Carry the outgoing hashed chunks first (-n never clobbers, so the
#    new manifest and assets always win; -a keeps mtimes for the prune).
# ---------------------------------------------------------------------------
if [[ -d "$BUILD_DIR" ]]; then
    cp -an "${BUILD_DIR}/." "${BUILD_NEW}/" 2>/dev/null || true
    mv "$BUILD_DIR" "$BUILD_OLD"
fi
mv "$BUILD_NEW" "$BUILD_DIR"
rm -rf "$BUILD_OLD"

# ---------------------------------------------------------------------------
# 6. Prune chunks no live manifest can still reference. Vite rewrites every
#    asset the current manifest points at, so this cannot remove one in use.
# ---------------------------------------------------------------------------
find "${BUILD_DIR}/assets" -type f -mtime +7 -delete 2>/dev/null || true

ELAPSED=$(( $(date +%s) - START ))
say "client bundle swapped in atomically in ${ELAPSED}s — manifest.json mtime $(date -r "$MANIFEST" '+%H:%M:%S'); site was never down"

# ---------------------------------------------------------------------------
# 7. Optional SSR bundle (bootstrap/ssr). Only useful where the inertia SSR
#    daemon runs; harmless elsewhere.
# ---------------------------------------------------------------------------
if [[ "$WITH_SSR" == "true" ]]; then
    say "building SSR bundle → bootstrap/ssr"
    set +e
    npx vite build --ssr >> "$LOG" 2>&1
    SSR_EXIT=$?
    set -e
    if (( SSR_EXIT != 0 )); then
        echo "SSR BUILD FAILED (exit ${SSR_EXIT}) — client bundle is already live; see ${LOG}" >&2
        exit 1
    fi
    say "SSR bundle built"
fi
