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

// Default epsilon for floating point comparisons
const DEFAULT_EPSILON = 0.0001;

/**
 * RelationalComparatorRule - Evaluates access by comparing values from relations,
 * treating values as intervals that "blur" over time based on decaying possibility.
 * 
 * Configuration:
 * {
 *   type: 'relational_comparator',
 *   leftOperand: {
 *     rule: {...},             // Any rule configuration
 *     extractValue: true,      // Extract value from relation (if false, rule's possibility is used as value)
 *     valueRelation: string,   // Optional: specific relation for value
 *     aggregator: string,      // 'max', 'min', 'sum', 'average', 'median', 'majority', 'priority', 'optimistic', etc.
 *     owaWeights: number[],    // Optional: custom OWA weights for aggregation
 *     decayRate: number,       // Governs speed of possibility decay & value blurring (e.g., 0.1 per period)
 *     decayPeriod: string,     // 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR' (default 'HOUR')
 *     possibilityDecayDirection: string, // 'down' (towards 0), 'neutral' (towards 0.5), 'up' (towards 1), 'stable' (no decay)
 *     valueBlurDirection: string, // 'neutral' (symmetric), 'down' (expands lower bound), 'up' (expands upper bound), 'stable' (minimal blur)
 *     baseBlurAmount: number,  // Factor scaling blur magnitude relative to possibility loss (e.g., 1.0)
 *     minOperandPossibility: number, // If operand's decayed possibility < this, considered no value (e.g., 0.05)
 *     evaluateFrom: string     // 'user', 'object', or 'auto' (for rule evaluation perspective)
 *   },
 *   rightOperand: {...},       // Same structure as leftOperand
 *   comparator: string,        // '>', '>=', '<', '<=', '==', '!='
 *   marginOfSafety: number,    // Optional: scales right operand's point value before blurring (e.g., 1.1 for 10% margin)
 *   minRulePossibility: number,// Optional: if final rule possibility < this, considered 0 (e.g., 0.1)
 *   fallbackBehavior: string   // 'allow' or 'deny' if values/operands are insufficient
 * }
 */
export class RelationalComparatorRule extends BaseRule {
  constructor(arbiter, ruleEvaluator) {
    super(arbiter);
    this.ruleEvaluator = ruleEvaluator;
    this.epsilon = DEFAULT_EPSILON; // Rule specific epsilon for '==' and '!='
    this._directValueCache = new Map();
  }

