import { DirectRule } from './rules/DirectRule.js';
import { ComputedRule } from './rules/ComputedRule.js';
import { ParentRule } from './rules/ParentRule.js';
import { TupleToUsersetRule } from './rules/TupleToUsersetRule.js';
import { MultiHopRule } from './rules/MultiHopRule.js';
import { LogicalOperators } from './rules/LogicalOperators.js';
import { RelationalComparatorRouter } from './rules/RelationalComparatorRouter.js';
import { ChainRule } from './rules/ChainRule.js';
import { ChallengeRule } from './rules/ChallengeRule.js';
import { ValueContext } from './ValueContext.js';

export class RuleEvaluator {
  constructor(arbiter) {
    this.arbiter = arbiter;
    
    this.ruleHandlers = {
      direct: new DirectRule(arbiter),
      computed: new ComputedRule(arbiter),
      parent: new ParentRule(arbiter),
      tuple_to_userset: new TupleToUsersetRule(arbiter),
      multi_hop: new MultiHopRule(arbiter),
      relational_comparator: new RelationalComparatorRouter(arbiter, this),
      chain: new ChainRule(arbiter, this),
      challenge: new ChallengeRule(arbiter)
    };
    
    this.logicalOperators = new LogicalOperators(arbiter, this);
    this._valueRequirementCache = new Map();
  }

  evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
    const { binary = false, valueContext = null, includeMeta = true } = options;

    
    // Convert string keys to numeric IDs if needed
    let numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
    let numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
    
    // Unary / subject-scoped rules (e.g. a DSL predicate call `banned(user)`
    // inside a binary evidence) check the relation on the SUBJECT itself — the
    // object is the user. The DSL generator marks these with _subjectAsObject;
    // the evaluator rewrites the object to the subject so (u, banned, u) matches
    // the unary fact's self-edge instead of (u, banned, object).
    if (rule._subjectAsObject) {
      numericObjectId = numericUserId;
      objectKey = userKey;
    }
    // A unary call whose subject entity IS the object parameter
    // (`trusted(other)` inside peer_trusted(user, other)) checks the relation
    // as a self-edge on the object — rewrite the subject to the object.
    if (rule._subjectIsObject) {
      numericUserId = numericObjectId;
      userKey = objectKey;
    }
    
    const needsValueContext = valueContext !== null && valueContext !== undefined
      ? true
      : (options.collectValues === true || this._ruleRequiresValues(rule, new Set()));
    const finalValueContext = needsValueContext ? (valueContext || new ValueContext(this.arbiter, options)) : null;
    const collectValues = options.collectValues !== undefined ? options.collectValues : (rule._needsValues ?? needsValueContext);
    const enhancedOptions = { ...options, minPossibility: options.minPossibility ?? options.minAllowPossibility, valueContext: finalValueContext, collectValues, includeMeta };

    // A caller-pinned clock (options.now) makes the result per-clock: the
    // result caches must not serve a decision evaluated at another time.
    const temporalPinned = options.now !== undefined && options.now !== null;
    const canCacheRuleResult = !!this.arbiter.ruleResultCache && currentRelation &&
      !binary && !options.partialGraphContext && !includeMeta && !temporalPinned &&
      options.cacheRuleResult !== false;
    const ruleCacheKey = canCacheRuleResult
      ? this._getRuleResultCacheKey(numericUserId, currentRelation, numericObjectId, rule) +
        (options.expectedValue !== undefined ? `:v${options.expectedValue}` : '')
      : null;

