import { GraphIndices } from './GraphIndices.js';
import { NodeManager } from './NodeManager.js';
import { RelationManager } from './RelationManager.js';
import { AuthorizationChecker } from '../authorization/AuthorizationChecker.js';
import { DecisionCache } from '../authorization/DecisionCache.js';
import { GraphTraversal } from '../traversal/GraphTraversal.js';
import { ValueManager } from './ValueManager.js';
import { defaultCacheFactory } from './cache.js';
import { ReachabilityChecker } from './ReachabilityChecker.js';
import { GraphManager } from './graph/GraphManager.js';
import { UnifiedKeyManager } from './UnifiedKeyManager.js';
import { PartialGraphContext } from './PartialGraphContext.js';
import { ExplainSerializer } from './ExplainSerializer.js';
import { RuleCompiler } from '../authorization/RuleCompiler.js';
import { ArbiterConfig } from './arbiter/ArbiterConfig.js';
import { ArbiterSnapshot } from './arbiter/ArbiterSnapshot.js';
import { ArbiterChecks } from './arbiter/ArbiterChecks.js';
import { SimilarityManager } from '../similarity/SimilarityManager.js';

export class Arbiter {
  static log(...args) {
    // No-op for production
  }

  constructor(options = {}) {
    this.options = options;

    // Monotonic graph mutation counter: incremented on every relation edge
    // mutation (and node removal). Derived-result caches (e.g. the ChainRule
    // result cache) stamp entries with the version they were computed at and
    // treat any newer version as a miss, so a graph change can never serve a
    // stale chain/authorization result.
    this._graphVersion = 0;

    // Audit affordance (caller-wired, zero cost when absent): invoked once
    // per check with a minimal decision record. The engine does NOT store
    // audit state — the caller owns persistence and retention. The record
    // is built only when the hook exists, so the default path is untouched.
    this._auditHook = typeof options.audit === 'function' ? options.audit : null;

    // The engine's clock. The caller may inject a clock (options.clock);
    // the wall clock is the fallback for unpinned callers only. Per-check
    // time is always the caller's `{ now }` / partialGraph.now — this
    // clock only drives UNPINNED cache-entry freshness.
    this.clock = typeof options.clock === 'function' ? options.clock : (() => Date.now());
    
    // Injectable cache factory (DI): defaults to the built-in SimpleLRUCache.
    // Pass options.cacheFactory to substitute another cache implementation.
    this.cacheFactory = options.cacheFactory || defaultCacheFactory;

    // Caching control options
    this.disableCaching = options.disableCaching || false;
    this.disableChainCaching = options.disableChainCaching || false;
    this.disableDirectCaching = options.disableDirectCaching || false;
    
    // Core data structures (needed by existing managers)
    this.nodes = new Map();
    this.nodeIdByKey = new Map();
    this.keyByNodeId = new Map();
    this.nextNodeId = 0;
    this.relations = [];
    this.hubIds = new Set();
    
    // Initialize unified key manager
    this.keyManager = new UnifiedKeyManager(options.keyManager);
    
    // Initialize graph manager
    this.graphManager = new GraphManager(this, options);
    
    // Direct check cache - use HyperbolicLRUCache for better memory management
    this.directCheckCacheSize = options.directCheckCacheSize || 10000;
    this.directCheckCacheTTL = options.directCheckCacheTTL || 60000; // 1 minute

    // Direct-check cache key indexes (keys are rolling hashes, so relation-
    // and node-level invalidation cannot parse them): relation -> keys,
    // nodeId -> keys, key -> relations. Populated via _trackDirectCheckKey.
    this.directCheckCacheKeysByRelation = new Map();
    this.directCheckCacheKeysByNode = new Map();
    this.directKeyToRelations = new Map();
    // Reverse of dependencyIndex: checked relation -> Set of base relations it
    // reads (so a base-relation mutation invalidates overridden-config keys).
    this.dependencyIndexByRelation = new Map();
    
    // Only create cache if caching is not disabled
    if (!this.disableCaching && !this.disableDirectCaching) {
      this.directCheckCache = this.cacheFactory(this.directCheckCacheSize, {
        onEvict: (key, value) => {
          // Track evictions for debugging
          this.directCheckCacheStats = this.directCheckCacheStats || { evictions: 0 };
          this.directCheckCacheStats.evictions++;
        }
      });
    } else {
      this.directCheckCache = null;
    }

    this.ruleResultCacheSize = options.ruleResultCacheSize || 20000;
    this.ruleResultCacheTTL = options.ruleResultCacheTTL || 60000;
    this.ruleResultCacheStats = { hits: 0, misses: 0, evictions: 0 };
    const enableRuleResultCache = options.enableRuleResultCache !== undefined
      ? options.enableRuleResultCache
      : true;
    this.ruleResultCache = (!this.disableCaching && enableRuleResultCache)
      ? this.cacheFactory(this.ruleResultCacheSize, {
        onEvict: (key) => {
          this.ruleResultCacheStats.evictions++;
          this._removeRuleResultCacheKey(key);
        }
      })
      : null;
    this.ruleResultCacheKeysByRelation = new Map();
    this.ruleResultCacheRelationByKey = new Map();
    this.dependencyIndex = new Map();

    
    // Initialize managers
    this.valueManager = new ValueManager(this);
    this.indices = new GraphIndices();
    // Build the DecisionCache port before any consumer so AuthorizationChecker
    // and NodeManager receive it via DI. RF-03 + RF-04 closure.
    this.decisionCache = options.decisionCache || new DecisionCache(this);
    this.nodeManager = new NodeManager(this, { decisionCache: this.decisionCache });
    this.relationManager = new RelationManager(this);
    this.embeddingManager = null;
    this.similarityManager = null;
    this.authChecker = new AuthorizationChecker(this, { decisionCache: this.decisionCache });
    this.traversal = new GraphTraversal(this);
    this.reachabilityChecker = new ReachabilityChecker(this);
    this.explainSerializer = new ExplainSerializer(this);
    
    // Initialize graph manager with dependencies
    this.graphManager.initialize({
      traversal: this.traversal,
      reachabilityChecker: this.reachabilityChecker,
      authChecker: this.authChecker
    });

    if (options.similarityParams !== undefined) {
      this.similarityManager = new SimilarityManager(this, options.similarityParams || {});
    }
    
    // Performance settings
    this.lazyStaleMarking = options.lazyStaleMarking !== undefined ? options.lazyStaleMarking : true;
    this.fastConstructionMode = options.fastConstructionMode !== undefined ? options.fastConstructionMode : false;
    this.indicesBuilt = false;
    
    // Hub settings for distance estimation
    this.maxLandmarkSample = options.maxLandmarkSample || 16;
    
    // Vector similarity
    this.jsonhash = null;
    
    // Relation configurations
    this.relationConfigs = new Map();
    this.ruleCompiler = new RuleCompiler(this);
    this.partialGraphPolicy = {
      conflict_mode: options?.partialGraphPolicy?.conflict_mode
        || options?.partialGraphPolicy?.conflictMode
        || 'deterministic',
      reducers: {
        ...(options?.partialGraphPolicy?.reducers || {})
      },
      // Size/reserved limits must survive construction: the partial-graph
      // context reads them from the policy, and dropping them silently
      // disabled configured DoS guards (defaults 1000/2000 applied).
      maxNodes: options?.partialGraphPolicy?.maxNodes,
      maxRelations: options?.partialGraphPolicy?.maxRelations,
      reservedRelations: options?.partialGraphPolicy?.reservedRelations
    };

    this.snapshotEnabled = false;
    this.snapshotGraph = null;
    this._config = new ArbiterConfig(this);
    this._snapshot = new ArbiterSnapshot(this);
    this._checks = new ArbiterChecks(this);
  }

