import { Arbiter } from '../src/core/Arbiter.js';

function parseArgs(argv) {
  const args = new Map();
  for (let i = 2; i < argv.length; i++) {
    const value = argv[i];
    if (!value.startsWith('--')) continue;
    const [key, inline] = value.slice(2).split('=');
    if (inline !== undefined) {
      args.set(key, inline);
      continue;
    }
    const next = argv[i + 1];
    if (next && !next.startsWith('--')) {
      args.set(key, next);
      i++;
    } else {
      args.set(key, true);
    }
  }
  return args;
}

function createRng(seed) {
  if (seed === null || seed === undefined) {
    return Math.random;
  }
  let state = seed >>> 0;
  return () => {
    state = (1664525 * state + 1013904223) >>> 0;
    return state / 0x100000000;
  };
}

function buildZipfSampler(size, skew, rng) {
  const cumulative = new Array(size);
  let sum = 0;
  for (let i = 1; i <= size; i++) {
    sum += 1 / Math.pow(i, skew);
    cumulative[i - 1] = sum;
  }
  return () => {
    const target = rng() * sum;
    let low = 0;
    let high = cumulative.length - 1;
    while (low < high) {
      const mid = Math.floor((low + high) / 2);
      if (target <= cumulative[mid]) {
        high = mid;
      } else {
        low = mid + 1;
      }
    }
    return low;
  };
}

function pickRandom(items, rng) {
  const index = Math.floor(rng() * items.length);
  return items[index];
}

function addToMapSet(map, key, value) {
  if (!map.has(key)) {
    map.set(key, new Set());
  }
  map.get(key).add(value);
}

function basicSimilarity(key1, key2) {
  if (key1 === key2) return 1.0;
  const type1 = key1.split(':')[0];
  const type2 = key2.split(':')[0];
  if (type1 === type2) {
    const id1 = key1.split(':')[1] || '';
    const id2 = key2.split(':')[1] || '';
    const num1 = parseInt(id1, 10);
    const num2 = parseInt(id2, 10);
    if (!isNaN(num1) && !isNaN(num2)) {
      const diff = Math.abs(num1 - num2);
      if (diff === 0) return 1.0;
      if (diff <= 5) return 0.8;
      if (diff <= 20) return 0.6;
      if (diff <= 100) return 0.4;
      return 0.2;
    }
    const maxLen = Math.max(id1.length, id2.length);
    const minLen = Math.min(id1.length, id2.length);
    let matches = 0;
    for (let i = 0; i < minLen; i++) {
      if (id1[i] === id2[i]) matches++;
    }
    return 0.5 + (maxLen > 0 ? (matches / maxLen) * 0.3 : 0);
  }
  return 0.1;
}

function convertToQualitative(value, scale) {
  let closestValue = scale[0];
  let minDistance = Math.abs(value - scale[0]);
  for (const scaleValue of scale) {
    const distance = Math.abs(value - scaleValue);
    if (distance < minDistance) {
      minDistance = distance;
      closestValue = scaleValue;
    }
  }
  return closestValue;
}

function estimateHitRate(frequencies, cacheSize) {
  if (!frequencies.size || cacheSize <= 0) return 0;
  const counts = Array.from(frequencies.values()).sort((a, b) => b - a);
  let total = 0;
  for (const count of counts) total += count;
  let top = 0;
  for (let i = 0; i < Math.min(cacheSize, counts.length); i++) {
    top += counts[i];
  }
  return top / total;
}

function percentile(sortedValues, percentileValue) {
  if (sortedValues.length === 0) return 0;
  const idx = Math.min(
    sortedValues.length - 1,
    Math.max(0, Math.ceil((percentileValue / 100) * sortedValues.length) - 1)
  );
  return sortedValues[idx];
}

