import { buildValidity, normalizeValidity, mergeValidity, minimalValidity, DEFAULT_VALIDITY } from '../core/validity.js';
import { Arbiter } from '../core/Arbiter.js';
import { RuleEvaluator } from './RuleEvaluator.js';
import { RuleCollector } from './RuleCollector.js';
import { ScratchBuffers } from './ScratchBuffers.js';
import { buildRemediation, extractRemediation, mergeRemediationOptions } from './remediation.js';
import { DecisionCache } from './DecisionCache.js';

export class AuthorizationChecker {
  constructor(arbiter, options = {}) {
    this.arbiter = arbiter;
    this.ruleEvaluator = new RuleEvaluator(arbiter);
    this.ruleCollector = new RuleCollector(arbiter);
    // DecisionCache port — RF-03 closure. When not injected, default to
    // an ArbiterDecisionCache that forwards to the arbiter's existing
    // cache fields, preserving the behavior every test relies on.
    this.decisionCache = options.decisionCache || new DecisionCache(arbiter);
  }

  check(userKey, relation, objectKey, options = {}) {
    // Handle backward compatibility
    if (options instanceof Set) {
      options = { _visited: options, _currentRelation: arguments[4] };
    }
    
    if (!options.scratch) {
      options.scratch = new ScratchBuffers();
    }

    const { 
      _visited = new Set(), 
      _currentRelation = null,
      // Threshold-based early exit options
      minAllowPossibility = null,
      maxDenyPossibility = null,
      fastPath = false,
      // NEW: Binary mode for ultra-fast decisive authorization
      binary = false
    } = options;
    const explain = options.explain === true;
    const includeMeta = options.includeMeta === undefined ? explain : options.includeMeta;
    const trackEvaluation = options.trackEvaluation === undefined ? (explain ? true : false) : options.trackEvaluation;
    let collectValues = options.collectValues;
    const hasPartialGraph = !!options.partialGraphContext;
    
    // BINARY MODE: Ultra-fast decisive authorization
    if (binary) {
      return this._checkBinary(userKey, relation, objectKey, {
        _visited,
        _currentRelation,
        minAllowPossibility: minAllowPossibility || 0.8, // Default strict threshold
        maxDenyPossibility: maxDenyPossibility || 0.8,
        includeMeta,
        trackEvaluation,
        // Preserve every caller option (partialGraphContext, clientStateId,
        // ...) — the binary path previously rebuilt a six-field object and
        // silently dropped the partial graph, so binary checks denied
        // grants that the normal path allowed.
        ...options
      });
    }
    
    const config = this.arbiter.relationConfigs.get(relation);
    if (collectValues === undefined) {
      collectValues = explain || config?._needsValues || false;
    }
    // CI-001 fix: the previous `!config._compiled` guard made the
    // direct-check fast path dead. `setRelationConfig` compiles
    // synchronously and sets `_compiled` immediately (ArbiterConfig.js),
    // so the guard was always false in production and `_cacheDirectCheckResult`
    // never fired. The fast-path decision is independent of compilation state.
    //
    // Narrowing: derived evidence rules normalize to type 'direct' but carry
    // `dependsOn` (e.g. session_authenticated_action { userIsActive(user) }).
    // Their semantics live in the compiled dependency evaluation, not in a raw
    // direct lookup — the fast path must not bypass them, or gate checks deny
    // with 'no_relation' where the full path derives possibility 1.0.
    const hasDerivedDependencies = Array.isArray(config?.dependsOn) && config.dependsOn.length > 0;
    const useFastPath = config && config.type === 'direct' && !config.union && !config.intersection && !config.exclusion && !hasDerivedDependencies;
    const effectiveThreshold = minAllowPossibility ?? config?.minPossibility ?? null;
    // A direct config may override the relation it checks (rule.relation).
    // The fast path must honor that override or it diverges from the
    // rule-evaluation path (e.g. can_read -> gateway_context_ref).
    const effectiveRelation = (config && (config.relation || config.rel)) || relation;
    if (useFastPath) {
      // Check cache first using composite key (if caching is enabled)
      let cachedResult = null;
      let cacheHint = null;
      const temporalPinned = options.now !== undefined && options.now !== null;
      if (!hasPartialGraph && this.decisionCache.directEnabled && !includeMeta && !temporalPinned) {
        const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
        const [hitResult, status] = this.decisionCache.peekDirect(cacheKey);
        cachedResult = status === 'hit' || status === 'expired' ? { result: hitResult, timestamp: 0 } : null;
        if (status === 'hit') {
          cacheHint = { hit: true, result: hitResult };
          if (!explain) {
            return hitResult;
          }
        } else if (explain && status === 'expired') {
          cacheHint = { hit: false, result: hitResult };
        }
      }
      
      // Ultra-fast direct check
      const userId = this.arbiter.resolveNodeId(userKey, options);
      const objectId = this.arbiter.resolveNodeId(objectKey, options);
      
      if (userId === undefined || objectId === undefined) {
        const result = {
          possibility: 0,
          reliability: 0,
          validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
          ...(includeMeta && { meta: { reason: 'missing_node' } }),
          reason: 'missing_node'
        };
        
        // Cache the result (only when no partial graph and the default
        // meta-less form — includeMeta callers and pinned-clock callers get
        // a fresh evaluation). Value-carrying results are NEVER cached: the
        // decision is timeless, but collected values are TTL-gated evidence
        // and a cached entry would serve stale values past their TTL (the
        // default TTL is 24h even without an explicit setTTL).
        if (!explain && !hasPartialGraph && !includeMeta && !temporalPinned && !result.collectedValues) {
          this._cacheDirectCheckResult(userKey, relation, objectKey, result);
        }
        return result;
      }

      if (_visited.size) {
        const useKeyedVisited = this._getVisitedMode(_visited);
        const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null;
        if (useKeyedVisited) {
          if (_visited.has(visitKey)) {
            return {
              possibility: 0,
              reliability: 0,
              validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
              ...(includeMeta && { meta: { reason: 'cycle' } }),
              reason: 'cycle'
            };
          }
        } else {
          for (const visited of _visited) {
            if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
              return {
                possibility: 0,
                reliability: 0,
                validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
                ...(includeMeta && { meta: { reason: 'cycle' } }),
                reason: 'cycle'
              };
            }
          }
        }
      }
      
      // Direct index lookup
      let directRel = null;
      let partialRel = null;
      if (hasPartialGraph) {
        partialRel = options.partialGraphContext.getDirectRelation(userId, effectiveRelation, objectId);
      }
      directRel = this.arbiter.indices.getDirectRelation(userId, effectiveRelation, objectId) || partialRel;
      
       let result;
       if (directRel) {
         // Check threshold-based early exit
         if (effectiveThreshold !== null && directRel.possibility < effectiveThreshold) {
           result = {
             possibility: 0,
             reliability: 0,
             ...(includeMeta && { meta: { reason: 'threshold_not_met', threshold: effectiveThreshold, actual: directRel.possibility } }),
            reason: 'threshold_not_met'
          };
         } else if (config?.expectedValue !== undefined && directRel.value !== config.expectedValue) {
           // Value gate (mirrors DirectRule): a config carrying expectedValue
           // (from a DSL literal like balance(user, 5)) only grants when the
           // edge carries exactly that value.
           result = {
             possibility: 0,
             reliability: 0,
             ...(includeMeta && {
               meta: { reason: 'value_mismatch', expectedValue: config.expectedValue, actualValue: directRel.value }
             }),
             reason: 'value_mismatch'
           };
         } else if (options.expectedValue !== undefined && directRel.value !== options.expectedValue) {
           // Per-check value-object override (check(user, can_withdraw, 5)):
           // the value arrives on the check options, not the config.
           result = {
             possibility: 0,
             reliability: 0,
             ...(includeMeta && {
               meta: { reason: 'value_mismatch', expectedValue: options.expectedValue, actualValue: directRel.value }
             }),
             reason: 'value_mismatch'
           };
         } else {
           result = {
             possibility: directRel.possibility,
             validity: includeMeta
               ? buildValidity('identity', [effectiveRelation], [directRel.validity !== undefined ? normalizeValidity(directRel.validity) : 'heuristic'], 1, directRel.possibility)
               : minimalValidity(directRel.validity !== undefined
                   ? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
                   : DEFAULT_VALIDITY),
             // A denied decision (possibility 0) must not leak the
             // relation's reliability — the rule-collection path zeroes it.
             reliability: directRel.possibility > 0
               ? (directRel.reliability !== undefined ? directRel.reliability : 1.0)
               : 0,
             ...(includeMeta && {
                meta: {
                 allow: {
                   ruleType: 'direct',
                   reason: 'direct',
                   source: directRel.source || 'persistent',
                   layer_name: directRel.layer_name || null,
                   source_class: directRel.source_class || null,
                   reducer_applied: directRel.reducer_applied || null
                 }
               }
             }),
            reason: 'direct_match'
          };
          
          // Collect values if present — but only while the value is
          // FRESH. TTL is a value-freshness gate (see ValueManager
          // _isValueExpired); an expired value must not surface in
          // collectedValues, matching the comparator path which skips
          // expired relations entirely.
          if (collectValues && directRel.value !== undefined) {
            const valueManager = this.arbiter.valueManager;
            const expired = valueManager && valueManager._isValueExpired
              ? valueManager._isValueExpired(directRel, options.now !== undefined && options.now !== null ? options.now : null)
              : false;
            if (!expired) {
              // Standard collected-value shape (parity with the rule
              // paths): value, possibility, path, source, metadata with a
              // caller-clock-honoring timestamp.
              const ts = directRel.changed_last_at || directRel.updated_last_at ||
                (options.now !== undefined && options.now !== null ? options.now : Date.now());
              result.collectedValues = [{
                value: directRel.value,
                possibility: directRel.possibility ?? 1.0,
                path: [userKey, objectKey],
                source: {
                  entityKey: userKey,
                  relation: relation,
                  step: 0
                },
                metadata: {
                  timestamp: ts,
                  reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0
                }
              }];
            }
          }
        }
      } else {
         // Fast-path miss: attach remediation when the missing (effective)
         // relation is declared as an injectable witness source — the caller
         // needs to know which relation to satisfy.
         const missingConfig = this.arbiter.relationConfigs.get(effectiveRelation);
         let remediation = null;
         if (missingConfig && missingConfig.injectable) {
           remediation = buildRemediation(null, {
             status: 'required',
             additional_options: [{ relation: effectiveRelation, object: objectKey }]
           });
         }
         result = {
           possibility: 0,
           reliability: 0,
           validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
           ...(includeMeta && { meta: { reason: 'no_relation' } }),
           reason: 'no_relation',
           ...(remediation ? { remediation } : {})
         };
       }

      if (explain && cacheHint) {
        result.meta = result.meta || {};
        result.meta.cache = cacheHint;
      }
      
      // Cache the result using composite key (meta-less form only).
      // Value-carrying results are never cached (collected values are
      // TTL-gated evidence; a cached entry would serve stale values).
      if (!explain && !hasPartialGraph && !includeMeta && !temporalPinned && !result.collectedValues) {
        this._cacheDirectCheckResult(userKey, relation, objectKey, result);
      }
      return result;
    }
    