  enableCondensedSnapshot(options = {}) {
    return this._snapshot.enableCondensedSnapshot(options);
  }

  toSnapshotBinary(options = {}) {
    return this._snapshot.toSnapshotBinary(options);
  }

  static fromSnapshotBinary(buffer, options = {}) {
    return ArbiterSnapshot.fromSnapshotBinary(buffer, options, (opts) => new Arbiter(opts));
  }

  registerDependencyIndex(index) {
    return this._config.registerDependencyIndex(index);
  }

  resolveNodeId(key, options = null) {
    if (options && options.partialGraphContext) {
      const context = options.partialGraphContext;
      if (context.nodeIdByKey.has(key)) return context.nodeIdByKey.get(key);
      const persistent = this.nodeIdByKey.get(key);
      if (persistent !== undefined) {
        context.nodeIdByKey.set(key, persistent);
        context.keyByNodeId.set(persistent, key);
        return persistent;
      }
      return undefined;
    }
    return this.nodeIdByKey.get(key);
  }

  resolveKey(nodeId, options = null) {
    if (options && options.partialGraphContext) {
      const context = options.partialGraphContext;
      if (context.keyByNodeId.has(nodeId)) return context.keyByNodeId.get(nodeId);
    }
    return this.keyByNodeId.get(nodeId);
  }