function buildGraph({
  users,
  groups,
  resources,
  memberPerUser,
  accessPerGroup,
  viewerPerUser,
  ownerPerUser,
  blockedPerUser,
  riskPerUser,
  linkPerUser,
  linkPerResource,
  linkReversePerResource,
  riskLimitBase,
  riskLimitVariance,
  trustPerUser,
  trustRequirementBase,
  trustRequirementVariance,
  rng
}) {
  const arbiter = new Arbiter();
  const userIds = [];
  const groupIds = [];
  const resourceIds = [];

  for (let i = 0; i < users; i++) {
    userIds.push(arbiter.addNode(`user:${i}`, 'user'));
  }
  for (let i = 0; i < groups; i++) {
    groupIds.push(arbiter.addNode(`group:${i}`, 'group'));
  }
  for (let i = 0; i < resources; i++) {
    resourceIds.push(arbiter.addNode(`resource:${i}`, 'resource'));
  }

  const viewerSet = new Set();
  const ownerSet = new Set();
  const blockedSet = new Set();
  const memberKeySet = new Set();
  const groupAccessKeySet = new Set();
  const riskScoreMap = new Map();
  const linkKeySet = new Set();
  const resourceAccessKeySet = new Set();

  const memberMap = new Map();
  const groupAccessMap = new Map();
  const viewerByResource = new Map();
  const viewerByUser = new Map();
  const linkAdjacency = new Map();
  const linkReverseAdjacency = new Map();
  const resourceAccessMap = new Map();

  const getKey = (nodeId) => arbiter.keyByNodeId.get(nodeId);
  const addDirectEdge = (relation, srcId, dstId, keySet, value) => {
    const key = arbiter.keyManager.createCompositeKey(srcId, relation, dstId);
    if (keySet.has(key)) return false;
    keySet.add(key);
    const srcKey = getKey(srcId);
    const dstKey = getKey(dstId);
    if (value !== undefined) {
      arbiter.addRelation(srcKey, relation, dstKey, 1.0, { value });
      riskScoreMap.set(key, value);
    } else if (relation === 'member') {
      arbiter.addRelation(srcKey, relation, dstKey, 1.0);
    } else if (relation === 'group_access') {
      arbiter.addRelation(srcKey, relation, dstKey, 1.0);
      arbiter.addRelation(dstKey, 'resource_access', srcKey, 1.0);
      addToMapSet(resourceAccessMap, dstId, srcId);
      resourceAccessKeySet.add(
        arbiter.keyManager.createCompositeKey(dstId, 'resource_access', srcId)
      );
    } else {
      arbiter.addRelation(srcKey, relation, dstKey, 1.0);
    }
    return true;
  };

  const addLinkEdge = (srcId, dstId) => {
    const srcKey = getKey(srcId);
    const dstKey = getKey(dstId);
    const key = arbiter.keyManager.createCompositeKey(srcId, 'link', dstId);
    arbiter.addRelation(srcKey, 'link', dstKey, 1.0);
    linkKeySet.add(key);
    addToMapSet(linkAdjacency, srcId, dstId);
    addToMapSet(linkReverseAdjacency, dstId, srcId);
    return key;
  };

  for (const userId of userIds) {
    const membershipCount = Math.max(1, Math.round(memberPerUser));
    for (let i = 0; i < membershipCount; i++) {
      const groupId = pickRandom(groupIds, rng);
      const key = arbiter.keyManager.createCompositeKey(userId, 'member', groupId);
      if (memberKeySet.has(key)) continue;
      memberKeySet.add(key);
      arbiter.addRelation(getKey(userId), 'member', getKey(groupId), 1.0);
      addToMapSet(memberMap, userId, groupId);
    }
  }

  for (const groupId of groupIds) {
    const accessCount = Math.max(1, Math.round(accessPerGroup));
    for (let i = 0; i < accessCount; i++) {
      const resourceId = pickRandom(resourceIds, rng);
      const key = arbiter.keyManager.createCompositeKey(groupId, 'group_access', resourceId);
      if (groupAccessKeySet.has(key)) continue;
      groupAccessKeySet.add(key);
      arbiter.addRelation(getKey(groupId), 'group_access', getKey(resourceId), 1.0);
      arbiter.addRelation(getKey(resourceId), 'resource_access', getKey(groupId), 1.0);
      resourceAccessKeySet.add(
        arbiter.keyManager.createCompositeKey(resourceId, 'resource_access', groupId)
      );
      addToMapSet(resourceAccessMap, resourceId, groupId);
      addToMapSet(groupAccessMap, groupId, resourceId);
    }
  }

  const addRandomUserResourceEdges = (relation, count, keySet) => {
    let added = 0;
    const maxAttempts = count * 3;
    for (let attempt = 0; attempt < maxAttempts && added < count; attempt++) {
      const userId = pickRandom(userIds, rng);
      const resourceId = pickRandom(resourceIds, rng);
      if (addDirectEdge(relation, userId, resourceId, keySet)) {
        added++;
      }
    }
  };

  addRandomUserResourceEdges('viewer', Math.round(users * viewerPerUser), viewerSet);
  addRandomUserResourceEdges('owner', Math.round(users * ownerPerUser), ownerSet);
  addRandomUserResourceEdges('blocked', Math.round(users * blockedPerUser), blockedSet);

  for (const key of viewerSet) {
    const [userId, , resourceId] = JSON.parse(key);
    addToMapSet(viewerByResource, resourceId, userId);
    addToMapSet(viewerByUser, userId, resourceId);
  }

  const riskCount = Math.round(users * riskPerUser);
  let riskAdded = 0;
  const riskAttempts = riskCount * 3;
  for (let attempt = 0; attempt < riskAttempts && riskAdded < riskCount; attempt++) {
    const userId = pickRandom(userIds, rng);
    const resourceId = pickRandom(resourceIds, rng);
    const key = arbiter.keyManager.createCompositeKey(userId, 'risk_score', resourceId);
    if (riskScoreMap.has(key)) continue;
    const value = rng() * 100;
    arbiter.addRelation(getKey(userId), 'risk_score', getKey(resourceId), 1.0, { value });
    riskScoreMap.set(key, value);
    riskAdded++;
  }

  const riskLimitMap = new Map();
  for (const resourceId of resourceIds) {
    const limit = riskLimitBase + rng() * riskLimitVariance;
    const resourceKey = getKey(resourceId);
    const key = arbiter.keyManager.createCompositeKey(resourceId, 'risk_limit', resourceId);
    arbiter.addRelation(resourceKey, 'risk_limit', resourceKey, 1.0, { value: limit });
    riskLimitMap.set(key, limit);
  }

  const trustLevelMap = new Map();
  const trustRequirementMap = new Map();
  const trustCount = Math.max(1, Math.round(users * trustPerUser));
  let trustAdded = 0;
  const trustAttempts = trustCount * 3;
  for (let attempt = 0; attempt < trustAttempts && trustAdded < trustCount; attempt++) {
    const userId = pickRandom(userIds, rng);
    const resourceId = pickRandom(resourceIds, rng);
    const value = rng();
    const key = arbiter.keyManager.createCompositeKey(userId, 'trust_level', resourceId);
    if (trustLevelMap.has(key)) continue;
    arbiter.addRelation(getKey(userId), 'trust_level', getKey(resourceId), 1.0, { value });
    trustLevelMap.set(key, value);
    trustAdded++;
  }
  for (const resourceId of resourceIds) {
    const value = trustRequirementBase + rng() * trustRequirementVariance;
    const resourceKey = getKey(resourceId);
    const key = arbiter.keyManager.createCompositeKey(resourceId, 'trust_requirement', resourceId);
    arbiter.addRelation(resourceKey, 'trust_requirement', resourceKey, 1.0, { value });
    trustRequirementMap.set(key, value);
  }

  const linkEdgesCount = Math.max(1, Math.round(users * linkPerUser));
  for (let i = 0; i < linkEdgesCount; i++) {
    const userId = pickRandom(userIds, rng);
    const resourceId = pickRandom(resourceIds, rng);
    addLinkEdge(userId, resourceId);
  }
  const resourceLinksCount = Math.max(1, Math.round(resources * linkPerResource));
  for (let i = 0; i < resourceLinksCount; i++) {
    const srcResource = pickRandom(resourceIds, rng);
    const dstResource = pickRandom(resourceIds, rng);
    if (srcResource !== dstResource) {
      addLinkEdge(srcResource, dstResource);
    }
  }
  const reverseLinksCount = Math.max(1, Math.round(resources * linkReversePerResource));
  for (let i = 0; i < reverseLinksCount; i++) {
    const srcResource = pickRandom(resourceIds, rng);
    const dstUser = pickRandom(userIds, rng);
    addLinkEdge(srcResource, dstUser);
  }

  for (const userId of userIds) {
    const node = arbiter.nodes.get(userId);
    if (node) {
      node.embedding = arbiter.jsonhash.generateSketch({ type: 'user', id: userId });
      node.stale = false;
    }
  }
  for (const resourceId of resourceIds) {
    const node = arbiter.nodes.get(resourceId);
    if (node) {
      node.embedding = arbiter.jsonhash.generateSketch({ type: 'resource', id: resourceId });
      node.stale = false;
    }
  }
  if (arbiter.embeddingManager) {
    arbiter.embeddingManager.clearPendingStaleNodes();
  }
  arbiter.embeddingManager = null;

  const relationPools = {
    direct: [
      { name: 'viewer', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: viewerSet },
      { name: 'owner', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: ownerSet },
      { name: 'blocked', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: blockedSet },
      { name: 'member', srcType: 'user', dstType: 'group', srcPool: userIds, dstPool: groupIds, keySet: memberKeySet },
      { name: 'group_access', srcType: 'group', dstType: 'resource', srcPool: groupIds, dstPool: resourceIds, keySet: groupAccessKeySet },
      { name: 'link', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: linkKeySet },
      { name: 'link', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keySet: linkKeySet },
      { name: 'link', srcType: 'resource', dstType: 'user', srcPool: resourceIds, dstPool: userIds, keySet: linkKeySet },
      { name: 'resource_access', srcType: 'resource', dstType: 'group', srcPool: resourceIds, dstPool: groupIds, keySet: resourceAccessKeySet }
    ],
    value: [
      { name: 'risk_score', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keyMap: riskScoreMap },
      { name: 'risk_limit', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keyMap: riskLimitMap },
      { name: 'trust_level', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keyMap: trustLevelMap },
      { name: 'trust_requirement', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keyMap: trustRequirementMap }
    ]
  };

  return {
    arbiter,
    userIds,
    groupIds,
    resourceIds,
    relationPools,
    groundTruth: {
      viewerSet,
      ownerSet,
      blockedSet,
      memberMap,
      groupAccessMap,
      viewerByResource,
      viewerByUser,
      riskScoreMap,
      riskLimitMap,
      trustLevelMap,
      trustRequirementMap,
      linkAdjacency,
      linkReverseAdjacency,
      resourceAccessMap
    }
  };
}

