/**
 * Fleet lease reaper — background loop that recovers and garbage-collects fleet
 * state. Each pass does two recovery scans plus a periodic GC sweep:
 *
 *  • reapExpiredLeases       — reclaim jobs whose lease TTL elapsed (§25 recovery:
 *                              fence the zombie via a leaseEpoch bump, return the
 *                              job to queued/blocked, preserve the checkpoint).
 *  • reclaimStaleFactoryLeases — reclaim held leases of factories that stopped
 *                              heartbeating, long before the (much longer) TTL.
 *  • sweepFleetGarbage       — (less frequently) delete finished leases and dead
 *                              factory docs, and — only when retention is set —
 *                              prune terminal jobs and their runs/events/artifacts.
 *
 * All of this is implemented as pure coordinator functions that do nothing until
 * something calls them; this module is that scheduler. A single process-wide timer
 * (the scans query across all products), started once at server boot and stopped
 * on graceful shutdown, mirroring the diagnostics trigger-job pattern.
 */

import { reapExpiredLeases, reclaimStaleFactoryLeases, sweepFleetGarbage } from './coordinator.js';

/** How often to scan for expired/stale leases. Well under the runner's 90s stale
 *  threshold so a dead factory's job is reclaimed promptly. */
const REAP_INTERVAL_MS = 30_000;

/** Run the (heavier, deletion) GC sweep at most this often, regardless of the
 *  faster recovery cadence above. */
const SWEEP_INTERVAL_MS = 60 * 60 * 1000; // hourly

/** Terminal-job retention from env: `FLEET_GC_RETENTION_DAYS`. 0/unset ⇒ jobs and
 *  their history are NEVER deleted (only ephemeral leases/dead-factory docs are). */
function jobRetentionMs(): number {
  const days = Number(process.env.FLEET_GC_RETENTION_DAYS ?? '0');
  return Number.isFinite(days) && days > 0 ? days * 24 * 60 * 60 * 1000 : 0;
}

let reaperInterval: ReturnType<typeof globalThis.setInterval> | null = null;
let lastSweepMs = 0;

interface ReaperLog {
  info: (...a: unknown[]) => void;
  warn?: (...a: unknown[]) => void;
}

/** Cumulative reaper/GC activity since the process started — operator telemetry. */
export interface ReaperStats {
  running: boolean;
  startedAt: string | null;
  lastReclaimAt: string | null;
  lastSweepAt: string | null;
  totals: {
    expiredReclaimed: number;
    staleReclaimed: number;
    leasesDeleted: number;
    factoriesDeleted: number;
    tokensDeleted: number;
    jobsDeleted: number;
    runsDeleted: number;
    eventsDeleted: number;
    artifactsDeleted: number;
  };
}

const stats: ReaperStats = {
  running: false,
  startedAt: null,
  lastReclaimAt: null,
  lastSweepAt: null,
  totals: {
    expiredReclaimed: 0,
    staleReclaimed: 0,
    leasesDeleted: 0,
    factoriesDeleted: 0,
    tokensDeleted: 0,
    jobsDeleted: 0,
    runsDeleted: 0,
    eventsDeleted: 0,
    artifactsDeleted: 0,
  },
};

/** Snapshot of cumulative reaper/GC activity (process-wide; surfaced in metrics). */
export function getReaperStats(): ReaperStats {
  return { ...stats, running: reaperInterval !== null, totals: { ...stats.totals } };
}

/** One reap pass: expiry reclaim + stale-factory reclaim, then a throttled GC
 *  sweep. Never throws — a transient datastore error is logged and the next tick
 *  retries (recovery must not crash the service). */
async function runReapPass(log?: ReaperLog): Promise<void> {
  const nowMs = Date.now();
  try {
    const expired = await reapExpiredLeases(new Date(nowMs).toISOString());
    const stale = await reclaimStaleFactoryLeases(nowMs);
    const reclaimed = [...expired.jobIds, ...stale.jobIds];
    if (reclaimed.length > 0) {
      stats.lastReclaimAt = new Date(nowMs).toISOString();
      stats.totals.expiredReclaimed += expired.reaped;
      stats.totals.staleReclaimed += stale.reaped;
      log?.info(
        `[fleet-reaper] reclaimed ${reclaimed.length} job(s) ` +
          `(${expired.reaped} expired-lease, ${stale.reaped} stale-factory): ${reclaimed.join(', ')}`
      );
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    (log?.warn ?? log?.info)?.(`[fleet-reaper] reclaim pass failed: ${msg}`);
  }

  // Throttled GC sweep — deletions, so run far less often than recovery.
  if (nowMs - lastSweepMs < SWEEP_INTERVAL_MS) return;
  lastSweepMs = nowMs;
  try {
    const gc = await sweepFleetGarbage({ now: nowMs, jobRetentionMs: jobRetentionMs() });
    stats.lastSweepAt = new Date(nowMs).toISOString();
    stats.totals.leasesDeleted += gc.leasesDeleted;
    stats.totals.factoriesDeleted += gc.factoriesDeleted;
    stats.totals.tokensDeleted += gc.tokensDeleted;
    stats.totals.jobsDeleted += gc.jobsDeleted;
    stats.totals.runsDeleted += gc.runsDeleted;
    stats.totals.eventsDeleted += gc.eventsDeleted;
    stats.totals.artifactsDeleted += gc.artifactsDeleted;
    const total =
      gc.leasesDeleted +
      gc.factoriesDeleted +
      gc.tokensDeleted +
      gc.jobsDeleted +
      gc.runsDeleted +
      gc.eventsDeleted;
    if (total > 0) {
      log?.info(
        `[fleet-reaper] gc: ${gc.leasesDeleted} lease(s), ${gc.factoriesDeleted} factory(ies), ` +
          `${gc.tokensDeleted} token(s), ${gc.jobsDeleted} job(s) + ${gc.runsDeleted} run(s)/` +
          `${gc.eventsDeleted} event(s)/${gc.artifactsDeleted} artifact(s)`
      );
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    (log?.warn ?? log?.info)?.(`[fleet-reaper] gc sweep failed: ${msg}`);
  }
}

/** Start the lease reaper. Idempotent: a second call while running is a no-op. */
export function startLeaseReaper(log?: ReaperLog): void {
  if (reaperInterval) return;
  // Let the first tick run the GC sweep too (don't skip it for an hour on boot).
  lastSweepMs = 0;
  stats.startedAt = new Date().toISOString();
  log?.info(`[fleet-reaper] starting (reclaim every ${REAP_INTERVAL_MS / 1000}s)`);
  // Run once on boot so a restart promptly recovers jobs orphaned while down.
  void runReapPass(log);
  reaperInterval = globalThis.setInterval(() => {
    void runReapPass(log);
  }, REAP_INTERVAL_MS);
}

/** Stop the lease reaper (graceful shutdown). Idempotent. */
export function stopLeaseReaper(): void {
  if (reaperInterval) {
    globalThis.clearInterval(reaperInterval);
    reaperInterval = null;
  }
}

/** Test/diagnostics helper: is the reaper currently scheduled? */
export function isLeaseReaperRunning(): boolean {
  return reaperInterval !== null;
}