  _createPartialGraphContext(partialGraph) {
    return new PartialGraphContext(this, partialGraph);
  }

  // Relation configuration
  setRelationConfig(relation, config) {
    const result = this._config.setRelationConfig(relation, config);
    this._rebuildDependencyIndexForRelation(relation, config);
    return result;
  }

  _rebuildDependencyIndexForRelation(relation, config) {
    const usages = this._collectRelationUsages(config);
    for (const usage of usages.values()) {
      const baseRel = usage.relation;
      if (baseRel && baseRel !== relation) {
        let entry = this.dependencyIndex.get(baseRel);
        if (!entry) {
          entry = { all: new Set() };
          this.dependencyIndex.set(baseRel, entry);
        }
        entry.all.add(relation);
        // Reverse map: the checked relation reads baseRel, so a baseRel
        // mutation must invalidate this relation's direct-check keys.
        let bases = this.dependencyIndexByRelation.get(relation);
        if (!bases) {
          bases = new Set();
          this.dependencyIndexByRelation.set(relation, bases);
        }
        bases.add(baseRel);
      }
    }
  }

  setPartialGraphPolicy(policy = {}) {
    return this._config.setPartialGraphPolicy(policy);
  }

  getPartialGraphPolicy() {
    return this._config.getPartialGraphPolicy();
  }

  getPartialGraphPolicySnapshot() {
    const policy = this.getPartialGraphPolicy();
    return {
      conflict_mode: policy.conflict_mode,
      reducers: { ...(policy.reducers || {}) },
      reducer_count: Object.keys(policy.reducers || {}).length
    };
  }

  // Node operations (delegate to NodeManager)
  addNode(key, type, data = {}) {
    return this.nodeManager.addNode(key, type, data);
  }

  getNodeData(key) {
    const node = this.nodeManager.getNodeByKey(key);
    return node ? node.data : null;
  }

  updateNodeData(key, data) {
    return this.nodeManager.updateNodeData(key, data);
  }

  // Relation operations (delegate to GraphManager)
  addRelation(srcKey, relation, dstKey, options) {
    // Handle backward compatibility for old API
    if (typeof options === 'number') {
      options = { possibility: options };
    }
    
    // Handle backward compatibility for 5th parameter (value)
    if (arguments.length > 4) {
      const valueOptions = arguments[4];
      if (typeof valueOptions === 'object' && valueOptions !== null) {
        if (!options || typeof options !== 'object') {
          options = {};
        }
        for (const key of Object.keys(valueOptions)) {
          options[key] = valueOptions[key];
        }
      }
    }
    
    const result = this.graphManager.addRelation(srcKey, relation, dstKey, options);
    
    // Invalidate caches when relations change
    this._invalidateDirectCheckCache(srcKey, relation, dstKey);
    
    return result;
  }

  removeRelation(srcKey, relation, dstKey) {
    const result = this.graphManager.removeRelation(srcKey, relation, dstKey);
    
    // Invalidate caches when relations change
    this._invalidateDirectCheckCache(srcKey, relation, dstKey);
    
    return result;
  }

  // Authorization checks (delegate to AuthorizationChecker)
  check(userKey, relation, objectKey, options = {}) {
    return this._checks.check(userKey, relation, objectKey, options);
  }

  explain(userKey, relation, objectKey, options = {}) {
    return this._checks.explain(userKey, relation, objectKey, options);
  }


  // Graph traversal operations (delegate to GraphManager)
  estimateGraphDistance(key1, key2) {
    return this.graphManager.estimateGraphDistance(key1, key2);
  }

  shortestPathLength(key1, key2) {
    return this.graphManager.shortestPathLength(key1, key2);
  }

  walk(startKey, steps, stepFn = () => {}, neighborWeightsFn = null) {
    return this.graphManager.walk(startKey, steps, stepFn, neighborWeightsFn);
  }

  monteCarloWalk(startKey, steps, options = {}) {
    return this.graphManager.monteCarloWalk(startKey, steps, options);
  }