function runQueries({
  arbiter,
  total,
  valueRatio,
  samplers,
  rng,
  recordFrequency,
  frequencies,
  relationPools,
  validation
}) {
  const relationManager = arbiter.relationManager;
  const latencies = [];
  let mismatches = 0;
  let falsePositives = 0;
  let falseNegatives = 0;
  for (let i = 0; i < total; i++) {
    const start = process.hrtime.bigint();
    const useValue = relationPools.value.length > 0 && rng() < valueRatio;
    const relationDef = useValue ? pickRandom(relationPools.value, rng) : pickRandom(relationPools.direct, rng);
    const srcSampler = samplers[relationDef.srcType];
    const dstSampler = samplers[relationDef.dstType];
    const srcId = relationDef.srcPool[srcSampler()];
    const dstId = relationDef.dstPool[dstSampler()];
    const relation = relationDef.name;
    const cacheKey = useValue
      ? relationManager._makeValueCacheKey(srcId, relation, dstId)
      : relationManager._makeDirectCacheKey(srcId, relation, dstId);

    if (recordFrequency) {
      const current = frequencies.get(cacheKey) || 0;
      frequencies.set(cacheKey, current + 1);
    }

    if (useValue) {
      const result = relationManager.getValueRelation(srcId, relation, dstId);
      if (validation) {
        const expected = relationDef.keyMap.has(cacheKey);
        if (expected && !result) {
          mismatches++;
          falseNegatives++;
        } else if (!expected && result) {
          mismatches++;
          falsePositives++;
        }
      }
    } else {
      const result = relationManager.getDirectRelation(srcId, relation, dstId);
      if (validation) {
        const expected = relationDef.keySet.has(cacheKey);
        if (expected && !result) {
          mismatches++;
          falseNegatives++;
        } else if (!expected && result) {
          mismatches++;
          falsePositives++;
        }
      }
    }
    const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
    latencies.push(durationMs);
  }
  return { mismatches, falsePositives, falseNegatives, latencies };
}