  /**
   * Evaluate relational comparison
   * @protected
   */
  _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
    const { left: leftOperand, right: rightOperand, comparator, marginOfSafety = 1.0, fallbackBehavior = 'deny', minRulePossibility = 0 } = rule;
    try {
      this.epsilon = rule.epsilon === undefined ? DEFAULT_EPSILON : rule.epsilon;
      const compiledRule = rule?._compiled && rule._compiled.type === 'relational_comparator' ? rule._compiled : null;
      const compiledLeft = compiledRule?.left || null;
      const compiledRight = compiledRule?.right || null;
      
      const ruleMetaBase = {
        ruleType: 'RelationalComparatorRule',
        userKey,
        objectKey,
        comparator,
        marginOfSafetyApplied: rightOperand ? marginOfSafety : 1.0, // Only if rightOperand exists
        fallbackBehavior,
        minRulePossibilityUsed: minRulePossibility,
        evaluationStarted: Date.now()
      };
      let evaluationMeta = options.trackEvaluation ? { ...ruleMetaBase, leftOperandDetails: {}, rightOperandDetails: {} } : null;

      // Evaluate left operand
      const leftOpResult = this._evaluateOperand(
        userId, userKey, objectId, objectKey,
        leftOperand, compiledLeft, visited, currentRelation, options, 'left', 1.0, evaluationMeta // No margin for left
      );
      if (evaluationMeta) evaluationMeta.leftOperandDetails = leftOpResult.meta || {};
      
      // Evaluate right operand
      const rightOpResult = this._evaluateOperand(
        userId, userKey, objectId, objectKey,
        rightOperand, compiledRight, visited, currentRelation, options, 'right', marginOfSafety, evaluationMeta // Apply margin for right
      );
      if (evaluationMeta) evaluationMeta.rightOperandDetails = rightOpResult.meta || {};
      
      // Perform comparison of blurred intervals
      let comparisonOutput = this._compareBlurredValues(
        leftOpResult, rightOpResult, comparator, fallbackBehavior, rule, options, ruleMetaBase
      );

      // Apply minimum rule possibility threshold
      if (comparisonOutput.possibility < minRulePossibility) {
        if (evaluationMeta && evaluationMeta.comparisonStep) evaluationMeta.comparisonStep.finalPossibilityBeforeMinRule = comparisonOutput.possibility;
        comparisonOutput.possibility = 0;
        comparisonOutput.reason = comparisonOutput.reason + '_belowMinRulePossibility';
        if (evaluationMeta && evaluationMeta.comparisonStep) evaluationMeta.comparisonStep.adjustedToZeroByMinRule = true;
      }
      
      if (evaluationMeta) {
        evaluationMeta.finalResult = { possibility: comparisonOutput.possibility, reliability: comparisonOutput.reliability, reason: comparisonOutput.reason };
        evaluationMeta.evaluationCompleted = Date.now();
        evaluationMeta.evaluationDuration = evaluationMeta.evaluationCompleted - ruleMetaBase.evaluationStarted;
        if (comparisonOutput.meta) comparisonOutput.meta.fullEvaluationTrace = evaluationMeta;
        else comparisonOutput.meta = { fullEvaluationTrace: evaluationMeta };
      }

      comparisonOutput.validity = this._validity('identity',
        [leftOperand?.rule?.relation, rightOperand?.rule?.relation].filter(Boolean), [], 1, comparisonOutput.possibility || 0);
      return comparisonOutput;
    } catch (error) {
      return {
        possibility: 0,
        reliability: 0,
        ...(options.includeMeta && {
          meta: {
            ruleType: 'RelationalComparatorRule',
            userKey,
            objectKey,
            comparator,
            marginOfSafetyApplied: rightOperand ? marginOfSafety : 1.0,
            fallbackBehavior,
            minRulePossibilityUsed: minRulePossibility,
            evaluationStarted: Date.now(),
            evaluationError: error.message
          }
        }),
        collectedValues: [],
        reason: 'evaluation_error'
      };
    }
  }

  /**
   * Evaluate an operand (left or right)
   * @private
   */
  _evaluateOperand(userId, userKey, objectId, objectKey, operandConfig, operandCompiled, visited, currentRelation, options, side, marginOfSafety = 1.0, parentEvaluationMeta) {
    const { 
      rule: nestedRuleConfig,
      extractValue = true, 
      valueRelation, 
      aggregator, // handles 'aggregation' for backward compatibility
      owaWeights, 
      minOperandPossibility = 0.01, // Default minimum possibility for an operand to be valid
      evaluateFrom = 'auto',
      ttl = 24 * 60 * 60 * 1000 // Default TTL: 24 hours in milliseconds
    } = operandConfig;
    
    const finalAggregator = aggregator || operandConfig.aggregation || 'max'; // Backward compatibility for 'aggregation'
    const resolvedEvaluateFrom = operandCompiled?.evaluateFrom || evaluateFrom;
    const resolvedValueRelation = operandCompiled?.valueRelationResolved || valueRelation;
    const resolvedRule = operandCompiled?.rule || nestedRuleConfig;
    const { valueContext = null } = options;

    const evaluationNow = options.now !== undefined && options.now !== null ? options.now : Date.now();
    const operandMetaBase = {
        operandSide: side,
        nestedRuleType: resolvedRule?.type || nestedRuleConfig.type,
        extractingValue: extractValue,
        valueRelationUsed: resolvedValueRelation || resolvedRule?.relation || nestedRuleConfig.relation || nestedRuleConfig.rel || nestedRuleConfig.label || nestedRuleConfig.name,
        aggregatorUsed: finalAggregator,
        minOperandPossibility,
        marginAppliedToValue: marginOfSafety,
        evaluateFromPerspective: resolvedEvaluateFrom,
        ttl
    };
    let operandEvalMeta = options.trackEvaluation ? { ...operandMetaBase, steps: [] } : null;

    // Determine evaluation perspective for the nested rule
    let evalUserId = userId, evalUserKey = userKey, evalObjectId = objectId, evalObjectKey = objectKey;
    const isLogicalRule = resolvedRule?.type === 'logical' || !!(nestedRuleConfig.union || nestedRuleConfig.intersection || nestedRuleConfig.exclusion);
    if (resolvedEvaluateFrom === 'user') {
      // Evaluate from user perspective
      if (resolvedRule?.type === 'direct' || resolvedRule?.type === 'computed' || isLogicalRule) {
        // Direct rules: user becomes both subject and object
        evalUserId = userId;
        evalUserKey = userKey;
        evalObjectId = userId;  
        evalObjectKey = userKey;
      } else {
        // Chain/traversal rules: handle based on whether we're doing value extraction
        if (nestedRuleConfig.extractValues) {
          // For value extraction chains, we want to traverse from the specified entity
          // and extract values from whatever entities we can reach
          evalUserId = userId;
          evalUserKey = userKey;
          evalObjectId = objectId;
          evalObjectKey = objectKey;
        } else {
          // For path-checking chains, reverse the traversal direction
          evalUserId = objectId;
          evalUserKey = objectKey;
          evalObjectId = userId;
          evalObjectKey = userKey;
        }
      }
    } else if (resolvedEvaluateFrom === 'object') {
      // Evaluate from object perspective
      if (resolvedRule?.type === 'direct' || resolvedRule?.type === 'computed' || isLogicalRule) {
        // Direct rules: object becomes both subject and object
        evalUserId = objectId;
        evalUserKey = objectKey;
        evalObjectId = objectId;
        evalObjectKey = objectKey;
      } else {
        // Chain/traversal rules: reverse the traversal direction
        evalUserId = objectId;
        evalUserKey = objectKey;
        evalObjectId = userId;
        evalObjectKey = userKey;
      }
    } else {
      // Auto mode - use conventional subject/object relationship
      evalUserId = userId;
      evalUserKey = userKey;
      evalObjectId = objectId;
      evalObjectKey = objectKey;
    }
    if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'PerspectiveSet', evalUserKey, evalObjectKey });

    const canCacheDerived = extractValue && !!this.arbiter.ruleResultCache &&
      !options.partialGraphContext && !options.includeMeta &&
      (options.now === undefined || options.now === null) &&
      options.cacheDerivedValues !== false;
    const operandIdentity = this._getOperandCacheIdentity(resolvedValueRelation, resolvedRule, nestedRuleConfig);
    const derivedCacheKey = canCacheDerived
      ? this.arbiter.keyManager.createCompositeKey(
        evalUserId,
        `${currentRelation}:operand:${side}:${operandIdentity}:${finalAggregator}:${resolvedEvaluateFrom}:${marginOfSafety}`,
        evalObjectId
      )
      : null;
    if (canCacheDerived) {
      const cached = this.arbiter.ruleResultCache.get(derivedCacheKey);
      const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
      if (cached && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
        const cachedResult = cached.result;
        let valueInterval = cachedResult.valueInterval;
        let operandPossibility = cachedResult.operandPossibility || 0;
        let finalReliability = cachedResult.reliability || 1.0;
        let hasValue = cachedResult.hasValue || false;
        let operandSource = cachedResult.source || null;
        const cachedCollectedValues = cachedResult.collectedValues || [];

        if (operandEvalMeta) {
          operandEvalMeta.steps.push({ step: 'DerivedValueCacheHit', derivedCacheKey });
        }

        if (options.collectValues && valueContext && cachedCollectedValues.length > 0) {
          valueContext.addCollectedValues(cachedCollectedValues, nestedRuleConfig.type || 'cached_operand', nestedRuleConfig);
        }

        if (operandPossibility < minOperandPossibility) {
          if (operandEvalMeta) operandEvalMeta.steps.push({
            step: 'OperandPossibilityBelowMinimum',
            operandPossibility,
            minOperandPossibility
          });
          operandPossibility = 0;
          hasValue = false;
        }

        if (operandEvalMeta && parentEvaluationMeta) {
          if (side === 'left') parentEvaluationMeta.leftOperandProcessed = operandEvalMeta;
          if (side === 'right') parentEvaluationMeta.rightOperandProcessed = operandEvalMeta;
        }

        return {
          valueInterval,
          operandPossibility,
          reliability: finalReliability,
          hasValue,
          source: operandSource,
          ...(options.collectValues && { collectedValues: cachedCollectedValues }),
          ...(options.includeMeta && { meta: operandEvalMeta })
        };
      }
    }

    const compiledDirect = operandCompiled?.rule?.type === 'direct' ? operandCompiled.rule : null;
    if (extractValue && compiledDirect) {
      const relationName = compiledDirect.relation;
      const directRel = this._getDirectRelationForValue(evalUserId, evalObjectId, relationName, resolvedEvaluateFrom, options);
      const operandCollectedValues = [];
      let valueInterval = null;
      let operandPossibility = 0;
      let finalReliability = 1.0;
      let hasValue = false;
      let operandSource = null;

      if (directRel && typeof directRel.value === 'number') {
        const cached = this._getCachedDirectValue(directRel, ttl, evaluationNow);
        if (cached) {
          const aggregatedResult = this._aggregateCrispValues(
            [cached], finalAggregator, owaWeights, operandEvalMeta, options
          );
          valueInterval = aggregatedResult.interval;
          operandPossibility = aggregatedResult.possibility;
          finalReliability = aggregatedResult.reliability;
          hasValue = true;
          operandSource = aggregatedResult.source || null;

          if (marginOfSafety !== 1.0 && valueInterval && typeof valueInterval.min === 'number' && typeof valueInterval.max === 'number') {
            valueInterval = {
              min: valueInterval.min * marginOfSafety,
              max: valueInterval.max * marginOfSafety
            };
          }

          if (options.collectValues) {
            const srcKey = this.arbiter.resolveKey(directRel.src, options);
            const dstKey = this.arbiter.resolveKey(directRel.dst, options);
            const path = [srcKey, dstKey];
            operandCollectedValues.push(this.ruleEvaluator.ruleHandlers.direct._createCollectedValue(
              directRel.value,
              directRel.possibility !== undefined ? directRel.possibility : 1.0,
              path,
              {
                entityKey: srcKey,
                relation: relationName,
                step: 0
              },
              {
                timestamp: directRel.changed_last_at || directRel.updated_last_at ||
              (evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
                reliability: directRel.reliability || 1.0,
                source: directRel.source || 'persistent'
              }
            ));
          }

          if (canCacheDerived && derivedCacheKey) {
            const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
            this.arbiter.ruleResultCache.set(derivedCacheKey, {
              result: {
                valueInterval,
                operandPossibility,
                reliability: finalReliability,
                hasValue,
                source: operandSource,
                collectedValues: operandCollectedValues
              },
              timestamp: cacheNow
            });
            this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
            if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
          }
        }
      }

      if (operandPossibility < minOperandPossibility) {
        if (operandEvalMeta) operandEvalMeta.steps.push({
          step: 'OperandPossibilityBelowMinimum',
          operandPossibility,
          minOperandPossibility
        });
        operandPossibility = 0;
        hasValue = false;
      }

      if (operandEvalMeta && parentEvaluationMeta) {
        if (side === 'left') parentEvaluationMeta.leftOperandProcessed = operandEvalMeta;
        if (side === 'right') parentEvaluationMeta.rightOperandProcessed = operandEvalMeta;
      }

      return {
        valueInterval,
        operandPossibility,
        reliability: finalReliability,
        hasValue,
        source: operandSource,
        ...(options.collectValues && { collectedValues: operandCollectedValues }),
        ...(options.includeMeta && { meta: operandEvalMeta })
      };
    }

    const compiledOperandRule = operandCompiled?.rule || null;
    const directList = (compiledOperandRule && compiledOperandRule.type === 'logical' && compiledOperandRule.op === 'union'
      && compiledOperandRule._optimized?.kind === 'direct_list')
      ? compiledOperandRule._optimized.direct
      : null;
    if (extractValue && Array.isArray(directList) && directList.length > 0) {
      const valueResults = [];
      const operandCollectedValues = options.collectValues ? [] : [];

      for (const direct of directList) {
        const reverse = !!direct.reverse;
        const relName = direct.relation;
        let srcId = evalUserId;
        let dstId = evalObjectId;
        let srcKey = evalUserKey;
        let dstKey = evalObjectKey;
        if (reverse) {
          srcId = evalObjectId;
          dstId = evalUserId;
          srcKey = evalObjectKey;
          dstKey = evalUserKey;
        }
        const rel = this.arbiter.relationManager.getDirectRelation(srcId, relName, dstId, options);
        if (!rel || typeof rel.value !== 'number') continue;
        if (this.arbiter.valueManager && this.arbiter.valueManager._isValueExpired(rel, evaluationNow)) continue;

        valueResults.push({
          value: rel.value,
          possibility: rel.possibility !== undefined ? rel.possibility : 1.0,
          reliability: rel.reliability || 1.0,
          timestamp: rel.changed_last_at || rel.updated_last_at ||
              (evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
          source: 'direct_relation',
          originalValue: rel.value
        });

        if (options.collectValues && direct.collectValues !== false) {
          const path = [srcKey, dstKey];
          operandCollectedValues.push(this.ruleEvaluator.ruleHandlers.direct._createCollectedValue(
            rel.value,
            rel.possibility !== undefined ? rel.possibility : 1.0,
            path,
            {
              entityKey: srcKey,
              relation: relName,
              step: 0
            },
            {
              timestamp: rel.changed_last_at || rel.updated_last_at ||
              (evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
              reliability: rel.reliability || 1.0,
              source: rel.source || 'persistent'
            }
          ));
        }
      }

      if (options.collectValues && valueContext && operandCollectedValues.length > 0) {
        valueContext.addCollectedValues(operandCollectedValues, 'direct_list', operandConfig);
      }

      let valueInterval = null;
      let operandPossibility = 0;
      let finalReliability = 1.0;
      let hasValue = false;
      let operandSource = null;

      if (valueResults.length > 0) {
        const aggregatedResult = this._aggregateCrispValues(
          valueResults, finalAggregator, owaWeights, operandEvalMeta, options
        );
        valueInterval = aggregatedResult.interval;
        operandPossibility = aggregatedResult.possibility;
        finalReliability = aggregatedResult.reliability;
        hasValue = true;
        operandSource = aggregatedResult.source || null;

        if (marginOfSafety !== 1.0 && valueInterval && typeof valueInterval.min === 'number' && typeof valueInterval.max === 'number') {
          valueInterval = {
            min: valueInterval.min * marginOfSafety,
            max: valueInterval.max * marginOfSafety
          };
        }

        if (canCacheDerived && derivedCacheKey) {
          const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
          this.arbiter.ruleResultCache.set(derivedCacheKey, {
            result: {
              valueInterval,
              operandPossibility,
              reliability: finalReliability,
              hasValue,
              source: operandSource,
              collectedValues: operandCollectedValues
            },
            timestamp: cacheNow
          });
          this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
          if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
        }
      }

      if (operandPossibility < minOperandPossibility) {
        if (operandEvalMeta) operandEvalMeta.steps.push({
          step: 'OperandPossibilityBelowMinimum',
          operandPossibility,
          minOperandPossibility
        });
        operandPossibility = 0;
        hasValue = false;
      }

      if (operandEvalMeta && parentEvaluationMeta) {
        if (side === 'left') parentEvaluationMeta.leftOperandProcessed = operandEvalMeta;
        if (side === 'right') parentEvaluationMeta.rightOperandProcessed = operandEvalMeta;
      }

      return {
        valueInterval,
        operandPossibility,
        reliability: finalReliability,
        hasValue,
        source: operandSource,
        ...(options.collectValues && { collectedValues: operandCollectedValues }),
        ...(options.includeMeta && { meta: operandEvalMeta })
      };
    }

    // Evaluate the nested rule
    const ruleResult = this.ruleEvaluator.evaluateRule(
      evalUserId, evalUserKey, evalObjectId, evalObjectKey,
      nestedRuleConfig, visited, currentRelation, { ...options, trackEvaluation: options.trackEvaluation } // Pass trackEvaluation
    );
    if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'NestedRuleEvaluated', ruleResultPossibility: ruleResult.possibility, ruleResultReliability: ruleResult.reliability, nestedMeta: ruleResult.meta });

    let valueInterval = null;
    let operandPossibility = 0;
    let finalReliability = ruleResult.reliability || 1.0;
    let hasValue = false;
    let operandSource = null;
    const operandCollectedValues = options.collectValues && Array.isArray(ruleResult.collectedValues)
      ? ruleResult.collectedValues
      : [];

    if (!extractValue) {
      // Use the rule's possibility as the value (point value)
      const pointValue = ruleResult.possibility;
      valueInterval = { min: pointValue, max: pointValue };
      operandPossibility = ruleResult.possibility; // Confidence in this value is the value itself
      hasValue = true;
      operandSource = 'rule_possibility';
      if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'ValueFromRulePossibility', valueInterval, operandPossibility });
    } else {
      // Extract values
      const valueResults = this._extractValues(
        evalUserId, evalUserKey, evalObjectId, evalObjectKey, 
        resolvedRule, resolvedValueRelation, ruleResult, resolvedEvaluateFrom, 
        valueContext, ttl, operandEvalMeta, options
      );
      
        if (valueResults.length > 0) {
          // Aggregate intervals using OWAFusion
        const aggregatedResult = this._aggregateCrispValues(
          valueResults, finalAggregator, owaWeights, operandEvalMeta, options
        );

        valueInterval = aggregatedResult.interval;
        operandPossibility = aggregatedResult.possibility;
        finalReliability = aggregatedResult.reliability;
        hasValue = true;
        operandSource = aggregatedResult.source || null;

        // Defensive: ensure interval is valid before margin or downstream use
        if (
          !valueInterval ||
          typeof valueInterval.min !== 'number' ||
          typeof valueInterval.max !== 'number' ||
          isNaN(valueInterval.min) ||
          isNaN(valueInterval.max)
        ) {
          valueInterval = null;
        }
        // Apply margin of safety to the interval
        if (marginOfSafety !== 1.0 && valueInterval && typeof valueInterval.min === 'number' && typeof valueInterval.max === 'number') {
          const originalInterval = { min: valueInterval.min, max: valueInterval.max };
          valueInterval = {
            min: valueInterval.min * marginOfSafety,
            max: valueInterval.max * marginOfSafety
          };
          if (operandEvalMeta) operandEvalMeta.steps.push({ 
            step: 'MarginApplied', 
            originalInterval,
            intervalAfterMargin: valueInterval 
          });
        } else if (marginOfSafety !== 1.0 && valueInterval) {
          // Optionally log a warning if margin requested but interval is invalid
          if (typeof console !== 'undefined' && console.warn) {
            console.warn('Margin of safety requested but interval is invalid:', valueInterval);
          }
        }

        if (canCacheDerived && derivedCacheKey) {
          const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
          this.arbiter.ruleResultCache.set(derivedCacheKey, {
            result: {
              valueInterval,
              operandPossibility,
              reliability: finalReliability,
              hasValue,
              source: operandSource,
              collectedValues: operandCollectedValues
            },
            timestamp: cacheNow
          });
          this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
          if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
        }
      }
    }
    
    // Check against minOperandPossibility
    if (operandPossibility < minOperandPossibility) {
      if (operandEvalMeta) operandEvalMeta.steps.push({ 
        step: 'OperandPossibilityBelowMinimum', 
        operandPossibility, 
        minOperandPossibility 
      });
      operandPossibility = 0;
      hasValue = false;
    }
    
    if (operandEvalMeta && parentEvaluationMeta) {
      if (side === 'left') parentEvaluationMeta.leftOperandProcessed = operandEvalMeta;
      if (side === 'right') parentEvaluationMeta.rightOperandProcessed = operandEvalMeta;
    }
      
    
    return {
      valueInterval,
      operandPossibility,
      reliability: finalReliability,
      hasValue,
      source: operandSource,
      ...(options.collectValues && { collectedValues: operandCollectedValues }),
      ...(options.includeMeta && { meta: operandEvalMeta })
    };
  }

  /**
   * Extract crisp values (no blur/decay)
   * @private
   */
  _extractValues(userId, userKey, objectId, objectKey, rule, valueRelation, ruleResult, evaluateFrom, valueContext, ttl, operandEvalMeta, options = null) {
    const results = [];
    const now = options && options.now !== undefined && options.now !== null ? options.now : Date.now();
    const ttlCutoff = now - ttl;
    
    // First check if rule result directly provides values
    if (ruleResult.value !== undefined) {
      // Single value from rule - use direct point value
      const relation = {
        src: userId,
        dst: objectId,
        rel: valueRelation || rule.relation || 'value',
        value: ruleResult.value,
        possibility: ruleResult.possibility,
        reliability: ruleResult.reliability || 1.0,
        changed_last_at: ruleResult.meta?.timestamp || now
      };

      results.push({
        value: relation.value,
        possibility: relation.possibility,
        reliability: relation.reliability,
        timestamp: relation.changed_last_at,
        source: 'direct_from_rule',
        originalValue: relation.value
      });

      if (operandEvalMeta) operandEvalMeta.steps.push({
        step: 'ValueDirectFromRule',
        value: relation.value
      });
      
    } else if (ruleResult.collectedValues && Array.isArray(ruleResult.collectedValues) && ruleResult.collectedValues.length > 0) {
      // Multiple collected values - get decayed intervals for each
      for (const cv of ruleResult.collectedValues) {
        // Extract numeric value from collected value structure
        const timestamp = cv.metadata?.timestamp || cv.meta?.timestamp || now;
        if (timestamp < ttlCutoff) {
          if (operandEvalMeta) operandEvalMeta.steps.push({ 
            step: 'ValueSkippedDueToTTL', 
            value: cv.value,
            timestamp,
            ttl,
            age: now - timestamp
          });
          continue;
        }

        let numericValue;
        if (typeof cv.value === 'number') {
          numericValue = cv.value;
        } else if (cv.value && typeof cv.value === 'object' && typeof cv.value.value === 'number') {
          numericValue = cv.value.value; // Extract from nested structure
        } else if (cv.value && typeof cv.value === 'object' && 
                   typeof cv.value.min === 'number' && typeof cv.value.max === 'number') {
          // Handle interval objects by taking the midpoint
          numericValue = (cv.value.min + cv.value.max) / 2;
        } else {
          continue;
        }

        results.push({
          value: numericValue,
          possibility: cv.possibility ?? 1.0,
          reliability: cv.metadata?.reliability || cv.meta?.reliability || 1.0,
          timestamp,
          source: cv.metadata?.source || cv.source || 'collected_value',
          originalValue: numericValue
        });
      }
      
      if (operandEvalMeta) operandEvalMeta.steps.push({ 
        step: 'ValuesFromCollected', 
        count: ruleResult.collectedValues.length,
        decayedCount: results.length 
      });
      
    } else {
      // Extract from relations directly - use point values
      const relationName = valueRelation || rule.relation || rule.rel || rule.label || rule.name;
      if (!relationName) return results;

      if (rule.type === 'direct') {
        const directRel = this._getDirectRelationForValue(userId, objectId, relationName, evaluateFrom, options);
        if (directRel && typeof directRel.value === 'number') {
          const cached = this._getCachedDirectValue(directRel, ttl, now);
          if (cached) {
            results.push(cached);
          }
          return results;
        }
      }
      
      const entityIdToExtract = evaluateFrom === 'user' ? userId : 
                               (evaluateFrom === 'object' ? objectId : userId);
      const preferredTargetId = evaluateFrom === 'object' ? objectId :
        (evaluateFrom === 'user' ? userId : objectId);
      
      // Get relations and use their point values
      const valueRels = this.arbiter.relationManager.getAllValueRelationsFromSrc(entityIdToExtract, relationName, options);
      let targetId = preferredTargetId;
      if (evaluateFrom === 'auto') {
        if (!valueRels.some(rel => rel.dst === targetId)) {
          if (valueRels.some(rel => rel.dst === userId)) {
            targetId = userId;
          } else if (valueRels.some(rel => rel.dst === objectId)) {
            targetId = objectId;
          }
        }
      }
      
      for (const rel of valueRels) {
        if (rel.dst !== targetId) {
          continue;
        }
        // Check TTL
        const timestamp = rel.changed_last_at || rel.updated_last_at || now;
        if (timestamp < ttlCutoff) {
          if (operandEvalMeta) operandEvalMeta.steps.push({ 
            step: 'RelationSkippedDueToTTL', 
            relation: relationName,
            timestamp,
            ttl,
            age: now - timestamp
          });
          continue;
        }
        
        if (typeof rel.value === 'number') {
          results.push({
            value: rel.value,
            possibility: rel.possibility !== undefined ? rel.possibility : 1.0,
            reliability: rel.reliability || 1.0,
            timestamp,
            source: 'direct_relation',
            originalValue: rel.value
          });
        }
      }
      
      if (operandEvalMeta) operandEvalMeta.steps.push({ 
        step: 'ValuesFromRelations', 
        relationName,
        count: valueRels.length,
        decayedCount: results.length 
      });
    }
    
    return results;
  }

  _getDirectRelationForValue(userId, objectId, relationName, evaluateFrom, options = null) {
    if (evaluateFrom === 'user') {
      return this.arbiter.relationManager.getDirectRelation(userId, relationName, userId, options);
    }
    if (evaluateFrom === 'object') {
      return this.arbiter.relationManager.getDirectRelation(objectId, relationName, objectId, options);
    }

    let directRel = this.arbiter.relationManager.getDirectRelation(userId, relationName, objectId, options);
    if (!directRel && userId !== objectId) {
      directRel = this.arbiter.relationManager.getDirectRelation(userId, relationName, userId, options);
    }
    if (!directRel && userId !== objectId) {
      directRel = this.arbiter.relationManager.getDirectRelation(objectId, relationName, objectId, options);
    }
    return directRel;
  }

  _getCachedDirectValue(relation, ttl, now = null) {
    const ts = now !== null && now !== undefined ? now : Date.now();
    if (this.arbiter.valueManager && this.arbiter.valueManager._isValueExpired(relation, ts)) {
      return null;
    }
    const cacheKey = `${relation.src}|${relation.rel}|${relation.dst}`;
    const cached = this._directValueCache.get(cacheKey);
    if (cached && cached.stateId === relation.stateId) {
      return cached;
    }
    const timestamp = relation.changed_last_at || relation.updated_last_at || ts;
    if (!OWAFusion.isWithinTTL(timestamp, ttl, ts)) {
      return null;
    }
    const entry = {
      value: relation.value,
      possibility: relation.possibility !== undefined ? relation.possibility : 1.0,
      reliability: relation.reliability || 1.0,
      timestamp,
      source: relation.source || 'persistent',
      originalValue: relation.value,
      stateId: relation.stateId
    };
    this._directValueCache.set(cacheKey, entry);
    return entry;
  }

  /**
   * Aggregate crisp values using OWAFusion
   * @private
   */
  _aggregateCrispValues(results, aggregator = 'max', owaWeights, operandEvalMeta, options = null) {
    if (results.length === 0) {
      return { interval: null, possibility: 0, reliability: 0 };
    }

    if (results.length === 1) {
      return {
        interval: { min: results[0].value, max: results[0].value },
        possibility: results[0].possibility,
        reliability: results[0].reliability,
        source: results[0].source || 'unknown'
      };
    }

    const count = results.length;
    const scratch = options && options.scratch ? options.scratch : null;
    const values = scratch ? scratch.getArray('owa_values', count) : new Array(count);
    const possibilities = scratch ? scratch.getArray('owa_possibilities', count) : new Array(count);
    const reliabilities = scratch ? scratch.getArray('owa_reliabilities', count) : new Array(count);
    const metas = scratch ? scratch.getArray('owa_metas', count) : new Array(count);
    for (let i = 0; i < count; i++) {
      const result = results[i];
      values[i] = result.value;
      possibilities[i] = result.possibility;
      reliabilities[i] = result.reliability;
      metas[i] = result;
    }

    const fused = OWAFusion.fuseTriplesWithMeta(values, possibilities, reliabilities, metas, owaWeights, aggregator);

    const result = {
      interval: { min: fused.value, max: fused.value },
      possibility: fused.possibility,
      reliability: fused.reliability,
      source: fused.meta?.source || 'unknown',
      aggregationMeta: {
        method: aggregator,
        sourceCount: results.length,
        selectedMeta: fused.meta
      }
    };

    if (operandEvalMeta) operandEvalMeta.steps.push({
      step: 'CrispValuesAggregated',
      aggregationResult: result
    });

    return result;
  }


  _aggregateSmallIntervals(blurredResults, weights) {
    const count = blurredResults.length;
    const midpoints = new Array(count);
    const metas = new Array(count);
    for (let i = 0; i < count; i++) {
      const interval = blurredResults[i].interval;
      midpoints[i] = (interval.min + interval.max) / 2;
      metas[i] = {
        source: blurredResults[i].source,
        timestamp: blurredResults[i].timestamp,
        originalValue: blurredResults[i].originalValue,
        possibility: blurredResults[i].possibility,
        reliability: blurredResults[i].reliability
      };
    }
    const order = this._sortSmallIndices(midpoints, null, null);
    let fusedMin = 0;
    let fusedMax = 0;
    let maxWeight = -Infinity;
    let selectedIdx = order[0];
    for (let i = 0; i < order.length; i++) {
      const idx = order[i];
      const interval = blurredResults[idx].interval;
      const weight = weights[i];
      fusedMin += weight * interval.min;
      fusedMax += weight * interval.max;
      if (weight > maxWeight) {
        maxWeight = weight;
        selectedIdx = idx;
      }
    }
    return { interval: { min: fusedMin, max: fusedMax }, meta: metas[selectedIdx] };
  }

  _aggregateSmallValues(blurredResults, weights) {
    const count = blurredResults.length;
    const values = new Array(count);
    const priorities = new Array(count);
    const ruleTypes = new Array(count);
    const metas = new Array(count);
    const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
    for (let i = 0; i < count; i++) {
      const value = blurredResults[i].possibility;
      values[i] = value;
      metas[i] = {
        source: blurredResults[i].source,
        timestamp: blurredResults[i].timestamp,
        originalValue: blurredResults[i].originalValue,
        possibility: blurredResults[i].possibility,
        reliability: blurredResults[i].reliability
      };
      priorities[i] = metas[i].rule?.priority ?? 0;
      ruleTypes[i] = typeOrder[metas[i].ruleType] ?? 0;
    }
    const order = this._sortSmallIndices(values, priorities, ruleTypes);
    let sum = 0;
    let selectedIdx = order[0];
    let maxContribution = 0;
    for (let i = 0; i < order.length; i++) {
      const idx = order[i];
      const contribution = weights[i] * values[idx];
      sum += contribution;
      const currentPriority = priorities[idx];
      const selectedPriority = priorities[selectedIdx];
      const currentRuleType = ruleTypes[idx];
      const selectedRuleType = ruleTypes[selectedIdx];
      const contributionDiff = Math.abs(contribution - maxContribution);
      const avgContribution = (contribution + maxContribution) / 2;
      const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;
      if (contribution > maxContribution ||
          (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
          (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
          (contribution === maxContribution && values[idx] > values[selectedIdx])) {
        maxContribution = contribution;
        selectedIdx = idx;
      }
    }
    return { value: sum, meta: metas[selectedIdx] };
  }

  _minReliability(blurredResults) {
    let minReliability = 1.0;
    for (let i = 0; i < blurredResults.length; i++) {
      const reliability = blurredResults[i].reliability;
      if (reliability < minReliability) minReliability = reliability;
    }
    return minReliability;
  }

  _sortSmallIndices(values, priorities, ruleTypes) {
    if (values.length === 2) {
      const aFirst = this._compareIndex(values, priorities, ruleTypes, 0, 1) <= 0;
      return aFirst ? [0, 1] : [1, 0];
    }
    if (values.length === 3) {
      let i0 = 0;
      let i1 = 1;
      let i2 = 2;
      if (this._compareIndex(values, priorities, ruleTypes, i0, i1) > 0) {
        const tmp = i0;
        i0 = i1;
        i1 = tmp;
      }
      if (this._compareIndex(values, priorities, ruleTypes, i0, i2) > 0) {
        const tmp = i0;
        i0 = i2;
        i2 = tmp;
      }
      if (this._compareIndex(values, priorities, ruleTypes, i1, i2) > 0) {
        const tmp = i1;
        i1 = i2;
        i2 = tmp;
      }
      return [i0, i1, i2];
    }
    const indices = values.map((_, i) => i);
    indices.sort((a, b) => this._compareIndex(values, priorities, ruleTypes, a, b));
    return indices;
  }

  _compareIndex(values, priorities, ruleTypes, a, b) {
    const valDiff = values[b] - values[a];
    if (valDiff !== 0) return valDiff;
    if (priorities && ruleTypes) {
      const prioDiff = priorities[b] - priorities[a];
      if (prioDiff !== 0) return prioDiff;
      return ruleTypes[b] - ruleTypes[a];
    }
    return 0;
  }

  _calculateIntervalComparisonPossibility(L_interval, R_interval, comparator) {
    const leftValue = this._extractPointValueFromInterval(L_interval);
    const rightValue = this._extractPointValueFromInterval(R_interval);
    const epsilon = this.epsilon;

    if (leftValue === null || rightValue === null) return 0;

    switch (comparator) {
      case '>': return leftValue > rightValue ? 1 : 0;
      case '>=': return leftValue >= rightValue ? 1 : 0;
      case '<': return leftValue < rightValue ? 1 : 0;
      case '<=': return leftValue <= rightValue ? 1 : 0;
      case '==': return Math.abs(leftValue - rightValue) < epsilon ? 1 : 0;
      case '!=': return Math.abs(leftValue - rightValue) >= epsilon ? 1 : 0;
      default:
        Arbiter.warn('Unknown comparator in _calculateIntervalComparisonPossibility:', comparator);
        return 0;
    }
  }

  _compareBlurredValues(leftOpResult, rightOpResult, comparator, fallbackBehavior, ruleConfig, options, ruleMetaBase) {
    // evaluationMetaFromOptions is for the parent rule's trace, if options.trackEvaluation is true
    const evaluationMetaFromOptions = options.trackEvaluation ? (options.evaluationMeta || ruleMetaBase) : null;

    let finalPossibility = 0;
    let finalReliability = Math.min(leftOpResult.reliability || 1.0, rightOpResult.reliability || 1.0);
    let topLevelReason = 'comparison_initialization_failed'; // Default reason

    // Basic meta structure, will be augmented based on outcome
    let topLevelMeta = {
        ...(ruleMetaBase || {}), // Includes original ruleType, userKey, objectKey etc.
        type: 'relational_comparator', // Override ruleType from base if needed
        leftOperandDetails: leftOpResult.meta,
        rightOperandDetails: rightOpResult.meta,
        // Store raw inputs for easier debugging from meta
        _leftOpRaw: { 
            hasValue: leftOpResult.hasValue, 
            interval: leftOpResult.valueInterval, 
            possibility: leftOpResult.operandPossibility 
        },
        _rightOpRaw: { 
            hasValue: rightOpResult.hasValue, 
            interval: rightOpResult.valueInterval, 
            possibility: rightOpResult.operandPossibility 
        },
        comparatorUsed: comparator
    };
    
    if (evaluationMetaFromOptions && !evaluationMetaFromOptions.comparisonStep) {
        evaluationMetaFromOptions.comparisonStep = {};
    }
    if (evaluationMetaFromOptions) {
        Object.assign(evaluationMetaFromOptions.comparisonStep, {
            leftValueIntervalAtCompare: leftOpResult.valueInterval,
            leftPossibilityAtCompare: leftOpResult.operandPossibility,
            rightValueIntervalAtCompare: rightOpResult.valueInterval,
            rightPossibilityAtCompare: rightOpResult.operandPossibility,
            comparatorForCompare: comparator,
        });
    }

    if (!leftOpResult.hasValue || !rightOpResult.hasValue) {
      if (!leftOpResult.hasValue && !rightOpResult.hasValue) { // Both missing
        finalPossibility = fallbackBehavior === 'allow' ? 1.0 : 0;
        topLevelReason = 'both_operands_missing_fallback_' + fallbackBehavior;
      } else { // Exactly one is missing
        const missingSide = !leftOpResult.hasValue ? 'left' : 'right';
        if (comparator === '!=') {
          finalPossibility = 1.0; 
          topLevelReason = `${missingSide}_operand_missing_inequality_true`;
        } else { // For >, <, >=, <=, ==
          finalPossibility = 0;
          topLevelReason = `${missingSide}_operand_missing_comparison_false`;
        }
      }
      // When an operand is missing, its contribution to reliability is via its (likely low) operandPossibility
      finalReliability = Math.min(finalReliability, leftOpResult.operandPossibility || 0, rightOpResult.operandPossibility || 0);
      if (evaluationMetaFromOptions) evaluationMetaFromOptions.comparisonStep.reasonForOutcome = topLevelReason;

    } else { // Both operands have values
      const comparisonResultPossibility = this._calculateIntervalComparisonPossibility(
        leftOpResult.valueInterval,
        rightOpResult.valueInterval,
        comparator
      );
      
      if (evaluationMetaFromOptions) evaluationMetaFromOptions.comparisonStep.possibilityFromIntervalComparison = comparisonResultPossibility;

      const averageOperandPossibility = (leftOpResult.operandPossibility + rightOpResult.operandPossibility) / 2;
      const confidenceWeight = Math.min(averageOperandPossibility * 2, 1.0); 
      finalPossibility = comparisonResultPossibility * confidenceWeight;
      
      // Reliability already initialized based on operand reliabilities
      // finalReliability = (leftOpResult.reliability || 1.0) * (rightOpResult.reliability || 1.0); // This might double-penalize if already low.

      if (evaluationMetaFromOptions) {
        evaluationMetaFromOptions.comparisonStep.finalCalculatedPossibility = finalPossibility;
        evaluationMetaFromOptions.comparisonStep.averageOperandPossibility = averageOperandPossibility;
        evaluationMetaFromOptions.comparisonStep.confidenceWeight = confidenceWeight;
      }

      const reasonSuffix = finalPossibility > (this.epsilon || DEFAULT_EPSILON) ? '_comparison_true' : '_comparison_false';
      topLevelReason = `values_compared${reasonSuffix}`;
      
      topLevelMeta.details = { 
          leftInterval: leftOpResult.valueInterval,
          rightInterval: rightOpResult.valueInterval,
          leftOperandPoss: leftOpResult.operandPossibility,
          rightOperandPoss: rightOpResult.operandPossibility,
          intervalComparePoss: comparisonResultPossibility,
          averageOperandPoss: averageOperandPossibility,
          confidenceWeight: confidenceWeight,
          leftSource: leftOpResult.source || null,
          rightSource: rightOpResult.source || null
      };
    }

    // Populate final meta fields for allow/deny based on possibility
    if (finalPossibility > (this.epsilon || DEFAULT_EPSILON)) {
        topLevelMeta.allow = {
            type: 'relational_comparator',
            leftValue: this._extractPointValueFromInterval(leftOpResult.valueInterval),
            rightValue: this._extractPointValueFromInterval(rightOpResult.valueInterval),
            leftInterval: leftOpResult.valueInterval,
            rightInterval: rightOpResult.valueInterval,
            comparator: comparator,
            possibility: finalPossibility,
            reason: topLevelReason,
            details: topLevelMeta.details
        };
    } else {
        topLevelMeta.deny = {
            type: 'relational_comparator',
            leftValue: this._extractPointValueFromInterval(leftOpResult.valueInterval),
            rightValue: this._extractPointValueFromInterval(rightOpResult.valueInterval),
            leftInterval: leftOpResult.valueInterval,
            rightInterval: rightOpResult.valueInterval,
            comparator: comparator,
            possibility: finalPossibility,
            reason: topLevelReason,
            details: topLevelMeta.details 
        };
    }

    topLevelMeta.reason = topLevelReason;
    topLevelMeta.finalOutcomeReason = topLevelReason; // For clarity in traces
    if (ruleConfig && ruleConfig.priority) topLevelMeta.priority = ruleConfig.priority;


    const collectedValues = [...(leftOpResult.collectedValues || []), ...(rightOpResult.collectedValues || [])];
    
    return {
      possibility: finalPossibility,
      reliability: finalReliability,
      ...(options.includeMeta && {
        meta: topLevelMeta,
        meta_allow: topLevelMeta.allow,  // Add this field for AuthorizationChecker
        meta_deny: topLevelMeta.deny    // Add this field for AuthorizationChecker
      }),
      collectedValues: collectedValues,
      reason: topLevelReason 
    };
  }

  _extractPointValueFromInterval(interval) {
    if (interval && typeof interval.min === 'number' && typeof interval.max === 'number') {
      return (interval.min + interval.max) / 2;
    }
    return null;
  }

  _getOperandCacheIdentity(resolvedValueRelation, resolvedRule, nestedRuleConfig) {
    if (resolvedValueRelation) return resolvedValueRelation;
    if (resolvedRule?.relation) return resolvedRule.relation;
    const fallbackRelation = nestedRuleConfig?.relation || nestedRuleConfig?.rel || nestedRuleConfig?.label || nestedRuleConfig?.name;
    if (fallbackRelation) return fallbackRelation;

    if (resolvedRule?.type === 'logical') {
      if (resolvedRule._optimized?.kind === 'direct_list') {
        const rels = resolvedRule._optimized.direct
          .map((direct) => `${direct.reverse ? 'rev:' : ''}${direct.relation}`)
          .join(',');
        return `direct_list:${resolvedRule.op || 'logical'}:${rels}`;
      }
      if (Array.isArray(resolvedRule.children)) {
        const childKeys = resolvedRule.children.map((child) => {
          if (child?.type === 'direct') {
            return `${child.reverse ? 'rev:' : ''}${child.relation}`;
          }
          return child?.type || 'unknown';
        }).join(',');
        return `logical:${resolvedRule.op || 'logical'}:${childKeys}`;
      }
      return `logical:${resolvedRule.op || 'logical'}`;
    }

    if (resolvedRule?.type) return `type:${resolvedRule.type}`;
    if (nestedRuleConfig?.type) return `type:${nestedRuleConfig.type}`;
    return 'value';
  }
}
