/**
 * Internal authentication middleware (Session 10).
 *
 * Protects internal-only endpoints — the grading pipeline, the
 * resolution poll-back, the corrections sweep — that are called by
 * pollers, n8n workflows, and cron jobs but NEVER by browser users.
 * Uses a shared secret in `VYNDR_INTERNAL_KEY` checked against the
 * request header.
 *
 * Deliberately separate from `requireAuth` (Supabase JWT). Internal
 * callers don't have user sessions.
 *
 * Header compatibility:
 *   `x-internal-key`            — Session 10 short form (n8n + new callers)
 *   `X-VYNDR-Internal-Key`      — legacy form, kept for backwards
 *                                  compatibility with the poller and
 *                                  the existing test suite. The
 *                                  middleware accepts either; callers
 *                                  should prefer the short form.
 *
 * Options:
 *   loopbackOnly (default false) — additionally enforce that the
 *     request originated from 127.0.0.1 / ::1. Use for endpoints that
 *     should ONLY be reachable from co-located processes (the poller
 *     pulling box scores). Endpoints called from n8n or other
 *     containers MUST omit this option.
 *
 * Responses:
 *   503 — VYNDR_INTERNAL_KEY env var not set (misconfigured)
 *   401 — header missing OR key mismatch
 *   403 — loopbackOnly=true and the request came from off-host
 */

const crypto = require('crypto');

const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);

// Timing-safe string compare. crypto.timingSafeEqual throws on
// length mismatch, so we pad to a fixed length first to keep the
// comparison constant-time regardless of input length.
function timingSafeStringEqual(a, b) {
  if (typeof a !== 'string' || typeof b !== 'string') return false;
  const len = Math.max(a.length, b.length);
  // Pad with a NUL byte (will never appear in a real key) so the
  // shorter side still has the same length.
  const ba = Buffer.alloc(len, 0);
  const bb = Buffer.alloc(len, 0);
  ba.write(a, 'utf8');
  bb.write(b, 'utf8');
  // Even on length mismatch the compare runs to completion; we just
  // also require lengths match to count as equal.
  const equal = crypto.timingSafeEqual(ba, bb);
  return equal && a.length === b.length;
}

function readHeader(req) {
  // Express normalizes header names to lowercase. Try the short form
  // first (the documented one), then the legacy long form.
  return req.get('x-internal-key') || req.get('X-VYNDR-Internal-Key') || null;
}

function requireInternalAuth(options = {}) {
  const loopbackOnly = !!options.loopbackOnly;
  return function internalAuthMiddleware(req, res, next) {
    const expected = process.env.VYNDR_INTERNAL_KEY;
    if (!expected) {
      // Refuse to serve when the secret is unset — better than
      // accidentally exposing the endpoint with a default empty
      // value. n8n uses this to distinguish "misconfigured" (503)
      // from "wrong key" (401).
      return res.status(503).json({ error: 'Internal auth not configured' });
    }

    const provided = readHeader(req);
    if (!provided || !timingSafeStringEqual(provided, expected)) {
      return res.status(401).json({ error: 'Invalid internal key' });
    }

    if (loopbackOnly) {
      const remoteIp = req.ip || req.socket?.remoteAddress;
      if (!LOOPBACK_IPS.has(remoteIp)) {
        return res.status(403).json({ error: 'Origin not permitted' });
      }
    }

    return next();
  };
}

module.exports = {
  requireInternalAuth,
  __internals: {
    LOOPBACK_IPS,
    timingSafeStringEqual,
    readHeader,
  },
};
