#!/usr/bin/env bash
#
# africa-alert-backup.sh
# ----------------------
# Hot online backup of the Africa Alert PWA SQLite database.
#
# Uses the SQLite `.backup` command (NOT plain `cp`) so the snapshot is taken
# safely while the server is running and the database is in WAL mode. Plain
# `cp` of a WAL-mode DB can produce a corrupted or torn snapshot because the
# -wal and -shm sidecars are still being written to.
#
# This script is meant to be run from cron on the production host
# (Ubuntu 22.04). It is NOT the recommended path on Windows. On a Windows
# dev box, run `node scripts/backup/africa-alert-backup.js` instead — that
# script uses better-sqlite3's online backup API and does not need the
# sqlite3 CLI. If you MUST run this .sh on Windows and the sqlite3 CLI is
# missing, the script will automatically delegate to the Node fallback so
# Windows `bash` invocations still produce a usable backup.
#
# Required tooling on the host (Linux production):
#   - bash 4+ (Ubuntu 22.04 ships 5.1)
#   - sqlite3 CLI (apt: `sudo apt-get install -y sqlite3`)
#   - write access to $BACKUP_DIR and $LOG_DIR
#
# Environment variables (all optional):
#   DB_PATH    Absolute path to the live SQLite database file.
#              Default: /opt/africa-alert/server/data/school.db
#   BACKUP_DIR Directory to write the timestamped .db snapshot into.
#              Default: /backups
#   LOG_DIR    Directory to write the human-readable log into.
#              Default: <project root>/logs  (i.e. the directory that
#              contains scripts/, two levels up from this script)
#   RETAIN_DAILY  Number of most-recent daily snapshots to keep.
#                 Default: 30
#
# Exit codes:
#   0  backup succeeded
#   1  configuration error (missing tool, unreadable DB, etc.)
#   2  sqlite3 .backup itself failed
#   3  retention sweep failed (the snapshot itself is still on disk)

set -eu

# --- Configuration -----------------------------------------------------------

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# scripts/backup/<this>.sh -> project root is two levels up.
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"

DB_PATH="${DB_PATH:-/opt/africa-alert/server/data/school.db}"
BACKUP_DIR="${BACKUP_DIR:-/backups}"
LOG_DIR="${LOG_DIR:-${PROJECT_ROOT}/logs}"
RETAIN_DAILY="${RETAIN_DAILY:-30}"

TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/school-${TIMESTAMP}.db"
LOG_FILE="${LOG_DIR}/backup.log"

# --- Pre-flight --------------------------------------------------------------

log() {
  # Tee to stdout (so cron can mail it) and to the log file.
  local msg="[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [backup] $*"
  echo "${msg}" | tee -a "${LOG_FILE}" >&2
}

mkdir -p "${LOG_DIR}"
touch "${LOG_FILE}"

if ! command -v sqlite3 >/dev/null 2>&1; then
  # Linux production hosts always have sqlite3 (apt: `sudo apt-get install
  # -y sqlite3`). The absence of sqlite3 here means either a misconfigured
  # production host, or a Windows dev box where the CLI is not installed
  # by default. In either case, fall through to the Node fallback which
  # uses the same primitive via better-sqlite3's online backup API.
  log "WARN: sqlite3 CLI not found in PATH; delegating to Node fallback (scripts/backup/africa-alert-backup.js)"
  if ! command -v node >/dev/null 2>&1; then
    log "ERROR: neither sqlite3 nor node is available. Install one (apt: sudo apt-get install -y sqlite3) before retrying."
    exit 1
  fi
  exec node "${SCRIPT_DIR}/africa-alert-backup.js"
fi

if [ ! -f "${DB_PATH}" ]; then
  log "ERROR: DB_PATH does not exist or is not a regular file: ${DB_PATH}"
  exit 1
fi

if [ ! -r "${DB_PATH}" ]; then
  log "ERROR: DB_PATH is not readable by $(id -un): ${DB_PATH}"
  exit 1
fi

# --- Backup ------------------------------------------------------------------
# Idempotency note: mkdir -p on BACKUP_DIR is what makes this script safe to
# run before the operator has manually created the destination.

mkdir -p "${BACKUP_DIR}"

log "starting backup of ${DB_PATH} -> ${BACKUP_FILE}"

# `.backup` is a SQLite dot-command; we invoke it via the CLI's stdin
# mechanism. The trailing path is the destination file. SQLite creates the
# destination atomically and flushes the WAL before copying.
if ! sqlite3 "${DB_PATH}" ".backup '${BACKUP_FILE}'"; then
  log "ERROR: sqlite3 .backup failed for ${DB_PATH}"
  exit 2
fi

if [ ! -s "${BACKUP_FILE}" ]; then
  log "ERROR: backup file was not created or is empty: ${BACKUP_FILE}"
  exit 2
fi

# Copy the -wal and -shm sidecars if they exist, so the snapshot is fully
# self-contained for restore. The sidecars may be absent if the live DB
# was idle (WAL checkpointed) — that is fine.
for ext in wal shm; do
  if [ -f "${DB_PATH}-${ext}" ]; then
    cp "${DB_PATH}-${ext}" "${BACKUP_FILE}-${ext}"
    log "copied sidecar ${DB_PATH}-${ext} -> ${BACKUP_FILE}-${ext}"
  fi
done

# Compute a checksum for tamper detection / off-host transfer verification.
if command -v sha256sum >/dev/null 2>&1; then
  ( cd "$(dirname "${BACKUP_FILE}")" && sha256sum "$(basename "${BACKUP_FILE}")" ) >> "${LOG_FILE}"
  log "wrote sha256 checksum next to backup file"
fi

# --- Retention sweep ---------------------------------------------------------
# Keep the most recent $RETAIN_DAILY daily snapshots. Old ones beyond that
# are deleted. (Weekly retention is handled separately by docs/BACKUP.md
# using a different selector that mirrors to /backups/weekly/.)

DELETED=0
if [ -d "${BACKUP_DIR}" ]; then
  # List backups newest-first, skip the first RETAIN_DAILY, delete the rest.
  # `2>/dev/null || true` keeps set -e from blowing up if there are fewer
  # than RETAIN_DAILY+1 files.
  TO_DELETE="$(ls -1t "${BACKUP_DIR}"/school-*.db 2>/dev/null \
    | tail -n +"$((RETAIN_DAILY + 1))" || true)"
  if [ -n "${TO_DELETE}" ]; then
    while IFS= read -r f; do
      [ -z "${f}" ] && continue
      rm -f -- "${f}" "${f}-wal" "${f}-shm" "${f}.sha256" 2>/dev/null || true
      DELETED=$((DELETED + 1))
      log "retention: deleted old backup ${f}"
    done <<< "${TO_DELETE}"
  fi
fi

log "done. file=${BACKUP_FILE} size=$(stat -c %s "${BACKUP_FILE}" 2>/dev/null || echo unknown) bytes retention_deleted=${DELETED}"
exit 0