function runAuthEval({
  arbiter,
  total,
  samplers,
  rng,
  groundTruth,
  userIds,
  resourceIds,
  debugLimit
}) {
  let tp = 0;
  let tn = 0;
  let fp = 0;
  let fn = 0;
  let predictedTrue = 0;
  let predictedFalse = 0;
  const latencies = [];
  const debugFailures = [];
  const debugFalsePositives = [];
  const perRuleStats = new Map();
  const authRelations = [
    { name: 'can_view', weight: 0.12 },
    { name: 'can_edit', weight: 0.06 },
    { name: 'can_view_ttu', weight: 0.1 },
    { name: 'can_view_ttu_reverse', weight: 0.08 },
    { name: 'can_reach', weight: 0.08 },
    { name: 'can_reach_reverse', weight: 0.08 },
    { name: 'similar_view', weight: 0.08 },
    { name: 'similar_view_reverse', weight: 0.08 },
    { name: 'risk_ok', weight: 0.06 },
    { name: 'trust_ok_qual', weight: 0.06 },
    { name: 'can_view_defeasible', weight: 0.08 },
    { name: 'can_view_mixed', weight: 0.06 },
    { name: 'viewer', weight: 0.03 },
    { name: 'owner', weight: 0.03 }
  ];
  const weightSum = authRelations.reduce((sum, item) => sum + item.weight, 0);
  const pickAuthRelation = () => {
    let roll = rng() * weightSum;
    for (const relation of authRelations) {
      if (roll <= relation.weight) return relation.name;
      roll -= relation.weight;
    }
    return authRelations[authRelations.length - 1].name;
  };
  const isBlocked = (userId, resourceId) =>
    groundTruth.blockedSet.has(arbiter.keyManager.createCompositeKey(userId, 'blocked', resourceId));
  const hasDirect = (relation, userId, resourceId) =>
    groundTruth[`${relation}Set`].has(arbiter.keyManager.createCompositeKey(userId, relation, resourceId));
  const ttuExpected = (userId, resourceId) => {
    const groups = groundTruth.memberMap.get(userId);
    if (!groups) return false;
    for (const groupId of groups) {
      const resources = groundTruth.groupAccessMap.get(groupId);
      if (resources && resources.has(resourceId)) {
        return true;
      }
    }
    return false;
  };
  const canViewExpected = (userId, resourceId) => {
    if (isBlocked(userId, resourceId)) return false;
    if (hasDirect('viewer', userId, resourceId) || hasDirect('owner', userId, resourceId)) {
      return true;
    }
    return ttuExpected(userId, resourceId);
  };
  const canReachCache = new Map();
  const canReachReverseCache = new Map();
  const canReachExpected = (userId, resourceId) => {
    if (!groundTruth.linkAdjacency) return false;
    if (!canReachCache.has(userId)) {
      const reachable = new Set();
      const queue = [{ id: userId, depth: 0 }];
      const visited = new Set([userId]);
      while (queue.length > 0) {
        const { id, depth } = queue.shift();
        if (depth >= 3) continue;
        const next = groundTruth.linkAdjacency.get(id);
        if (!next) continue;
        for (const neighbor of next) {
          if (!visited.has(neighbor)) {
            visited.add(neighbor);
            reachable.add(neighbor);
            queue.push({ id: neighbor, depth: depth + 1 });
          }
        }
      }
      canReachCache.set(userId, reachable);
    }
    return canReachCache.get(userId).has(resourceId);
  };
  const canReachReverseExpected = (userId, resourceId) => {
    if (!groundTruth.linkReverseAdjacency) return false;
    if (!canReachReverseCache.has(userId)) {
      const reachable = new Set();
      const queue = [{ id: userId, depth: 0 }];
      const visited = new Set([userId]);
      while (queue.length > 0) {
        const { id, depth } = queue.shift();
        if (depth >= 3) continue;
        const next = groundTruth.linkReverseAdjacency.get(id);
        if (!next) continue;
        for (const neighbor of next) {
          if (!visited.has(neighbor)) {
            visited.add(neighbor);
            reachable.add(neighbor);
            queue.push({ id: neighbor, depth: depth + 1 });
          }
        }
      }
      canReachReverseCache.set(userId, reachable);
    }
    return canReachReverseCache.get(userId).has(resourceId);
  };
  const similarityThreshold = 0.6;
  const similarViewExpected = (userId, resourceId) => {
    const viewers = groundTruth.viewerByResource.get(resourceId);
    if (!viewers) return false;
    const userKey = arbiter.keyByNodeId.get(userId);
    const userNode = arbiter.nodes.get(userId);
    for (const viewerId of viewers) {
      const viewerKey = arbiter.keyByNodeId.get(viewerId);
      const viewerNode = arbiter.nodes.get(viewerId);
      if (userNode?.embedding && viewerNode?.embedding) {
        if (arbiter.jsonhash.compareSketches(userNode.embedding, viewerNode.embedding) >= similarityThreshold) {
          return true;
        }
      } else if (basicSimilarity(userKey, viewerKey) >= similarityThreshold) {
        return true;
      }
    }
    return false;
  };
  const similarViewReverseExpected = (userId, resourceId) => {
    const resources = groundTruth.viewerByUser.get(userId);
    if (!resources) return false;
    const resourceKey = arbiter.keyByNodeId.get(resourceId);
    const resourceNode = arbiter.nodes.get(resourceId);
    for (const viewedResourceId of resources) {
      const viewedKey = arbiter.keyByNodeId.get(viewedResourceId);
      const viewedNode = arbiter.nodes.get(viewedResourceId);
      if (resourceNode?.embedding && viewedNode?.embedding) {
        if (arbiter.jsonhash.compareSketches(resourceNode.embedding, viewedNode.embedding) >= similarityThreshold) {
          return true;
        }
      } else if (basicSimilarity(resourceKey, viewedKey) >= similarityThreshold) {
        return true;
      }
    }
    return false;
  };
  const riskOkExpected = (userId, resourceId) => {
    const riskRel = arbiter.relationManager.getDirectRelation(userId, 'risk_score', resourceId);
    const limitRel = arbiter.relationManager.getDirectRelation(resourceId, 'risk_limit', resourceId);
    if (!riskRel || !limitRel) {
      return false;
    }
    const riskValue = arbiter.valueManager.getBlurredValue(riskRel);
    const limitValue = arbiter.valueManager.getBlurredValue(limitRel);
    if (!riskValue || !limitValue || !riskValue.interval || !limitValue.interval) {
      return false;
    }
    const possibility = arbiter.valueManager.compareIntervals(
      riskValue.interval,
      limitValue.interval,
      '<='
    );
    return possibility > 0;
  };
  const trustScale = [0, 0.25, 0.5, 0.75, 1];
  const trustOkQualExpected = (userId, resourceId) => {
    const trustLevel = groundTruth.trustLevelMap.get(
      arbiter.keyManager.createCompositeKey(userId, 'trust_level', resourceId)
    );
    const trustRequirement = groundTruth.trustRequirementMap.get(
      arbiter.keyManager.createCompositeKey(resourceId, 'trust_requirement', resourceId)
    );
    if (trustLevel === undefined || trustRequirement === undefined) return false;
    const left = convertToQualitative(trustLevel, trustScale);
    const right = convertToQualitative(trustRequirement, trustScale);
    return left >= right;
  };
  const canViewDefeasibleExpected = (userId, resourceId) => {
    if (!riskOkExpected(userId, resourceId)) return false;
    if (isBlocked(userId, resourceId)) return false;
    return hasDirect('viewer', userId, resourceId) ||
      hasDirect('owner', userId, resourceId) ||
      ttuExpected(userId, resourceId);
  };
  const ttuReverseExpected = (userId, resourceId) => {
    const groups = groundTruth.memberMap.get(userId);
    if (!groups) return false;
    const resourceGroups = groundTruth.resourceAccessMap.get(resourceId);
    if (!resourceGroups) return false;
    for (const groupId of groups) {
      if (resourceGroups.has(groupId)) {
        return true;
      }
    }
    return false;
  };
  const canViewMixedExpected = (userId, resourceId) =>
    canViewExpected(userId, resourceId) ||
    canReachReverseExpected(userId, resourceId) ||
    similarViewReverseExpected(userId, resourceId) ||
    trustOkQualExpected(userId, resourceId);
  const pickFromArray = (arr) => arr[Math.floor(rng() * arr.length)];
  const viewerPairs = Array.from(groundTruth.viewerSet, key => {
    const [userId, , resourceId] = JSON.parse(key);
    return { userId, resourceId };
  });
  const ownerPairs = Array.from(groundTruth.ownerSet, key => {
    const [userId, , resourceId] = JSON.parse(key);
    return { userId, resourceId };
  });
  const ttuPairs = [];
  const groupMembers = new Map();
  for (const [userId, groups] of groundTruth.memberMap.entries()) {
    for (const groupId of groups) {
      addToMapSet(groupMembers, groupId, userId);
      const resources = groundTruth.groupAccessMap.get(groupId);
      if (!resources) continue;
      for (const resourceId of resources) {
        ttuPairs.push({ userId, resourceId });
      }
    }
  }
  const ttuReversePairs = [];
  for (const [resourceId, groups] of groundTruth.resourceAccessMap.entries()) {
    for (const groupId of groups) {
      const members = groupMembers.get(groupId);
      if (!members) continue;
      for (const userId of members) {
        ttuReversePairs.push({ userId, resourceId });
      }
    }
  }
  const buildReachPairs = (adjacency, sourceIds) => {
    const pairs = [];
    for (const sourceId of sourceIds) {
      const reachable = new Set();
      const queue = [{ id: sourceId, depth: 0 }];
      const visited = new Set([sourceId]);
      while (queue.length > 0) {
        const { id, depth } = queue.shift();
        if (depth >= 3) continue;
        const next = adjacency.get(id);
        if (!next) continue;
        for (const neighbor of next) {
          if (!visited.has(neighbor)) {
            visited.add(neighbor);
            reachable.add(neighbor);
            queue.push({ id: neighbor, depth: depth + 1 });
          }
        }
      }
      let added = 0;
      for (const targetId of reachable) {
        pairs.push({ userId: sourceId, resourceId: targetId });
        added++;
        if (added >= 50) break;
      }
    }
    return pairs;
  };
  const canReachPairs = buildReachPairs(groundTruth.linkAdjacency, userIds);
  const userIdSet = new Set(userIds);
  const resourceIdSet = new Set(resourceIds);
  const canReachReversePairs = buildReachPairs(groundTruth.linkReverseAdjacency, userIds)
    .filter(pair => resourceIdSet.has(pair.resourceId));
  const similarViewReversePairs = [];
  for (const [userId, resources] of groundTruth.viewerByUser.entries()) {
    for (const resourceId of resources) {
      similarViewReversePairs.push({ userId, resourceId });
    }
  }
  const riskOkPairs = [];
  for (const key of groundTruth.riskScoreMap.keys()) {
    const [userId, , resourceId] = JSON.parse(key);
    if (riskOkExpected(userId, resourceId)) {
      riskOkPairs.push({ userId, resourceId });
    }
  }
  const trustOkPairs = [];
  for (const key of groundTruth.trustLevelMap.keys()) {
    const [userId, , resourceId] = JSON.parse(key);
    if (trustOkQualExpected(userId, resourceId)) {
      trustOkPairs.push({ userId, resourceId });
    }
  }
  const canViewDefeasiblePairs = ttuPairs.filter(pair => canViewDefeasibleExpected(pair.userId, pair.resourceId));
  const canViewPairs = [...viewerPairs, ...ownerPairs, ...ttuPairs];
  const canViewMixedPairs = [
    ...canViewPairs,
    ...canReachReversePairs,
    ...similarViewReversePairs,
    ...trustOkPairs
  ].filter(pair => canViewMixedExpected(pair.userId, pair.resourceId));
  const expectedForRelation = (relation, userId, resourceId) => {
    if (relation === 'viewer' || relation === 'owner') {
      return hasDirect(relation, userId, resourceId);
    }
    if (relation === 'can_view') {
      return canViewExpected(userId, resourceId);
    }
    if (relation === 'can_edit') {
      return hasDirect('owner', userId, resourceId) && canViewExpected(userId, resourceId);
    }
    if (relation === 'can_view_ttu') {
      return ttuExpected(userId, resourceId);
    }
    if (relation === 'can_view_ttu_reverse') {
      return ttuReverseExpected(userId, resourceId);
    }
    if (relation === 'can_reach') {
      return canReachExpected(userId, resourceId);
    }
    if (relation === 'can_reach_reverse') {
      return canReachReverseExpected(userId, resourceId);
    }
    if (relation === 'similar_view') {
      return similarViewExpected(userId, resourceId);
    }
    if (relation === 'similar_view_reverse') {
      return similarViewReverseExpected(userId, resourceId);
    }
    if (relation === 'risk_ok') {
      return riskOkExpected(userId, resourceId);
    }
    if (relation === 'trust_ok_qual') {
      return trustOkQualExpected(userId, resourceId);
    }
    if (relation === 'can_view_defeasible') {
      return canViewDefeasibleExpected(userId, resourceId);
    }
    if (relation === 'can_view_mixed') {
      return canViewMixedExpected(userId, resourceId);
    }
    return false;
  };
  const positiveSampleRate = 0.45;
  const maxSampleAttempts = 40;
  const pickPairForRelation = (relation) => {
    const positivePools = {
      viewer: viewerPairs,
      owner: ownerPairs,
      can_view: canViewPairs,
      can_edit: canViewPairs,
      can_view_ttu: ttuPairs,
      can_view_ttu_reverse: ttuReversePairs,
      can_reach: canReachPairs,
      can_reach_reverse: canReachReversePairs,
      similar_view: viewerPairs,
      similar_view_reverse: similarViewReversePairs,
      risk_ok: riskOkPairs,
      trust_ok_qual: trustOkPairs,
      can_view_defeasible: canViewDefeasiblePairs,
      can_view_mixed: canViewMixedPairs
    };
    const pool = positivePools[relation] || [];
    const forcePositive = new Set([
      'can_view_ttu_reverse',
      'can_reach_reverse',
      'similar_view_reverse',
      'trust_ok_qual'
    ]);
    if (pool.length > 0 && (forcePositive.has(relation) || rng() < positiveSampleRate)) {
      return pickFromArray(pool);
    }
    if (rng() < positiveSampleRate) {
      const attempts = relation.endsWith('_reverse') ? maxSampleAttempts * 2 : maxSampleAttempts;
      for (let attempt = 0; attempt < attempts; attempt++) {
        const candidateUser = userIds[samplers.user()];
        const candidateResource = resourceIds[samplers.resource()];
        if (expectedForRelation(relation, candidateUser, candidateResource)) {
          return { userId: candidateUser, resourceId: candidateResource };
        }
      }
    }
    return {
      userId: userIds[samplers.user()],
      resourceId: resourceIds[samplers.resource()]
    };
  };
  for (let i = 0; i < total; i++) {
    const relation = pickAuthRelation();
    const sampled = pickPairForRelation(relation);
    const userId = sampled.userId;
    const resourceId = sampled.resourceId;
    const expected = expectedForRelation(relation, userId, resourceId);
    const start = process.hrtime.bigint();
    const result = arbiter.check(
      arbiter.keyByNodeId.get(userId),
      relation,
      arbiter.keyByNodeId.get(resourceId)
    );
    const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
    latencies.push(durationMs);
    const predicted = result && result.possibility > 0;
    if (predicted) {
      predictedTrue++;
    } else {
      predictedFalse++;
    }
    if (expected && predicted) {
      tp++;
    } else if (!expected && !predicted) {
      tn++;
    } else if (!expected && predicted) {
      fp++;
      if (debugFalsePositives.length < debugLimit) {
        let riskDetails = null;
        if (relation === 'risk_ok') {
          const riskRel = arbiter.relationManager.getDirectRelation(userId, 'risk_score', resourceId);
          const limitRel = arbiter.relationManager.getDirectRelation(resourceId, 'risk_limit', resourceId);
          const riskValue = riskRel ? arbiter.valueManager.getBlurredValue(riskRel) : null;
          const limitValue = limitRel ? arbiter.valueManager.getBlurredValue(limitRel) : null;
          const comparison = riskValue && limitValue && riskValue.interval && limitValue.interval
            ? arbiter.valueManager.compareIntervals(riskValue.interval, limitValue.interval, '<=')
            : null;
          riskDetails = {
            riskRelationExists: !!riskRel,
            riskValue: riskRel ? riskRel.value : null,
            riskInterval: riskValue ? riskValue.interval : null,
            limitRelationExists: !!limitRel,
            limitValue: limitRel ? limitRel.value : null,
            limitInterval: limitValue ? limitValue.interval : null,
            comparison
          };
        }
        debugFalsePositives.push({
          relation,
          userId,
          resourceId,
          predictedPossibility: result ? result.possibility : 0,
          predictedReason: result ? result.reason : null,
          predictedMeta: result ? result.meta : null,
          riskDetails
        });
      }
    } else if (expected && !predicted) {
      fn++;
      if (debugFailures.length < debugLimit) {
        const viewerKey = arbiter.keyManager.createCompositeKey(userId, 'viewer', resourceId);
        const ownerKey = arbiter.keyManager.createCompositeKey(userId, 'owner', resourceId);
        const blockedKey = arbiter.keyManager.createCompositeKey(userId, 'blocked', resourceId);
        const viewerDirect = groundTruth.viewerSet.has(viewerKey);
        const ownerDirect = groundTruth.ownerSet.has(ownerKey);
        const blocked = groundTruth.blockedSet.has(blockedKey);
        const memberGroups = groundTruth.memberMap.get(userId) || new Set();
        const accessGroups = [];
        for (const groupId of memberGroups) {
          const resources = groundTruth.groupAccessMap.get(groupId);
          if (resources && resources.has(resourceId)) {
            accessGroups.push(groupId);
          }
        }
        const userKey = arbiter.keyByNodeId.get(userId);
        const resourceKey = arbiter.keyByNodeId.get(resourceId);
        const bypassResult = arbiter.authChecker.check(userKey, relation, resourceKey, { bypassPLTC: true });
        const memberRelations = arbiter.relationManager.getRelationsFromSrc(userId, 'member');
        const groupAccessHasResource = accessGroups.length > 0
          ? arbiter.relationManager.getRelationsFromSrc(accessGroups[0], 'group_access').some(rel => rel.dst === resourceId)
          : false;
        const chainRule = {
          type: 'chain',
          steps: [
            { relation: 'member', direction: 'out' },
            { relation: 'group_access', direction: 'out' }
          ]
        };
        const chainResult = arbiter.authChecker.ruleEvaluator.evaluateRule(
          userId,
          userKey,
          resourceId,
          resourceKey,
          chainRule,
          new Set(),
          relation,
          { bypassPLTC: true }
        );
        debugFailures.push({
          relation,
          userId,
          resourceId,
          viewerDirect,
          ownerDirect,
          blocked,
          memberGroups: Array.from(memberGroups),
          accessGroups,
          memberRelationsCount: memberRelations.length,
          groupAccessHasResource,
          chainPossibility: chainResult ? chainResult.possibility : 0,
          predictedPossibility: result ? result.possibility : 0,
          bypassPossibility: bypassResult ? bypassResult.possibility : 0,
          bypassReason: bypassResult ? bypassResult.reason : null,
          bypassMeta: bypassResult ? bypassResult.meta : null
        });
      }
    }
    if (!perRuleStats.has(relation)) {
      perRuleStats.set(relation, {
        tp: 0,
        tn: 0,
        fp: 0,
        fn: 0,
        predictedTrue: 0,
        predictedFalse: 0,
        latencies: []
      });
    }
    const ruleStats = perRuleStats.get(relation);
    ruleStats.latencies.push(durationMs);
    if (predicted) {
      ruleStats.predictedTrue++;
    } else {
      ruleStats.predictedFalse++;
    }
    if (expected && predicted) {
      ruleStats.tp++;
    } else if (!expected && !predicted) {
      ruleStats.tn++;
    } else if (!expected && predicted) {
      ruleStats.fp++;
    } else if (expected && !predicted) {
      ruleStats.fn++;
    }
  }
  const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
  const recall = tp + fn > 0 ? tp / (tp + fn) : 0;
  const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
  return {
    tp,
    tn,
    fp,
    fn,
    predictedTrue,
    predictedFalse,
    precision,
    recall,
    f1,
    debugFailures,
    debugFalsePositives,
    latencies,
    perRuleStats
  };
}

