#!/usr/bin/env node
/**
 * An op in a catalog with no way to reach it from the UI fails the build.
 *
 * Four dead surfaces turned up in one week — fleet-health probes computed and
 * rendered nowhere, an ETCC card pointed at a tier with no ETCC, and two I
 * reported as dead that turned out to be reachable. Finding them one at a time,
 * by accident, is the wrong method.
 *
 * THE HARD PART IS FALSE POSITIVES. A naive "is the key a literal in public/?"
 * check flagged 114 of 117 db-ops (that page FETCHES its catalog and renders it
 * dynamically) and 16 more ops whose keys are BUILT at call time
 * (`'adop_' + phase`, `op_key + '_bg'`). A gate that cries wolf gets disabled, so
 * reachability is established three ways, in order:
 *
 *   1. the page fetches the catalog endpoint  -> the whole catalog is reachable;
 *   2. the op key appears literally anywhere in public/;
 *   3. the catalog entry DECLARES why it needs neither.
 *
 * (3) is a declaration, never an inference: `uiRef` names where it is reached
 * from, `noUi` says why it has no UI. Both are reviewable in the diff. An op with
 * none of the three fails the build — the same treatment an ungranted queried
 * object already gets.
 */
'use strict';
process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgres://ci/unused';

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

const ROOT = path.join(__dirname, '..', '..');
const rd = p => fs.readFileSync(path.join(ROOT, p), 'utf8');

const publicBlob = fs.readdirSync(path.join(ROOT, 'public'))
  .filter(f => /\.(html|js)$/.test(f))
  .map(f => rd(path.join('public', f)))
  .join('\n');

let failures = 0;
const note = [];

/**
 * @param {string}   name           catalog label for output
 * @param {string[]} keys           op keys
 * @param {object}   opts
 * @param {string}  [opts.fetchedVia] endpoint a page fetches to render the whole catalog
 * @param {object}  [opts.declared]   key -> { uiRef?, noUi? }
 */
function audit(name, keys, opts = {}) {
  if (opts.fetchedVia && publicBlob.includes(opts.fetchedVia)) {
    note.push(`  ${name}: ${keys.length} ops — rendered dynamically via ${opts.fetchedVia}, all reachable`);
    return;
  }
  const declared = opts.declared || {};
  const dead = keys.filter(k => {
    if (publicBlob.includes(k)) return false;                 // literal reference
    const d = declared[k];
    if (d && (d.uiRef || d.noUi)) return false;               // declared exception
    return true;
  });
  if (!dead.length) {
    note.push(`  ${name}: ${keys.length} ops — all reachable or declared`);
    return;
  }
  failures += dead.length;
  console.error(`\n${name}: ${dead.length} op(s) reachable from no page and with no declaration:`);
  for (const k of dead) console.error(`   ${k}`);
}

// ── db-ops: db-ops.html fetches the catalog and renders every entry ─────────
audit('db-ops (services/db-ops-executor.js)',
  require(path.join(ROOT, 'services', 'db-ops-executor.js')).getOpCatalog().map(o => o.key),
  { fetchedVia: '/api/db-ops/catalog' });

// ── EBS middleware ops: ebs-ops.html hardcodes its cards ───────────────────
const eo = rd('routes/ebs-ops.js');
const mwBlock = eo.slice(eo.indexOf('MIDDLEWARE_OPS_CATALOG'),
                        eo.indexOf('\n};', eo.indexOf('MIDDLEWARE_OPS_CATALOG')));
const mwKeys = [...new Set([...mwBlock.matchAll(/^\s{2}([a-z0-9_]+):\s*\{/gm)].map(m => m[1]))];

// Declared exceptions. Each states HOW the op is reached, so a reader can check it.
const MW_DECLARED = {};
for (const phase of ['validate','prepare','apply','actualize','finalize','cutover','cleanup','fsclone','abort']) {
  MW_DECLARED['adop_' + phase] = { uiRef: "public/ebs-ops.html builds op_key as 'adop_' + phase in runAdopPhase()" };
}
audit('EBS middleware ops (routes/ebs-ops.js)', mwKeys, { declared: MW_DECLARED });

// ── agent /api/ebs-ctrl: reachability means a CLOUD caller, not a page ──────
const routesBlob = ['routes', 'services']
  .flatMap(d => fs.readdirSync(path.join(ROOT, d)).filter(f => f.endsWith('.js')).map(f => rd(path.join(d, f))))
  .join('\n');
const py = rd('oracle-proxy.py');
const ctrlStart = py.indexOf('_CTRL_ALLOWED = {');
const ctrlKeys = [...new Set([...py.slice(ctrlStart, py.indexOf('}', ctrlStart)).matchAll(/"([a-z0-9_]+)"/g)].map(m => m[1]))];

// Declared, but NOT as "fine". This one is dead and the entry says so, so that
// the gate can protect against NEW dead ops while an existing one waits for a
// decision. Declaring it as deliberate would defeat the point of the gate.
const CTRL_DECLARED = {};
for (const k of ctrlKeys) {
  // Background variants are dispatched as `op_key + '_bg'`, never as a literal.
  if (k.endsWith('_bg')) CTRL_DECLARED[k] = { uiRef: "routes/ebs-ops.js dispatches op_key + '_bg'" };
}
// Match anywhere in server code OR any page — an agent op can be reached from a
// route, or named directly by a page that posts it. An earlier revision of this
// gate checked only QUOTED occurrences in routes/, and reported bsu_list_patches
// as dead when routes/patches.js and public/ebs-patches.html both reference it.
// A gate's own false positive is the thing most likely to get it switched off.
const ctrlDead = ctrlKeys.filter(k => {
  if (routesBlob.includes(k) || publicBlob.includes(k)) return false;
  return !CTRL_DECLARED[k];
});
if (ctrlDead.length) {
  failures += ctrlDead.length;
  console.error(`\nagent /api/ebs-ctrl: ${ctrlDead.length} op(s) with no cloud caller and no declaration:`);
  for (const k of ctrlDead) console.error(`   ${k}`);
} else {
  note.push(`  agent /api/ebs-ctrl: ${ctrlKeys.length} ops — all called or declared`);
}

console.log('Catalog reachability:');
note.forEach(n => console.log(n));

if (failures) {
  console.error(`\n${failures} unreachable op(s).`);
  console.error('Fix by giving it a UI, or DECLARE the exception in the catalog entry:');
  console.error("   uiRef: 'where it is reached from'   — reached by a constructed key or another page");
  console.error("   noUi:  'why it has no UI'           — deliberate, e.g. an internal-only op");
  console.error('A declaration is reviewable in the diff; an inference is not.');
  process.exit(1);
}
console.log('\nEvery catalog op is reachable or explicitly declared.');
