import { BaseRule } from './BaseRule.js';
import { Arbiter } from '../../core/Arbiter.js';
import { OWAFusion } from '../../utils/OWAFusion.js';
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
import { QualitativeScale } from '../../qualitative/QualitativeScale.js';

/**
 * ChainRule - Evaluates access by following a chain of relations and collecting values along the path
 * 
 * This rule enables traversing through multiple entities via relations and collects
 * values from each step in the path. Perfect for scenarios like:
 * "Sum balances from all accounts user can debit from"
 * 
 * CLEAN SEMANTICS:
 * - Authorization: Based on path reachability to target entity (returns raw possibility)
 * - Value Collection: Collects values along the path with full path tracking
 * - Uses ValueContext for efficient caching and aggregation
 * 
 * Path Semantics:
 * - Along chain: MIN operator (possibilistic conjunction)
 * - Across paths: MAX/OWA operator (disjunctive) 
 * - Values: Collected with full path metadata for aggregation at logical level
 * 
 * Configuration:
 * {
 *   type: 'chain',
 *   steps: [
 *     { relation: 'can_debit', direction: 'out' },     // user → accounts
 *     { relation: 'has_balance', direction: 'out' }    // accounts → currency
 *   ],
 *   // Value collection (optional - defaults to enabled)
 *   collectValues: true,              // Whether to collect values (default: true)
 *   valueFilters: {                   // Optional filters for value collection
 *     steps: [0, 1],                  // Which steps to collect from (default: all)
 *     relations: ['has_balance'],     // Which relations to collect from (default: step relations)
 *     minValue: 0,                    // Minimum value threshold
 *     maxValue: 1000                  // Maximum value threshold
 *   },
 *   valueAggregation: 'sum',          // How to pre-aggregate VALUES ('sum', 'max', 'min', 'average')
 *   
 *   // Standard rule fields
 *   reverse: false                    // Reverse traversal direction (default: false)
 * }
 */
export class ChainRule extends BaseRule {
  constructor(arbiter, ruleEvaluator = null) {
    super(arbiter);
    this.ruleEvaluator = ruleEvaluator;
    
    // Chain-specific caching with HyperbolicLRUCache for better memory management
    this.maxCacheSize = 2000;
    this.cacheTTL = 600000; // 10 minutes
    this.pathCacheTTL = 300000; // 5 minutes
    
    // Only create caches if caching is not disabled
    if (!arbiter.disableCaching && !arbiter.disableChainCaching) {
      // Chain result cache with HyperbolicLRUCache
      this.chainResultCache = arbiter.cacheFactory(this.maxCacheSize, {
        onEvict: (key, value) => {
          // Optional: track evictions for debugging
          this.stats = this.stats || {};
          this.stats.evictedResults = (this.stats.evictedResults || 0) + 1;
        }
      });
    } else {
      this.chainResultCache = null;
    }
  }

  /**
   * Evaluate chain rule with smart reachability optimization
   * Uses reachability check as a hint, but doesn't fail fast if TreeCover index is uncertain
   */
  evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
    // Normalize rule to get reverse flag
    const normalizedRule = this._normalizeRule(rule);
    const { reverse = false } = normalizedRule;
    
    // Check if PLTC bypass is requested (for testing/ground truth computation)
    // Also skip when a partial graph is present — PLTC is built from persistent data only
    const { bypassPLTC = false } = options;
    const hasPartialGraph = !!options.partialGraphContext;
    
    if (!bypassPLTC && !hasPartialGraph) {
      // Smart reachability check: PLTC is 100% accurate, so we can fail fast on false
      // Use backward index for reverse chains
      const direction = reverse ? 'backward' : 'forward';
      const reachabilityResult = this._quickReachabilityCheck(userKey, objectKey, { direction });
    
    if (reachabilityResult === false) {
        // PLTC is 100% accurate - if it says false, definitely no path exists
        // Fast fail for unreachable cases
        return this._createStandardResult({
          possibility: 0,
          reliability: 1.0,
          ...(options.includeMeta && {
            meta: {
              method: 'pltc_fast_fail',
              reason: 'not_reachable',
              sourceKey: userKey,
              targetKey: objectKey,
              direction
            }
          }),
          reason: 'not_reachable'
        }, []);
      }
      
      // If reachabilityResult is true, path exists but we still need to check relation types
      // If null, PLTC not initialized, proceed with chain evaluation
    }
    