const args = parseArgs(process.argv);
const config = {
  users: Number(args.get('users') || 200),
  groups: Number(args.get('groups') || 100),
  resources: Number(args.get('resources') || 300),
  memberPerUser: Number(args.get('member-per-user') || 3),
  accessPerGroup: Number(args.get('access-per-group') || 6),
  viewerPerUser: Number(args.get('viewer-per-user') || 2),
  ownerPerUser: Number(args.get('owner-per-user') || 1),
  blockedPerUser: Number(args.get('blocked-per-user') || 0.2),
  riskPerUser: Number(args.get('risk-per-user') || 1.5),
  linkPerUser: Number(args.get('link-per-user') || 2),
  linkPerResource: Number(args.get('link-per-resource') || 2),
  linkReversePerResource: Number(args.get('link-reverse-per-resource') || 1),
  riskLimitBase: Number(args.get('risk-limit-base') || 50),
  riskLimitVariance: Number(args.get('risk-limit-variance') || 30),
  trustPerUser: Number(args.get('trust-per-user') || 2),
  trustRequirementBase: Number(args.get('trust-requirement-base') || 0.3),
  trustRequirementVariance: Number(args.get('trust-requirement-variance') || 0.5),
  queries: Number(args.get('queries') || 20000),
  warmup: Number(args.get('warmup') || 2000),
  zipf: Number(args.get('zipf') || 1.1),
  valueRatio: Number(args.get('value-ratio') || 0.2),
  seed: args.has('seed') ? Number(args.get('seed')) : null,
  debugFailures: Number(args.get('debug-failures') || 5)
};

