#!/usr/bin/env node
/**
 * africa-alert-backup.js
 * ----------------------
 * Cross-platform Node fallback for `africa-alert-backup.sh`.
 *
 * Why this exists:
 *   The shell script in this directory uses the `sqlite3` CLI's `.backup`
 *   command, which is reliable on Linux but is not installed by default on
 *   Windows. This Node script uses `better-sqlite3`'s `.backup(path)` API
 *   — the same primitive under the hood, available wherever the project's
 *   server dependencies are installed.
 *
 * Usage:
 *   # From the repo root (where node_modules/server is the project's server):
 *   DB_PATH=server/data/school.db \
 *   BACKUP_DIR=./backups \
 *   node scripts/backup/africa-alert-backup.js
 *
 * Environment variables (all optional, all honoured by the shell script too):
 *   DB_PATH       Live SQLite database file. Default: server/data/school.db
 *                 relative to the repo root (two levels up from this file).
 *   BACKUP_DIR    Directory to write the timestamped .db snapshot.
 *                 Default: ./backups relative to the repo root.
 *   LOG_DIR       Directory for the human-readable log file.
 *                 Default: <repo root>/logs.
 *   RETAIN_DAILY  Number of most-recent daily snapshots to keep. Default: 30.
 *
 * Exit codes (matched to the shell script for parity):
 *   0  backup succeeded
 *   1  configuration error (missing tool, unreadable DB, etc.)
 *   2  backup itself failed
 *   3  retention sweep failed (the snapshot itself is still on disk)
 *
 * This script is safe to run while the server is up: SQLite's online
 * backup API flushes the WAL before copying and never produces a torn
 * snapshot, even if writers are active.
 */

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

// Resolve repo root (scripts/backup/<this>.js -> two levels up).
const SCRIPT_DIR = __dirname;
const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '..', '..');

const DB_PATH    = process.env.DB_PATH    || path.join(PROJECT_ROOT, 'server', 'data', 'school.db');
const BACKUP_DIR = process.env.BACKUP_DIR || path.join(PROJECT_ROOT, 'backups');
const LOG_DIR    = process.env.LOG_DIR    || path.join(PROJECT_ROOT, 'logs');
const RETAIN     = parseInt(process.env.RETAIN_DAILY || '30', 10);

const TIMESTAMP = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
const BACKUP_FILE = path.join(BACKUP_DIR, `school-${TIMESTAMP}.db`);
const LOG_FILE    = path.join(LOG_DIR, 'backup.log');

function log(line) {
  const stamped = `[${new Date().toISOString()}] [backup-js] ${line}\n`;
  fs.mkdirSync(LOG_DIR, { recursive: true });
  fs.appendFileSync(LOG_FILE, stamped);
  process.stderr.write(stamped);
}

function fail(code, msg) {
  log(`ERROR: ${msg}`);
  process.exit(code);
}

async function main() {
  if (!fs.existsSync(DB_PATH)) {
    fail(1, `DB_PATH does not exist or is not a regular file: ${DB_PATH}`);
  }
  try {
    fs.accessSync(DB_PATH, fs.constants.R_OK);
  } catch {
    fail(1, `DB_PATH is not readable: ${DB_PATH}`);
  }

  fs.mkdirSync(BACKUP_DIR, { recursive: true });

  log(`starting backup of ${DB_PATH} -> ${BACKUP_FILE}`);

  let Database;
  try {
    // Resolve better-sqlite3 against this script's dir first, then fall
    // back to <repo>/server/node_modules. The first path makes the script
    // self-contained; the second lets us share the server's installed
    // native bindings when invoked from outside server/.
    const Module = require('module');
    const candidates = [
      path.join(SCRIPT_DIR, 'node_modules', 'better-sqlite3'),
      path.join(PROJECT_ROOT, 'node_modules', 'better-sqlite3'),
      path.join(PROJECT_ROOT, 'server', 'node_modules', 'better-sqlite3'),
    ];
    let resolved = null;
    for (const c of candidates) {
      try { resolved = require.resolve(c); break; } catch {}
    }
    if (!resolved) throw new Error('not found in: ' + candidates.join(', '));
    Database = require(resolved);
  } catch (e) {
    fail(1, `better-sqlite3 not available: ${e.message}. Run 'npm install' inside server/ first.`);
  }

  let db;
  try {
    db = new Database(DB_PATH, { readonly: true, fileMustExist: true });
  } catch (e) {
    fail(2, `could not open ${DB_PATH}: ${e.message}`);
  }

  try {
    // SQLite online backup API: flushes the WAL, then copies page-by-page.
    // No torn snapshots even if writers are active.
    await db.backup(BACKUP_FILE);
  } catch (e) {
    try { db.close(); } catch {}
    fail(2, `sqlite backup failed: ${e.message}`);
  }
  db.close();

  if (!fs.existsSync(BACKUP_FILE) || fs.statSync(BACKUP_FILE).size === 0) {
    fail(2, `backup file was not created or is empty: ${BACKUP_FILE}`);
  }

  // Copy WAL/SHM sidecars if present, so the snapshot is self-contained.
  for (const ext of ['wal', 'shm']) {
    const side = `${DB_PATH}-${ext}`;
    if (fs.existsSync(side)) {
      fs.copyFileSync(side, `${BACKUP_FILE}-${ext}`);
      log(`copied sidecar ${side} -> ${BACKUP_FILE}-${ext}`);
    }
  }

  // sha256 for off-host transfer verification.
  try {
    const buf = fs.readFileSync(BACKUP_FILE);
    const hash = crypto.createHash('sha256').update(buf).digest('hex');
    fs.writeFileSync(`${BACKUP_FILE}.sha256`, `${hash}  ${path.basename(BACKUP_FILE)}\n`);
    log(`wrote sha256 checksum: ${hash}`);
  } catch (e) {
    log(`WARN: could not write sha256: ${e.message}`);
  }

  // Retention sweep: keep newest RETAIN snapshots.
  let deleted = 0;
  try {
    const files = fs.readdirSync(BACKUP_DIR)
      .filter(f => f.startsWith('school-') && f.endsWith('.db'))
      .map(f => ({ f, mtime: fs.statSync(path.join(BACKUP_DIR, f)).mtimeMs }))
      .sort((a, b) => b.mtime - a.mtime);

    const stale = files.slice(RETAIN);
    for (const entry of stale) {
      for (const ext of ['', '-wal', '-shm', '.sha256']) {
        const victim = path.join(BACKUP_DIR, entry.f + ext);
        if (fs.existsSync(victim)) {
          try { fs.unlinkSync(victim); } catch {}
        }
      }
      deleted++;
      log(`retention: deleted old backup ${entry.f}`);
    }
  } catch (e) {
    log(`ERROR: retention sweep failed: ${e.message}`);
    process.exit(3);
  }

  const size = fs.statSync(BACKUP_FILE).size;
  log(`done. file=${BACKUP_FILE} size=${size} bytes retention_deleted=${deleted}`);
  process.exit(0);
}

main().catch((e) => {
  fail(2, `unhandled error: ${e && e.message ? e.message : String(e)}`);
});