  getSituationTree(nodeId, maxDepth = 2, visited = new Set()) {
    return this.graphManager.getSituationTree(nodeId, maxDepth, visited);
  }

  // Helper method for multi-hop rules
  _findPaths(startId, endId, relation, maxDepth, visited, currentPath = [], currentPoss = 1.0, reverse = false) {
    return this.graphManager.findPaths(startId, endId, relation, maxDepth, visited, currentPath, currentPoss, reverse);
  }

  // Embedding operations (delegate to EmbeddingManager)
  regenerateVectors() {
    return this.embeddingManager.regenerateVectors();
  }

  forceRegenerateEmbeddings() {
    return this.embeddingManager.forceRegenerateEmbeddings();
  }

  // Performance tuning
  setLazyStaleMarking(enabled) {
    this.lazyStaleMarking = enabled;
    if (!enabled && this.embeddingManager.pendingStaleNodes.size > 0) {
      this.embeddingManager.processPendingStaleNodes();
    }
    return this;
  }

  setFastConstructionMode(enabled) {
    this.fastConstructionMode = enabled;
    if (!enabled && !this.indicesBuilt) {
      this.indices.buildIndices(this.relations);
      this.indicesBuilt = true;
    }
  }

  flushPendingStaleNodes() {
    if (this.embeddingManager.pendingStaleNodes.size > 0) {
      if (this.lazyStaleMarking) {
        this.embeddingManager.processPendingStaleNodesLazy();
      } else {
        this.embeddingManager.processPendingStaleNodes();
      }
      this.embeddingManager.pendingStaleNodes.clear();
    }
    return this;
  }

  // Ensure indices are built (called before any query operation)
  _ensureIndicesBuilt() {
    if (this.fastConstructionMode && (!this.indicesBuilt || this.relationManager._relationsChanged)) {
      this.indices.buildIndices(this.graphManager.relations);
      this.indicesBuilt = true;
      this.relationManager._relationsChanged = false;
    }
  }

  // Initialize reachability checker with optimized indexes
  async initializeReachabilityChecker(options = {}) {
    return await this.graphManager.initializeReachabilityChecker(options);
  }

  // Check reachability using optimized indexes
  isReachable(sourceKey, targetKey) {
    return this.graphManager.isReachable(sourceKey, targetKey);
  }

  // Get all nodes reachable from source using optimized indexes
  getReachableNodes(sourceKey, maxResults = 1000) {
    return this.graphManager.getReachableNodes(sourceKey, maxResults);
  }

  // Get all nodes that can reach target using optimized indexes
  getReachingNodes(targetKey, maxResults = 1000) {
    return this.graphManager.getReachingNodes(targetKey, maxResults);
  }

  // Get reachability checker statistics
  getReachabilityStats() {
    return this.graphManager.getReachabilityStats();
  }

  // Adapt to false positive in reachability indexes
  adaptToFalsePositive(sourceKey, targetKey, falsePositiveKey, conflictingKey) {
    return this.graphManager.adaptToFalsePositive(sourceKey, targetKey, falsePositiveKey, conflictingKey);
  }

  // Get adaptation statistics
  getAdaptationStats() {
    return this.graphManager.getAdaptationStats();
  }


  _invalidateDirectCheckCache(srcKey, relation, dstKey) {
    // Only invalidate if cache exists
    if (!this.directCheckCache) return;

    // The direct-check cache is keyed by the CHECKED relation (e.g.
    // 'can_read') while mutations arrive with the BASE relation (e.g.
    // 'owner' when can_read is configured with a relation override).
    // Walk the dependency index so every dependent relation is invalidated
    // too — otherwise overridden-config checks serve stale grants after
    // edge mutations.
    const affected = new Set([relation]);
    const entry = this.dependencyIndex.get(relation);
    if (entry?.all) {
      for (const rel of entry.all) affected.add(rel);
    }

    // Keys are rolling hashes (no pipe-delimited string form to match), so
    // invalidation uses the per-relation key index populated at cache-set
    // time: every direct-check result is tracked under the CHECKED relation
    // AND its base relations (via _trackDirectCheckKey), so deleting the
    // tracked keys for an affected relation covers overridden-config checks.
    for (const rel of affected) {
      const keys = this.directCheckCacheKeysByRelation.get(rel);
      if (!keys) continue;
      for (const key of keys) {
        this._deleteFromCache(this.directCheckCache, key);
        this.directKeyToRelations.delete(key);
      }
      keys.clear();
    }

    // Explicit composite-key deletes (recomputed hashes) — cheap belt for the
    // exact (src, relation, dst) pair.
    const srcId = this.keyManager.getStringId(srcKey);
    const dstId = this.keyManager.getStringId(dstKey);
    const key1 = this.keyManager.createCompositeKey(srcId, relation, dstId);
    const key2 = this.keyManager.createCompositeKey(dstId, relation, srcId);
    this._deleteFromCache(this.directCheckCache, key1);
    this._deleteFromCache(this.directCheckCache, key2);

    // Also invalidate rule caches when relations change
    if (this.authChecker) {
      this.authChecker.invalidateRuleCaches(relation);
    }

    this.invalidateRuleResultCacheByRelation(relation);
  }

