/**
 * ScheduleAutoPublish — auto-publishes schedules at their start_time.
 *
 * What it does
 *   On a periodic tick, finds every exam_schedules row that is
 *     - is_active = 0
 *     - is_deleted = 0
 *     - start_time <= now()  (i.e. the exam should already be live)
 *     - has an exam_group_id
 *   and runs the same publish logic as the manual endpoint
 *   (POST /api/exams/schedules/:id/publish) — flips is_active=1,
 *   approves the parent group, writes an exam_review_audit row.
 *
 * Why not a real cron lib
 *   This is a single-process in-memory ticker. It does NOT survive
 *   restarts cleanly (misses any due schedules while down — those
 *   are picked up on the next tick after restart, so OK in practice
 *   for an exam-management tool). When the school moves to a
 *   multi-process deploy, swap for a real scheduler (pg_cron on
 *   Supabase, BullMQ, etc.). The DB schema and the publish logic
 *   don't change.
 *
 * Env knobs
 *   EXAM_AUTO_PUBLISH=0      disable entirely (default: enabled)
 *   EXAM_AUTO_PUBLISH_INTERVAL_MS=60000  tick interval (default 60s)
 *   EXAM_AUTO_PUBLISH_LEAD_MS=0   publish N ms before start_time
 *                                  (default 0 — at start_time exactly)
 *
 * Idempotency
 *   start() is safe to call multiple times. The interval handle is
 *   cached on the module; subsequent calls are no-ops.
 *
 * Test mode
 *   The ticker writes to a 'pending' sync_status on every tick, and
 *   a one-line helper `runOnce()` is exported so unit tests can
 *   trigger a single sweep without waiting for the timer.
 */

const path = require('path');
const { v4: uuidv4 } = require('uuid');

let tickHandle = null;
let tickIntervalMs = 60_000;

const isEnabled = () => process.env.EXAM_AUTO_PUBLISH !== '0';

const openDb = () => {
  const Database = require('better-sqlite3');
  const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/school.db');
  const db = new Database(dbPath);
  db.pragma('foreign_keys = ON');
  return db;
};

// Mirror of the publish SQL in exams.controller.js. Kept in sync by
// hand — the alternative is exposing a controller-internal helper,
// which is fine but cross-module for what's a 30-line function.
const publishOne = (db, schedule, systemActorId) => {
  const approveGroup = db.prepare(`
    UPDATE exam_groups
    SET status = 'approved',
        rejection_reason = NULL,
        reviewed_at = CURRENT_TIMESTAMP,
        reviewed_by = ?,
        sync_status = 'pending',
        updated_at = CURRENT_TIMESTAMP
    WHERE id = ?
  `);
  const publishSchedule = db.prepare(`
    UPDATE exam_schedules
    SET is_active = 1,
        sync_status = 'pending',
        updated_at = CURRENT_TIMESTAMP
    WHERE id = ?
  `);
  const insertAudit = db.prepare(`
    INSERT INTO exam_review_audit
      (uid, exam_group_id, schedule_id, action, actor_id, reason, sync_status)
    VALUES (?, ?, ?, 'publish', ?, ?, 'pending')
  `);

  publishSchedule.run(schedule.id);
  if (schedule.exam_group_id) {
    approveGroup.run(systemActorId, schedule.exam_group_id);
  }
  insertAudit.run(uuidv4(), schedule.exam_group_id, schedule.id, systemActorId, 'auto-publish');
  return { scheduleId: schedule.id, groupId: schedule.exam_group_id };
};

// One pass over the due-schedules table. Returns an array of what
// was published so the caller (or test) can assert on it.
const runOnce = (db) => {
  if (!isEnabled()) return [];
  const leadMs = parseInt(process.env.EXAM_AUTO_PUBLISH_LEAD_MS || '0', 10) || 0;
  // Use the DB's own clock via datetime('now') so we don't depend on
  // the host clock being in sync. SQLite's 'milliseconds' modifier
  // is not portable, so for non-zero lead we use seconds.
  const nowExpr = leadMs > 0
    ? `datetime('now', '-' || ${Math.ceil(leadMs / 1000)} || ' seconds')`
    : `datetime('now')`;
  const due = db.prepare(`
    SELECT id, exam_group_id, is_active, start_time
    FROM exam_schedules
    WHERE is_deleted = 0
      AND is_active = 0
      AND exam_group_id IS NOT NULL
      AND datetime(start_time) <= ${nowExpr}
  `).all();

  // System actor: a sentinel user 'system' is preferred but the
  // schema requires a real users.id. Use the first admin user we
  // find. (The audit row records actor_id for traceability; if no
  // admin exists the function no-ops safely.)
  const admin = db.prepare(
    "SELECT id FROM users WHERE role IN ('school_admin','systems_admin') AND is_deleted = 0 ORDER BY id LIMIT 1"
  ).get();
  if (!admin) {
    if (due.length > 0) {
      console.warn('[auto-publish] no admin user found; skipping', due.length, 'due schedules');
    }
    return [];
  }

  const published = [];
  for (const s of due) {
    try {
      const result = publishOne(db, s, admin.id);
      published.push(result);
      console.log(`[auto-publish] schedule ${s.id} (group ${s.exam_group_id}) -> is_active=1`);
    } catch (e) {
      console.warn(`[auto-publish] failed for schedule ${s.id}:`, e.message);
    }
  }
  return published;
};

const start = () => {
  if (tickHandle) return; // idempotent
  if (!isEnabled()) {
    console.log('[auto-publish] disabled (EXAM_AUTO_PUBLISH=0)');
    return;
  }
  tickIntervalMs = parseInt(process.env.EXAM_AUTO_PUBLISH_INTERVAL_MS || '60000', 10) || 60000;
  const db = openDb();
  // First tick: after one full interval. Don't sweep on boot — that
  // would race with manual admin actions during a deploy.
  tickHandle = setInterval(() => {
    try { runOnce(db); } catch (e) {
      console.warn('[auto-publish] tick error:', e.message);
    }
  }, tickIntervalMs);
  // Don't keep the event loop alive solely for this timer. The HTTP
  // server keeps the process running; this timer just rides along.
  if (tickHandle.unref) tickHandle.unref();
  console.log(`[auto-publish] started (interval ${tickIntervalMs}ms)`);
};

const stop = () => {
  if (tickHandle) {
    clearInterval(tickHandle);
    tickHandle = null;
    console.log('[auto-publish] stopped');
  }
};

module.exports = { start, stop, runOnce };
