#!/bin/bash
#
# blut24-landing — release script. Runs ON the target host (192.168.12.81),
# invoked over SSH by Jenkins after it has rsynced the checkout into
# $APP_ROOT/releases/$RELEASE_ID.
#
#   RELEASE_ID=42-a1b2c3d bash /var/www/blut24.com/releases/42-a1b2c3d/deploy/deploy.sh
#
# WHY THE BUILD HAPPENS HERE AND NOT ON JENKINS
#   better-sqlite3 is a native addon compiled against a specific Node ABI, OS and
#   libc. Building on the Jenkins host (192.168.12.65) and copying the artifact to
#   .81 works only while both machines stay byte-compatible — and fails silently
#   later, at runtime, with "invalid ELF header". Building on the target removes
#   that whole class of problem. It costs nothing in availability, because the
#   PREVIOUS release keeps serving traffic throughout.
#
# ZERO DOWNTIME
#   1. build the new release completely, while the old one serves
#   2. boot it on a throw-away port and health-check it — a build can compile fine
#      and still crash on start (bad env, missing native binding)
#   3. swap the `web` symlink with an atomic rename
#   4. `pm2 reload` — cluster mode retires workers one at a time
#   5. health-check the live site; on failure, swap back and reload again
#
set -euo pipefail

APP_ROOT="/var/www/blut24.com/web"                       # code (docroot; proxied, never served as files)
SHARED_ROOT="/var/www/blut24.com/private/blut24-landing" # secrets + SQLite, outside the docroot
APP_NAME="blut24-landing"
NODE_VERSION="v22.22.3"
NVM_DIR="/root/.nvm"
KEEP_RELEASES=5
PROBE_PORT="${PROBE_PORT:-3999}"
HEALTH_PATH="/"

# Serialise deploys on the TARGET. Two releases swapping `current` at once would
# interleave the symlink rename and the pm2 reload, and could leave the live link
# pointing at a half-built release. flock is held for the whole run and released
# when the script exits, however it exits. 30 min is far beyond a normal deploy
# (~40 s) but short enough that a wedged run cannot block the next one forever.
LOCK_FILE="/var/lock/blut24-landing-deploy.lock"
exec 9>"$LOCK_FILE"
if ! flock -w 1800 9; then
  echo "another deploy is holding $LOCK_FILE and did not finish within 30 min" >&2
  exit 1
fi

RELEASE_ID="${RELEASE_ID:?RELEASE_ID must be set}"
RELEASE_DIR="$APP_ROOT/releases/$RELEASE_ID"
SHARED_DIR="$SHARED_ROOT/shared"
LIVE_LINK="$APP_ROOT/current"

export NVM_DIR="$NVM_DIR"
# shellcheck disable=SC1090
. "$NVM_DIR/nvm.sh"
# install-if-missing: bumping NODE_VERSION in git is then all that is needed —
# without this a new pin fails the deploy with "N/A: version not yet installed".
nvm install "$NODE_VERSION" >/dev/null 2>&1 || nvm use "$NODE_VERSION" >/dev/null
command -v "$NVM_DIR/versions/node/$NODE_VERSION/bin/npm" >/dev/null \
  || fail "node $NODE_VERSION unavailable on this host"
"$NVM_DIR/versions/node/$NODE_VERSION/bin/npm" ls -g pm2 >/dev/null 2>&1 \
  || "$NVM_DIR/versions/node/$NODE_VERSION/bin/npm" install -g pm2 >/dev/null 2>&1

NODE_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/node"
NPM_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/npm"
PM2_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/pm2"

log() { echo "[$(date '+%H:%M:%S')] $*"; }
fail() { echo "[$(date '+%H:%M:%S')] FAILED: $*" >&2; exit 1; }

[ -d "$RELEASE_DIR" ] || fail "release dir $RELEASE_DIR does not exist (did rsync run?)"
log "release $RELEASE_ID  node $("$NODE_BIN" -v)  abi $("$NODE_BIN" -p 'process.versions.modules')"