  /**
   * Record a direct-check cache key so relation- and node-level invalidation
   * can find it without parsing the (hashed) key. Tracked under the CHECKED
   * relation and its base relations (so a base-relation mutation invalidates
   * overridden-config checks) and under the subject/object node ids.
   */
  _trackDirectCheckKey(relation, srcNodeId, dstNodeId, cacheKey) {
    if (!cacheKey) return;
    const rels = new Set([relation]);
    const baseEntry = this.dependencyIndexByRelation.get(relation);
    if (baseEntry) for (const base of baseEntry) rels.add(base);
    for (const rel of rels) {
      let set = this.directCheckCacheKeysByRelation.get(rel);
      if (!set) {
        set = new Set();
        this.directCheckCacheKeysByRelation.set(rel, set);
      }
      set.add(cacheKey);
    }
    if (srcNodeId !== undefined && srcNodeId !== null) {
      let set = this.directCheckCacheKeysByNode.get(srcNodeId);
      if (!set) {
        set = new Set();
        this.directCheckCacheKeysByNode.set(srcNodeId, set);
      }
      set.add(cacheKey);
    }
    if (dstNodeId !== undefined && dstNodeId !== null && dstNodeId !== srcNodeId) {
      let set = this.directCheckCacheKeysByNode.get(dstNodeId);
      if (!set) {
        set = new Set();
        this.directCheckCacheKeysByNode.set(dstNodeId, set);
      }
      set.add(cacheKey);
    }
    let byKey = this.directKeyToRelations.get(cacheKey);
    if (!byKey) {
      byKey = new Set();
      this.directKeyToRelations.set(cacheKey, byKey);
    }
    for (const rel of rels) byKey.add(rel);
  }

  /**
   * Invalidate direct-check entries involving a node id (node removal).
   */
  _invalidateDirectCheckCacheByNode(nodeId) {
    if (!this.directCheckCache || nodeId === undefined) return;
    const keys = this.directCheckCacheKeysByNode.get(nodeId);
    if (!keys) return;
    for (const key of keys) {
      this._deleteFromCache(this.directCheckCache, key);
      const rels = this.directKeyToRelations.get(key);
      if (rels) for (const rel of rels) this.directCheckCacheKeysByRelation.get(rel)?.delete(key);
      this.directKeyToRelations.delete(key);
    }
    keys.clear();
  }

  _cacheRuleResult(relation, cacheKey) {
    if (!this.ruleResultCache || !cacheKey) return;
    let set = this.ruleResultCacheKeysByRelation.get(relation);
    if (!set) {
      set = new Set();
      this.ruleResultCacheKeysByRelation.set(relation, set);
    }
    set.add(cacheKey);
    this.ruleResultCacheRelationByKey.set(cacheKey, relation);
  }

  _deleteRuleResultCacheKey(cacheKey) {
    if (!this.ruleResultCache) return;
    this._deleteFromCache(this.ruleResultCache, cacheKey);
    this._removeRuleResultCacheKey(cacheKey);
  }

  _removeRuleResultCacheKey(cacheKey) {
    if (!cacheKey) return;
    const relation = this.ruleResultCacheRelationByKey.get(cacheKey);
    if (relation) {
      const keys = this.ruleResultCacheKeysByRelation.get(relation);
      if (keys) {
        keys.delete(cacheKey);
        if (keys.size === 0) {
          this.ruleResultCacheKeysByRelation.delete(relation);
        }
      }
      this.ruleResultCacheRelationByKey.delete(cacheKey);
    }
  }

