#!/usr/bin/env node
/**
 * lint_paid_handler_logging.js
 *
 * Anti-staleness guardrail for the /live payer board.
 *
 * Every x402 paid handler MUST flow through one of two wrappers that record
 * the call to KV:
 *
 *   1. withActionLogging(handler, price, path)      — for POST action endpoints
 *   2. withX402MultiNet(handler, routeConfig)        — for GET trust + oracle endpoints
 *
 * If a route file uses x402-next's `withX402(...)` (the bare paid-POST wrapper)
 * WITHOUT also using `withActionLogging(...)`, the paid call will succeed but
 * never appear on the /live board — which makes the board lie about activity.
 *
 * This linter scans every route under app/api/x402/ for that pattern and
 * fails the build if found. Allow-list one-off exceptions via inline comment:
 *
 *     // lint-paid-logging: allow  (reason: <why>)
 *
 * Exit codes:
 *   0  all paid handlers are instrumented
 *   1  one or more paid handlers bypass the recorder
 */

const fs = require('fs');
const path = require('path');

const ROOT = process.cwd();
const SCAN_DIR = path.join(ROOT, 'app', 'api', 'x402');
const SCAN_EXTS = new Set(['.ts', '.tsx']);
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '_lib']);

// Patterns that indicate a route uses a paid wrapper.
const RE_WITH_X402 = /\bwithX402\s*\(/;
const RE_WITH_ACTION_LOGGING = /\bwithActionLogging\s*\(/;
const RE_WITH_MULTINET = /\bwithX402MultiNet\s*\(/;
const RE_ALLOW_COMMENT = /lint-paid-logging:\s*allow/;

function walk(dir, out) {
  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch (e) {
    return;
  }
  for (const entry of entries) {
    if (IGNORE_DIRS.has(entry.name)) continue;
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      walk(full, out);
    } else if (SCAN_EXTS.has(path.extname(entry.name)) && entry.name === 'route.ts') {
      out.push(full);
    }
  }
}

function check(file) {
  const content = fs.readFileSync(file, 'utf8');
  const usesWithX402 = RE_WITH_X402.test(content);
  const usesActionLogging = RE_WITH_ACTION_LOGGING.test(content);
  const usesMultiNet = RE_WITH_MULTINET.test(content);
  const hasAllowComment = RE_ALLOW_COMMENT.test(content);

  if (!usesWithX402 && !usesMultiNet) {
    // Not a paid handler. Skip.
    return null;
  }

  // withX402MultiNet always records (the wrapper itself calls recordPaidCall).
  // withX402 (bare) requires withActionLogging to record the call.
  if (usesMultiNet) {
    return null;
  }

  if (usesWithX402 && !usesActionLogging && !hasAllowComment) {
    return {
      file: path.relative(ROOT, file),
      reason:
        'uses withX402(...) but does not use withActionLogging(...). ' +
        'Paid calls to this endpoint will not appear on /live. ' +
        'Wrap the handler: withActionLogging(handler, price, path) before passing to withX402, ' +
        'OR add the comment "// lint-paid-logging: allow (reason: ...)" to the file.',
    };
  }

  return null;
}

function main() {
  const files = [];
  walk(SCAN_DIR, files);
  const failures = [];
  for (const f of files) {
    const result = check(f);
    if (result) failures.push(result);
  }

  if (failures.length === 0) {
    console.log(`[lint-paid-logging] OK — scanned ${files.length} route files, all paid handlers are instrumented.`);
    process.exit(0);
  }

  console.error(`[lint-paid-logging] FAIL — ${failures.length} paid handler(s) bypass the /live recorder:`);
  for (const f of failures) {
    console.error(`  - ${f.file}`);
    console.error(`      ${f.reason}`);
  }
  process.exit(1);
}

main();
