import fs from 'node:fs';

// Zipfian distribution generator
function zipfian(n, s = 1.0) {
  const harmonic = Array.from({ length: n }, (_, i) => 1 / Math.pow(i + 1, s))
    .reduce((sum, val) => sum + val, 0);
  
  return function() {
    const r = Math.random() * harmonic;
    let sum = 0;
    for (let i = 0; i < n; i++) {
      sum += 1 / Math.pow(i + 1, s);
      if (sum >= r) return i;
    }
    return n - 1;
  };
}

// Preferential attachment with aging
class PreferentialAttachment {
  constructor() {
    this.degrees = new Map();
    this.totalDegree = 0;
    this.ageDecay = 0.95; // Older connections become less likely
  }
  
  addNode(nodeKey) {
    this.degrees.set(nodeKey, 1); // Start with degree 1
    this.totalDegree += 1;
  }
  
  selectNode() {
    if (this.totalDegree === 0) return null;
    
    let r = Math.random() * this.totalDegree;
    for (const [nodeKey, degree] of this.degrees) {
      r -= degree;
      if (r <= 0) return nodeKey;
    }
    return Array.from(this.degrees.keys())[0]; // Fallback
  }
  
  addEdge(srcKey, dstKey) {
    this.degrees.set(srcKey, (this.degrees.get(srcKey) || 0) + 1);
    this.degrees.set(dstKey, (this.degrees.get(dstKey) || 0) + 1);
    this.totalDegree += 2;
  }
  
  decay() {
    // Age all connections slightly
    for (const [nodeKey, degree] of this.degrees) {
      const newDegree = Math.max(1, degree * this.ageDecay);
      this.totalDegree += newDegree - degree;
      this.degrees.set(nodeKey, newDegree);
    }
  }
}

