/**
 * Inventory every Oracle object the product queries, per surface, and diff it
 * against what db/setup/tunevault_reader.sql grants.
 *
 * Objects are collected from FROM/JOIN clauses. Bind-built or concatenated SQL is
 * not resolvable statically and is reported separately rather than silently
 * dropped — a clean-looking audit that quietly skipped half the queries would be
 * worse than no audit.
 */
const fs = require('fs');
const path = require('path');

const SURFACES = {
  'Health check (agent)': ['oracle-proxy.py'],
  'DB Ops':               ['services/db-ops-executor.js'],
  'EBS Ops':              ['routes/ebs-ops.js', 'routes/ebs-concurrent.js', 'routes/ebs-deep.js'],
  'Tune advisors':        ['routes/awr-advisor.js','routes/space-advisor.js','routes/index-advisor.js',
                           'routes/memory-advisor.js','routes/segment-advisor.js','routes/stats-advisor.js',
                           'routes/compression-advisor.js','routes/access-advisor.js','routes/performance.js',
                           'routes/sql-tuning.js','routes/addm.js','routes/performance-advisor.js'],
  'Vault':                ['routes/security-posture.js','routes/resilience.js','routes/vulnerability.js'],
  'Other checks':         ['oracle-client.js','routes/patches.js','routes/sessions.js',
                           'routes/invalidobjects.js','routes/stalestatistics.js','routes/unusableindexes.js',
                           'routes/parameters.js','routes/topsql.js','routes/housekeeping.js'],
};

// FROM / JOIN <object>, allowing schema-qualified names.
const OBJ_RE = /\b(?:FROM|JOIN)\s+((?:[A-Z0-9_$#]+\.)?[A-Z0-9_$#]+)/gi;
const NOISE = new Set(['DUAL','SELECT','LATERAL','TABLE','XMLTABLE','JSON_TABLE','THE','SYS','A','B','S','I','U','D','T','X']);

function objectsIn(text) {
  const out = new Set();
  let m;
  while ((m = OBJ_RE.exec(text))) {
    let o = m[1].toUpperCase().replace(/[",;)]/g, '');
    if (NOISE.has(o) || /^\d/.test(o) || o.length < 3) continue;
    if (!/^(V\$|GV\$|V_\$|DBA_|CDB_|ALL_|USER_|APPS\.|APPLSYS\.|SYS\.|PERFSTAT\.|X\$)/.test(o)) continue;
    out.add(o);
  }
  return out;
}

const grantsSql = fs.readFileSync('db/setup/tunevault_reader.sql', 'utf8').toUpperCase();
const granted = new Set();
for (const m of grantsSql.matchAll(/GRANT\s+SELECT\s+ON\s+([A-Z0-9_$#.]+)\s+TO/g)) granted.add(m[1]);
const hasSelectCatalog = /GRANT\s+SELECT_CATALOG_ROLE/.test(grantsSql);
const hasSelectAnyDict = /GRANT\s+SELECT\s+ANY\s+DICTIONARY/.test(grantsSql);

// SELECT_CATALOG_ROLE covers DBA_*/V$ dictionary views; APPS.* is never covered.
function coveredByRole(o) {
  if (o.startsWith('APPS.') || o.startsWith('APPLSYS.') || o.startsWith('PERFSTAT.')) return false;
  return hasSelectCatalog || hasSelectAnyDict;
}

const all = new Map();   // object -> Set(surface)
for (const [surface, files] of Object.entries(SURFACES)) {
  for (const f of files) {
    if (!fs.existsSync(f)) continue;
    for (const o of objectsIn(fs.readFileSync(f, 'utf8'))) {
      if (!all.has(o)) all.set(o, new Set());
      all.get(o).add(surface);
    }
  }
}

const appsObjs = [...all.keys()].filter(o => o.startsWith('APPS.') || o.startsWith('APPLSYS.')).sort();
const dictObjs = [...all.keys()].filter(o => !o.startsWith('APPS.') && !o.startsWith('APPLSYS.')).sort();

console.log('role script: SELECT_CATALOG_ROLE=%s  SELECT ANY DICTIONARY=%s  explicit grants=%d',
  hasSelectCatalog, hasSelectAnyDict, granted.size);
console.log('objects referenced: %d dictionary/V$, %d APPS\n', dictObjs.length, appsObjs.length);

const missingApps = appsObjs.filter(o => !granted.has(o));
console.log('APPS objects queried but NOT granted in the role script: %d', missingApps.length);
missingApps.forEach(o => console.log('   ' + o.padEnd(42) + ' [' + [...all.get(o)].join(', ') + ']'));

if (!hasSelectCatalog && !hasSelectAnyDict) {
  const missingDict = dictObjs.filter(o => !granted.has(o) && !granted.has(o.replace(/^V\$/, 'V_$')));
  console.log('\ndictionary objects not granted (no catalog role): %d', missingDict.length);
  missingDict.slice(0, 40).forEach(o => console.log('   %s', o));
} else {
  console.log('\ndictionary/V$ objects: covered by the catalog role grant');
}

// ---- Exit code ------------------------------------------------------------
// A check that queries an ungranted object fails with ORA-00942 every run and,
// before 3.24.0, rendered as an empty panel. That is how 15 missing grants
// survived unnoticed. Failing the build is what stops it recurring.
//
// Rot guard: if the extractor suddenly finds almost nothing, that is a broken
// regex, not a clean codebase.
if (all.size < 50) {
  console.error("\nFATAL: only " + all.size + " objects extracted - parser broken, not the code.");
  process.exit(2);
}
if (missingApps.length) {
  console.error("\n" + missingApps.length + " object(s) queried but never granted.");
  console.error("Add them to db/setup/tunevault_reader.sql (Step 4 for APPS), or remove the query.");
  process.exit(1);
}
console.log("\nno gaps: every queried object is granted");
