import { BaseRule } from './BaseRule.js';
import { Arbiter } from '../../core/Arbiter.js';
import { OWAFusion } from '../../utils/OWAFusion.js';

/**
 * MultiHopRule - Evaluates access through multi-hop path finding and collects values along paths
 * 
 * This rule implements path-based access control where access is granted if there exists 
 * a valid path of relations from user to object (or vice versa) within specified constraints.
 * Simultaneously collects values along discovered paths.
 * 
 * Supports multiple path aggregation strategies,
 * and fallback to basic connectivity when sophisticated search fails.
 * 
 * Returns raw possibility values - polarity determined by logical context.
 * 
 * Configuration:
 * {
 *   type: 'multi_hop',
 *   relation: string,                    // Relation to traverse
 *   maxDepth: number,                   // Maximum path depth (default: 5)
 *   pathAggregation: string,            // 'max', 'sum', 'owa' (default: 'max')
 *   reverse: boolean,                   // Search in reverse direction
 *   fallbackToBasicPaths: boolean,      // Use basic BFS fallback (default: true)
 *   owaWeights: Array<number>,          // OWA weights for path aggregation
 *   
 *   // Value collection (optional)
 *   collectValues: boolean,             // Whether to collect values along paths (default: true)
 *   valueFilters: {                     // Optional filters for value collection
 *     relations: ['has_balance'],       // Which relations to collect from (default: path relation)
 *     minValue: 0,                      // Minimum value threshold
 *     maxValue: 1000,                   // Maximum value threshold
 *   },
 *   valueAggregation: 'sum'             // How to aggregate values ('sum', 'max', 'min', 'average')
 * }
 */
export class MultiHopRule extends BaseRule {
  constructor(arbiter) {
    super(arbiter);
  }

  /**
   * Evaluate multi-hop path-based access with value collection and reachability optimization
   * @protected
   */
  _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
    const { fastPath = false, minPossibility = 0, valueContext, includeMeta = true } = options;
    // Normalize rule configuration
    const normalizedRule = this._normalizeRule(rule);
    const {
      relation,
      maxDepth = 5,
      pathAggregation = 'max',
      reverse = false,
      fallbackToBasicPaths = true,
      owaWeights,
      collectValues = true,
      valueFilters = {},
      valueAggregation = 'sum',
      trackPaths = true,
      skipReachabilityCheck = false,
      allowZeroHop = false
    } = normalizedRule;
    const shouldSkipReachability = skipReachabilityCheck || !!options.partialGraphContext || (allowZeroHop && userId === objectId);
    const collectValuesEnabled = collectValues && !!valueContext;
    const shouldFastExit = fastPath && pathAggregation === 'max' && !collectValuesEnabled && !trackPaths;
    const stopSignal = shouldFastExit ? { stop: false } : null;

    if (!shouldSkipReachability) {
      // Quick reachability failure check before expensive path-finding
      const quickFailure = this._quickReachabilityFailure(
        userKey,
        objectKey,
        'Multi-hop path not reachable via reachability index',
        reverse ? { direction: 'backward' } : undefined
      );
      if (quickFailure) {
        return quickFailure;
      }
    }
    
    // Early exit for unknown relation
    if (!relation) {
      return this._createStandardResult({
        possibility: 0,
        reliability: 1.0,
        meta: null,
        reason: 'no_relation_specified'
      }, []);
    }
    
    Arbiter.DEBUG && Arbiter.log('MultiHopRule evaluating:', {
      userKey,
      objectKey,
      relation,
      maxDepth,
      pathAggregation,
      reverse,
      collectValues,
      hasValueContext: !!valueContext
    });
    
    // Track evaluation details
    const evaluation = {
      type: 'multi_hop',
      userKey,
      objectKey,
      relation,
      maxDepth,
      searchStarted: Date.now(),
      pathsFound: 0,
      fallbackUsed: false
    };
    
    // Find paths and collect values in single traversal
    const pathsWithValues = this._findPathsAndCollectValues(
      userId, 
      objectId, 
      relation, 
      maxDepth, 
      new Set(), 
      [], 
      1.0, 
      1.0,
      reverse,
      collectValuesEnabled,
      trackPaths,
      stopSignal,
      valueFilters,
      valueContext,
      evaluation,
      fastPath,
      minPossibility,
      options,
      allowZeroHop
    );
    
    evaluation.searchCompleted = Date.now();
    evaluation.searchDuration = evaluation.searchCompleted - evaluation.searchStarted;
    evaluation.pathsFound = pathsWithValues.length;
    