    this.arbiter.relationManager._ensureIndicesBuilt();
    
    
    const userId = this.arbiter.resolveNodeId(userKey, options);
    const objectId = this.arbiter.resolveNodeId(objectKey, options);
    
    if (userId === undefined || objectId === undefined) {
      const missingNode = userId === undefined ? userKey : objectKey;
      const missingType = userId === undefined ? 'user' : 'object';
      
      return {
        possibility: 0,
        reliability: 0,
        validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
        ...(includeMeta && {
          meta: { 
            reason: 'missing_node',
            missingNode,
            missingType
          }
        }),
        reason: 'missing_node'
      };
    }
    
    if (!config) {
      return {
        possibility: 0,
        reliability: 0,
        validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
        ...(includeMeta && { meta: { reason: 'no_config' } }),
        reason: 'no_config'
      };
    }

    const temporalPinned = options.now !== undefined && options.now !== null;
    const canCacheRuleResult = !hasPartialGraph && !explain && !includeMeta &&
      !binary && !temporalPinned && options.cacheRuleResult !== false && this.decisionCache.ruleEnabled;
    const ruleCacheKey = canCacheRuleResult
      ? this._getRuleResultCacheKey(userId, relation, objectId) +
        (options.expectedValue !== undefined ? `:v${options.expectedValue}` : '')
      : null;
    if (canCacheRuleResult) {
      const cached = this.decisionCache.getRule(ruleCacheKey);
      if (cached) {
        return cached;
      }
    }