const rng = createRng(config.seed);
const samplers = {
  user: buildZipfSampler(config.users, config.zipf, rng),
  group: buildZipfSampler(config.groups, config.zipf, rng),
  resource: buildZipfSampler(config.resources, config.zipf, rng)
};
const { arbiter, relationPools, groundTruth, userIds, resourceIds } = buildGraph({
  users: config.users,
  groups: config.groups,
  resources: config.resources,
  memberPerUser: config.memberPerUser,
  accessPerGroup: config.accessPerGroup,
  viewerPerUser: config.viewerPerUser,
  ownerPerUser: config.ownerPerUser,
  blockedPerUser: config.blockedPerUser,
  riskPerUser: config.riskPerUser,
  linkPerUser: config.linkPerUser,
  linkPerResource: config.linkPerResource,
  linkReversePerResource: config.linkReversePerResource,
  riskLimitBase: config.riskLimitBase,
  riskLimitVariance: config.riskLimitVariance,
  trustPerUser: config.trustPerUser,
  trustRequirementBase: config.trustRequirementBase,
  trustRequirementVariance: config.trustRequirementVariance,
  rng
});

arbiter.setRelationConfig('viewer', { type: 'direct' });
arbiter.setRelationConfig('owner', { type: 'direct' });
arbiter.setRelationConfig('member', { type: 'direct' });
arbiter.setRelationConfig('group_access', { type: 'direct' });
arbiter.setRelationConfig('blocked', { type: 'direct' });
arbiter.setRelationConfig('risk_score', { type: 'direct' });
arbiter.setRelationConfig('risk_limit', { type: 'direct' });
arbiter.setRelationConfig('link', { type: 'direct' });
arbiter.setRelationConfig('resource_access', { type: 'direct' });
arbiter.setRelationConfig('trust_level', { type: 'direct' });
arbiter.setRelationConfig('trust_requirement', { type: 'direct' });
arbiter.setRelationConfig('can_view', {
  union: [
    { type: 'direct', relation: 'viewer' },
    { type: 'direct', relation: 'owner' },
    {
      type: 'chain',
      steps: [
        { relation: 'member', direction: 'out' },
        { relation: 'group_access', direction: 'out' }
      ]
    }
  ],
  unless: {
    union: [
      { type: 'direct', relation: 'blocked' }
    ]
  }
});
arbiter.setRelationConfig('can_edit', {
  intersection: [
    { type: 'direct', relation: 'owner' },
    { type: 'computed', relation: 'can_view' }
  ]
});
arbiter.setRelationConfig('can_view_ttu', {
  type: 'tuple_to_userset',
  tuplesetRelation: 'group_access',
  tuplesetDirection: 'in',
  computedRelation: 'member'
});
arbiter.setRelationConfig('can_view_ttu_reverse', {
  type: 'tuple_to_userset',
  tuplesetRelation: 'member',
  reverse: true,
  computedRelation: 'resource_access'
});
arbiter.setRelationConfig('can_reach', {
  type: 'multi_hop',
  relation: 'link',
  maxDepth: 4
});
arbiter.setRelationConfig('can_reach_reverse', {
  type: 'multi_hop',
  relation: 'link',
  maxDepth: 4,
  reverse: true
});
arbiter.setRelationConfig('similar_view', {
  type: 'similar_to',
  relation: 'viewer',
  k: 5,
  similarityThreshold: 0.6,
  fallbackToBasicSimilarity: true
});
arbiter.setRelationConfig('similar_view_reverse', {
  type: 'similar_to',
  relation: 'viewer',
  reverse: true,
  k: 5,
  similarityThreshold: 0.6,
  fallbackToBasicSimilarity: true
});
arbiter.setRelationConfig('risk_ok', {
  type: 'relational_comparator',
  comparator: '<=',
  fallbackBehavior: 'deny',
  left: {
    rule: { type: 'direct', relation: 'risk_score' },
    extractValue: true,
    valueRelation: 'risk_score'
  },
  right: {
    rule: { type: 'direct', relation: 'risk_limit' },
    extractValue: true,
    valueRelation: 'risk_limit',
    evaluateFrom: 'object'
  }
});
arbiter.setRelationConfig('trust_ok_qual', {
  type: 'relational_comparator',
  qualitative: true,
  scaleName: 'five-point',
  comparator: '>=',
  fallbackBehavior: 'deny',
  left: {
    rule: { type: 'direct', relation: 'trust_level' },
    extractValue: true,
    valueRelation: 'trust_level'
  },
  right: {
    rule: { type: 'direct', relation: 'trust_requirement' },
    extractValue: true,
    valueRelation: 'trust_requirement',
    evaluateFrom: 'object'
  }
});
arbiter.setRelationConfig('can_view_defeasible', {
  union: [
    { type: 'direct', relation: 'viewer' },
    { type: 'direct', relation: 'owner' },
    { type: 'computed', relation: 'can_view_ttu' }
  ],
  unless: {
    union: [
      { type: 'direct', relation: 'blocked' }
    ]
  },
  requires: {
    union: [
      { type: 'computed', relation: 'risk_ok' }
    ]
  }
});
arbiter.setRelationConfig('can_view_mixed', {
  union: [
    { type: 'computed', relation: 'can_view' },
    { type: 'computed', relation: 'can_reach_reverse' },
    { type: 'computed', relation: 'similar_view_reverse' },
    { type: 'computed', relation: 'trust_ok_qual' }
  ]
});

arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true });


if (config.warmup > 0) {
  runQueries({
    arbiter,
    total: config.warmup,
    valueRatio: config.valueRatio,
    samplers,
    rng,
    recordFrequency: false,
    frequencies: new Map(),
    relationPools,
    validation: false
  });
}

arbiter.relationManager.resetCacheStats();
const frequencies = new Map();
const startTime = Date.now();
const validation = runQueries({
  arbiter,
  total: config.queries,
  valueRatio: config.valueRatio,
  samplers,
  rng,
  recordFrequency: true,
  frequencies,
  relationPools,
  validation: true
});
const elapsedMs = Date.now() - startTime;
const qps = elapsedMs > 0 ? Math.round((config.queries / elapsedMs) * 1000) : 0;
const lookupLatencies = [...validation.latencies].sort((a, b) => a - b);
const lookupP95 = percentile(lookupLatencies, 95).toFixed(3);
const lookupP99 = percentile(lookupLatencies, 99).toFixed(3);

const stats = arbiter.relationManager.getCacheStats();
const estimatedHitRate = estimateHitRate(frequencies, arbiter.relationManager._maxCacheSize);

const authStart = Date.now();
const authEval = runAuthEval({
  arbiter,
  total: config.queries,
  samplers,
  rng,
  groundTruth,
  userIds,
  resourceIds,
  debugLimit: config.debugFailures
});
const authElapsed = Date.now() - authStart;
const authQps = authElapsed > 0 ? Math.round((config.queries / authElapsed) * 1000) : 0;
const authLatencies = [...authEval.latencies].sort((a, b) => a - b);
const authP95 = percentile(authLatencies, 95).toFixed(3);
const authP99 = percentile(authLatencies, 99).toFixed(3);
const perRuleOutput = [];
for (const [relation, stats] of authEval.perRuleStats.entries()) {
  const ruleLatencies = [...stats.latencies].sort((a, b) => a - b);
  const ruleP95 = percentile(ruleLatencies, 95);
  const ruleP99 = percentile(ruleLatencies, 99);
  const precision = stats.tp + stats.fp > 0 ? stats.tp / (stats.tp + stats.fp) : 0;
  const recall = stats.tp + stats.fn > 0 ? stats.tp / (stats.tp + stats.fn) : 0;
  const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
  perRuleOutput.push({
    relation,
    tp: stats.tp,
    tn: stats.tn,
    fp: stats.fp,
    fn: stats.fn,
    predictedTrue: stats.predictedTrue,
    predictedFalse: stats.predictedFalse,
    precision: Number(precision.toFixed(3)),
    recall: Number(recall.toFixed(3)),
    f1: Number(f1.toFixed(3)),
    p95: Number(ruleP95.toFixed(3)),
    p99: Number(ruleP99.toFixed(3))
  });
}
perRuleOutput.sort((a, b) => a.relation.localeCompare(b.relation));