    if (canCacheRuleResult) {
      const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
      const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
      // A graph mutation since the entry was computed invalidates it — the
      // result is derived from the graph and would be stale. _graphVersion
      // increments on every relation mutation.
      const freshGraph = cached && cached.graphVersion === (this.arbiter._graphVersion ?? 0);
      if (freshGraph && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
        this.arbiter.ruleResultCacheStats.hits++;
        if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
          const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
          finalValueContext.addCollectedValues(cached.result.collectedValues, ruleType, rule);
        }
        return cached.result;
      }
      this.arbiter.ruleResultCacheStats.misses++;
    }
    
    // BINARY MODE: Fast evaluation with early termination
    if (binary) {
      return this._evaluateRuleBinary(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
    }

    if (rule.union) {
      const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    if (rule.intersection) {
      const result = this.logicalOperators.evaluateIntersection(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    if (rule.exclusion) {
      const result = this.logicalOperators.evaluateExclusion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(this._applyNegate(result, rule), currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    // Defeasible configs ({ type: 'defeasible', when/unless/never/always })
    // have no rule-handler entry; route them to the defeasible evaluator
    // with the components normalized to the union/intersection format.
    if (this.logicalOperators._isDefeasibleLogic(rule)) {
      const result = this.logicalOperators.evaluateDefeasible(
        numericUserId, userKey, numericObjectId, objectKey,
        wrapDefeasibleComponents(rule), visited, currentRelation, enhancedOptions
      );
      return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    // For other rule types, evaluate normally first
    // Shorthand operand objects ({ relation: 'owner' } inside logical rules,
    // or caller-supplied raw configs) carry no type: treat them as direct
    // rules instead of failing with unknown_rule_type.
    const handler = this.ruleHandlers[rule.type || (rule.relation || rule.rel || rule.label || rule.name ? 'direct' : null)];
    if (!handler) {
      return {
        possibility_allow: 0,
        possibility_deny: 0,
        ...(collectValues && { collectedValues: [] }),
        ...(includeMeta && { meta_allow: null, meta_deny: null }),
        reason: 'unknown_rule_type'
      };
    }
    
    const result = handler.evaluate(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
    
    // Add collected values to value context
    if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
      finalValueContext.addCollectedValues(result.collectedValues, rule.type, rule);
    }
    
    // Return original result if inference didn't help or values weren't sufficient
    return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
  }

  _getRuleResultCacheKey(userId, relation, objectId, rule) {
    const base = this.arbiter.keyManager.createCompositeKey(userId, relation, objectId);
    let suffix = 'rule';
    if (rule) {
      if (rule.union || rule.intersection || rule.exclusion) {
        suffix = 'logical';
      } else if (rule.type === 'direct' || (!rule.type && (rule.relation || rule.rel || rule.label || rule.name) && !rule.union && !rule.intersection && !rule.exclusion)) {
        // Shorthand operands ({ relation: 'editor' }) dispatch to the direct
        // handler but carry no type; without this, every shorthand child of
        // a logical rule shares one cache key and the first child's result
        // is served for all of them.
        suffix = `direct:${rule.relation || rule.rel || rule.label || rule.name || 'unknown'}`;
      } else if (rule.type === 'tuple_to_userset') {
        suffix = `tupleset:${rule.tuplesetRelation || 'unknown'}:${rule.computedRelation || 'unknown'}`;
      } else if (rule.type === 'chain' && Array.isArray(rule.steps)) {
        const stepSig = rule.steps.map(step => {
          if (typeof step === 'string') return step;
          return `${step.relation || 'unknown'}:${step.direction || 'out'}`;
        }).join('>');
        suffix = `chain:${stepSig}`;
      } else if (rule.type === 'multi_hop') {
        suffix = `multihop:${rule.relation || 'unknown'}:${rule.maxDepth || 'auto'}`;
      } else if (rule.type === 'parent') {
        suffix = `parent:${rule.parentRelation || 'unknown'}`;
     } else if (rule.type === 'relational_comparator') {
        const signature = this._getComparatorCacheSignature(rule);
        suffix = `comparator:${rule.comparator || 'unknown'}:${signature}`;
     } else {
        suffix = rule.type || 'rule';
     }
    }
    return `${base}|${suffix}`;
  }

  _maybeCacheRuleResult(result, relation, cacheKey, enabled) {
    if (!enabled || !cacheKey || !relation) return result;
    const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
    this.arbiter.ruleResultCache.set(cacheKey, {
      result,
      timestamp: cacheNow,
      graphVersion: this.arbiter._graphVersion ?? 0
    });
    this.arbiter._cacheRuleResult(relation, cacheKey);
    return result;
  }

  /**
   * Apply the NOT operator's negation to a logical rule result. The DSL emits
   * `NOT x` as { type:'logical', intersection:{ rules:[x], negate:true } };
   * the individual union/intersection/exclusion evaluators don't honor the
   * flag, so it is applied here after evaluation (possibility -> 1 - p).
   */
  _applyNegate(result, rule) {
    const negate = Boolean(
      (rule.union && rule.union.negate) ||
      (rule.intersection && rule.intersection.negate) ||
      (rule.exclusion && rule.exclusion.negate)
    );
    if (!negate || !result || typeof result.possibility !== 'number') return result;
    return {
      ...result,
      possibility: Math.max(0, 1 - result.possibility),
      ...(result.meta ? { meta: { ...result.meta, negated: true } } : {}),
      reason: 'negated'
    };
  }

  _getComparatorCacheSignature(rule) {
    const leftSig = this._getComparatorOperandSignature(rule.left || rule.leftOperand);
    const rightSig = this._getComparatorOperandSignature(rule.right || rule.rightOperand);
    return `L(${leftSig})|R(${rightSig})`;
  }

  _getComparatorOperandSignature(operand) {
    if (!operand || typeof operand !== 'object') return 'none';
    const valueRelation = operand.valueRelation || operand.valueRelationResolved;
    if (valueRelation) return `relation:${valueRelation}`;

    const nestedRule = operand.rule || null;
    if (!nestedRule || typeof nestedRule !== 'object') return 'value';

    if (nestedRule.type === 'direct') {
      const rel = nestedRule.relation || nestedRule.rel || nestedRule.label || nestedRule.name || 'unknown';
      return `${nestedRule.reverse ? 'rev:' : ''}${rel}`;
    }

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

    if (nestedRule.union) {
      const rules = Array.isArray(nestedRule.union.rules) ? nestedRule.union.rules : (Array.isArray(nestedRule.union) ? nestedRule.union : []);
      const rels = rules.map((child) => {
        if (child?.type === 'direct') {
          const rel = child.relation || child.rel || child.label || child.name || 'unknown';
          return `${child.reverse ? 'rev:' : ''}${rel}`;
        }
        return child?.type || 'unknown';
      }).join(',');
      return `logical:union:${rels}`;
    }

    if (nestedRule.intersection) {
      const rules = Array.isArray(nestedRule.intersection.rules) ? nestedRule.intersection.rules : (Array.isArray(nestedRule.intersection) ? nestedRule.intersection : []);
      const rels = rules.map((child) => {
        if (child?.type === 'direct') {
          const rel = child.relation || child.rel || child.label || child.name || 'unknown';
          return `${child.reverse ? 'rev:' : ''}${rel}`;
        }
        return child?.type || 'unknown';
      }).join(',');
      return `logical:intersection:${rels}`;
    }

    if (nestedRule.exclusion) {
      const rules = Array.isArray(nestedRule.exclusion.rules) ? nestedRule.exclusion.rules : (Array.isArray(nestedRule.exclusion) ? nestedRule.exclusion : []);
      const rels = rules.map((child) => {
        if (child?.type === 'direct') {
          const rel = child.relation || child.rel || child.label || child.name || 'unknown';
          return `${child.reverse ? 'rev:' : ''}${rel}`;
        }
        return child?.type || 'unknown';
      }).join(',');
      return `logical:exclusion:${rels}`;
    }

    return nestedRule.type || 'value';
  }

  /**
   * Binary mode rule evaluation: Ultra-fast with strict thresholds and value context
   */
  _evaluateRuleBinary(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
    const { 
      minAllowPossibility = 0.8,
      maxDenyPossibility = 0.8,
      fastPath = true,
      valueContext = null,
      includeMeta = true
    } = options;

    const needsValueContext = valueContext !== null && valueContext !== undefined
      ? true
      : (options.collectValues === true || this._ruleRequiresValues(rule, new Set()));
    const finalValueContext = needsValueContext ? (valueContext || new ValueContext(this.arbiter, options)) : null;
    const collectValues = options.collectValues !== undefined ? options.collectValues : (rule._needsValues ?? needsValueContext);
    const enhancedOptions = { ...options, minPossibility: options.minPossibility ?? options.minAllowPossibility, valueContext: finalValueContext, collectValues, includeMeta };

    // Handle logical operators in binary mode
    if (rule.union) {
      return this.logicalOperators.evaluateUnion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
        ...enhancedOptions,
        binary: true,
        fastPath: true
      });
    }
    
    if (rule.intersection) {
      return this.logicalOperators.evaluateIntersection(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
        ...enhancedOptions,
        binary: true,
        fastPath: true
      });
    }
    
    if (rule.exclusion) {
      return this.logicalOperators.evaluateExclusion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
        ...enhancedOptions,
        binary: true,
        fastPath: true
      });
    }

    // Defeasible rules (when/unless/never/always/requires) have no entry in
    // ruleHandlers — normal mode reaches them via the compiled evaluator,
    // but binary mode short-circuits to this method, where a bare handler
    // lookup returned unknown_rule_type (a hard binary deny for every
    // defeasible config). Route them through the defeasible evaluator,
    // which implements binary/threshold modes natively.
    if (this.logicalOperators._isDefeasibleLogic(rule)) {
      // The compiled path ran defeasible rules in normal mode regardless of
      // the binary flag (compiled.mode defaulted to 'normal'), so the binary
      // decision was always the thresholded normal combination. Preserve
      // that contract: force normal mode; the checker applies the binary
      // thresholds to the result.
      const wrapped = wrapDefeasibleComponents(rule);
      wrapped.mode = 'normal';
      return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapped, visited, currentRelation, {
        ...enhancedOptions,
        binary: false,
        fastPath: false
      });
    }

    // Get rule handler
    const handler = this.ruleHandlers[rule.type];
    if (!handler) {
      return {
        possibility_allow: 0,
        possibility_deny: 0,
        ...(collectValues && { collectedValues: [] }),
        ...(includeMeta && { meta_allow: null, meta_deny: null }),
        reason: 'unknown_rule_type',
        binary: true
      };
    }

    // Evaluate rule with binary options
    const result = handler.evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
      ...enhancedOptions,
      binary: true,
      fastPath: true
    });

    // Add collected values to value context in binary mode too
    // This is important if downstream rules in a logical operator need them
    if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
      finalValueContext.addCollectedValues(result.collectedValues, rule.type, rule);
    }
    
    // Binary mode means we only care about allow/deny based on thresholds
    const allow = result.possibility_allow >= minAllowPossibility;
    const deny = result.possibility_deny >= maxDenyPossibility;
    
    return {
      possibility_allow: allow ? 1 : 0,
      possibility_deny: deny ? 1 : 0,
      // Preserve the real continuous possibility so logical operators
      // (union/intersection/exclusion) can aggregate child strengths —
      // binarized 0/1 values alone corrupt max/min/product aggregation.
      possibility: result.possibility ?? result.possibility_allow ?? (allow ? 1 : 0),
      ...(includeMeta && { meta_allow: allow ? (result.meta_allow || result.meta) : null, meta_deny: deny ? result.meta_deny : null }),
      reason: result.reason || (allow ? 'binary_allow_threshold_met' : (deny ? 'binary_deny_threshold_met' : 'binary_threshold_not_met')),
      binary: true,
      collectedValues: result.collectedValues || [] // Preserve collected values
    };
  }

  /**
   * Get value context statistics for debugging
   */
  getValueContextStats(options = {}) {
    const { valueContext } = options;
    return valueContext ? valueContext.getStats() : null;
  }

  _ruleRequiresValues(rule, visitedRelations) {
    if (!rule || typeof rule !== 'object') return false;

    if (rule.type === 'relational_comparator' || rule.qualitative === true) {
      rule._needsValues = true;
      return true;
    }

    if (rule.type === 'computed') {
      const relationName = rule.relation;
      if (relationName) {
        if (visitedRelations.has(relationName)) return false;
        visitedRelations.add(relationName);
        if (this._valueRequirementCache.has(relationName)) {
          return this._valueRequirementCache.get(relationName);
        }
        const config = this.arbiter.relationConfigs.get(relationName);
        const requiresValues = this._ruleRequiresValues(config, visitedRelations);
        this._valueRequirementCache.set(relationName, requiresValues);
        if (config && typeof config === 'object') {
          config._needsValues = requiresValues;
        }
        return requiresValues;
      }
    }

    const traverse = (child) => this._ruleRequiresValues(child, visitedRelations);

    if (Array.isArray(rule.union) && rule.union.some(traverse)) {
      rule._needsValues = true;
      return true;
    }
    if (rule.union && Array.isArray(rule.union.rules) && rule.union.rules.some(traverse)) {
      rule._needsValues = true;
      return true;
    }
    if (Array.isArray(rule.intersection) && rule.intersection.some(traverse)) {
      rule._needsValues = true;
      return true;
    }
    if (rule.intersection && Array.isArray(rule.intersection.rules) && rule.intersection.rules.some(traverse)) {
      rule._needsValues = true;
      return true;
    }
    if (Array.isArray(rule.exclusion) && rule.exclusion.some(traverse)) {
      rule._needsValues = true;
      return true;
    }

    if (rule.when && this._ruleRequiresValues(rule.when, visitedRelations)) {
      rule._needsValues = true;
      return true;
    }
    if (rule.unless && this._ruleRequiresValues(rule.unless, visitedRelations)) {
      rule._needsValues = true;
      return true;
    }
    if (rule.requires && this._ruleRequiresValues(rule.requires, visitedRelations)) {
      rule._needsValues = true;
      return true;
    }

    rule._needsValues = false;
    return false;
  }
}

