// benchmark-pacbayes-vs-ann.js
// Benchmark comparing PACBayesInference vs PACBayesInferenceANN on realistic authorization queries
// Usage: node benchmark-pacbayes-vs-ann.js [big-graph-data.json]

import { performance } from 'perf_hooks';
import { Arbiter } from '../src/index.js';
import { PACBayesInference } from '../src/inference/PACBayesInference.js';
import { PACBayesInferenceANN } from '../src/inference/PACBayesInferenceANN.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function loadGraphFromFile(filename) {
  const filePath = path.resolve(__dirname, filename);
  if (!fs.existsSync(filePath)) {
    return null;
  }
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  const arbiter = new Arbiter({
    enableInference: true,
    useOptimizedInference: true,
    fastConstructionMode: true
  });
  // Add nodes
  for (const node of data.nodes) {
    arbiter.addNode(node.key, node.type || 'unknown', node);
  }
  // Add relations
  for (const rel of data.relations) {
    arbiter.addRelation(rel.src, rel.rel, rel.dst);
  }
  // Collect users and documents
  const users = data.nodes.filter(n => n.type === 'user' || n.type === 'admin' || n.type === 'test_user' || n.key.startsWith('user:') || n.key.startsWith('admin:'));
  const documents = data.nodes.filter(n => n.type === 'doc' || n.type === 'document' || n.key.startsWith('doc:'));
  return { arbiter, users, documents, relations: data.relations };
}

// --- Fallback synthetic generator (for dev/testing) ---
class RealisticDataGenerator {
  constructor() {
    this.departments = ['engineering', 'sales', 'marketing', 'finance', 'hr', 'legal', 'operations'];
    this.roles = ['intern', 'junior', 'senior', 'lead', 'manager', 'director', 'vp'];
    this.clearanceLevels = ['public', 'internal', 'confidential', 'secret', 'top-secret'];
    this.documentTypes = ['report', 'contract', 'proposal', 'specification', 'policy', 'manual'];
    this.projects = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta'];
  }
  generateUsers(count) {
    const users = [];
    for (let i = 0; i < count; i++) {
      const dept = this.departments[Math.floor(Math.random() * this.departments.length)];
      const role = this.roles[Math.floor(Math.random() * this.roles.length)];
      users.push({
        key: `user:${dept}_${role}_${i}`,
        department: dept,
        role: role,
        clearance: this.clearanceLevels[Math.floor(Math.random() * this.clearanceLevels.length)]
      });
    }
    return users;
  }
  generateGroups(departments, roles) {
    const groups = [];
    departments.forEach(dept => {
      groups.push({ key: `group:dept_${dept}`, type: 'department', name: dept });
    });
    roles.forEach(role => {
      groups.push({ key: `group:role_${role}`, type: 'role', name: role });
    });
    this.projects.forEach(project => {
      groups.push({ key: `group:project_${project}`, type: 'project', name: project });
    });
    ['security_team', 'architecture_board', 'exec_team'].forEach(team => {
      groups.push({ key: `group:${team}`, type: 'special', name: team });
    });
    return groups;
  }
  generateDocuments(count) {
    const documents = [];
    for (let i = 0; i < count; i++) {
      const type = this.documentTypes[Math.floor(Math.random() * this.documentTypes.length)];
      const classification = this.clearanceLevels[Math.floor(Math.random() * this.clearanceLevels.length)];
      const project = this.projects[Math.floor(Math.random() * this.projects.length)];
      const dept = this.departments[Math.floor(Math.random() * this.departments.length)];
      documents.push({
        key: `doc:${type}_${project}_${dept}_${i}`,
        type: type,
        classification: classification,
        project: project,
        department: dept
      });
    }
    return documents;
  }
}

function setupGraphFallback(scale = 'medium') {
  const config = { users: 1000, docs: 5000 };
  const generator = new RealisticDataGenerator();
  const arbiter = new Arbiter({
    enableInference: true,
    useOptimizedInference: true,
    fastConstructionMode: true
  });
  const users = generator.generateUsers(config.users);
  const groups = generator.generateGroups(generator.departments, generator.roles);
  const documents = generator.generateDocuments(config.docs);
  users.forEach(user => arbiter.addNode(user.key, 'user', user));
  groups.forEach(group => arbiter.addNode(group.key, 'group', group));
  documents.forEach(doc => arbiter.addNode(doc.key, 'document', doc));
  generator.clearanceLevels.forEach(level => {
    arbiter.addNode(`clearance:${level}`, 'clearance', { level });
  });
  users.forEach(user => {
    arbiter.addRelation(user.key, 'member_of', `group:dept_${user.department}`);
    arbiter.addRelation(user.key, 'member_of', `group:role_${user.role}`);
    arbiter.addRelation(user.key, 'has_clearance', `clearance:${user.clearance}`);
  });
  documents.forEach(doc => {
    arbiter.addRelation(doc.key, 'classified_as', `clearance:${doc.classification}`);
  });
  return { arbiter, users, documents, relations: [...arbiter.relations] };
}

function recordObservedDecisions(engine, relations) {
  if (!engine || typeof engine.recordDecision !== 'function') return;
  for (const rel of relations) {
    if (rel.rel === 'can_read') {
      engine.recordDecision(rel.src, rel.rel, rel.dst, 'allow');
    }
    // Optionally: handle 'deny' if you have such relations
  }
}