console.log('Zipfian cache evaluation');
console.log(`users=${config.users} groups=${config.groups} resources=${config.resources} queries=${config.queries}`);
console.log(`zipf=${config.zipf} value_ratio=${config.valueRatio} warmup=${config.warmup}`);
console.log(`cache_hits=${stats.cacheHits} cache_misses=${stats.cacheMisses} hit_rate=${stats.hitRate}`);
console.log(`relation_cache_size=${stats.relationCacheSize} value_cache_size=${stats.valueCacheSize}`);
console.log(`estimated_topk_hit_rate=${estimatedHitRate.toFixed(3)}`);
console.log(`elapsed_ms=${elapsedMs} qps=${qps}`);
console.log(`lookup_p95_ms=${lookupP95} lookup_p99_ms=${lookupP99}`);
console.log(`validation_mismatches=${validation.mismatches} false_pos=${validation.falsePositives} false_neg=${validation.falseNegatives}`);
console.log(`auth_elapsed_ms=${authElapsed} auth_qps=${authQps}`);
console.log(`auth_p95_ms=${authP95} auth_p99_ms=${authP99}`);
console.log('auth_per_rule');
for (const entry of perRuleOutput) {
  console.log(JSON.stringify(entry));
}
console.log(`auth_predicted_true=${authEval.predictedTrue} auth_predicted_false=${authEval.predictedFalse}`);
console.log(`auth_tp=${authEval.tp} auth_tn=${authEval.tn} auth_fp=${authEval.fp} auth_fn=${authEval.fn}`);
console.log(`auth_precision=${authEval.precision.toFixed(3)} auth_recall=${authEval.recall.toFixed(3)} auth_f1=${authEval.f1.toFixed(3)}`);
if (authEval.debugFailures.length) {
  console.log('auth_failure_samples');
  for (const sample of authEval.debugFailures) {
    console.log(JSON.stringify(sample));
  }
}
if (authEval.debugFalsePositives.length) {
  console.log('auth_false_positive_samples');
  for (const sample of authEval.debugFalsePositives) {
    console.log(JSON.stringify(sample));
  }
}