/**
 * Wrap raw defeasible components (single rules like
 * `{ when: { type: 'direct', relation: 'x' } }`) into the object shape
 * LogicalOperators.evaluateDefeasible expects
 * (`when.intersection.rules[]`, `unless.union.rules[]`, `always.direct`).
 * Logical (union/intersection/exclusion) components pass through.
 */
function wrapDefeasibleComponents(rule) {
  // The defeasible evaluator's normalize expects the component format
  // when.intersection.rules[] / unless.union.rules[] (not bare {rules}).
  const wrapUnion = (component) => {
    if (!component) return component;
    if (component.union) return component;
    if (Array.isArray(component)) return { union: { rules: component } };
    return { union: { rules: [component] } };
  };
  const wrapIntersection = (component) => {
    if (!component) return component;
    if (component.intersection) return component;
    if (Array.isArray(component)) return { intersection: { rules: component } };
    return { intersection: { rules: [component] } };
  };
  return {
    ...rule,
    when: rule.when ? wrapIntersection(rule.when) : rule.when,
    unless: rule.unless ? wrapUnion(rule.unless) : rule.unless,
    never: rule.never ? wrapUnion(rule.never) : rule.never,
    requires: rule.requires ? wrapUnion(rule.requires) : rule.requires,
    always: rule.always ? { direct: rule.always.direct || rule.always } : rule.always
  };
}