  invalidateRuleResultCacheByRelation(relation) {
    if (!this.ruleResultCache) return;
    // Transitive closure over the dependency index: a mutation of a base
    // relation (e.g. owner) must invalidate cached results of every rule
    // that depends on it through any chain (owner -> is_owner ->
    // can_read/computed), not just direct dependents.
    const affected = new Set([relation]);
    const queue = [relation];
    while (queue.length) {
      const rel = queue.pop();
      const entry = this.dependencyIndex.get(rel);
      if (!entry || !entry.all) continue;
      for (const dep of entry.all) {
        if (!affected.has(dep)) {
          affected.add(dep);
          queue.push(dep);
        }
      }
    }

    for (const rel of affected) {
      const keys = this.ruleResultCacheKeysByRelation.get(rel);
      if (!keys) continue;
      for (const key of keys) {
        this._deleteRuleResultCacheKey(key);
      }
      this.ruleResultCacheKeysByRelation.delete(rel);
    }
  }

  invalidateAllRuleResultCache() {
    if (!this.ruleResultCache) return;
    for (const keys of this.ruleResultCacheKeysByRelation.values()) {
      for (const key of Array.from(keys)) {
        this._deleteRuleResultCacheKey(key);
      }
    }
    this.ruleResultCacheKeysByRelation.clear();
    this.ruleResultCacheRelationByKey.clear();
  }


  _collectRelationUsages(rule, acc = new Map()) {
    if (!rule || typeof rule !== 'object') return acc;
    if (rule.type === 'direct' || (!rule.type && !rule.union && !rule.intersection && !rule.exclusion && (rule.relation || rule.rel || rule.label || rule.name))) {
      // Shorthand operand objects ({ relation: 'owner' } inside union/
      // intersection/exclusion) are direct usages too; failing to register
      // them left the dependency index empty for logical rules, so
      // invalidateRuleResultCacheByRelation never cleared their cached
      // decisions after relation writes.
      const relName = rule.relation || rule.rel || rule.label || rule.name;
      if (relName) {
        const reverse = rule.reverse === true;
        acc.set(`${relName}|${reverse}`, { relation: relName, reverse });
      }
      return acc;
    }

    if (rule.type === 'tuple_to_userset') {
      if (rule.tuplesetRelation) acc.set(`${rule.tuplesetRelation}|false`, { relation: rule.tuplesetRelation, reverse: false });
      if (rule.computedRelation) acc.set(`${rule.computedRelation}|false`, { relation: rule.computedRelation, reverse: false });
    }

    if (rule.type === 'chain' && Array.isArray(rule.steps)) {
      for (const step of rule.steps) {
        const stepRel = step.relation || step.rel || step.label || step.name;
        if (stepRel) {
          const reverse = step.direction === 'in' || step.reverse === true;
          acc.set(`${stepRel}|${reverse}`, { relation: stepRel, reverse });
        }
      }
    }

    if (rule.type === 'computed' && rule.relation) {
      acc.set(`${rule.relation}|false`, { relation: rule.relation, reverse: false });
    }

    if (rule.type === 'multi_hop' && rule.relation) {
      acc.set(`${rule.relation}|false`, { relation: rule.relation, reverse: false });
    }

    if (rule.type === 'parent') {
      if (rule.parentRelation) {
        acc.set(`${rule.parentRelation}|false`, { relation: rule.parentRelation, reverse: false });
      }
      if (rule.relation && rule.relation !== rule.parentRelation) {
        acc.set(`${rule.relation}|false`, { relation: rule.relation, reverse: false });
      }
    }

    const unionRules = Array.isArray(rule.union) ? rule.union : rule.union?.rules;
    if (Array.isArray(unionRules)) {
      for (const child of unionRules) this._collectRelationUsages(child, acc);
    }

    const intersectionRules = Array.isArray(rule.intersection) ? rule.intersection : rule.intersection?.rules;
    if (Array.isArray(intersectionRules)) {
      for (const child of intersectionRules) this._collectRelationUsages(child, acc);
    }

    const exclusionRules = Array.isArray(rule.exclusion) ? rule.exclusion : rule.exclusion?.rules;
    if (Array.isArray(exclusionRules)) {
      for (const child of exclusionRules) this._collectRelationUsages(child, acc);
    }

    if (rule.type === 'relational_comparator') {
      const left = rule.left || rule.leftOperand || null;
      const right = rule.right || rule.rightOperand || null;
      if (left?.rule) this._collectRelationUsages(left.rule, acc);
      if (right?.rule) this._collectRelationUsages(right.rule, acc);
      if (left?.valueRelation) acc.set(`${left.valueRelation}|false`, { relation: left.valueRelation, reverse: false });
      if (right?.valueRelation) acc.set(`${right.valueRelation}|false`, { relation: right.valueRelation, reverse: false });
    }

    // Defeasible components (when/unless/never/always/requires) are base
    // relations too: mutating them must invalidate cached checks of the
    // defeasible config, or rule-result caches serve stale decisions.
    for (const component of ['when', 'unless', 'never', 'always', 'requires']) {
      if (rule[component]) this._collectRelationUsages(rule[component], acc);
    }

    return acc;
  }

