#!/usr/bin/env bash
#
# TuneVault OCI auto-deploy — pull `main` and reload PM2 when the GitHub remote
# advances. Designed to run from cron every 5 minutes on the OCI Compute VM.
#
#   */5 * * * * opc /opt/tunevault/deploy/auto-deploy.sh   (see docs/OCI_COMPUTE_SETUP.md, Part 6)
#
# "Deployed" is tracked by a MARKER file — the last sha for which `npm ci` AND `pm2
# reload` BOTH succeeded — NOT by git HEAD. `git reset --hard` advances HEAD to put the
# new code on disk, but the marker only moves after a fully successful deploy. So if
# `npm ci` or `pm2 reload` fails, the marker still points at the old sha and the NEXT tick
# RETRIES — instead of seeing HEAD==origin and silently wedging on stale code (checked out
# but never reloaded) forever, which is what the old head-based check did.
#
# Idempotent + cheap: when the marker already matches origin/main it exits silently, so the
# 5-min tick is a no-op 99% of the time and the log only grows on real deploys / failures.
# `npm ci` runs only when package-lock.json / package.json changed between the marker and
# remote (it is slow).
#
# This is the CLOUD instance's deploy path (git pull), NOT the self-hosted release flow. It
# does `git reset --hard origin/main` — the VM is a deploy target, not a dev box; local edits
# to tracked files are discarded. Untracked files (.env, wallet/, the marker) are never
# touched by reset --hard.

set -uo pipefail
# NOTE: `set -e` is intentionally NOT enabled globally. The deploy steps (reset, npm ci,
# pm2 reload) are checked EXPLICITLY so a failure logs loudly and returns WITHOUT moving the
# marker — leaving the next tick free to retry. A blanket `set -e` would abort mid-deploy in
# ways that are harder to reason about for that retry contract.

# cron runs with a minimal PATH and no login profile — make node/npm/pm2/git/flock
# resolvable, and point PM2 at the app user's daemon explicitly.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"

# ── config (override via env in the cron line if your layout differs) ─────────
APP_DIR="${TUNEVAULT_APP_DIR:-/opt/tunevault}"
BRANCH="${TUNEVAULT_BRANCH:-main}"
PM2_APP="${TUNEVAULT_PM2_APP:-tunevault}"
APP_USER="${TUNEVAULT_APP_USER:-opc}"
LOG="${TUNEVAULT_DEPLOY_LOG:-/home/${APP_USER}/auto-deploy.log}"
# Last SUCCESSFULLY deployed sha. Kept OUTSIDE the repo so `git reset --hard` never touches
# it. This — not HEAD — is the source of truth for "already deployed".
MARKER="${TUNEVAULT_DEPLOY_MARKER:-/home/${APP_USER}/.tunevault-last-deployed-sha}"
export PM2_HOME="${PM2_HOME:-/home/${APP_USER}/.pm2}"
# ──────────────────────────────────────────────────────────────────────────────

exec >>"$LOG" 2>&1
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

# Single-flight: a slow deploy (npm ci) must not overlap the next cron tick.
exec 9>"/tmp/tunevault-auto-deploy.lock"
if ! flock -n 9; then
  log "another auto-deploy is still running — skipping this tick"
  exit 0
fi

cd "$APP_DIR" || { log "ERROR: cannot cd to $APP_DIR — skipping"; exit 1; }

# A transient network blip on fetch must be a quiet skip, not a cron error email.
if ! git fetch --quiet origin "$BRANCH" 2>/dev/null; then
  log "git fetch failed (network?) — skipping this tick"
  exit 0
fi

REMOTE="$(git rev-parse "origin/${BRANCH}" 2>/dev/null || true)"
LOCAL="$(git rev-parse HEAD 2>/dev/null || true)"
if [ -z "$REMOTE" ] || [ -z "$LOCAL" ]; then
  log "ERROR: could not resolve HEAD ($LOCAL) / origin/${BRANCH} ($REMOTE) — skipping"
  exit 1
fi
LAST_DEPLOYED="$(cat "$MARKER" 2>/dev/null || true)"

# First run against an already-current checkout (marker not yet created): seed the marker
# to the current sha and deploy nothing, so we don't force one spurious reload on upgrade.
if [ -z "$LAST_DEPLOYED" ] && [ "$LOCAL" = "$REMOTE" ]; then
  echo "$REMOTE" > "$MARKER"
  log "seeded deploy marker at $(git rev-parse --short "$REMOTE") (already current) — no deploy"
  exit 0
fi

# Already SUCCESSFULLY deployed this sha → silent no-op (keeps the log clean across quiet periods).
[ "$LAST_DEPLOYED" = "$REMOTE" ] && exit 0

log "deploy needed: last-deployed=${LAST_DEPLOYED:-none} head=${LOCAL} -> remote=${REMOTE}"

# npm ci runs when package files differ between the last SUCCESSFULLY deployed sha and
# remote — NOT head->remote. After a failed tick's `reset --hard`, HEAD already == remote, so
# a head->remote diff would show "no change" and skip the reinstall the failed deploy still
# owes. An empty/invalid marker → run it (safe default).
DEPS_CHANGED=1
if [ -n "$LAST_DEPLOYED" ] && git cat-file -e "${LAST_DEPLOYED}^{commit}" 2>/dev/null; then
  if git diff --quiet "$LAST_DEPLOYED" "$REMOTE" -- package-lock.json package.json 2>/dev/null; then
    DEPS_CHANGED=""
  fi
fi

# Put the new code on disk. HEAD advances here; the MARKER does NOT — so any failure below
# still retries on the next tick rather than wedging on checked-out-but-unreloaded code.
if ! git reset --hard "origin/${BRANCH}"; then
  log "ERROR: git reset --hard failed — marker unmoved, will retry next tick"
  exit 1
fi

if [ -n "$DEPS_CHANGED" ]; then
  log "dependencies changed — running npm ci --omit=dev"
  if ! npm ci --omit=dev; then
    log "ERROR: npm ci failed — marker unmoved (code checked out, PM2 NOT reloaded); will retry next tick"
    exit 1
  fi
fi

# fork mode → reload is effectively a restart; single instance keeps the in-process
# scheduler / agent poll loop from double-firing (see ecosystem.config.js).
if ! pm2 reload "$PM2_APP"; then
  log "ERROR: pm2 reload failed — marker unmoved; will retry next tick"
  exit 1
fi
pm2 save --force >/dev/null 2>&1 || true

# Fully deployed (reset + npm ci + pm2 reload all succeeded) — record the sha so the next
# tick treats it as done. Writing the marker is deliberately the LAST step.
echo "$REMOTE" > "$MARKER"
log "deployed $(git rev-parse --short HEAD) — PM2 reloaded (marker updated)"