    // If no paths found and fallback enabled, try basic connectivity
    if (pathsWithValues.length === 0 && fallbackToBasicPaths) {
      const fallbackResult = this._findBasicPathWithValues(
        userId, objectId, relation, maxDepth, minPossibility, 
        reverse, collectValuesEnabled, trackPaths, valueFilters, valueContext, evaluation, options, allowZeroHop
      );
      if (fallbackResult) {
        pathsWithValues.push(fallbackResult);
        evaluation.fallbackUsed = true;
      }
    }
    
    if (pathsWithValues.length === 0) {
      evaluation.outcome = 'no_path_found';
      
      return this._createStandardResult({
        possibility: 0,
        reliability: 1.0,
        ...(includeMeta && { meta: null }),
        reason: undefined
      }, []);
    }
    
    // Aggregate paths to get final possibility
    const { finalPossibility, finalReliability = 1.0, bestPath } = this._aggregatePaths(
      pathsWithValues, pathAggregation, owaWeights, evaluation, options.trackEvaluation
    );
    
    // Collect all values from all paths
    let allCollectedValues = [];
    for (const pathResult of pathsWithValues) {
      if (pathResult.collectedValues && pathResult.collectedValues.length > 0) {
        allCollectedValues.push(...pathResult.collectedValues);
      }
    }
    
    // Add collected values to ValueContext if available
    if (valueContext && allCollectedValues.length > 0) {
      valueContext.addCollectedValues(allCollectedValues, 'multi_hop', rule);
    }
    
    // Apply value aggregation if specified
    const aggregatedCollectedValues = this._aggregateCollectedValues(
      allCollectedValues,
      valueAggregation
    );
    
    evaluation.outcome = 'path_found';
    evaluation.finalPossibility = finalPossibility;
    evaluation.bestPath = bestPath;
    evaluation.totalValuesCollected = aggregatedCollectedValues.length;
    
    Arbiter.DEBUG && Arbiter.log('MultiHopRule evaluation complete:', {
      pathsFound: pathsWithValues.length,
      finalPossibility,
      valuesCollected: aggregatedCollectedValues.length,
      fallbackUsed: evaluation.fallbackUsed
    });
    
    // Build authorization result (raw possibility values)
    const resolvedPathSteps = bestPath && bestPath.pathSteps
      ? bestPath.pathSteps
      : (pathsWithValues[0] && pathsWithValues[0].pathSteps ? pathsWithValues[0].pathSteps : null);

    const allowMeta = finalPossibility > 0 ? {
      ruleType: 'multi_hop',
      reason: 'multi_hop_path_found',
      rule,
      pathsFound: pathsWithValues.length,
      bestPath: bestPath,
      pathSteps: resolvedPathSteps,
      fallbackUsed: evaluation.fallbackUsed,
      evaluation
    } : null;

    const authResult = {
      possibility: finalPossibility,
      reliability: finalReliability,
      validity: this._validity('min', [relation], [], Math.max(1, bestPath ? bestPath.hops : 1), finalPossibility),
      ...(includeMeta && {
        meta: allowMeta
      }),
      ...(includeMeta && { meta_allow: allowMeta }),
      reason: undefined
    };
    