  _collectPairsFromRelationUsages(relationUsages) {
    const pairs = new Map();
    if (!relationUsages || relationUsages.size === 0) return [];

    for (const usage of relationUsages.values()) {
      const relations = this.relationManager.getRelationsByName(usage.relation);
      if (!relations || relations.length === 0) continue;
      for (const rel of relations) {
        const userId = usage.reverse ? rel.dst : rel.src;
        const objectId = usage.reverse ? rel.src : rel.dst;
        pairs.set(`${userId}|${objectId}`, { userId, objectId });
      }
    }

    return Array.from(pairs.values());
  }

  _collectTupleToUsersetPairs(config) {
    const tuplesetRelation = config.tuplesetRelation;
    const computedRelation = config.computedRelation;
    if (!tuplesetRelation || !computedRelation) return [];

    const tuples = this.relationManager.getRelationsByName(tuplesetRelation);
    const usersets = this.relationManager.getRelationsByName(computedRelation);
    if (!tuples.length || !usersets.length) return [];

    const usersByUserset = new Map();
    for (const rel of usersets) {
      const key = rel.dst;
      let set = usersByUserset.get(key);
      if (!set) {
        set = new Set();
        usersByUserset.set(key, set);
      }
      set.add(rel.src);
    }

    const pairs = new Map();
    for (const tuple of tuples) {
      const users = usersByUserset.get(tuple.dst);
      if (!users) continue;
      for (const userId of users) {
        pairs.set(`${userId}|${tuple.src}`, { userId, objectId: tuple.src });
      }
    }
    return Array.from(pairs.values());
  }

  _deleteFromCache(cache, key) {
    if (!cache) return;
    if (typeof cache.delete === 'function') {
      cache.delete(key);
      return;
    }
    if (typeof cache._deleteKey === 'function') {
      cache._deleteKey(key);
      return;
    }
    if (cache.cache && typeof cache.cache.delete === 'function') {
      cache.cache.delete(key);
    }
  }

  /**
   * Processes a batch of stale items from the registered managers.
   * @param {object} countsPerManager - Optional. An object specifying max items to refresh per manager.
   *                                    Example: { values: 10, embeddings: 20 }
   * @returns {object} An object with remaining counts for each manager's stale queue.
   *                   Example: { valuesRemaining: 5, embeddingsRemaining: 15 }
   */
  processStaleItems(countsPerManager = {}) {
    const results = {};

    const valueRefreshCount = countsPerManager.values === undefined ? 10 : countsPerManager.values; // Default to 10
    if (this.valueManager && typeof this.valueManager.refreshStaleValues === 'function') {
      if (valueRefreshCount > 0) {
        results.valuesRemaining = this.valueManager.refreshStaleValues(valueRefreshCount);
      } else {
        // When refresh count is 0, don't call refresh but return current queue size
        results.valuesRemaining = this.valueManager.staleValuesQueue.size();
      }
    } else {
      results.valuesRemaining = 0;
    }

    const embeddingRefreshCount = countsPerManager.embeddings === undefined ? 10 : countsPerManager.embeddings; // Default to 10
    if (this.similarityManager && typeof this.similarityManager.refreshStaleEmbeddings === 'function') {
      if (embeddingRefreshCount > 0) {
        results.embeddingsRemaining = this.similarityManager.refreshStaleEmbeddings(embeddingRefreshCount);
      } else {
        // When refresh count is 0, don't call refresh but return current queue size
        results.embeddingsRemaining = this.similarityManager.staleNodesForEmbeddingQueue.size();
      }
    } else {
      results.embeddingsRemaining = 0;
    }

    // Add calls for other managers with stale queues here if implemented

    return results;
  }

} 