# ---------------------------------------------------------------------------
# 1. Wire the release to shared state
# ---------------------------------------------------------------------------
# The source XML ships in git but the databases do NOT (they are gitignored), so
# the release's own data/ dir must be REPLACED by a symlink to shared/data —
# otherwise every deploy would serve an empty registrations.db and silently lose
# every pending registration.
log "linking shared state"
mkdir -p "$SHARED_DIR/data"
for xml in "$RELEASE_DIR"/data/*.xml "$RELEASE_DIR"/data/*.xsd; do
  [ -e "$xml" ] || continue
  target="$SHARED_DIR/data/$(basename "$xml")"
  if [ ! -f "$target" ] || [ "$xml" -nt "$target" ]; then
    cp -f "$xml" "$target"
    log "  source data: $(basename "$xml") -> shared"
  fi
done
rm -rf "$RELEASE_DIR/data"
ln -sfn "$SHARED_DIR/data" "$RELEASE_DIR/data"

[ -f "$SHARED_DIR/.env" ] || fail "$SHARED_DIR/.env missing — run deploy/bootstrap-server.sh first"
ln -sfn "$SHARED_DIR/.env" "$RELEASE_DIR/.env"

# ---------------------------------------------------------------------------
# 2. Install + build (old release still serving)
# ---------------------------------------------------------------------------
cd "$RELEASE_DIR"
log "npm ci"
# npm ci installs exactly the committed lock file and compiles native addons for
# THIS host. --legacy-peer-deps mirrors what the project README documents.
"$NPM_BIN" ci --legacy-peer-deps --no-audit --no-fund

log "verifying the native addon actually loads on this host"
"$NODE_BIN" -e "new (require('better-sqlite3'))(':memory:').close(); console.log('  better-sqlite3 OK')" \
  || fail "better-sqlite3 cannot load — native build problem"

log "nuxt build"
"$NPM_BIN" run build

# clinics.db is derived from the shipped XML; rebuild it only when it is missing
# or older than the source, so a deploy never needlessly churns it.
XML_SRC=$(ls -t "$SHARED_DIR"/data/*TVERZ_Export.xml 2>/dev/null | head -1 || true)
if [ -n "$XML_SRC" ]; then
  if [ ! -f "$SHARED_DIR/data/clinics.db" ] || [ "$XML_SRC" -nt "$SHARED_DIR/data/clinics.db" ]; then
    log "rebuilding clinics.db from $(basename "$XML_SRC")"
    "$NPM_BIN" run import:clinics
  else
    log "clinics.db is current, skipping import"
  fi
fi

# ---------------------------------------------------------------------------
# 3. Pre-flight: boot the new build in isolation before it can touch traffic
# ---------------------------------------------------------------------------
log "pre-flight boot on port $PROBE_PORT"
set -a; . "$SHARED_DIR/.env"; set +a
PROBE_LOG=$(mktemp)
NITRO_PORT="$PROBE_PORT" NITRO_HOST=127.0.0.1 "$NODE_BIN" "$RELEASE_DIR/.output/server/index.mjs" >"$PROBE_LOG" 2>&1 &
PROBE_PID=$!
probe_ok=0
for _ in $(seq 1 30); do
  sleep 1
  if curl -fsS -m 3 "http://127.0.0.1:$PROBE_PORT$HEALTH_PATH" -o /dev/null 2>/dev/null; then probe_ok=1; break; fi
  kill -0 "$PROBE_PID" 2>/dev/null || break   # process died
done
kill "$PROBE_PID" 2>/dev/null || true
wait "$PROBE_PID" 2>/dev/null || true
if [ "$probe_ok" -ne 1 ]; then
  echo "--- pre-flight output ---"; tail -40 "$PROBE_LOG"; rm -f "$PROBE_LOG"
  fail "new release does not answer on $HEALTH_PATH — NOT swapping, live site untouched"
fi
rm -f "$PROBE_LOG"
log "  pre-flight OK"

# ---------------------------------------------------------------------------
# 4. Atomic cutover
# ---------------------------------------------------------------------------
PREVIOUS=""
if [ -L "$LIVE_LINK" ]; then PREVIOUS=$(readlink -f "$LIVE_LINK"); fi

log "swapping current -> releases/$RELEASE_ID"
# ln -sfn is NOT atomic (it unlinks, then creates). Create a temp link and rename
# it over the old one — rename(2) is atomic, so no request ever sees a missing current/.
ln -sfn "$RELEASE_DIR" "$APP_ROOT/.current.tmp.$$"
mv -Tf "$APP_ROOT/.current.tmp.$$" "$LIVE_LINK"

cp -f "$RELEASE_DIR/deploy/ecosystem.config.cjs" "$SHARED_ROOT/ecosystem.config.cjs"

log "pm2 reload (rolling)"
if "$PM2_BIN" describe "$APP_NAME" >/dev/null 2>&1; then
  "$PM2_BIN" reload "$SHARED_ROOT/ecosystem.config.cjs" --update-env
else
  "$PM2_BIN" start "$SHARED_ROOT/ecosystem.config.cjs"
fi

# ---------------------------------------------------------------------------
# 5. Verify live, roll back if the new release misbehaves under PM2
# ---------------------------------------------------------------------------
PORT="${NITRO_PORT:-3002}"
live_ok=0
for _ in $(seq 1 30); do
  sleep 1
  if curl -fsS -m 3 "http://127.0.0.1:$PORT$HEALTH_PATH" -o /dev/null 2>/dev/null; then live_ok=1; break; fi
done

if [ "$live_ok" -ne 1 ]; then
  log "LIVE HEALTH CHECK FAILED — rolling back"
  if [ -n "$PREVIOUS" ] && [ -d "$PREVIOUS" ]; then
    ln -sfn "$PREVIOUS" "$APP_ROOT/.current.tmp.$$"
    mv -Tf "$APP_ROOT/.current.tmp.$$" "$LIVE_LINK"
    "$PM2_BIN" reload "$SHARED_ROOT/ecosystem.config.cjs" --update-env || true
    log "rolled back to $(basename "$PREVIOUS")"
  else
    log "no previous release to roll back to"
  fi
  "$PM2_BIN" logs "$APP_NAME" --lines 40 --nostream || true
  fail "release $RELEASE_ID unhealthy"
fi

"$PM2_BIN" save >/dev/null 2>&1 || true
log "live on port $PORT"

# ---------------------------------------------------------------------------
# 6. Prune
# ---------------------------------------------------------------------------
CURRENT=$(basename "$(readlink -f "$LIVE_LINK")")
cd "$APP_ROOT/releases"
ls -1dt */ 2>/dev/null | sed 's#/##' | tail -n +$((KEEP_RELEASES + 1)) | while read -r old; do
  [ "$old" = "$CURRENT" ] && continue
  log "pruning old release $old"
  rm -rf "${APP_ROOT:?}/releases/${old:?}"
done

log "deploy $RELEASE_ID complete"