function generateCanonicalBenchmarkGraph(config = {}) {
  const {
    numUsers = 10000,
    numGroups = 100,
    numRoles = 20,
    numProjects = 200,
    numDocs = 5000,
    numDevices = 1000,
    numLocations = 50,
    zipfianExponent = 1.2,
    preferentialStrength = 0.8
  } = config;
  
  console.log('🏗️  Generating Canonical Benchmark Graph');
  console.log(`Users: ${numUsers}, Groups: ${numGroups}, Docs: ${numDocs}`);
  console.log(`Zipfian exponent: ${zipfianExponent}, Preferential strength: ${preferentialStrength}`);
  
  const nodes = [];
  const relations = [];
  const metadata = {
    generation: {
      timestamp: new Date().toISOString(),
      config,
      stats: {}
    },
    testCases: {
      highConfidenceAllow: [],
      mediumConfidenceAllow: [],
      lowConfidenceAllow: [],
      shouldDeny: [],
      uncertain: [],
      admins: [],
      newEmployees: []
    },
    relationConfigs: {}
  };
  
  // Zipfian generators for realistic distributions
  const groupZipf = zipfian(numGroups, zipfianExponent);
  const roleZipf = zipfian(numRoles, zipfianExponent);
  const projectZipf = zipfian(numProjects, zipfianExponent);
  const docZipf = zipfian(numDocs, zipfianExponent * 0.8); // Slightly flatter for docs
  
  // Preferential attachment trackers
  const groupPA = new PreferentialAttachment();
  const projectPA = new PreferentialAttachment();
  const userPA = new PreferentialAttachment();
  
  console.log('Creating nodes...');
  
  // Create organizational structure
  for (let i = 0; i < numGroups; i++) {
    const groupKey = `group:${i}`;
    nodes.push({ key: groupKey, type: 'group' });
    groupPA.addNode(groupKey);
  }
  
  for (let i = 0; i < numRoles; i++) {
    nodes.push({ key: `role:${i}`, type: 'role' });
  }
  
  for (let i = 0; i < numProjects; i++) {
    const projectKey = `project:${i}`;
    nodes.push({ key: projectKey, type: 'project' });
    projectPA.addNode(projectKey);
  }
  
  for (let i = 0; i < numLocations; i++) {
    nodes.push({ key: `location:${i}`, type: 'location' });
  }
  
  for (let i = 0; i < numDevices; i++) {
    nodes.push({ key: `device:${i}`, type: 'device' });
  }
  
  // Create users with realistic distributions
  console.log('Creating users and basic relations...');
  for (let i = 0; i < numUsers; i++) {
    const userKey = `user:${i}`;
    nodes.push({ key: userKey, type: 'user' });
    userPA.addNode(userKey);
    
    // Zipfian group membership (some groups are much more popular)
    const numGroupMemberships = Math.min(5, Math.floor(Math.random() * 3) + 1);
    for (let j = 0; j < numGroupMemberships; j++) {
      const groupIdx = groupZipf();
      const groupKey = `group:${groupIdx}`;
      relations.push({ 
        src: userKey, 
        rel: 'member_of', 
        dst: groupKey,
        possibility: 0.95 + Math.random() * 0.05 // High confidence
      });
      groupPA.addEdge(userKey, groupKey);
    }
    
    // Role assignment (Zipfian - some roles much more common)
    if (Math.random() < 0.8) { // 80% of users have roles
      const roleIdx = roleZipf();
      relations.push({ 
        src: userKey, 
        rel: 'has_role', 
        dst: `role:${roleIdx}`,
        possibility: 0.9 + Math.random() * 0.1
      });
    }
    
    // Project assignment (preferential attachment)
    const numProjects = Math.floor(Math.random() * 3) + 1;
    for (let j = 0; j < numProjects; j++) {
      let projectKey;
      if (Math.random() < preferentialStrength) {
        projectKey = projectPA.selectNode();
      } else {
        projectKey = `project:${Math.floor(Math.random() * numProjects)}`;
      }
      if (projectKey) {
        relations.push({ 
          src: userKey, 
          rel: 'works_on', 
          dst: projectKey,
          possibility: 0.85 + Math.random() * 0.15
        });
        projectPA.addEdge(userKey, projectKey);
      }
    }
    
    // Location assignment
    if (Math.random() < 0.9) { // 90% have locations
      const locationIdx = Math.floor(Math.random() * numLocations);
      relations.push({ 
        src: userKey, 
        rel: 'located_at', 
        dst: `location:${locationIdx}`,
        possibility: 0.98
      });
    }
    
    // Device assignment
    if (Math.random() < 0.7) { // 70% have assigned devices
      const deviceIdx = Math.floor(Math.random() * numDevices);
      relations.push({ 
        src: userKey, 
        rel: 'uses_device', 
        dst: `device:${deviceIdx}`,
        possibility: 0.9 + Math.random() * 0.1
      });
    }
    
    if (i % 1000 === 0) console.log(`  Created ${i} users...`);
  }
  
  // Create documents with realistic access patterns
  console.log('Creating documents and access patterns...');
  for (let i = 0; i < numDocs; i++) {
    const docKey = `doc:${i}`;
    nodes.push({ key: docKey, type: 'doc' });
    
    // Document ownership (Zipfian - some groups own many docs)
    const ownerGroupIdx = groupZipf();
    const ownerGroupKey = `group:${ownerGroupIdx}`;
    relations.push({ 
      src: ownerGroupKey, 
      rel: 'owns', 
      dst: docKey,
      possibility: 1.0
    });
    
    // Direct user access (some users have direct access)
    if (Math.random() < 0.3) { // 30% of docs have direct user access
      const numDirectUsers = Math.floor(Math.random() * 5) + 1;
      for (let j = 0; j < numDirectUsers; j++) {
        const userIdx = Math.floor(Math.random() * numUsers);
        const confidence = Math.random();
        let possibility;
        if (confidence < 0.3) possibility = 0.95 + Math.random() * 0.05; // High confidence
        else if (confidence < 0.7) possibility = 0.7 + Math.random() * 0.2; // Medium confidence  
        else possibility = 0.4 + Math.random() * 0.3; // Lower confidence
        
        relations.push({ 
          src: `user:${userIdx}`, 
          rel: 'can_read', 
          dst: docKey,
          possibility
        });
      }
    }
    
    // Project-based access
    if (Math.random() < 0.4) { // 40% of docs are project-related
      const projectIdx = Math.floor(Math.random() * numProjects);
      relations.push({ 
        src: `project:${projectIdx}`, 
        rel: 'can_read', 
        dst: docKey,
        possibility: 0.8 + Math.random() * 0.2
      });
    }
    
    if (i % 500 === 0) console.log(`  Created ${i} documents...`);
  }
  
  // Create hierarchical relationships
  console.log('Creating hierarchical relationships...');
  
  // Group hierarchies (some groups are parents of others)
  for (let i = 0; i < numGroups; i++) {
    if (Math.random() < 0.3) { // 30% chance of having a parent
      const parentIdx = Math.floor(Math.random() * numGroups);
      if (parentIdx !== i) {
        relations.push({ 
          src: `group:${parentIdx}`, 
          rel: 'parent_of', 
          dst: `group:${i}`,
          possibility: 1.0
        });
      }
    }
  }
  
  // Role hierarchies
  for (let i = 1; i < numRoles; i++) {
    if (Math.random() < 0.4) { // 40% chance of role inheritance
      const parentRoleIdx = Math.floor(Math.random() * i);
      relations.push({ 
        src: `role:${parentRoleIdx}`, 
        rel: 'inherits_from', 
        dst: `role:${i}`,
        possibility: 1.0
      });
    }
  }
  
  // Create special test users
  console.log('Creating special test users...');
  
  // Super admins (high access to everything)
  for (let i = 0; i < 3; i++) {
    const adminKey = `admin:${i}`;
    nodes.push({ key: adminKey, type: 'admin' });
    metadata.testCases.admins.push(adminKey);
    
    // Admin access to all groups and many docs
    for (let g = 0; g < numGroups; g++) {
      relations.push({ 
        src: adminKey, 
        rel: 'admin_of', 
        dst: `group:${g}`,
        possibility: 1.0
      });
    }
    
    // Admin access to many documents
    for (let d = 0; d < Math.min(1000, numDocs); d++) {
      relations.push({ 
        src: adminKey, 
        rel: 'can_read', 
        dst: `doc:${d}`,
        possibility: 0.98
      });
    }
  }
  
  // New employees (minimal access)
  for (let i = 0; i < 10; i++) {
    const newEmpKey = `newbie:${i}`;
    nodes.push({ key: newEmpKey, type: 'user' });
    metadata.testCases.newEmployees.push(newEmpKey);
    
    // Only basic group membership
    relations.push({ 
      src: newEmpKey, 
      rel: 'member_of', 
      dst: 'group:0', // Everyone group
      possibility: 1.0
    });
  }
  
  // Generate test cases for different confidence levels
  console.log('Generating test cases...');
  
  // High confidence cases (direct access)
  for (let i = 0; i < 100; i++) {
    const userIdx = Math.floor(Math.random() * numUsers);
    const docIdx = Math.floor(Math.random() * numDocs);
    metadata.testCases.highConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
  }
  
  // Medium confidence cases (group-based access)
  for (let i = 0; i < 100; i++) {
    const userIdx = Math.floor(Math.random() * numUsers);
    const docIdx = Math.floor(Math.random() * numDocs);
    metadata.testCases.mediumConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
  }
  
  // Low confidence cases (similarity-based)
  for (let i = 0; i < 100; i++) {
    const userIdx = Math.floor(Math.random() * numUsers);
    const docIdx = Math.floor(Math.random() * numDocs);
    metadata.testCases.lowConfidenceAllow.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
  }
  
  // Should deny cases (cross-tenant, etc.)
  for (let i = 0; i < 50; i++) {
    const newEmpIdx = Math.floor(Math.random() * 10);
    const docIdx = Math.floor(Math.random() * 1000) + 1000; // Higher doc numbers
    metadata.testCases.shouldDeny.push([`newbie:${newEmpIdx}`, 'can_read', `doc:${docIdx}`]);
  }
  
  // Uncertain cases (edge cases)
  for (let i = 0; i < 50; i++) {
    const userIdx = Math.floor(Math.random() * numUsers);
    const docIdx = Math.floor(Math.random() * numDocs);
    metadata.testCases.uncertain.push([`user:${userIdx}`, 'can_read', `doc:${docIdx}`]);
  }
  
  // Define complex relation configurations
  metadata.relationConfigs = {
    can_read: {
      union: [
        { type: 'direct', priority: 10, weight: 1.0 },
        { 
          type: 'tuple_to_userset',
          tuplesetRelation: 'member_of',
          computedRelation: 'can_read',
          priority: 8,
          weight: 0.9
        },
        {
          type: 'tuple_to_userset',
          tuplesetRelation: 'works_on',
          computedRelation: 'can_read',
          priority: 7,
          weight: 0.8
        },
        {
          type: 'similar_to',
          relation: 'can_read',
          k: 5,
          similarityThreshold: 0.4,
          priority: 5,
          weight: 0.6
        },
        {
          type: 'multi_hop',
          path: ['has_role', 'can_read'],
          priority: 4,
          weight: 0.7
        }
      ]
    },
    admin_of: {
      union: [
        { type: 'direct', priority: 10 },
        {
          type: 'tuple_to_userset',
          tuplesetRelation: 'inherits_from',
          computedRelation: 'admin_of',
          priority: 8
        }
      ]
    }
  };
  
  // Calculate statistics
  metadata.generation.stats = {
    totalNodes: nodes.length,
    totalRelations: relations.length,
    nodeTypes: {
      users: nodes.filter(n => n.type === 'user').length,
      groups: nodes.filter(n => n.type === 'group').length,
      docs: nodes.filter(n => n.type === 'doc').length,
      roles: nodes.filter(n => n.type === 'role').length,
      projects: nodes.filter(n => n.type === 'project').length,
      devices: nodes.filter(n => n.type === 'device').length,
      locations: nodes.filter(n => n.type === 'location').length,
      admins: nodes.filter(n => n.type === 'admin').length
    },
    relationTypes: relations.reduce((acc, rel) => {
      acc[rel.rel] = (acc[rel.rel] || 0) + 1;
      return acc;
    }, {}),
    avgPossibility: relations.filter(r => r.possibility).reduce((sum, r) => sum + r.possibility, 0) / relations.filter(r => r.possibility).length
  };
  
  console.log('\\n📊 Graph Statistics:');
  console.log(`Total nodes: ${metadata.generation.stats.totalNodes}`);
  console.log(`Total relations: ${metadata.generation.stats.totalRelations}`);
  console.log(`Node types:`, metadata.generation.stats.nodeTypes);
  console.log(`Relation types:`, metadata.generation.stats.relationTypes);
  console.log(`Average possibility: ${metadata.generation.stats.avgPossibility.toFixed(3)}`);
  
  return { nodes, relations, metadata };
}

// Generate the canonical benchmark graph
console.log('🚀 Generating Canonical Benchmark Graph...');
const canonicalGraph = generateCanonicalBenchmarkGraph({
  numUsers: 10000,
  numGroups: 100,
  numRoles: 20,
  numProjects: 200,
  numDocs: 5000,
  numDevices: 1000,
  numLocations: 50,
  zipfianExponent: 1.2,
  preferentialStrength: 0.8
});

// Save to file
const filename = 'canonical-benchmark-graph.json';
console.log(`\\n💾 Saving to ${filename}...`);
fs.writeFileSync(filename, JSON.stringify(canonicalGraph, null, 2));

console.log('\\n✅ Canonical benchmark graph generated successfully!');
console.log(`📁 Saved to: ${filename}`);
console.log(`📏 Size: ${(fs.statSync(filename).size / 1024 / 1024).toFixed(2)} MB`); 