// In-process tick token for unauthenticated loopback routes.
//
// Problem: /api/cron/tick and /api/heartbeat/worker are dispatched by the
// in-process auto-tick in lib/db.js via HTTP fetch (Next.js dev bundler
// makes dynamic await import() inside setInterval unreliable — see the
// note in lib/db.js::startInProcDaemons). The worker route is gated by
// requireSession; the cron route is intended to be loopback-only.
// Authenticated dashboard users don't issue these calls; the daemon does.
//
// Without a way for the daemon to identify itself, the daemon's calls
// would either 401 (heartbeat) or be reachable by any caller (cron).
//
// Resolution: a shared in-process token.
//   - The daemon sends X-Inproc-Token: <token> on every tick.
//   - The route handlers call verifyInprocToken(req); on match they skip
//     the session check.
//   - The daemon (lib/db.js::startInProcDaemons) and the routes (under
//     app/api) run in the same Node.js process, so the token can be a
//     per-process in-memory random string.
//   - Operators may override with MADARX_INPROC_TICK_TOKEN for diagnostic
//     scripts or split-process deployments.
//
// Defense properties:
//   - The token never leaves the local process. A remote attacker cannot
//     mint or guess it (cryptographically random, 32 bytes / 256 bits).
//   - On process restart, the in-memory token rotates. Any leaked token
//     becomes useless. (The env-override case persists across restarts —
//     operators should rotate by unsetting the env var.)
//   - This is in addition to keeping the routes loopback-only at the
//     network boundary (firewall, reverse-proxy rule). It is a depth
//     layer, not the sole defense.

import { randomBytes, timingSafeEqual } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { DATA_DIR } from './paths.js';

const HEADER_NAME = 'x-inproc-token';

// Token resolution must be stable ACROSS PROCESSES for one install, not just
// per-process. The poke loop (lib/db.js cron/heartbeat) and the route handlers
// can run in DIFFERENT processes:
//   - the machine-wide daemon lock (lib/db.js) lets any process that opened the
//     DB own the loop — historically the WhatsApp daemon, the hermes job runner,
//     or an orphaned web server from a prior `npm i -g` upgrade;
//   - that owner POSTs the web tier's /api/heartbeat/worker over loopback.
// A per-process random token (or even a globalThis-pinned one) means the poker
// and the verifier mint different tokens → every tick 401s. So the token is
// persisted to a 0600 file under DATA_DIR and shared by every process for this
// install. globalThis still caches it per process to avoid re-reading the file.
//
// Precedence: MADARX_INPROC_TICK_TOKEN env (operator override, split-process
// deploys) → globalThis cache → persisted DATA_DIR file → freshly generated +
// persisted. Threat model matches MADARX_MASTER_KEY / the auth recovery token:
// both are 0600 files in DATA_DIR; a reader of DATA_DIR already holds the vault.
// The routes stay loopback-only at the network boundary regardless.
const TOKEN_KEY = '__madarxInprocToken';
const TOKEN_FILE = path.join(DATA_DIR, '.inproc-token');

function generateToken() {
  return randomBytes(32).toString('base64url');
}

// Read the install-shared token from disk, or create it atomically. O_EXCL on
// create means concurrent processes converge on the first writer's token instead
// of clobbering each other. Falls back to a process-local token if DATA_DIR is
// unwritable (degrades to the old per-process behavior — no worse than before).
function loadOrCreatePersistentToken() {
  try {
    const existing = fs.readFileSync(TOKEN_FILE, 'utf8').trim();
    if (existing) return existing;
  } catch { /* not created yet */ }

  const fresh = generateToken();
  try {
    fs.mkdirSync(DATA_DIR, { recursive: true });
    const fd = fs.openSync(TOKEN_FILE, 'wx', 0o600); // O_EXCL — fail if it exists
    try { fs.writeSync(fd, `${fresh}\n`); } finally { fs.closeSync(fd); }
    try { fs.chmodSync(TOKEN_FILE, 0o600); } catch { /* best-effort */ }
    return fresh;
  } catch {
    // Lost the create race (EEXIST) or FS unwritable — prefer the persisted value.
    try {
      const again = fs.readFileSync(TOKEN_FILE, 'utf8').trim();
      if (again) return again;
    } catch { /* unwritable */ }
    return fresh; // last resort: process-local only
  }
}

/**
 * Return the in-process tick token. Resolves from the env override, the
 * per-process globalThis cache, or the install-shared DATA_DIR file (creating
 * it on first use). Stable across every process for this install.
 */
export function getInprocToken() {
  const envValue = process.env.MADARX_INPROC_TICK_TOKEN;
  if (envValue && envValue.trim().length > 0) return envValue.trim();
  if (globalThis[TOKEN_KEY]) return globalThis[TOKEN_KEY];
  globalThis[TOKEN_KEY] = loadOrCreatePersistentToken();
  return globalThis[TOKEN_KEY];
}

/**
 * Constant-time comparison of the request's `X-Inproc-Token` header against
 * the resolved token. Returns true on match.
 */
export function verifyInprocToken(req) {
  if (!req || typeof req.headers?.get !== 'function') return false;
  const supplied = req.headers.get(HEADER_NAME);
  if (!supplied) return false;
  const expected = getInprocToken();
  const a = Buffer.from(supplied);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false;
  try {
    return timingSafeEqual(a, b);
  } catch {
    return false;
  }
}

export const INPROC_TOKEN_HEADER = HEADER_NAME;

/** Reset the cached + persisted token (used in tests). */
export function resetInprocToken() {
  delete globalThis[TOKEN_KEY];
  try { fs.rmSync(TOKEN_FILE, { force: true }); } catch { /* best-effort */ }
}
