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';
import { CompiledEvaluator } from './CompiledEvaluator.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),
      challenge: new ChallengeRule(arbiter)
    };
    
    this.logicalOperators = new LogicalOperators(arbiter, this);
    this.compiledEvaluator = new CompiledEvaluator(arbiter, this, this.logicalOperators);
    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
    const numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
    const numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
    
    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 };

    const canCacheRuleResult = !!this.arbiter.ruleResultCache && currentRelation &&
      !binary && !options.partialGraphContext && !includeMeta &&
      options.cacheRuleResult !== false;
    const ruleCacheKey = canCacheRuleResult
      ? this._getRuleResultCacheKey(numericUserId, currentRelation, numericObjectId, rule)
      : null;

    if (canCacheRuleResult) {
      const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
      if (cached && Date.now() - 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 && rule._compiled && options.useCompiled !== false && !(rule._compileErrors && rule._compileErrors.length)) {
      const result = this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, enhancedOptions);
      if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
        const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
        finalValueContext.addCollectedValues(result.collectedValues, ruleType, rule);
      }
      return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    if (rule.union) {
      const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    if (rule.intersection) {
      const result = this.logicalOperators.evaluateIntersection(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    if (rule.exclusion) {
      const result = this.logicalOperators.evaluateExclusion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
      return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
    }
    
    // For other rule types, evaluate normally first
    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'
      };
    }
    
    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') {
        suffix = `direct:${rule.relation || '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;
    this.arbiter.ruleResultCache.set(cacheKey, {
      result,
      timestamp: Date.now()
    });
    this.arbiter._cacheRuleResult(relation, cacheKey);
    return result;
  }

  _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)) {
      if (rule._compiled && !(rule._compileErrors && rule._compileErrors.length)) {
        return this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, {
          ...enhancedOptions,
          binary: true,
          fastPath: true
        });
      }
      return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapDefeasibleComponents(rule), visited, currentRelation, {
        ...enhancedOptions,
        binary: true,
        fastPath: true
      });
    }

    // 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) {
  const wrapSingle = (component) => ({ rules: [component] });
  const wrapUnion = (component) => {
    if (!component) return component;
    if (Array.isArray(component)) return { rules: component };
    if (component.union || component.intersection || component.exclusion) return component;
    return wrapSingle(component);
  };
  const wrapIntersection = (component) => {
    if (!component) return component;
    if (Array.isArray(component)) return { rules: component };
    if (component.union || component.intersection || component.exclusion) return component;
    return wrapSingle(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
  };
}
