// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * NERF REST API Server — zero-dependency SaaS server.
 *
 * Built on node:http. Provides authenticated, rate-limited endpoints
 * that expose NERF's scanning, memory, leaderboard, and workflow
 * capabilities over HTTP/JSON.
 */

import { createServer as httpCreateServer } from 'node:http';
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { URL } from 'node:url';
import { isIP } from 'node:net';
import { createWriteStream, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';

// Import memory sanitizer for content sanitization on store.
// Falls back to inline patterns if module unavailable (e.g., standalone API deployment).
let _sanitizeContent = null;
try {
  const mod = await import('../memory/sanitizer.js');
  _sanitizeContent = mod.sanitizeContent;
} catch {
  // Fallback set in sanitiseMemoryContent below
}

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

/** Server-level configuration with safe defaults. */
export class APIConfig {
  constructor(opts = {}) {
    this.host = opts.host ?? '127.0.0.1';
    this.port = opts.port ?? 8443;
    this.apiVersion = opts.apiVersion ?? 'v1';
    this.rateLimitRpm = opts.rateLimitRpm ?? 60;
    this.maxRequestSize = opts.maxRequestSize ?? 1_048_576; // 1 MB
    this.requireAuth = opts.requireAuth ?? true;
    this.auditLogPath = opts.auditLogPath ?? null; // JSONL audit log file path

    // CORS origins must be explicit — '*' is rejected to prevent cross-origin
    // credential exposure when requireAuth is true (AUDIT-CORS-001).
    const origins = opts.corsOrigins ?? ['http://localhost:8443', 'http://127.0.0.1:8443'];
    if (Array.isArray(origins) && origins.includes('*')) {
      throw new Error(
        'APIConfig: corsOrigins must not contain \'*\'. ' +
        'Wildcard CORS combined with token auth allows any origin to make ' +
        'credentialed requests. Specify explicit allowed origins instead.',
      );
    }
    this.corsOrigins = origins;

    // trustedProxies: list of socket-level IPs (e.g. '127.0.0.1') whose
    // X-Forwarded-For header is trusted for client-IP resolution.
    // Leave empty (default) when the server is not behind a reverse proxy.
    this.trustedProxies = opts.trustedProxies ?? [];
  }
}

// ---------------------------------------------------------------------------
// Response envelope
// ---------------------------------------------------------------------------

/** Uniform JSON response wrapper. */
export class APIResponse {
  constructor({ status = 200, data = null, error = null, meta = null } = {}) {
    this.status = status;
    this.data = data;
    this.error = error;
    this.meta = meta;
  }

  toJson() {
    const payload = { status: this.status };
    if (this.data !== null) payload.data = this.data;
    if (this.error !== null) payload.error = this.error;
    if (this.meta !== null) payload.meta = this.meta;
    return JSON.stringify(payload);
  }
}

// ---------------------------------------------------------------------------
// Rate limiter — sliding-window per client
// ---------------------------------------------------------------------------

/** Sliding-window rate limiter. */
export class RateLimiter {
  constructor(rpm = 60) {
    this._rpm = rpm;
    this._window = 60_000; // ms
    /** @type {Map<string, number[]>} */
    this._requests = new Map();
  }

  /**
   * Return true if the request is allowed.
   * @param {string} clientId
   * @returns {boolean}
   */
  check(clientId) {
    const now = Date.now();
    const cutoff = now - this._window;
    let timestamps = this._requests.get(clientId) || [];
    timestamps = timestamps.filter((t) => t > cutoff);
    if (timestamps.length >= this._rpm) {
      this._requests.set(clientId, timestamps);
      return false;
    }
    timestamps.push(now);
    this._requests.set(clientId, timestamps);
    return true;
  }

  /** Get current state for diagnostics. */
  getState() {
    return {
      trackedClients: this._requests.size,
      rpm: this._rpm,
    };
  }
}

// ---------------------------------------------------------------------------
// Auth — HMAC-SHA256 bearer tokens
// ---------------------------------------------------------------------------

/**
 * HMAC-SHA256 token auth.
 * Tokens: `<client_id>:<random_hex>:<hmac_hex>`
 */
export class AuthHandler {
  constructor(host = '127.0.0.1') {
    this._secret = process.env.NERF_API_SECRET || randomBytes(32).toString('hex');
    /** @type {Map<string, string[]>} */
    this._scopes = new Map();

    // Warn when using an ephemeral secret on a non-loopback interface.
    // Tokens generated before a restart will be invalid after restart, and
    // operators may not notice until auth failures appear in production.
    if (!process.env.NERF_API_SECRET) {
      const loopback = ['127.0.0.1', '::1', 'localhost'];
      if (!loopback.includes(host)) {
        process.stderr.write(
          '[nerf-api] WARNING: NERF_API_SECRET is not set. ' +
          'An ephemeral secret is being used — all tokens will be invalidated on restart. ' +
          'Set NERF_API_SECRET in your environment to persist tokens across restarts.\n',
        );
      }
    }
  }

  /**
   * Create a signed bearer token.
   * @param {string} clientId
   * @param {string[]} [scopes]
   * @returns {string}
   */
  generateToken(clientId, scopes = ['read', 'write']) {
    this._scopes.set(clientId, scopes);
    const nonce = randomBytes(16).toString('hex');
    const payload = `${clientId}:${nonce}`;
    const sig = createHmac('sha256', this._secret).update(payload).digest('hex');
    return `${payload}:${sig}`;
  }

  /**
   * Validate a bearer token.
   * @param {string} token
   * @returns {{ clientId: string, scopes: string[] } | null}
   */
  validateToken(token) {
    const parts = token.split(':');
    if (parts.length !== 3) return null;
    const [clientId, nonce, sig] = parts;
    const expected = createHmac('sha256', this._secret)
      .update(`${clientId}:${nonce}`)
      .digest('hex');

    // Length pre-check to avoid timing oracle on different-length strings
    if (sig.length !== expected.length) return null;
    const sigBuf = Buffer.from(sig, 'utf8');
    const expBuf = Buffer.from(expected, 'utf8');
    if (!timingSafeEqual(sigBuf, expBuf)) return null;

    const scopes = this._scopes.get(clientId) || ['read'];
    return { clientId, scopes };
  }
}

// ---------------------------------------------------------------------------
// SSRF validation
// ---------------------------------------------------------------------------

/**
 * Validate scan target to prevent SSRF against internal networks.
 * @param {string} target
 * @returns {boolean}
 */

/**
 * Strip known prompt injection patterns from memory content.
 * Not a complete defence (prompt injection is an open problem), but catches
 * the most obvious system-override and instruction-hijacking patterns.
 * @param {string} content
 * @returns {string}
 */
/**
 * Strip known prompt injection patterns from memory content.
 * Delegates to the dedicated sanitizer module for comprehensive coverage.
 * Not a complete defence (prompt injection is an open problem), but catches
 * the most common patterns as defense-in-depth. The primary defense is
 * structural isolation at the retrieval/prompt-assembly layer.
 * @param {string} content
 * @returns {string}
 */
export function sanitiseMemoryContent(content) {
  if (typeof content !== 'string') return '';
  if (_sanitizeContent) {
    const { sanitized } = _sanitizeContent(content);
    return sanitized;
  }
  // Fallback: basic patterns if sanitizer module was not available at import time
  const patterns = [
    /\[SYSTEM\s*(?:DIRECTIVE|OVERRIDE|INSTRUCTION)\]/gi,
    /IGNORE\s+(?:ALL\s+)?PREVIOUS\s+INSTRUCTIONS/gi,
    /YOU\s+ARE\s+NOW\s+(?:A|AN)\s+/gi,
    /(?:^|\n)\s*CRITICAL\s+OVERRIDE\s*:/gim,
    /THIS\s+(?:DIRECTIVE|INSTRUCTION)\s+SUPERSEDES/gi,
    /MANDATORY\s+COMPLIANCE\s+DIRECTIVE/gi,
  ];
  let sanitised = content;
  for (const pattern of patterns) {
    sanitised = sanitised.replace(pattern, '[REDACTED-INJECTION]');
  }
  return sanitised;
}

export function validateScanTarget(target) {
  if (!target || typeof target !== 'string' || target.length > 2048) return false;

  let host;
  try {
    const parsed = new URL(target.includes('://') ? target : `http://${target}`);
    host = parsed.hostname;
  } catch {
    return false;
  }
  if (!host) return false;

  return _validateHost(host);
}

/**
 * Async version that resolves DNS before validating.
 * Blocks DNS rebinding by checking the resolved IP, not just the hostname.
 * @param {string} target
 * @returns {Promise<boolean>}
 */
export async function validateScanTargetAsync(target) {
  if (!target || typeof target !== 'string' || target.length > 2048) return false;

  let host;
  try {
    const parsed = new URL(target.includes('://') ? target : `http://${target}`);
    host = parsed.hostname;
  } catch {
    return false;
  }
  if (!host) return false;

  // First check the hostname itself
  if (!_validateHost(host)) return false;

  // If it's already an IP, no DNS resolution needed
  if (isIP(host)) return true;

  // Resolve DNS and validate the resolved IP
  try {
    const { resolve4, resolve6 } = await import('node:dns/promises');
    const [v4Addrs, v6Addrs] = await Promise.allSettled([
      resolve4(host).catch(() => []),
      resolve6(host).catch(() => []),
    ]);
    const addrs = [
      ...(v4Addrs.status === 'fulfilled' ? v4Addrs.value : []),
      ...(v6Addrs.status === 'fulfilled' ? v6Addrs.value : []),
    ];
    if (addrs.length === 0) return false; // Unresolvable host
    // Every resolved IP must pass validation
    for (const addr of addrs) {
      if (!_validateHost(addr)) return false;
    }
    return true;
  } catch {
    return false; // DNS resolution failed — block it
  }
}

/** Validate a host string (IP or hostname) against blocklists. */
function _validateHost(host) {
  if (!host) return false;

  const lh = host.toLowerCase();

  // Block known metadata and loopback hostnames
  const blockedHosts = new Set([
    'metadata.google.internal',
    'metadata.google',
    'metadata.aws.internal',
    '169.254.169.254',
    'fd00:ec2::254',
    'localhost',
    'localhost.localdomain',
    '0.0.0.0',
    '0',
  ]);
  if (blockedHosts.has(lh)) return false;

  // Strip IPv6 brackets and IPv4-mapped IPv6 prefix
  let normalised = host;
  if (normalised.startsWith('[') && normalised.endsWith(']')) {
    normalised = normalised.slice(1, -1);
  }
  // Reject IPv4-mapped IPv6 — dotted form (::ffff:1.2.3.4)
  const v4Mapped = normalised.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
  if (v4Mapped) {
    // Recursively validate the embedded IPv4 address
    return validateScanTarget(`http://${v4Mapped[1]}/`);
  }
  // Reject IPv4-mapped IPv6 — hex form (::ffff:7f00:1) as URL parser converts to this
  const v4MappedHex = normalised.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
  if (v4MappedHex) {
    const hi = parseInt(v4MappedHex[1], 16);
    const lo = parseInt(v4MappedHex[2], 16);
    const ip = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
    return validateScanTarget(`http://${ip}/`);
  }

  if (isIP(host)) {
    // Block entire loopback range 127.0.0.0/8
    if (host.startsWith('127.')) return false;

    // Block RFC 1918 private ranges
    if (host.startsWith('10.')) return false;
    if (host.startsWith('192.168.')) return false;
    if (host.startsWith('172.')) {
      const second = parseInt(host.split('.')[1], 10);
      if (second >= 16 && second <= 31) return false;
    }

    // Block link-local
    if (host.startsWith('169.254.')) return false;

    // Block IPv6 loopback, link-local, unique local
    if (host === '::1' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) return false;
  }

  // Reject numeric-looking hosts that might be encoded IPs (decimal, octal, hex)
  if (/^[0-9x.]+$/i.test(host) && !isIP(host)) return false;
  // Reject octal-notation IPs (e.g. 0177.0.0.1)
  if (/^0\d/.test(host)) return false;

  return true;
}

// ---------------------------------------------------------------------------
// Main server
// ---------------------------------------------------------------------------

/**
 * NERF SaaS REST API server.
 * @param {APIConfig} [config]
 * @returns {{ server: import('node:http').Server, config: APIConfig, auth: AuthHandler, rateLimiter: RateLimiter, start: () => Promise<number>, stop: () => Promise<void> }}
 */
export function createAPIServer(config) {
  config = config instanceof APIConfig ? config : new APIConfig(config);
  const auth = new AuthHandler(config.host);
  const rateLimiter = new RateLimiter(config.rateLimitRpm);
  const startTime = Date.now();
  let requestCount = 0;
  const v = config.apiVersion;

  // Audit logger — writes JSONL to file if configured
  let _auditStream = null;
  if (config.auditLogPath) {
    mkdirSync(dirname(config.auditLogPath), { recursive: true });
    _auditStream = createWriteStream(config.auditLogPath, { flags: 'a' });
  }
  function auditLog(entry) {
    if (!_auditStream) return;
    _auditStream.write(JSON.stringify(entry) + '\n');
  }

  // -- Route table ----------------------------------------------------------

  const routes = [
    { method: 'GET', path: `/${v}/health`, handler: handleHealth, authRequired: false, scopes: [] },
    { method: 'GET', path: `/${v}/stats`, handler: handleStats, authRequired: true, scopes: [] },
    { method: 'POST', path: `/${v}/scan`, handler: handleScan, authRequired: true, scopes: ['write'] },
    { method: 'POST', path: `/${v}/diff`, handler: handleDiff, authRequired: true, scopes: ['write'] },
    { method: 'POST', path: `/${v}/secrets`, handler: handleSecrets, authRequired: true, scopes: ['write'] },
    { method: 'GET', path: `/${v}/skills`, handler: handleSkillsList, authRequired: true, scopes: [] },
    { method: 'GET', path: `/${v}/skills/search`, handler: handleSkillsSearch, authRequired: true, scopes: [] },
    { method: 'POST', path: `/${v}/memory/store`, handler: handleMemoryStore, authRequired: true, scopes: ['write'] },
    { method: 'POST', path: `/${v}/memory/search`, handler: handleMemorySearch, authRequired: true, scopes: [] },
    { method: 'GET', path: `/${v}/memory/stats`, handler: handleMemoryStats, authRequired: true, scopes: [] },
    { method: 'POST', path: `/${v}/score`, handler: handleScore, authRequired: true, scopes: ['write'] },
    { method: 'GET', path: `/${v}/leaderboard`, handler: handleLeaderboard, authRequired: true, scopes: [] },
    { method: 'GET', path: `/${v}/leaderboard/dashboard`, handler: handleLeaderboardDashboard, authRequired: true, scopes: [] },
    { method: 'POST', path: `/${v}/workflow`, handler: handleWorkflow, authRequired: true, scopes: ['write'] },
  ];

  // -- Helpers --------------------------------------------------------------

  function meta(t0) {
    return { version: v, elapsed_ms: Math.round((Date.now() - t0) * 100) / 100 };
  }

  function clientIp(req) {
    // Use socket remote address for rate limiting — never trust XFF for identity
    // since it's trivially spoofable. Only use XFF for logging/display.
    return req.socket?.remoteAddress || 'unknown';
  }

  function extractAuth(headers) {
    const authHeader = headers['authorization'];
    if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
      return authHeader.slice(7).trim();
    }
    return null;
  }

  function matchRoute(method, pathname) {
    const clean = pathname.replace(/\/+$/, '') || '/';
    return routes.find((r) => r.method === method && r.path === clean) || null;
  }

  function setCors(res, reqOrigin) {
    const allowed = config.corsOrigins;
    // Only echo the request origin back if it is explicitly in the allow-list.
    // Never fall back to another origin or '*' — that would allow any origin
    // to read responses on behalf of a valid origin (AUDIT-CORS-001).
    const origin = allowed.includes(reqOrigin) ? reqOrigin : '';
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
    // Security headers (AUDIT-019)
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('Content-Security-Policy', "default-src 'self'");
  }

  function sendJson(res, response) {
    const body = response.toJson();
    res.writeHead(response.status, {
      'Content-Type': 'application/json; charset=utf-8',
      'Content-Length': Buffer.byteLength(body),
    });
    res.end(body);
  }

  // -- Request parsing ------------------------------------------------------

  function parseBody(req) {
    return new Promise((resolve) => {
      const chunks = [];
      let size = 0;
      req.on('data', (chunk) => {
        size += chunk.length;
        if (size > config.maxRequestSize) {
          resolve({ __error__: 'payload too large' });
          req.destroy();
          return;
        }
        chunks.push(chunk);
      });
      req.on('end', () => {
        if (size === 0) return resolve({});
        try {
          resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
        } catch {
          resolve({ __error__: 'invalid JSON' });
        }
      });
      req.on('error', () => resolve({ __error__: 'request error' }));
    });
  }

  // -- Dispatch engine -------------------------------------------------------

  async function handleRequest(req, res) {
    const t0 = Date.now();
    requestCount++;

    // Abort slow-drip body attacks: if the request body takes more than 30s
    // to arrive, destroy the socket. CWE-400: Uncontrolled Resource Consumption.
    req.setTimeout(30_000, () => {
      req.destroy(new Error('Request body timeout'));
    });

    const origin = req.headers['origin'] || '*';
    setCors(res, origin);

    // CORS preflight
    if (req.method === 'OPTIONS') {
      res.writeHead(204);
      res.end();
      return;
    }

    const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
    const pathname = parsedUrl.pathname;
    const params = Object.fromEntries(
      [...parsedUrl.searchParams].map(([k, v]) => [k, v]),
    );

    const body =
      req.method === 'POST' || req.method === 'PUT' ? await parseBody(req) : {};

    /** Shared audit helper used by every response path. */
    const audit = (status) =>
      auditLog({ ts: new Date().toISOString(), ip: req.socket?.remoteAddress, method: req.method, path: pathname, status, latency_ms: meta(t0).elapsed_ms });

    if (body.__error__) {
      const r = new APIResponse({ status: 400, error: body.__error__, meta: meta(t0) });
      audit(400);
      return sendJson(res, r);
    }

    const route = matchRoute(req.method, pathname);
    if (!route) {
      const r = new APIResponse({ status: 404, error: 'not found', meta: meta(t0) });
      audit(404);
      return sendJson(res, r);
    }

    // Auth
    let clientId;
    if (route.authRequired && config.requireAuth) {
      const token = extractAuth(req.headers);
      if (!token) {
        audit(401);
        return sendJson(res, new APIResponse({ status: 401, error: 'missing authorization', meta: meta(t0) }));
      }
      const identity = auth.validateToken(token);
      if (!identity) {
        audit(401);
        return sendJson(res, new APIResponse({ status: 401, error: 'invalid token', meta: meta(t0) }));
      }
      if (route.scopes.length && !route.scopes.some((s) => identity.scopes.includes(s))) {
        audit(403);
        return sendJson(res, new APIResponse({ status: 403, error: 'insufficient scope', meta: meta(t0) }));
      }
      clientId = identity.clientId;
    } else {
      clientId = clientIp(req);
    }

    // Rate limit
    if (!rateLimiter.check(clientId)) {
      audit(429);
      return sendJson(res, new APIResponse({ status: 429, error: 'rate limit exceeded', meta: meta(t0) }));
    }

    // Execute handler
    try {
      const response = await route.handler({ body, params, headers: req.headers });
      response.meta = meta(t0);
      sendJson(res, response);
      audit(response.status);
    } catch (err) {
      // Log structured error for operator visibility — no user data, no request body.
      const msg = err?.message ?? String(err);
      const stack = err?.stack ?? '';
      process.stderr.write(
        `[nerf-api] handler error: ${req.method} ${pathname} — ${msg}\n${stack}\n`,
      );
      sendJson(res, new APIResponse({ status: 500, error: 'internal server error', meta: meta(t0) }));
      audit(500);
    }
  }

  // -- Route handlers -------------------------------------------------------

  function handleHealth() {
    const uptime = (Date.now() - startTime) / 1000;
    return new APIResponse({
      data: { status: 'healthy', uptime_s: Math.round(uptime * 100) / 100, version: v },
    });
  }

  function handleStats() {
    const rss = process.memoryUsage?.().rss;
    return new APIResponse({
      data: {
        requests_served: requestCount,
        uptime_s: Math.round((Date.now() - startTime) / 10) / 100,
        node_version: process.version,
        rate_limit_rpm: config.rateLimitRpm,
        memory_rss_kb: rss ? Math.round(rss / 1024) : -1,
      },
    });
  }

  async function handleScan({ body }) {
    const target = body.target;
    if (!target) return new APIResponse({ status: 400, error: "'target' is required" });
    if (!(await validateScanTargetAsync(String(target).trim()))) {
      return new APIResponse({
        status: 400,
        error: 'invalid scan target: internal/private addresses are not permitted',
      });
    }
    const scanId = randomBytes(8).toString('hex');
    // Lazy import — scanner may not be available in all environments
    try {
      const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
      const runner = new NucleiRunner();
      const profile = body.profile || 'standard';
      const severity = body.severity || 'medium';
      const result = await runner.scan(target, { profile: ScanProfile.fromDomain(profile), severity });
      return new APIResponse({
        data: {
          scan_id: scanId,
          target,
          profile,
          min_severity: severity,
          status: 'completed',
          findings: (result.findings || []).map((f) => ({
            id: f.templateId,
            name: f.name,
            severity: f.severity,
            url: f.matchedAt,
            description: f.description,
          })),
          total_findings: result.findings?.length || 0,
        },
      });
    } catch {
      return new APIResponse({
        data: {
          scan_id: scanId,
          target,
          status: 'error',
          error: 'scanner not available',
          findings: [],
        },
      });
    }
  }

  async function handleDiff({ body }) {
    const diffText = body.diff_text;
    if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
    try {
      const { SecurityDiffAnalyzer } = await import('../pipeline/github-actions.js');
      const analyzer = new SecurityDiffAnalyzer();
      const analysis = analyzer.analyzeDiff(diffText);
      return new APIResponse({
        data: {
          risk_level: analysis.riskLevel?.value ?? String(analysis.riskLevel),
          files_changed: analysis.filesChanged,
          auth_changes: analysis.authChanges,
          sql_changes: analysis.sqlChanges,
          crypto_changes: analysis.cryptoChanges,
          secrets_found: analysis.secrets?.length ?? 0,
          endpoints_added: analysis.endpointsAdded,
          has_security_findings: analysis.hasSecurityFindings,
          summary: analysis.summary,
        },
      });
    } catch {
      return new APIResponse({ status: 500, error: 'diff analysis not available' });
    }
  }

  function handleSecrets({ body }) {
    const diffText = body.diff_text;
    if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
    const patterns = {
      aws_key: /AKIA[0-9A-Z]{16}/g,
      generic_secret: /(?:secret|password|token|apikey)\s*[=:]\s*['"][^\s'"]{8,}/gi,
      private_key: /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH)\s+PRIVATE\s+KEY-----/g,
      github_token: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
    };
    const findings = [];
    for (const [name, pat] of Object.entries(patterns)) {
      let m;
      while ((m = pat.exec(diffText)) !== null) {
        findings.push({
          type: name,
          line: diffText.slice(0, m.index).split('\n').length,
          match: m[0].slice(0, 8) + '****',
        });
      }
    }
    return new APIResponse({ data: { secrets_found: findings.length, findings } });
  }

  async function handleSkillsList() {
    try {
      const { readdirSync, statSync, existsSync } = await import('node:fs');
      const { join } = await import('node:path');
      const skillsDir = 'skills';
      const domains = {};
      if (existsSync(skillsDir)) {
        for (const d of readdirSync(skillsDir).sort()) {
          const full = join(skillsDir, d);
          if (!statSync(full).isDirectory() || d.startsWith('.')) continue;
          const techs = join(full, 'techniques');
          if (existsSync(techs)) {
            domains[d] = readdirSync(techs).filter((t) => statSync(join(techs, t)).isDirectory()).length;
          } else {
            domains[d] = readdirSync(full).filter((t) => {
              const p = join(full, t);
              return statSync(p).isDirectory() && existsSync(join(p, 'SKILL.md'));
            }).length;
          }
        }
      }
      return new APIResponse({
        data: { domains, count: Object.keys(domains).length, total_techniques: Object.values(domains).reduce((a, b) => a + b, 0) },
      });
    } catch {
      return new APIResponse({ data: { domains: {}, count: 0, total_techniques: 0 } });
    }
  }

  function handleSkillsSearch() {
    // Skill search is not yet implemented.
    // The route exists as a reserved endpoint; return 501 so callers can
    // distinguish "no results" from "feature not available".
    return new APIResponse({
      status: 501,
      error: 'skill search is not yet implemented',
    });
  }

  async function handleMemoryStore({ body }) {
    const content = body.content;
    if (!content) return new APIResponse({ status: 400, error: "'content' is required" });
    // Basic content sanitization — strip known prompt injection patterns (AUDIT-030)
    const sanitised = sanitiseMemoryContent(String(content));
    try {
      const { NerfMemory } = await import('../memory/engine.js');
      const memory = new NerfMemory();
      const entryId = memory.store({
        content: sanitised,
        memoryType: body.type || 'note',
        severity: body.severity || '',
        engagementId: body.engagement_id || '',
        tags: body.tags || [],
      });
      memory.close();
      return new APIResponse({ status: 201, data: { entry_id: entryId, type: body.type || 'note', stored: true } });
    } catch {
      return new APIResponse({ status: 500, error: 'memory engine not available' });
    }
  }

  async function handleMemorySearch({ body }) {
    const query = body.query;
    if (!query) return new APIResponse({ status: 400, error: "'query' is required" });
    try {
      const { NerfMemory } = await import('../memory/engine.js');
      const memory = new NerfMemory();
      const limit = Math.min(Math.max(parseInt(body.limit, 10) || 10, 1), 100);
      const results = memory.search(query, limit);
      memory.close();
      return new APIResponse({
        data: {
          query,
          results: results.map((r) => ({
            content: r.content,
            type: r.memoryType,
            severity: r.severity,
            created: r.createdAt,
          })),
          total: results.length,
        },
      });
    } catch {
      return new APIResponse({ data: { query, results: [], total: 0 } });
    }
  }

  async function handleMemoryStats() {
    try {
      const { NerfMemory } = await import('../memory/engine.js');
      const memory = new NerfMemory();
      const stats = memory.stats();
      memory.close();
      return new APIResponse({ data: stats });
    } catch {
      return new APIResponse({ data: {} });
    }
  }

  async function handleScore({ body }) {
    const query = body.query;
    const responseText = body.response;
    if (!query || !responseText) {
      return new APIResponse({ status: 400, error: "'query' and 'response' are required" });
    }
    try {
      const { ResponseScorer } = await import('../memory/evolution.js');
      const scorer = new ResponseScorer();
      const scored = scorer.score({ query, response: responseText, mode: body.mode || '' });
      return new APIResponse({ data: { score: scored.score, votes: scored.votes, feedback: scored.feedback } });
    } catch {
      return new APIResponse({ status: 500, error: 'scorer not available' });
    }
  }

  async function handleLeaderboard() {
    try {
      const { SkillLeaderboard } = await import('../execution-engine/leaderboard.js');
      const lb = new SkillLeaderboard();
      const topSkills = lb.getTopSkills(20);
      return new APIResponse({ data: { top_skills: topSkills, total_tracked: topSkills.length } });
    } catch (err) {
      debug?.('leaderboard stats error:', err.message);
      return new APIResponse({ data: { top_skills: [], total_tracked: 0, error: 'Internal error retrieving leaderboard data' } });
    }
  }

  async function handleLeaderboardDashboard() {
    try {
      const { SkillLeaderboard } = await import('../execution-engine/leaderboard.js');
      const lb = new SkillLeaderboard();
      const dashboard = lb.getDashboard();
      return new APIResponse({ data: dashboard });
    } catch (err) {
      debug?.('leaderboard dashboard error:', err.message);
      return new APIResponse({ data: { error: 'Internal error retrieving dashboard data' } });
    }
  }

  async function handleWorkflow({ body }) {
    try {
      const { WorkflowGenerator } = await import('../pipeline/github-actions.js');
      const gen = new WorkflowGenerator();
      const profile = body.profile || 'standard';
      const yaml = gen.generateWorkflow({ scanProfile: profile });
      return new APIResponse({ data: { profile, workflow: yaml } });
    } catch {
      return new APIResponse({ status: 500, error: 'workflow generator not available' });
    }
  }

  // -- Server creation -------------------------------------------------------

  const server = httpCreateServer(handleRequest);

  return {
    server,
    config,
    auth,
    rateLimiter,
    /** Start listening. Returns the actual port (useful with port 0). */
    start() {
      return new Promise((resolve, reject) => {
        server.listen(config.port, config.host, () => {
          const addr = server.address();
          resolve(addr.port);
        });
        server.once('error', reject);
      });
    },
    /** Stop the server. */
    stop() {
      return new Promise((resolve) => {
        server.close(() => resolve());
      });
    },
  };
}