    const useKeyedVisited = this._getVisitedMode(_visited);
    const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null;
    if (useKeyedVisited) {
      if (_visited.has(visitKey)) {
        return {
          possibility: 0,
          reliability: 0,
          validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
          ...(includeMeta && { meta: { reason: 'cycle' } }),
          reason: 'cycle'
        };
      }
    } else {
      for (const visited of _visited) {
        if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
          return {
            possibility: 0,
            reliability: 0,
            validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
            ...(includeMeta && { meta: { reason: 'cycle' } }),
            reason: 'cycle'
          };
        }
      }
    }

    _visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey });
    
    const shouldTrackEvaluation = trackEvaluation && includeMeta;
    const evaluationErrors = shouldTrackEvaluation ? [] : null;
    const evaluationPath = shouldTrackEvaluation ? {
      userKey,
      relation,
      objectKey,
      config,
      rules: [],
      visitedPath: Array.from(_visited),
      errors: evaluationErrors
    } : null;

    const evalOptions = {
      ...options,
      fastPath,
      minAllowPossibility,
      minPossibility: minAllowPossibility,
      maxDenyPossibility,
      trackEvaluation: shouldTrackEvaluation,
      collectValues,
      includeMeta
    };
    
    if (config.union || config.intersection || config.exclusion) {
      const res = this.ruleEvaluator.evaluateRule(
        userId,
        userKey,
        objectId,
        objectKey,
        config,
        _visited,
        relation,
        evalOptions
      );
      
      if (evaluationPath) {
        evaluationPath.type = 'logical_operator';
        evaluationPath.operator = config.union ? 'union' : config.intersection ? 'intersection' : 'exclusion';
        evaluationPath.result = res;
      }
      
      // Handle both old format (possibility_allow) and new format (possibility)
      const resPossibility = res.possibility_allow !== undefined ? res.possibility_allow : res.possibility;
      
      // Extract allow/deny from meta to avoid conflicts
      const { allow: metaAllow, deny: metaDeny, ...restMeta } = res.meta || {};
      
       const remediation = buildRemediation(extractRemediation(res));
       const finalResult = {
         possibility: resPossibility || 0,
         reliability: res.reliability !== undefined ? res.reliability : 1.0,
         validity: includeMeta ? (res.validity || DEFAULT_VALIDITY) : minimalValidity(res.validity || DEFAULT_VALIDITY),
         ...(collectValues && res.collectedValues && Array.isArray(res.collectedValues) && { collectedValues: res.collectedValues }),
         ...(includeMeta && {
           meta: { 
             ...restMeta,  // Spread meta without allow/deny
             allow: res.meta_allow || metaAllow,  // Use meta_allow if available, otherwise meta.allow
             deny: res.meta_deny || metaDeny,
             ...(evaluationPath && { evaluation: evaluationPath }),
             earlyExit: res.meta?.earlyExit || false
           }
         }),
          ...(remediation ? { remediation } : {}),
          reason: res.reason || 'logical_operator_evaluation'
        };
      return this._maybeCacheRuleResult(finalResult, relation, ruleCacheKey, canCacheRuleResult);
    }
    
    const rules = this.ruleCollector.collectRules(config, null, relation);
    
    if (evaluationPath) {
      evaluationPath.type = 'rule_collection';
      evaluationPath.collectedRules = rules.length;
    }
    
     let maxAllow = 0;
     let maxDeny = 0;
     let bestAllowReliability = 0;
     let bestDenyReliability = 0;
    let bestAllow = null;
    let bestDeny = null;
    let bestAllowValidity = null;
    let ruleValidityBlocks = [];
    let reason = undefined;
    let allRuleResults = shouldTrackEvaluation ? [] : null;
    let allCollectedValues = collectValues ? [] : null; // Collect values from all evaluated rules
    
    const remediationOptions = [];
    let ruleIndex = 0;
    for (const rule of rules) {
      const res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, rule, _visited, relation, evalOptions);
      
      // Handle both old format (possibility_allow/deny) and new format (possibility)
      const resAllowPossibility = res.possibility_allow !== undefined ? res.possibility_allow : res.possibility;
      const resDenyPossibility = res.possibility_deny !== undefined ? res.possibility_deny : 0;
      const resMeta = res.meta_allow || res.meta?.allow || null;
      mergeRemediationOptions(remediationOptions, extractRemediation(res));

      if (evaluationErrors && (res.error || res.reason === 'evaluation_error')) {
        evaluationErrors.push({
          ruleIndex,
          level: rule.ruleType || null,
          relation: rule.relation || relation,
          message: res.details?.error || res.reason || 'evaluation_error',
          stack: res.details?.stack || null
        });
      }
      
      // Track each rule evaluation
      if (shouldTrackEvaluation) {
        const ruleEvaluation = {
          rule: {
            type: rule.type,
            relation: rule.relation,
            ruleType: rule.ruleType,
            reverse: rule.reverse
          },
          result: {
            possibility_allow: resAllowPossibility,
            possibility_deny: resDenyPossibility,
            reason: res.reason
          },
          meta: {
            allow: resMeta,
            deny: res.meta_deny,
            full: res.meta || null
          }
        };
        allRuleResults.push(ruleEvaluation);
      }

      ruleIndex += 1;
      
      // Preserve specific reasons from rule evaluations
      if (res.reason === 'cycle') reason = 'cycle';
      if (res.reason === 'no_path') reason = 'no_path';
      if (res.reason === 'no_similar_users') reason = 'no_similar_users';
      if (res.reason === 'no_similar_authorized') reason = 'no_similar_authorized';
      if (res.reason === 'no_similar_objects') reason = 'no_similar_objects';
      if (res.reason === 'no_user_objects') reason = 'no_user_objects';
      if (res.reason === 'no_target_embedding') reason = 'no_target_embedding';
      if (res.reason === 'no_embedding') reason = 'no_embedding';
      // Add chain rule reasons
      if (res.reason === 'chain_path_found') reason = 'chain_path_found';
      if (res.reason === 'no_chain_path_found') reason = 'no_chain_path_found';
      if (res.reason === 'no_chain_steps_defined') reason = 'no_chain_steps_defined';
      // Add parent rule reasons
      if (res.reason === 'no_parent_relationship_found') reason = 'no_parent_relationship_found';
      if (res.reason === 'no_parent_relationship_path_above_threshold') reason = 'no_parent_relationship_path_above_threshold';
      // Add multi-hop rule reasons
      if (res.reason === 'no_multihop_path_found') reason = 'no_multihop_path_found';
      if (res.reason === 'multihop_path_found') reason = 'multihop_path_found';
      // Add direct rule reasons
      if (res.reason === 'direct_match') reason = 'direct_match';
      if (res.reason === 'no_direct_match') reason = 'no_direct_match';
      // Add tuple-to-userset reasons
      if (res.reason === 'tuple_to_userset_match') reason = 'tuple_to_userset_match';
      if (res.reason === 'no_tuple_to_userset_match') reason = 'no_tuple_to_userset_match';
      // Add relational comparator reasons
      if (res.reason === 'values_compared_comparison_true') reason = 'values_compared_comparison_true';
      if (res.reason === 'values_compared_comparison_false') reason = 'values_compared_comparison_false';
      if (res.reason === 'values_compared_comparison_insufficient') reason = 'values_compared_comparison_insufficient';
      // Add defeasible logic reasons (normal mode surfaces these when a
      // defeasible rule resolves to 0 — never, requirements, or defeaters).
      if (res.reason === 'defeated_by_unless') reason = 'defeated_by_unless';
      if (res.reason === 'never_rule_triggered') reason = 'never_rule_triggered';
      if (res.reason === 'requirements_not_met') reason = 'requirements_not_met';
      if (res.reason === 'strict_rule_failed') reason = 'strict_rule_failed';
      
       if (resAllowPossibility > maxAllow) {
         maxAllow = resAllowPossibility;
         bestAllow = resMeta;
         bestAllowReliability = res.reliability !== undefined ? res.reliability : 1.0;
         bestAllowValidity = res.validity || null;
       }
       if (res.validity) ruleValidityBlocks.push(res.validity);
      
       if (resDenyPossibility > maxDeny) {
         maxDeny = resDenyPossibility;
         bestDeny = res.meta_deny;
         bestDenyReliability = res.reliability !== undefined ? res.reliability : 1.0;
       }
      
      // Fast path early exit checks
      if (fastPath) {
        let shouldExit = false;
        let exitReason = null;
        
        // Check allow threshold
        if (minAllowPossibility !== null && maxAllow >= minAllowPossibility) {
          shouldExit = true;
          exitReason = 'allow_threshold_met';
        }
        
        // Check deny threshold
        if (maxDenyPossibility !== null && maxDeny >= maxDenyPossibility) {
          shouldExit = true;
          exitReason = 'deny_threshold_met';
        }
        
        if (shouldExit) {
          if (evaluationPath) {
            evaluationPath.rules = allRuleResults;
            evaluationPath.earlyExit = {
              reason: exitReason,
              threshold: exitReason === 'allow_threshold_met' ? minAllowPossibility : maxDenyPossibility,
              actualValue: exitReason === 'allow_threshold_met' ? maxAllow : maxDeny,
              rulesEvaluated: allRuleResults.length,
              totalRules: rules.length
            };
            Arbiter.log('early exit triggered:', evaluationPath.earlyExit);
          }
          
           const finalResult = {
             possibility: maxAllow,
             reliability: maxAllow > 0 ? bestAllowReliability : 0,
             ...(includeMeta && {
               meta: { 
                allow: bestAllow, 
                deny: bestDeny,
                ...(evaluationPath && { evaluation: evaluationPath }),
                earlyExit: true,
                maxDeny: maxDeny
              }
            }),
            reason: exitReason
          };
          return this._maybeCacheRuleResult(finalResult, relation, ruleCacheKey, canCacheRuleResult);
        }
      }
      
      // Collect values from all evaluated rules
      if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) {
        allCollectedValues.push(...res.collectedValues);
      }
    }
    
    if (evaluationPath) {
      evaluationPath.rules = allRuleResults;
      evaluationPath.finalResult = {
        maxAllow,
        maxDeny,
        reason
      };
    }
    
    // Determine final reason based on evaluation
    let finalReason = reason;
    if (maxAllow === 0 && maxDeny === 0) {
      finalReason = reason || 'no_matching_rule';
    } else if (maxAllow > 0 && maxDeny > 0) {
      finalReason = 'conflicting_rules';
    } else if (maxAllow > 0) {
      finalReason = 'allow_rule_matched';
    } else if (maxDeny > 0) {
      finalReason = 'deny_rule_matched';
    }
    
     const remediation = maxAllow === 0
       ? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
       : null;
     // Possibilistic validity of the decision: the winning rule's block when
    // a single rule drove it, otherwise the weakest-merge of all
    // contributors (a heuristic/banned contributor downgrades the whole).
    let finalValidity = DEFAULT_VALIDITY;
    if (ruleValidityBlocks.length === 1) {
      finalValidity = ruleValidityBlocks[0];
    } else if (ruleValidityBlocks.length > 1) {
      finalValidity = mergeValidity(ruleValidityBlocks);
    } else if (maxAllow > 0 && bestAllowValidity) {
      finalValidity = bestAllowValidity;
    }
    const result = {
       possibility: maxAllow,
       reliability: maxAllow > 0 ? bestAllowReliability : maxDeny > 0 ? bestDenyReliability : 0,
       validity: includeMeta ? finalValidity : minimalValidity(finalValidity),
       ...(includeMeta && {
         meta: { 
          allow: bestAllow, 
          deny: bestDeny,
          ...(evaluationPath && { evaluation: evaluationPath }),
          maxDeny: maxDeny, // Keep deny info in meta for debugging
           ...(collectValues && allCollectedValues.length > 0 && { collectedValues: allCollectedValues }),
           ...(maxAllow === 0 && remediation ? { remediation } : {})
         }
       }),
       ...(maxAllow === 0 && remediation ? { remediation } : {}),
       reason: finalReason
     };
    
    // Add collectedValues at top level if there are any
    if (collectValues && allCollectedValues.length > 0) {
      result.collectedValues = allCollectedValues;
    }
    
    return this._maybeCacheRuleResult(result, relation, ruleCacheKey, canCacheRuleResult);
  }

  _getRuleResultCacheKey(userId, relation, objectId) {
    return this.arbiter.keyManager.createCompositeKey(userId, relation, objectId);
  }

  _maybeCacheRuleResult(result, relation, cacheKey, enabled) {
    if (!enabled || !cacheKey) return result;
    this.decisionCache.setRule(cacheKey, result);
    this.decisionCache.trackRuleKeyForRelation(relation, cacheKey);
    return result;
  }

  /**
   * Binary mode: Ultra-fast decisive authorization with strict thresholds
   * Returns simple allow/deny decisions with minimal overhead
   */
  _checkBinary(userKey, relation, objectKey, options = {}) {
    const {
      _visited = new Set(),
      _currentRelation = null,
      minAllowPossibility = 0.8,
      maxDenyPossibility = 0.8,
      includeMeta = false,
      trackEvaluation = false
    } = options;

    // Track evaluation for binary mode
    const evaluation = (includeMeta || trackEvaluation) ? {
      type: 'binary',
      userKey,
      relation,
      objectKey,
      thresholds: { minAllowPossibility, maxDenyPossibility },
      evaluationStarted: Date.now()
    } : null;

    const userId = this.arbiter.resolveNodeId(userKey, options);
    const objectId = this.arbiter.resolveNodeId(objectKey, options);

    if (userId === undefined || objectId === undefined) {
      return {
        possibility: 0,
        reliability: 0,
        validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
        reason: 'missing_node',
        binary: true,
        ...(evaluation && { evaluation })
      };
    }

    const config = this.arbiter.relationConfigs.get(relation);
    if (!config) {
      return {
        possibility: 0,
        reliability: 0,
        validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
        reason: 'no_config',
        binary: true,
        ...(evaluation && { evaluation })
      };
    }

    if (evaluation) {
      evaluation.rulesEvaluated = 0;
      evaluation.earlyTermination = false;
    }

    // Fast path for direct relations in binary mode
    if (config.type === 'direct') {
      let directRel = null;
      let partialRel = null;
      // A direct config may alias an underlying relation (config.relation);
      // the checked relation name alone is the wrong lookup key.
      const effectiveRelation = config.relation || relation;
      if (options.partialGraphContext) {
        partialRel = options.partialGraphContext.getDirectRelation(userId, effectiveRelation, objectId);
      }
      directRel = this.arbiter.indices.getDirectRelation(userId, effectiveRelation, objectId) || partialRel;
      if (directRel) {
        const allow = directRel.possibility >= minAllowPossibility;
        const deny = false; // Direct relations don't have explicit deny values
        
        if (evaluation) {
          evaluation.rulesEvaluated = 1;
          evaluation.evaluationCompleted = Date.now();
          evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
        }
        
        return {
          possibility: directRel.possibility,
          reliability: directRel.possibility > 0
            ? (directRel.reliability !== undefined ? directRel.reliability : 1.0)
            : 0,
          validity: includeMeta
            ? (directRel.validity !== undefined
              ? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
              : DEFAULT_VALIDITY)
            : minimalValidity(directRel.validity !== undefined
              ? buildValidity('identity', [effectiveRelation], [normalizeValidity(directRel.validity)], 1, directRel.possibility)
              : DEFAULT_VALIDITY),
          reason: allow ? 'allow' : deny ? 'deny' : 'insufficient_confidence',
          binary: true,
          ...(evaluation && { evaluation }),
          allow,
          deny
        };
      } else {
        if (evaluation) {
          evaluation.rulesEvaluated = 1;
          evaluation.evaluationCompleted = Date.now();
          evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
        }
        
        return {
          possibility: 0,
          reliability: 0,
          validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
          reason: 'insufficient_confidence',
          binary: true,
          ...(evaluation && { evaluation }),
          allow: false,
          deny: false
        };
      }
    }

    // Check for cycles using efficient approach.
    // Deliberately placed AFTER the direct fast path: direct checks never
    // recurse, so they must not pay for key allocation / Set mutation. Only
    // the rule-evaluation branches (which recurse via evaluateRule with the
    // shared _visited) need cycle detection.
    const useKeyedVisited = this._getVisitedMode(_visited);
    const visitKey = useKeyedVisited ? this._getVisitedKey(userId, relation, objectId) : null;
    if (useKeyedVisited) {
      if (_visited.has(visitKey)) {
        return {
          possibility: 0,
          reliability: 0,
          validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
          reason: 'cycle',
          binary: true,
          ...(evaluation && { evaluation })
        };
      }
    } else {
      for (const visited of _visited) {
        if (visited.userKey === userKey && visited.relation === relation && visited.objectKey === objectKey) {
          return {
            possibility: 0,
            reliability: 0,
            validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
            reason: 'cycle',
            binary: true,
            ...(evaluation && { evaluation })
          };
        }
      }
    }

    _visited.add(useKeyedVisited ? visitKey : { userKey, relation, objectKey });

    // Handle logical operators with binary evaluation
    if (config.union || config.intersection || config.exclusion) {
      const res = this.ruleEvaluator.evaluateRule(
        userId,
        userKey,
        objectId,
        objectKey,
        config,
        _visited,
        relation,
        { 
          fastPath: true,
          minAllowPossibility,
          maxDenyPossibility,
          binary: true,
          ...options
        }
      );

      if (evaluation) {
        evaluation.rulesEvaluated = 1;
        evaluation.evaluationCompleted = Date.now();
        evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
      }

      // Handle both old format (possibility_allow/deny) and new format (possibility)
      const resAllowPossibility = res.possibility_allow !== undefined ? res.possibility_allow : res.possibility;
      const resDenyPossibility = res.possibility_deny !== undefined ? res.possibility_deny : 0;

      // Binary decision based on strict thresholds
      const allow = resAllowPossibility >= minAllowPossibility;
      const deny = resDenyPossibility >= maxDenyPossibility;

      return {
        possibility: resAllowPossibility || 0,
        validity: includeMeta ? (res.validity || DEFAULT_VALIDITY) : minimalValidity(res.validity || DEFAULT_VALIDITY),
        reason: allow ? 'allow' : deny ? 'deny' : 'insufficient_confidence',
        binary: true,
        ...(evaluation && { evaluation }),
        allow,  // Keep for backwards compatibility
        deny    // Keep for backwards compatibility
      };
    }

    // Collect and evaluate rules with early termination
    const rules = this.ruleCollector.collectRules(config, null, relation);
    
    let maxAllow = 0;
    let maxDeny = 0;
    let bestValidity = null;

    for (const rule of rules) {
      if (evaluation) {
        evaluation.rulesEvaluated++;
      }

      const res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, rule, _visited, relation, { 
        fastPath: true,
        minAllowPossibility,
        maxDenyPossibility,
        binary: true,
        ...options
      });

      // Prefer the continuous possibility; binarized possibility_allow (1|0)
      // must only be a fallback so reported strengths stay continuous.
      const resAllowPossibility = typeof res.possibility === 'number' ? res.possibility : (res.possibility_allow !== undefined ? res.possibility_allow : 0);
      const resDenyPossibility = res.possibility_deny !== undefined ? res.possibility_deny : 0;

      if (resAllowPossibility > maxAllow) {
        maxAllow = resAllowPossibility;
        if (res.validity) bestValidity = res.validity;
      }

      if (resDenyPossibility > maxDeny) {
        maxDeny = resDenyPossibility;
      }

      // BINARY EARLY TERMINATION: Stop as soon as we hit a threshold
      if (maxAllow >= minAllowPossibility) {
        if (evaluation) {
          evaluation.earlyTermination = true;
          evaluation.terminationReason = 'allow_threshold_met';
          evaluation.evaluationCompleted = Date.now();
          evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
        }

        return {
          possibility: maxAllow,
          validity: includeMeta ? (bestValidity || DEFAULT_VALIDITY) : minimalValidity(bestValidity || DEFAULT_VALIDITY),
          reason: 'allow',
          binary: true,
          ...(evaluation && { evaluation }),
          allow: true,   // Keep for backwards compatibility
          deny: false    // Keep for backwards compatibility
        };
      }

      if (maxDeny >= maxDenyPossibility) {
        if (evaluation) {
          evaluation.earlyTermination = true;
          evaluation.terminationReason = 'deny_threshold_met';
          evaluation.evaluationCompleted = Date.now();
          evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
        }

        return {
          possibility: 0,
          reliability: 0,
          validity: includeMeta ? DEFAULT_VALIDITY : minimalValidity(DEFAULT_VALIDITY),
          reason: 'deny',
          binary: true,
          ...(evaluation && { evaluation }),
          allow: false,  // Keep for backwards compatibility
          deny: true     // Keep for backwards compatibility
        };
      }

      // Limit rule evaluation in binary mode for performance.
      // maxBinaryRules defaults to Infinity (no cap) — set lower if you
      // understand the false-denial risk for policies with many rules.
      if (evaluation && evaluation.rulesEvaluated >= (options.maxBinaryRules ?? Infinity)) {
        evaluation.earlyTermination = true;
        evaluation.terminationReason = 'max_rules_evaluated';
        break;
      }
    }

    if (evaluation) {
      evaluation.evaluationCompleted = Date.now();
      evaluation.evaluationDuration = evaluation.evaluationCompleted - evaluation.evaluationStarted;
    }

    // Final binary decision
    const allow = maxAllow >= minAllowPossibility;
    const deny = maxDeny >= maxDenyPossibility;

    return {
      possibility: maxAllow,
      reason: allow ? 'allow' : deny ? 'deny' : 'insufficient_confidence',
      binary: true,
      ...(evaluation && { evaluation }),
      allow,   // Keep for backwards compatibility
      deny     // Keep for backwards compatibility
    };
  }

  /**
   * Generate cache key for direct check
   * @private
   */
  _getDirectCheckCacheKey(userKey, relation, objectKey) {
    // Use composite key for better performance - keyManager handles string-to-ID conversion internally
    return this.arbiter.keyManager.createCompositeKey(
      this.arbiter.keyManager.getStringId(userKey), 
      relation, 
      this.arbiter.keyManager.getStringId(objectKey)
    );
  }

  _cacheDirectCheckResult(userKey, relation, objectKey, result) {
    if (!this.decisionCache.directEnabled) return;
    const cacheKey = this._getDirectCheckCacheKey(userKey, relation, objectKey);
    this.decisionCache.setDirect(cacheKey, result);
    // Track the (hashed) key under the checked relation + its base relations
    // + the subject/object nodes so relation- and node-level invalidation can
    // delete it without parsing the key.
    const arb = this.arbiter;
    if (arb && typeof arb._trackDirectCheckKey === 'function') {
      const srcId = arb.resolveNodeId(userKey);
      const dstId = arb.resolveNodeId(objectKey);
      arb._trackDirectCheckKey(relation, srcId, dstId, cacheKey);
    }
  }

  _getVisitedMode(visited) {
    if (visited.__fastKeyed !== undefined) {
      return visited.__fastKeyed;
    }
    if (visited.size === 0) {
      visited.__fastKeyed = true;
      return true;
    }
    for (const entry of visited) {
      const isKeyed = typeof entry === 'string';
      visited.__fastKeyed = isKeyed;
      return isKeyed;
    }
    visited.__fastKeyed = true;
    return true;
  }

  _getVisitedKey(userId, relation, objectId) {
    const relationId = this.arbiter.keyManager._getRelationId(relation);
    return `${userId}|${relationId}|${objectId}`;
  }

  invalidateRuleCaches(relation) {
    // Invalidate ChainRule caches if it exists
    if (this.ruleEvaluator && this.ruleEvaluator.ruleHandlers && this.ruleEvaluator.ruleHandlers.chain) {
      this.ruleEvaluator.ruleHandlers.chain._invalidateAllChainCaches();
    }
    
    // Invalidate other rule caches as needed
    // TODO: Add invalidation for other rule types that have caches
  }

}