    return this._createStandardResult(authResult, aggregatedCollectedValues);
  }

  /**
   * Find paths and collect values in a single traversal
   * @private
   */
  _findPathsAndCollectValues(startId, endId, relation, maxDepth, 
                           visited, currentPath = [], currentPoss = 1.0, currentReliability = 1.0,
                           reverse = false, collectValues = true,
                           trackPaths = true,
                           stopSignal = null,
                           valueFilters = {}, valueContext = null, evaluation, fastPath = false, minPossibility = 0, options = null,
                           allowZeroHop = false) {
    if (stopSignal?.stop) {
      return [];
    }
    
    if (startId === endId && (allowZeroHop || currentPath.length > 0)) {
      const pathKeys = trackPaths
        ? [...currentPath.map(step => step.nodeKey), this.arbiter.resolveKey(endId, options)]
        : null;
      const pathResult = {
        nodes: pathKeys,
        nodeIds: trackPaths ? [...currentPath.map(step => step.nodeId), endId] : [endId],
        hops: currentPath.length,
        possibility: currentPoss,
        reliability: currentReliability,
        collectedValues: [],
        pathSteps: trackPaths ? [...currentPath] : null
      };
      
      // Collect values from the complete path if enabled
      if (collectValues) {
        pathResult.collectedValues = this._collectValuesFromPath(
          currentPath, relation, valueFilters, valueContext
        );
      }
      
      Arbiter.DEBUG && Arbiter.log('Found path with values:', {
        path: pathKeys,
        possibility: currentPoss,
        valuesCollected: pathResult.collectedValues.length
      });
      
      if (stopSignal) {
        stopSignal.stop = true;
      }
      return [pathResult];
    }
    
    if (maxDepth <= 0 || currentPoss < minPossibility || visited.has(startId)) {
      return [];
    }
    
    visited.add(startId);
    const pathsWithValues = [];
    
    // Find direct edges using indexed lookups
    let directEdges;
    if (reverse) {
      directEdges = this.arbiter.relationManager.getRelationsToDst(startId, relation, options);
    } else {
      directEdges = this.arbiter.relationManager.getRelationsFromSrc(startId, relation, options);
    }
    
    Arbiter.DEBUG && Arbiter.log('MultiHop exploring from', this.arbiter.resolveKey(startId, options), 'found direct edges:', directEdges.length);
    
    // Process direct edges
    for (const edge of directEdges) {
      if (stopSignal?.stop) {
        break;
      }
      const nextId = reverse ? edge.src : edge.dst;
      const nextKey = this.arbiter.resolveKey(nextId, options);
      if (!nextKey) continue;
      
      const nextPoss = Math.min(currentPoss, edge.possibility ?? 1.0);
      const nextReliability = currentReliability * (edge.reliability !== undefined ? edge.reliability : 1.0);
      if (fastPath && nextPoss < minPossibility) continue;
      
      const pathStep = (collectValues || trackPaths) ? {
        nodeId: startId,
        nodeKey: this.arbiter.resolveKey(startId, options),
        relation: relation,
        edge: edge,
        inferred: false,
        possibility: edge.possibility ?? 1.0,
        source: edge.source || 'persistent'
      } : null;
      const nextPath = (collectValues || trackPaths) ? [...currentPath, pathStep] : currentPath;
      
      const subPaths = this._findPathsAndCollectValues(
        nextId,
        endId,
        relation,
        maxDepth - 1,
        visited,
        nextPath,
        nextPoss,
        nextReliability,
        reverse,
        collectValues,
        trackPaths,
        stopSignal,
        valueFilters,
        valueContext,
        evaluation,
        fastPath,
        minPossibility,
        options
      );
      
      pathsWithValues.push(...subPaths);
    }
    
    visited.delete(startId);
    return pathsWithValues;
  }

  /**
   * Collect values from a complete path
   * @private
   */
  _collectValuesFromPath(pathSteps, defaultRelation, valueFilters, valueContext, options = null) {
    const collectedValues = [];
    const evaluationNow = options && options.now !== undefined && options.now !== null ? options.now : Date.now();
    // Get blurred interval from ValueManager (arbiter-level; some stubs and
    // older layouts keep it on the relation manager)
    const valueManager = this.arbiter.valueManager || this.arbiter.relationManager?.valueManager;
    
    for (let stepIndex = 0; stepIndex < pathSteps.length; stepIndex++) {
      const step = pathSteps[stepIndex];
      
      if (step.inferred) {
        continue;
      }
      
      // Determine which relations to collect from
      const relationsToCollect = valueFilters.relations || [step.relation || defaultRelation];
      
      for (const relationName of relationsToCollect) {
        // Collect from direct edge if available
        if (step.edge && step.edge.value !== undefined && step.edge.value !== null) {
          if (this._passesValueFilters(step.edge.value, valueFilters)) {
            // Check TTL. The valueManager's per-relation TTL is the
            // authority (matching chain/comparator paths); valueFilters.ttl
            // is an explicit override. Timestamps and the age computation
            // use the caller-pinned clock, never the wall clock.
            const ttl = valueFilters.ttl || (valueManager && valueManager.getTTL
              ? valueManager.getTTL(step.edge.rel)
              : 24 * 60 * 60 * 1000);
            const timestamp = step.edge.changed_last_at || step.edge.updated_last_at ||
              (evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now());
            
            if (!OWAFusion.isWithinTTL(timestamp, ttl, evaluationNow)) {
              Arbiter.DEBUG && Arbiter.log('MultiHop skipping value due to TTL:', {
                value: step.edge.value,
                timestamp,
                ttl,
                age: evaluationNow - timestamp
              });
              continue;
            }
            
            const blurred = valueManager.getBlurredValue(step.edge, evaluationNow);
            
            if (blurred.interval) {
              const collectedValue = this._createCollectedValue(
                blurred.interval, 
                blurred.possibility, 
                pathSteps.map(s => s.nodeKey),
                {
                  entityKey: step.nodeKey,
                  relation: relationName,
                  step: stepIndex,
                  inferred: false,
                  fullPath: pathSteps.map(s => s.nodeKey),
                  stepPosition: stepIndex,
                  originalValue: step.edge.value 
                },
                {
                  timestamp,
                  pathPossibility: step.possibility,
                  relationPossibility: step.edge.possibility ?? 1.0,
                  currentPossibility: blurred.possibility,
                  interval: blurred.interval
                }
              );
              
              collectedValues.push(collectedValue);
              
              Arbiter.DEBUG && Arbiter.log('MultiHop collected blurred value from edge:', {
                originalValue: step.edge.value,
                interval: blurred.interval,
                step: stepIndex,
                relation: relationName,
                node: step.nodeKey,
                inferred: false,
                decayedPossibility: blurred.possibility
              });
            }
          }
        }
        
        // Also collect from ValueContext if available
        if (valueContext) {
          const contextValues = valueContext.getValues(step.nodeId, relationName);
          
          for (const contextValue of contextValues) {
            if (this._passesValueFilters(contextValue.value, valueFilters)) {
              // Check TTL
              const ttl = valueFilters.ttl || 24 * 60 * 60 * 1000;
              if (!OWAFusion.isWithinTTL(contextValue.timestamp, ttl, evaluationNow)) {
                continue;
              }
              
              // Create temporary relation for ValueManager
              const tempRelation = {
                src: step.nodeId,
                dst: step.nodeId,
                rel: relationName,
                value: contextValue.value,
                possibility: contextValue.possibility,
                reliability: contextValue.reliability,
                changed_last_at: contextValue.timestamp
              };
              
              const blurred = valueManager.getBlurredValue(tempRelation, evaluationNow);
              
              if (blurred.interval) {
                const collectedValue = this._createCollectedValue(
                  blurred.interval,
                  Math.min(step.possibility, blurred.possibility),
                  pathSteps.map(s => s.nodeKey),
                  {
                    entityKey: step.nodeKey,
                    relation: relationName,
                    step: stepIndex,
                    inferred: false,
                    fullPath: pathSteps.map(s => s.nodeKey),
                    stepPosition: stepIndex,
                    fromValueContext: true,
                    originalValue: contextValue.value
                  },
                  {
                    timestamp: contextValue.timestamp,
                    pathPossibility: step.possibility,
                    relationPossibility: contextValue.possibility,
                    currentPossibility: blurred.possibility,
                    interval: blurred.interval
                  }
                );
                
                collectedValues.push(collectedValue);
              }
            }
          }
        }
      }
    }
    
    return collectedValues;
  }

  /**
   * Check if a value passes the filters
   * @private
   */
  _passesValueFilters(value, valueFilters) {
    if (valueFilters.minValue !== undefined && value < valueFilters.minValue) {
      return false;
    }
    if (valueFilters.maxValue !== undefined && value > valueFilters.maxValue) {
      return false;
    }
    return true;
  }

  /**
   * Aggregate multiple paths to get final possibility using OWAFusion
   * @private
   */
  _aggregatePaths(pathsWithValues, pathAggregation, owaWeights, evaluation, trackOwa = false) {
    if (pathsWithValues.length === 0) {
      return { finalPossibility: 0, bestPath: null };
    }
    
    if (pathsWithValues.length === 1) {
      const path = pathsWithValues[0];
      return {
        finalPossibility: path.possibility,
        finalReliability: path.reliability !== undefined ? path.reliability : 1.0,
        bestPath: path
      };
    }
    
    // Prepare data for OWAFusion
    const possibilities = pathsWithValues.map(p => p.possibility);
    const metas = pathsWithValues.map(p => ({ 
      path: p.nodes, 
      hops: p.hops, 
      inferred: false,
      fallback: p.fallback || false,
      nodeIds: p.nodeIds,
      collectedValuesCount: p.collectedValues ? p.collectedValues.length : 0,
      pathPossibility: p.possibility,
      pathSteps: p.pathSteps || null
    }));
    
    // Use OWAFusion for path aggregation
    let possibilityResult;
    
    const owaTraceOptions = trackOwa ? { includeTrace: true } : null;

    if (pathAggregation === 'owa' && owaWeights) {
      // Use custom OWA weights
      possibilityResult = OWAFusion.fuseWithMeta(possibilities, metas, owaWeights, 'owa', true, owaTraceOptions);
      
      evaluation.aggregation = { 
        method: 'owa', 
        weights: owaWeights,
        fusedPossibility: possibilityResult.value,
        selectedPath: possibilityResult.meta,
        ...(trackOwa && possibilityResult.trace ? {
          owa: {
            level: null,
            aggregator: 'owa',
            weights: possibilityResult.trace.weights,
            sortedValues: possibilityResult.trace.sortedValues,
            contributions: possibilityResult.trace.contributions,
            selectedIndex: possibilityResult.trace.selectedIndex
          }
        } : {})
      };
    } else {
      // Use standard aggregation methods
      possibilityResult = OWAFusion.fuseWithMeta(possibilities, metas, null, pathAggregation, true, owaTraceOptions);
      
      evaluation.aggregation = { 
        method: pathAggregation,
        fusedPossibility: possibilityResult.value,
        selectedPath: possibilityResult.meta,
        pathCount: pathsWithValues.length,
        ...(trackOwa && possibilityResult.trace ? {
          owa: {
            level: null,
            aggregator: pathAggregation,
            weights: possibilityResult.trace.weights,
            sortedValues: possibilityResult.trace.sortedValues,
            contributions: possibilityResult.trace.contributions,
            selectedIndex: possibilityResult.trace.selectedIndex
          }
        } : {})
      };
    }
    
    // Find the best path based on the selected metadata
    let bestPath = possibilityResult.meta;
    
    // If meta doesn't contain the full path object, find it in the original paths
    if (!bestPath || !bestPath.nodes) {
      // Fall back to finding the path that contributed most to the result
      const selectedIndex = metas.findIndex(m => 
        m.path === possibilityResult.meta?.path || 
        m.pathPossibility === possibilityResult.meta?.pathPossibility
      );
      
      if (selectedIndex >= 0) {
        bestPath = pathsWithValues[selectedIndex];
      } else {
        // Ultimate fallback: use the first path
        bestPath = pathsWithValues[0];
      }
    }

    if (bestPath && !bestPath.pathSteps) {
      const targetPath = bestPath.path || bestPath.nodes;
      if (Array.isArray(targetPath)) {
        const match = pathsWithValues.find(p => Array.isArray(p.nodes) && p.nodes.join('|') === targetPath.join('|'));
        if (match) {
          bestPath = match;
        }
      }
    }
    
    return { 
      finalPossibility: possibilityResult.value,
      finalReliability: bestPath ? (bestPath.reliability !== undefined ? bestPath.reliability : 1.0) : 1.0,
      bestPath 
    };
  }

  /**
   * Aggregate collected values based on aggregation method using OWAFusion 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;
      }
      
      // Use OWAFusion for interval aggregation
      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,
        timestamp: cv.metadata?.timestamp,
        originalInterval: cv.value
      }));
      
      // Aggregate intervals using OWAFusion
      const intervalResult = OWAFusion.fuseIntervalsWithMeta(
        intervals, metas, null, aggregationMethod
      );
      
      const possibilityResult = OWAFusion.fuseWithMeta(
        possibilities, metas, null, aggregationMethod
      );
      
      // Create aggregated collected value
      const aggregatedCV = this._createCollectedValue(
        intervalResult.interval,
        possibilityResult.value,
        pathValues[0].path,
        {
          ...pathValues[0].source,
          aggregationMethod,
          aggregatedFromCount: pathValues.length,
          selectedMeta: intervalResult.meta
        },
        {
          ...pathValues[0].metadata,
          aggregatedFromIntervals: intervals,
          interval: intervalResult.interval,
          aggregationMeta: {
            method: aggregationMethod,
            sourceCount: pathValues.length,
            intervalContribution: intervalResult.interval,
            possibilityContribution: possibilityResult.value,
            selectedSource: intervalResult.meta
          }
        }
      );
      
      aggregatedValues.push(aggregatedCV);
    }
    
    return aggregatedValues;
  }

  /**
   * Find a basic path using simple BFS when sophisticated multi-hop fails
   * @private
   */
  _findBasicPathWithValues(startId, endId, relation, maxDepth, minPossibility = 0, 
                          reverse = false, collectValues = true, trackPaths = true, valueFilters = {},
                          valueContext = null, evaluation, options = null, allowZeroHop = false) {
    if (startId === endId && allowZeroHop) {
      const startKey = this.arbiter.resolveKey(startId, options);
      return {
        nodes: trackPaths ? [startKey] : null,
        nodeIds: [startId],
        hops: 0,
        possibility: 1.0,
        fallback: true,
        basic: true,
        collectedValues: []
      };
    }
    
    const visited = new Set();
    const queue = [{
      nodeId: startId,
      path: [],
      pathSteps: [],
      possibility: 1.0,
      depth: 0
    }];
    let queueIndex = 0;
    
    while (queueIndex < queue.length) {
      const current = queue[queueIndex++];
      
      if (current.depth >= maxDepth || visited.has(current.nodeId) || current.possibility < minPossibility) {
        continue;
      }
      
      visited.add(current.nodeId);
      
      // Look for direct connections
      let edges;
      const useRelationGraph = !options?.partialGraphContext;
      const useGraphNeighbors = useRelationGraph &&
        this.arbiter.relationManager.shouldUseRelationGraphTraversal(current.nodeId, relation, reverse);
      const relationGraphNeighbors = useGraphNeighbors
        ? this.arbiter.relationManager.getRelationGraphNeighbors(current.nodeId, relation, reverse)
        : null;

      if (relationGraphNeighbors) {
        edges = [];
        for (const neighborId of relationGraphNeighbors) {
          const edge = reverse
            ? this.arbiter.relationManager.getDirectRelation(neighborId, relation, current.nodeId, options)
            : this.arbiter.relationManager.getDirectRelation(current.nodeId, relation, neighborId, options);
          if (edge) edges.push(edge);
        }
      } else if (reverse) {
        edges = this.arbiter.relationManager.getRelationsToDst(current.nodeId, relation, options);
      } else {
        edges = this.arbiter.relationManager.getRelationsFromSrc(current.nodeId, relation, options);
      }
      
      for (const edge of edges) {
        const nextId = reverse ? edge.src : edge.dst;
        const nextKey = this.arbiter.resolveKey(nextId, options);
        
        if (!nextKey || visited.has(nextId)) continue;
        
        const nextPath = trackPaths
          ? [...current.path, this.arbiter.resolveKey(current.nodeId, options)]
          : current.path;
        const nextPossibility = Math.min(current.possibility, edge.possibility ?? 1.0);
        
        const pathStep = (collectValues || trackPaths) ? {
          nodeId: current.nodeId,
          nodeKey: this.arbiter.resolveKey(current.nodeId, options),
          relation: relation,
          edge: edge,
          inferred: false,
          possibility: edge.possibility ?? 1.0,
          source: edge.source || 'persistent'
        } : null;
        
        const nextPathSteps = (collectValues || trackPaths)
          ? [...current.pathSteps, pathStep]
          : current.pathSteps;
        
        // Check if we reached the target
        if (nextId === endId) {
          const finalPath = trackPaths ? [...nextPath, nextKey] : null;
          
          const pathResult = {
            nodes: finalPath,
            nodeIds: trackPaths ? [...nextPath.map(key => this.arbiter.resolveNodeId(key, options)), nextId] : [startId, nextId],
            hops: finalPath.length - 1,
            possibility: nextPossibility,
            fallback: true,
            basic: true,
            collectedValues: [],
            pathSteps: trackPaths ? nextPathSteps : null
          };
          
          // Collect values from the path if enabled
          if (collectValues) {
            pathResult.collectedValues = this._collectValuesFromPath(
              nextPathSteps, relation, valueFilters, valueContext
            );
          }
          
          Arbiter.DEBUG && Arbiter.log('Basic fallback found path with values:', {
            path: finalPath,
            valuesCollected: pathResult.collectedValues.length
          });
          
          return pathResult;
        }
        
        // Continue searching if possibility is still acceptable
        if (nextPossibility >= minPossibility ) {
          queue.push({
            nodeId: nextId,
            path: nextPath,
            pathSteps: nextPathSteps,
            possibility: nextPossibility,
            depth: current.depth + 1
          });
        }
      }
    }
    
    Arbiter.DEBUG && Arbiter.log('Basic fallback: no path found within constraints');
    return null;
  }

  /**
   * Normalize rule configuration
   * @private
   */
  _normalizeRule(rule) {
    return { ...rule };
  }
} 