async function benchmarkEngine(name, InferenceClass, { arbiter, users, documents, relations }, opts = {}) {
  arbiter.inferenceEngine = new InferenceClass(arbiter, opts);
  recordObservedDecisions(arbiter.inferenceEngine, relations);
  if (arbiter.embeddingManager) {
    arbiter.embeddingManager.forceRegenerateEmbeddings();
    if (arbiter.flatnav) arbiter.embeddingManager.ensureFlatNavIndex();
  }
  // Collect all can_read relations for sampling
  const canReadRels = relations.filter(rel => rel.rel === 'can_read');
  let totalTime = 0;
  let totalCIWidth = 0;
  let allowCount = 0;
  let denyCount = 0;
  let undeterminedCount = 0;
  let truePositive = 0;
  let falseNegative = 0;
  let missed = 0;
  const sampleResults = [];
  const iterations = Math.min(1000, canReadRels.length);
  // Remove direct can_read relations for the sampled queries
  const removedRels = [];
  for (let i = 0; i < iterations; i++) {
    const rel = canReadRels[Math.floor(Math.random() * canReadRels.length)];
    // Remove the direct relation from the graph
    if (typeof arbiter.removeRelation === 'function') {
      arbiter.removeRelation(rel.src, rel.rel, rel.dst);
      removedRels.push(rel);
    } else if (arbiter.relationManager && typeof arbiter.relationManager.removeRelation === 'function') {
      arbiter.relationManager.removeRelation(rel.src, rel.rel, rel.dst);
      removedRels.push(rel);
    }
    // Mark nodes as stale and refresh embeddings
    if (arbiter.embeddingManager) {
      arbiter.embeddingManager.markNodeStale(rel.src);
      arbiter.embeddingManager.markNodeStale(rel.dst);
      arbiter.embeddingManager.ensureFreshEmbedding(rel.src);
      arbiter.embeddingManager.ensureFreshEmbedding(rel.dst);
      if (arbiter.flatnav) arbiter.embeddingManager.ensureFlatNavIndex();
    }
    if (arbiter.inferenceEngine && typeof arbiter.inferenceEngine.invalidateFeatureSets === 'function') {
      arbiter.inferenceEngine.invalidateFeatureSets();
    }
    const t0 = performance.now();
    const result = await arbiter.check(rel.src, 'can_read', rel.dst);
    const t1 = performance.now();
    totalTime += (t1 - t0);
    if (result && result.confidenceInterval) {
      totalCIWidth += (result.confidenceInterval[1] - result.confidenceInterval[0]);
    }
    if (result && result.outcome === 'allow') {
      allowCount++;
      truePositive++;
    } else if (result && result.outcome === 'deny') {
      denyCount++;
      falseNegative++;
    } else {
      undeterminedCount++;
      missed++;
    }
    if (i < 5) sampleResults.push({src: rel.src, dst: rel.dst, outcome: result && result.outcome, probability: result && result.probability, ci: result && result.confidenceInterval});
  }
  // Optionally restore the removed relations (not strictly needed for benchmarking)
  // for (const rel of removedRels) {
  //   arbiter.addRelation(rel.src, rel.rel, rel.dst);
  // }
  const avgLatency = totalTime / iterations;
  const avgCIWidth = totalCIWidth / iterations;
  const qps = iterations / (totalTime / 1000);
  const accuracy = truePositive / iterations;
  return {
    name,
    avgLatency: avgLatency.toFixed(3),
    qps: Math.round(qps),
    avgCIWidth: avgCIWidth.toFixed(3),
    allowCount,
    denyCount,
    undeterminedCount,
    truePositive,
    falseNegative,
    missed,
    accuracy: accuracy,
    sampleResults
  };
}

(async function main() {
  const graphFile = process.argv[2] || 'big-graph-data-medium.json';
  let graph = loadGraphFromFile(graphFile);
  if (graph) {
    console.log(`📦 Loaded pregenerated graph from ${graphFile}`);
  } else {
    console.log('⚠️  Pregenerated graph not found, using fallback synthetic generator.');
    graph = setupGraphFallback('medium');
  }
  const results = [];
  results.push(await benchmarkEngine('PACBayesInference', PACBayesInference, graph));
  results.push(await benchmarkEngine('PACBayesInferenceANN', PACBayesInferenceANN, graph, { useANN: true, annK: 100, annEfSearch: 200 }));
  console.log('\nResults:');
  console.log('Engine                 | Avg Latency (ms) | QPS   | Avg CI Width | Allow | Deny | Undet');
  console.log('-----------------------|------------------|-------|--------------|-------|------|-------');
  for (const r of results) {
    console.log(`${r.name.padEnd(23)} | ${r.avgLatency.padStart(16)} | ${r.qps.toString().padStart(5)} | ${r.avgCIWidth.padStart(12)} | ${r.allowCount.toString().padStart(5)} | ${r.denyCount.toString().padStart(4)} | ${r.undeterminedCount.toString().padStart(5)}`);
    console.log(`  Accuracy: ${(r.accuracy * 100).toFixed(2)}% | True Positives: ${r.truePositive} | False Negatives: ${r.falseNegative} | Missed: ${r.missed}`);
    console.log('  Sample results:', r.sampleResults);
  }
})(); 