    // Proceed with chain evaluation (either PLTC said true/null, or bypassPLTC is enabled)
    return this._evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options);
  }

  /**
   * Evaluate chain traversal and value collection using ValueContext
   * @protected
   */
  _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
    const { fastPath, minPossibility, valueContext, includeMeta = true } = options;
    // Normalize rule configuration 
    const normalizedRule = this._normalizeRule(rule);
    const { 
      steps, 
      collectValues = true,  // Default to true
      valueFilters = {},
      valueAggregation = 'sum',
      reverse = false
    } = normalizedRule;
    const collectValuesEnabled = collectValues && !!valueContext;
    
    if (!steps || steps.length === 0) {
      return this._createStandardResult({
        possibility: 0,
        reliability: 1.0,
        ...(includeMeta && { meta: null }),
        reason: 'no_chain_steps_defined'
      }, []);
    }

    // Convert string keys to numeric IDs if needed
    const userIdNum = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
    const objectIdNum = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
    
    const hasPartialGraph = !!(options.partialGraphContext);

    // Threshold-mode (binary / fastPath-with-threshold) evaluations collapse
    // sub-threshold paths to 0 and early-exit; their results are NOT
    // interchangeable with full-mode values. The shared chain result cache
    // must be neither consulted nor populated in threshold mode, or binary
    // checks get served full-mode values (and vice versa).
    const isThresholdEval = options.binary === true || (options.fastPath === true && options.minPossibility != null);

    // A caller-pinned clock (options.now) makes the result per-clock: a
    // chain result captured at one time (with then-fresh values) must not
    // be served to a caller asking about another time. Same contract as
    // the rule result cache (RuleEvaluator): pinned-clock callers bypass
    // the chain cache entirely — both reads and writes.
    const temporalPinned = options.now !== undefined && options.now !== null;

    // Check for cached chain result (use numeric IDs) - only if caching is enabled
    // Skip cache when a partial graph is present to prevent cross-request leakage
    if (this.chainResultCache && !hasPartialGraph && !isThresholdEval && !temporalPinned) {
      const cachedResult = this._getCachedChainResult(userIdNum, objectIdNum, steps);
      if (cachedResult) {
        return cachedResult;
      }
    }
    
    // Determine starting point
    let startId, startKey;
    if (reverse) {
      startId = objectIdNum;
      startKey = objectKey;
    } else {
      startId = userIdNum;
      startKey = userKey;
    }
    
    // Track all paths through the chain with their accumulated possibilities
    let currentPaths = [{
      id: startId,
      key: startKey,
      possibility: 1.0,
      path: [startKey], // Track the full path
      pathEntities: [{ id: startId, key: startKey, source: 'persistent' }]
    }];
    
    let allCollectedValues = [];
    
    // Traverse through each step
    for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
      // Normalize string steps (emitted by the DSL compiler as
      // ['works_in','has_access']) to the object form the traversal expects.
      const rawStep = steps[stepIndex];
      const step = typeof rawStep === 'string'
        ? { relation: rawStep, direction: 'out' }
        : rawStep;
      const { relation: stepRelation, direction } = step;

      // CONDITION STEP (rule-based step): a step carrying a `rule` config is a
      // condition-gated hop, not a plain edge traversal. The DSL compiler emits
      // these when a chain step references a logical/defeasible evidence.
      //   - FINAL step: the object is known, so each current path's node is
      //     checked against the object through the referenced rule.
      //   - INTERMEDIATE step: the rule is EXPANDED from each current node
      //     (rule-based reachability) — the reachable destinations of the
      //     rule's base edges, filtered by its defeaters/requirements — and
      //     the traversal continues from each discovered node.
      // The rule config is part of the chain cache key, so cache correctness
      // is preserved.
      if (step.rule) {
        if (!this.ruleEvaluator) {
          return this._createStandardResult({
            possibility: 0,
            reliability: 1.0,
            ...(includeMeta && { meta: null }),
            reason: 'condition_step_requires_rule_evaluator'
          }, []);
        }
        if (stepIndex === steps.length - 1) {
          const conditionPaths = [];
          for (const currentPath of currentPaths) {
            const condResult = this.ruleEvaluator.evaluateRule(
              currentPath.id, currentPath.key, objectIdNum, objectKey,
              step.rule, new Set(visited || []), currentRelation, options
            );
            const condPossibility = condResult.possibility ?? 0;
            if (condPossibility <= 0) continue;
            const nextPossibility = Math.min(currentPath.possibility, condPossibility);
            if (fastPath && nextPossibility < minPossibility) continue;
            conditionPaths.push({
              id: objectIdNum,
              key: objectKey,
              possibility: nextPossibility,
              reliability: (currentPath.reliability ?? 1.0) * (condResult.reliability ?? 1.0),
              path: [...currentPath.path, objectKey],
              pathEntities: [...currentPath.pathEntities, { id: objectIdNum, key: objectKey, source: 'condition' }]
            });
          }
          currentPaths = conditionPaths;
          break;
        }

        // INTERMEDIATE condition step: expand the rule from each current node.
        const pathMap = new Map();
        for (const currentPath of currentPaths) {
          const reachable = this._expandRuleFromSrc(currentPath.id, step.rule, options);
          for (const [nextId, cand] of reachable) {
            const nextKey = this.arbiter.resolveKey(nextId, options);
            if (!nextKey) continue;
            const nextPossibility = Math.min(currentPath.possibility, cand.possibility);
            if (fastPath && nextPossibility < minPossibility) continue;
            const nextReliability = (currentPath.reliability ?? 1.0) * cand.reliability;
            const existing = pathMap.get(nextId);
            if (existing && existing.possibility > nextPossibility) continue;
            if (existing && existing.possibility === nextPossibility && existing.reliability >= nextReliability) continue;
            pathMap.set(nextId, {
              id: nextId,
              key: nextKey,
              possibility: nextPossibility,
              reliability: nextReliability,
              path: [...currentPath.path, nextKey],
              pathEntities: [...currentPath.pathEntities, { id: nextId, key: nextKey, source: 'condition' }]
            });
          }
        }
        currentPaths = Array.from(pathMap.values());
        if (currentPaths.length === 0) {
          break;
        }
        continue;
      }

      if (!stepRelation || !direction) {
        currentPaths = [];
        break;
      }
      
      
      // For each current path, extend it via the relation
      const pathMap = new Map();
      const MAX_PATHS_PER_STEP = 100;

      for (const currentPath of currentPaths) {
        if (pathMap.size >= MAX_PATHS_PER_STEP) break;

        const relations = this._getRelationsForStep(currentPath.id, stepRelation, direction, options);
        
        for (const rel of relations) {
          const nextId = direction === 'in' ? rel.src : rel.dst;
          const nextKey = this.arbiter.resolveKey(nextId, options);
          
          if (nextKey) {
            // Calculate path possibility (MIN along chain)
            const nextPossibility = Math.min(currentPath.possibility, rel.possibility ?? 1.0);

            if (fastPath && nextPossibility < minPossibility) {
              continue;
            }

            // Collect values from this step BEFORE path deduplication:
            // a weaker parallel path is still a valid value source, and
            // dropping it first would silently discard its contribution.
            if (collectValuesEnabled && this._shouldCollectFromStep(stepIndex, valueFilters)) {
              const stepValues = this._collectValuesFromStep(
                currentPath,
                rel,
                stepIndex,
                stepRelation,
                direction,
                valueFilters,
                valueContext,
                options
              );
              allCollectedValues.push(...stepValues);
            }

            // Deduplicate: keep best path per node
            const existing = pathMap.get(nextId);
            const nextReliability = (currentPath.reliability ?? 1.0) * (rel.reliability ?? 1.0);
            if (existing && existing.possibility > nextPossibility) continue;
            if (existing && existing.possibility === nextPossibility && (existing.reliability ?? 1.0) >= nextReliability) continue;

            // Create extended path
            const extendedPath = {
              id: nextId,
              key: nextKey,
              possibility: nextPossibility,
              reliability: nextReliability,
              path: [...currentPath.path, nextKey],
              pathEntities: [...currentPath.pathEntities, { id: nextId, key: nextKey, source: rel.source || 'persistent' }]
            };
            
            pathMap.set(nextId, extendedPath);
          }
        }
      }

      currentPaths = Array.from(pathMap.values());
      
      
      // Early exit if no paths found
      if (currentPaths.length === 0) {
        break;
      }
    }
    
    // Determine if target was reached and calculate final possibility
    const targetKey = reverse ? userKey : objectKey;
    const targetId = reverse ? userIdNum : objectIdNum;
    const targetPaths = currentPaths.filter(path => path.id === targetId);
    let finalPossibility = 0;
    let finalReliability = 1.0;
    
    if (targetPaths.length > 0) {
      // Use MAX across paths (disjunctive); the winning path's reliability
      // is the product of its edges' reliabilities (MIN along the chain).
      const best = targetPaths.reduce((a, b) =>
        (b.possibility > a.possibility ||
         (b.possibility === a.possibility && (b.reliability ?? 1.0) > (a.reliability ?? 1.0)))
          ? b : a
      );
      finalPossibility = best.possibility;
      finalReliability = best.reliability ?? 1.0;
    }

    if (fastPath && finalPossibility < minPossibility) {
      return this._createStandardResult({
        possibility: 0,
        reliability: 1.0,
        meta: null,
        reason: 'no_chain_path_found'
      }, []);
    }
    // Add collected values to ValueContext if available
    if (valueContext && allCollectedValues.length > 0) {
      valueContext.addCollectedValues(allCollectedValues, 'chain', rule);
    }
    
    // Apply value aggregation with optional bilattice reasoning
    const useBilattice = normalizedRule.useBilattice || false;
    const epistemicMode = normalizedRule.epistemicMode || 'hybrid';
    const capacityType = normalizedRule.capacityType || 'simple_support';
    
    let aggregatedCollectedValues;
    let epistemicAnalysis = null;
    
    if (useBilattice && allCollectedValues.length > 0) {
      const scale = QualitativeScale.fivePoint(); // Default scale for bilattice analysis
      const capacity = this._createCapacityFromValues(allCollectedValues, scale, capacityType);
      
      const bilatticeResult = this._combineEvidenceWithBilattice(allCollectedValues, {
        method: valueAggregation,
        useBilattice: true,
        capacity: capacity,
        scale: scale,
        epistemicMode: epistemicMode
      });
      
      // Convert bilattice result back to collected values format
      aggregatedCollectedValues = [{
        value: bilatticeResult.value,
        possibility: bilatticeResult.possibility,
        path: ['bilattice_aggregated'],
        source: {
          entityKey: 'chain_aggregation',
          relation: 'bilattice_fusion',
          step: -1
        },
        metadata: {
          timestamp: Date.now(),
          reliability: 1.0,
          aggregationMethod: `bilattice_${epistemicMode}`,
          epistemicAnalysis: bilatticeResult.epistemicAnalysis
        }
      }];
      
      epistemicAnalysis = bilatticeResult.epistemicAnalysis;
    } else {
      aggregatedCollectedValues = this._aggregateCollectedValues(
        allCollectedValues,
        valueAggregation
      );
    }
    
    
    // Build authorization result with single possibility value
    const authResult = {
      possibility: finalPossibility,
      reliability: finalReliability,
      validity: this._validity('min', normalizedRule.steps.map(s => s.relation).filter(Boolean), [], normalizedRule.steps.length, finalPossibility),
      ...(includeMeta && {
        meta: finalPossibility > 0 ? {
          ruleType: 'chain',
          reason: 'chain_path_found',
          rule,
          chainLength: steps.length,
          pathsToTarget: targetPaths.length,
          totalPaths: currentPaths.length,
          bestPath: targetPaths.length > 0 ? targetPaths[0].path : null,
          bestPathEntities: targetPaths.length > 0 ? targetPaths[0].pathEntities : null,
          pathSteps: targetPaths.length > 0 ? targetPaths[0].pathEntities : null,
          useBilattice: useBilattice,
          epistemicMode: useBilattice ? epistemicMode : undefined,
          epistemicAnalysis: epistemicAnalysis
        } : null
      }),
      ...(includeMeta && {
        meta_allow: finalPossibility > 0 ? {
          ruleType: 'chain',
          reason: 'chain_path_found',
          rule,
          chainLength: steps.length,
          pathsToTarget: targetPaths.length,
          totalPaths: currentPaths.length,
          bestPath: targetPaths.length > 0 ? targetPaths[0].path : null,
          bestPathEntities: targetPaths.length > 0 ? targetPaths[0].pathEntities : null,
          pathSteps: targetPaths.length > 0 ? targetPaths[0].pathEntities : null
        } : null
      }),
      reason: finalPossibility > 0 ? 'chain_path_found' : 'no_chain_path_found'
    };
    
    const result = this._createStandardResult(authResult, aggregatedCollectedValues);
    
    // Cache the chain result (use numeric IDs) - only if caching is enabled
    // Do not cache when a partial graph is present to prevent cross-request leakage
    // Do not cache threshold-mode results (see isThresholdEval above).
    // Do not cache pinned-clock results either — the entry is per-clock
    // and would be served to later unpinned callers as if it were timeless.
    if (this.chainResultCache && !hasPartialGraph && !isThresholdEval && !temporalPinned) {
      this._cacheChainResult(userIdNum, objectIdNum, steps, result);
    }
    
    return result;
  }

  /**
   * Get relations for a step based on direction
   * @private
   */
   _getRelationsForStep(entityId, relation, direction, options = null) {
     if (direction === 'out') {
       return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
     } else if (direction === 'in') {
       return this.arbiter.relationManager.getRelationsToDst(entityId, relation, options);
     } else {
       // Default to 'out' for backward compatibility
       return this.arbiter.relationManager.getRelationsFromSrc(entityId, relation, options);
     }
   }

  // ---------------------------------------------------------------------------
  // Rule-based reachability: expand a rule config from a source node into its
  // reachable destinations. Used by intermediate condition steps — a
  // logical/defeasible evidence as a non-final chain step discovers its
  // reachable nodes (the base edges' destinations, filtered by the rule's
  // defeaters/requirements) instead of checking a single (src, dst) pair.
  // ---------------------------------------------------------------------------

  /**
   * Expand a rule config from a source node into Map<dstId, {possibility, reliability}>.
   * Supports direct rules, logical union/intersection nodes, defeasible rules
   * (when/unless/never/requires/always), and nested chains.
   */
  _expandRuleFromSrc(srcId, rule, options = null) {
    if (!rule || typeof rule !== 'object') return new Map();
    if (rule.type === 'direct' && rule.relation) {
      return this._expandDirectFromSrc(srcId, rule.relation, options);
    }
    if (rule.type === 'chain' && Array.isArray(rule.steps)) {
      return this._expandChainRuleFromSrc(srcId, rule, options);
    }
    if (rule.type === 'tuple_to_userset') {
      return this._expandTtuFromSrc(srcId, rule, options);
    }
    if (rule.type === 'logical') {
      if (rule.when || rule.unless || rule.never || rule.requires || rule.always) {
        return this._expandDefeasibleFromSrc(srcId, rule, options);
      }
      if (rule.union) {
        return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
      }
      if (rule.intersection) {
        return this._expandLogicalNodeFromSrc(srcId, rule.intersection, 'intersection', options);
      }
    }
    return new Map();
  }

  _expandDirectFromSrc(srcId, relation, options = null) {
    const edges = this.arbiter.relationManager.getRelationsFromSrc(srcId, relation, options);
    const out = new Map();
    for (const e of edges || []) {
      out.set(e.dst, { possibility: e.possibility ?? 1, reliability: e.reliability ?? 1 });
    }
    return out;
  }

  /**
   * Expand a tuple_to_userset config from a source node. Semantics (forward,
   * the common case): src →computed→ intermediate, and intermediate ↔object
   * via tupleset (direction decides which end the intermediate sits on). The
   * reachable set is the OBJECTS sharing an intermediate with src. Combined
   * possibility is the weakest link across the two hops.
   */
  _expandTtuFromSrc(srcId, rule, options = null) {
    const { tuplesetRelation, computedRelation, tuplesetDirection = 'out', reverse = false } = rule;
    if (!tuplesetRelation || !computedRelation) return new Map();
    // reverse swaps the roles: src is the object, intermediates come from the
    // tupleset side, and computed edges go from intermediate to the user.
    const srcAsUser = !reverse;
    const computedRel = srcAsUser ? computedRelation : tuplesetRelation;
    const tuplesetRel = srcAsUser ? tuplesetRelation : computedRelation;
    const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(srcId, computedRel, options);
    const out = new Map();
    for (const ce of computedEdges || []) {
      const intermediateId = ce.dst;
      const tsEdges = tuplesetDirection === 'in'
        ? this.arbiter.relationManager.getRelationsFromSrc(intermediateId, tuplesetRel, options)
        : this.arbiter.relationManager.getRelationsToDst(intermediateId, tuplesetRel, options);
      for (const te of tsEdges || []) {
        const objId = tuplesetDirection === 'in' ? te.dst : te.src;
        const poss = Math.min(ce.possibility ?? 1, te.possibility ?? 1);
        const rel = (ce.reliability ?? 1) * (te.reliability ?? 1);
        const cur = out.get(objId);
        if (!cur || poss > cur.possibility) out.set(objId, { possibility: poss, reliability: rel });
      }
    }
    return out;
  }

  _expandLogicalNodeFromSrc(srcId, node, op, options = null) {
    const rules = (node && node.rules) || [];
    if (rules.length === 0) return new Map();
    if (op === 'union') {
      const out = new Map();
      for (const r of rules) {
        const m = this._expandRuleFromSrc(srcId, r, options);
        for (const [id, v] of m) {
          const cur = out.get(id);
          if (!cur || v.possibility > cur.possibility) out.set(id, v);
        }
      }
      return out;
    }
    // intersection: nodes reachable via every branch, weakest-link combined
    const maps = rules.map(r => this._expandRuleFromSrc(srcId, r, options));
    const out = new Map();
    for (const [id, v] of maps[0]) {
      let ok = true;
      let min = v.possibility;
      let rel = v.reliability;
      for (let i = 1; i < maps.length; i++) {
        const other = maps[i].get(id);
        if (!other) { ok = false; break; }
        min = Math.min(min, other.possibility);
        rel *= other.reliability;
      }
      if (ok) out.set(id, { possibility: min, reliability: rel });
    }
    return out;
  }

  _expandDefeasibleFromSrc(srcId, rule, options = null) {
    const base = this._expandBaseFromSrc(srcId, rule, options);
    if (base.size === 0) return base;
    const out = new Map();
    const evalAt = (candId, candKey, node) => {
      if (!node) return { poss: 1, rel: 1 };
      const wrapped = node.union ? { union: node.union } : node;
      const r = this.ruleEvaluator.evaluateRule(
        srcId, null, candId, candKey, wrapped, new Set(), null, options || {}
      );
      return { poss: r.possibility ?? 0, rel: r.reliability ?? 1 };
    };
    for (const [candId, cand] of base) {
      const candKey = this.arbiter.resolveKey(candId, options);
      if (rule.never) {
        const n = evalAt(candId, candKey, rule.never);
        if (n.poss > 0.5) continue;
      }
      let defeat = 0;
      let defeatRel = 1;
      if (rule.unless) {
        const u = evalAt(candId, candKey, rule.unless);
        defeat = u.poss;
        defeatRel = u.rel;
      }
      let req = 1;
      let reqRel = 1;
      if (rule.requires) {
        const q = evalAt(candId, candKey, rule.requires);
        req = q.poss;
        reqRel = q.rel;
      }
      const poss = cand.possibility * req * (1 - defeat);
      if (poss <= 0) continue;
      out.set(candId, {
        possibility: poss,
        reliability: cand.reliability * reqRel * defeatRel
      });
    }
    return out;
  }

  _expandBaseFromSrc(srcId, rule, options = null) {
    if (rule.when && rule.when.intersection) {
      return this._expandLogicalNodeFromSrc(srcId, rule.when.intersection, 'intersection', options);
    }
    if (rule.when && rule.when.union) {
      return this._expandLogicalNodeFromSrc(srcId, rule.when.union, 'union', options);
    }
    if (rule.when && rule.when.rules) {
      return this._expandLogicalNodeFromSrc(srcId, rule.when, 'union', options);
    }
    if (rule.union) {
      return this._expandLogicalNodeFromSrc(srcId, rule.union, 'union', options);
    }
    if (rule.always && rule.always.direct) {
      return this._expandRuleFromSrc(srcId, rule.always.direct, options);
    }
    return new Map();
  }

  _expandChainRuleFromSrc(srcId, rule, options = null) {
    let current = [{ id: srcId, possibility: 1, reliability: 1 }];
    for (const step of rule.steps || []) {
      const stepName = typeof step === 'string' ? step : step.relation;
      const stepRule = typeof step === 'string' ? null : step.rule;
      const next = [];
      for (const node of current) {
        const m = stepRule
          ? this._expandRuleFromSrc(node.id, stepRule, options)
          : this._expandDirectFromSrc(node.id, stepName, options);
        for (const [id, v] of m) {
          next.push({
            id,
            possibility: Math.min(node.possibility, v.possibility),
            reliability: node.reliability * v.reliability
          });
        }
      }
      current = next;
      if (current.length === 0) break;
    }
    const out = new Map();
    for (const n of current) out.set(n.id, { possibility: n.possibility, reliability: n.reliability });
    return out;
  }

  /**
   * Generate cache key for chain result using numeric IDs
   * @private
   */
  _getChainResultCacheKey(userId, objectId, steps) {
    return this.arbiter.keyManager.createChainKey(userId, objectId, steps);
  }

  _getCachedChainResult(userId, objectId, steps) {
    if (!this.chainResultCache) return null;
    
    const key = this._getChainResultCacheKey(userId, objectId, steps);
    const entry = this.chainResultCache.get(key);
    
    // A graph mutation since the entry was computed invalidates it: the
    // reachability it captured is stale. The arbiter increments _graphVersion
    // on every relation mutation, so any mismatch is a miss.
    if (entry && entry.graphVersion !== (this.arbiter._graphVersion ?? 0)) {
      return null;
    }
    const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
    if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
      return entry.result;
    }
    
    // HyperbolicLRUCache handles eviction automatically, no need to manually delete
    return null;
  }

  /**
   * Cache chain result
   * @private
   */
  _cacheChainResult(userId, objectId, steps, result) {
    if (!this.chainResultCache) return;
    
    const key = this._getChainResultCacheKey(userId, objectId, steps);
    
    // HyperbolicLRUCache handles eviction automatically based on frequency and recency
    const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
    this.chainResultCache.set(key, {
      result,
      timestamp: cacheNow,
      graphVersion: this.arbiter._graphVersion ?? 0
    });
  }

  _invalidateAllChainCaches() {
    if (this.chainResultCache) {
      this.chainResultCache.clear();
    }
  }

  /**
   * Check if we should collect values from this step
   * @private
   */
  _shouldCollectFromStep(stepIndex, valueFilters) {
    if (!valueFilters) return true;
    
    // Check step filter
    if (valueFilters.steps && Array.isArray(valueFilters.steps)) {
      return valueFilters.steps.includes(stepIndex);
    }
    
    return true;
  }

  /**
   * Collect values from a single step in the chain
   * @private
   */
  _collectValuesFromStep(currentPath, relation, stepIndex, stepRelation, direction, valueFilters, valueContext, options = null) {
    const collectedValues = [];
    
    // Check if this relation should be collected based on filters
    if (valueFilters.relations && Array.isArray(valueFilters.relations)) {
      if (!valueFilters.relations.includes(stepRelation)) {
        return collectedValues;
      }
    }
    
    // If relation has a value, collect it as a blurred interval
    if (relation.value !== undefined && relation.value !== null) {
      // Apply value filters
      if (valueFilters.minValue !== undefined && relation.value < valueFilters.minValue) {
        return collectedValues;
      }
      if (valueFilters.maxValue !== undefined && relation.value > valueFilters.maxValue) {
        return collectedValues;
      }
      
      // Let ValueManager handle age-based blurring instead of hard TTL filtering
      // The ValueManager will apply appropriate blurring based on relation age
      
      // Get blurred interval from ValueManager
      if (!relation) {
        return collectedValues;
      }
      
      if (!this.arbiter.valueManager) {
        return collectedValues;
      }
      
      const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
      const blurred = this.arbiter.valueManager.getBlurredValue(relation, callerNow);
      
      if (blurred.interval) {
        const sourceEntity = direction === 'in' ? 
          this.arbiter.resolveKey(relation.dst, options) : 
          this.arbiter.resolveKey(relation.src, options);
        const targetEntity = direction === 'in' ? 
          this.arbiter.resolveKey(relation.src, options) : 
          this.arbiter.resolveKey(relation.dst, options);
        
        const collectedValue = this._createCollectedValue(
          blurred.interval, // Pass interval instead of point value
          blurred.possibility, // Use decayed possibility
          currentPath.path,
          {
            entityKey: sourceEntity,
            relation: stepRelation,
            step: stepIndex,
            direction: direction,
            fullPath: currentPath.path,
            stepPosition: stepIndex,
            originalValue: relation.value, // Keep original point value for reference
            source: relation.source || 'persistent'
          },
          {
            timestamp: relation.changed_last_at || relation.updated_last_at ||
              (options && options.now !== undefined && options.now !== null ? options.now : Date.now()),
            reliability: blurred.reliability,
            pathPossibility: currentPath.possibility,
            relationPossibility: relation.possibility ?? 1.0,
            currentPossibility: blurred.possibility, // Add current (decayed) possibility
            interval: blurred.interval, // Include interval in metadata
            source: relation.source || 'persistent'
          }
        );
        
        collectedValues.push(collectedValue);
      }
    }
    
    // Also try to get values from ValueContext if available
    if (valueContext && currentPath.pathEntities.length > stepIndex) {
      const entity = currentPath.pathEntities[stepIndex];
      const contextValues = valueContext.getValues(entity.id, stepRelation);
      
      for (const contextValue of contextValues) {
        // Apply value filters
        if (valueFilters.minValue !== undefined && contextValue.value < valueFilters.minValue) {
          continue;
        }
        if (valueFilters.maxValue !== undefined && contextValue.value > valueFilters.maxValue) {
          continue;
        }
        
        // Let ValueManager handle age-based blurring for ValueContext values too
        
        // Create a temporary relation object for ValueManager
        const tempRelation = {
          src: entity.id,
          dst: entity.id, // Self-relation for value storage
          rel: stepRelation,
          value: contextValue.value,
          possibility: contextValue.possibility,
          reliability: contextValue.reliability,
          changed_last_at: contextValue.timestamp
        };
        
        if (!tempRelation) {
          Arbiter.DEBUG && Arbiter.log('ChainRule: tempRelation is undefined, skipping value collection');
          return collectedValues;
        }
        
        if (!this.arbiter.relationManager || !this.arbiter.relationManager.valueManager) {
          Arbiter.DEBUG && Arbiter.log('ChainRule: relationManager or valueManager is undefined');
          return collectedValues;
        }
        const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
        const blurred = this.arbiter.relationManager.valueManager.getBlurredValue(tempRelation, callerNow);
        
        if (blurred.interval) {
          const collectedValue = this._createCollectedValue(
            blurred.interval,
            Math.min(currentPath.possibility, blurred.possibility),
            currentPath.path,
            {
              entityKey: entity.key,
              relation: stepRelation,
              step: stepIndex,
              direction: direction,
              fullPath: currentPath.path,
              stepPosition: stepIndex,
              fromValueContext: true,
              originalValue: contextValue.value
            },
            {
              timestamp: contextValue.timestamp,
              reliability: blurred.reliability,
              pathPossibility: currentPath.possibility,
              relationPossibility: contextValue.possibility,
              currentPossibility: blurred.possibility,
              interval: blurred.interval,
              source: contextValue.source || 'persistent'
            }
          );
          
          collectedValues.push(collectedValue);
        }
      }
    }
    
    return collectedValues;
  }

  /**
   * Aggregate collected values based on aggregation method using interval arithmetic
   * @private
   */
  _aggregateCollectedValues(collectedValues, aggregationMethod) {
    if (collectedValues.length === 0) return [];
    if (collectedValues.length === 1) return collectedValues;
    
    // Group values by path for aggregation
    const valuesByPath = new Map();
    
    for (const cv of collectedValues) {
      const pathKey = cv.source?.fullPath?.join('->') || 'unknown_path';
      if (!valuesByPath.has(pathKey)) {
        valuesByPath.set(pathKey, []);
      }
      valuesByPath.get(pathKey).push(cv);
    }
    
    const aggregatedValues = [];
    
    for (const [pathKey, pathValues] of valuesByPath) {
      if (pathValues.length === 1) {
        aggregatedValues.push(pathValues[0]);
        continue;
      }
      
      // Extract intervals and metadata for OWA fusion
      const intervals = pathValues.map(cv => cv.value); // cv.value is already an interval
      const possibilities = pathValues.map(cv => cv.possibility);
      const metas = pathValues.map(cv => ({
        ...cv.source,
        ...cv.metadata,
        originalInterval: cv.value
      }));
      
      // Use OWAFusion's interval arithmetic
      const intervalResult = OWAFusion.fuseIntervalsWithMeta(
        intervals, 
        metas, 
        null, // Use default weights 
        aggregationMethod
      );
      
      // Aggregate possibilities
      const possibilityResult = OWAFusion.fuseWithMeta(
        possibilities, 
        metas, 
        null, 
        aggregationMethod === 'sum' ? 'average' : aggregationMethod // For sum, average possibilities
      );
      
      // Create aggregated collected value
      const aggregatedCV = this._createCollectedValue(
        intervalResult.interval,
        possibilityResult.value,
        pathValues[0].path,
        {
          ...pathValues[0].source,
          aggregationMethod,
          aggregatedFromCount: pathValues.length
        },
        {
          ...pathValues[0].metadata,
          reliability: intervalResult.meta?.reliability || 
                      Math.min(...pathValues.map(cv => cv.metadata?.reliability || 1.0)),
          aggregatedFromIntervals: intervals,
          interval: intervalResult.interval
        }
      );
      
      aggregatedValues.push(aggregatedCV);
    }
    
    return aggregatedValues;
  }

  /**
   * Normalize rule configuration (for future extensibility)
   * @private
   */
   _normalizeRule(rule) {
     if (rule.chain) return { ...rule, ...rule.chain, chain: undefined };
     return rule;
   }

} 
