import { ProgramNode, DefinitionNode, FactNode, EvidenceNode, MeasureNode, DirectEvidenceNode, PatternMatchNode, DefeasibleLogicNode, FusionNode, PredicateNode, ExpressionNode } from '../nodes/index.js';

/**
 * Rule Generator for converting AST to setRelationConfig calls
 * Generates rule configurations that interface with the existing rule system
 */
export class RuleGenerator {
  constructor(arbiter) {
    this.arbiter = arbiter;
    this.generatedRules = new Map();
    this.errors = [];
    this.dependencyIndex = new Map();
  }

  /**
   * Generate rules from AST program
   * @param {ProgramNode} program - AST program to generate rules from
   * @returns {Object} Generation result with success status and errors
   */
  generateRules(program) {
    this.errors = [];
    this.generatedRules.clear();
    this.dependencyIndex.clear();

    try {
      // Generate rules for each evidence definition
      program.evidence.forEach(evidence => {
        this.generateEvidenceRules(evidence);
      });

      // Generate rules for each measure definition
      program.measures.forEach(measure => {
        this.generateMeasureRules(measure);
      });

      // Generate fact relation configs (requires injection at check time)
      program.facts.forEach(fact => {
        this.generateFactConfig(fact);
      });

      // Apply generated rules to arbiter
      this.applyRulesToArbiter();

      return {
        success: this.errors.length === 0,
        errors: this.errors,
        generatedCount: this.generatedRules.size
      };
    } catch (error) {
      this.errors.push(`Generation error: ${error.message}`);
      return {
        success: false,
        errors: this.errors,
        generatedCount: 0
      };
    }
  }

  /**
   * Generate rules for an evidence definition
   * @param {EvidenceNode} evidence - Evidence to generate rules for
   */
  generateEvidenceRules(evidence) {
    const relationName = evidence.name;
    const ruleConfig = this.buildRuleConfig(evidence);
    
    if (ruleConfig) {
      this._annotateDependencies(relationName, ruleConfig);
      this.generatedRules.set(relationName, ruleConfig);
    }
  }

  /**
   * Generate rules for a measure definition
   * @param {MeasureNode} measure - Measure to generate rules for
   */
  generateMeasureRules(measure) {
    const relationName = measure.name;
    const ruleConfig = this.buildMeasureRuleConfig(measure);
    
    if (ruleConfig) {
      this._annotateDependencies(relationName, ruleConfig);
      this.generatedRules.set(relationName, ruleConfig);
    }
  }

  /**
   * Generate relation config for a fact declaration.
   * Facts require injection at check time (e.g. from user DB, session store).
   * Registered as type: 'direct' with requiresInjection flag so the gate-check
   * pipeline can pre-fetch facts before calling graphStore.check().
   * @param {FactNode} fact - Fact to generate config for
   */
  generateFactConfig(fact) {
    const name = fact.name;
    const params = fact.params || [];
    const paramNames = params.map(p => p.name);
    const paramTypes = params.map(p => p.type);

    this.generatedRules.set(name, {
      type: 'direct',
      relation: name,
      isFactRelation: true,
      requiresInjection: true,
      arity: paramTypes.length,
      paramTypes: paramTypes,
      paramNames: paramNames,
      cacheDirective: fact.cacheDirective || fact.cache || 'lazy',
      properties: Object.fromEntries(
        (fact.properties || []).map(p =>
          typeof p === 'string' ? [p, true] : Array.isArray(p) ? p : [p, true]
        )
      )
    });
  }

  _annotateDependencies(relationName, ruleConfig) {
    if (!ruleConfig || typeof ruleConfig !== 'object') return;
    const dependsOn = new Set();
    const dependsByLevel = {
      never: new Set(),
      always: new Set(),
      requires: new Set(),
      when: new Set(),
      unless: new Set(),
      ordinary: new Set()
    };

    const collect = (rule, targetSet) => {
      if (!rule || typeof rule !== 'object') return;
      if (rule.type === 'direct' && rule.relation) {
        targetSet.add(rule.relation);
      }
      if (rule.type === 'tuple_to_userset') {
        if (rule.tuplesetRelation) targetSet.add(rule.tuplesetRelation);
        if (rule.computedRelation) targetSet.add(rule.computedRelation);
      }
      if (rule.type === 'parent' && rule.parentRelation) {
        targetSet.add(rule.parentRelation);
      }
      if (rule.type === 'multi_hop' && rule.relation) {
        targetSet.add(rule.relation);
      }
      if (rule.type === 'chain' && Array.isArray(rule.steps)) {
        for (const step of rule.steps) {
          if (typeof step === 'string') targetSet.add(step);
          else if (step && typeof step.relation === 'string') targetSet.add(step.relation);
        }
      }
      if (rule.type === 'relational_comparator') {
        if (rule.left?.valueRelation) targetSet.add(rule.left.valueRelation);
        if (rule.right?.valueRelation) targetSet.add(rule.right.valueRelation);
        if (rule.left?.rule) collect(rule.left.rule, targetSet);
        if (rule.right?.rule) collect(rule.right.rule, targetSet);
      }

      const logicalKeys = ['union', 'intersection', 'exclusion', 'never', 'always', 'requires', 'when', 'unless'];
      for (const key of logicalKeys) {
        const node = rule[key];
        const ruleList = node?.rules || node?.union?.rules || node?.intersection?.rules;
        if (Array.isArray(ruleList)) {
          for (const child of ruleList) collect(child, targetSet);
        }
        if (node?.rule) collect(node.rule, targetSet);
      }
    };

    const collectWithKeys = (rule, keys, targetSet) => {
      if (!rule || typeof rule !== 'object') return;
      for (const key of keys) {
        const node = rule[key];
        const ruleList = node?.rules || node?.union?.rules || node?.intersection?.rules;
        if (Array.isArray(ruleList)) {
          for (const child of ruleList) collect(child, targetSet);
        }
        if (node?.rule) collect(node.rule, targetSet);
      }
    };

    if (ruleConfig.type === 'logical') {
      if (ruleConfig.never) collect(ruleConfig.never, dependsByLevel.never);
      if (ruleConfig.always) collect(ruleConfig.always, dependsByLevel.always);
      if (ruleConfig.requires) collect(ruleConfig.requires, dependsByLevel.requires);
      if (ruleConfig.when) collect(ruleConfig.when, dependsByLevel.when);
      if (ruleConfig.unless) collect(ruleConfig.unless, dependsByLevel.unless);
      collectWithKeys(ruleConfig, ['union', 'intersection', 'exclusion'], dependsByLevel.ordinary);
    } else {
      collect(ruleConfig, dependsOn);
    }

    for (const level of Object.keys(dependsByLevel)) {
      for (const rel of dependsByLevel[level]) {
        dependsOn.add(rel);
      }
    }

    const orderedDependsOn = Array.from(dependsOn);
    if (orderedDependsOn.length > 0) {
      ruleConfig.dependsOn = orderedDependsOn;
    }

    if (Object.values(dependsByLevel).some(set => set.size > 0)) {
      ruleConfig.dependsByLevel = {
        never: Array.from(dependsByLevel.never),
        always: Array.from(dependsByLevel.always),
        requires: Array.from(dependsByLevel.requires),
        when: Array.from(dependsByLevel.when),
        unless: Array.from(dependsByLevel.unless),
        ordinary: Array.from(dependsByLevel.ordinary)
      };
    }

    if (orderedDependsOn.length > 0) {
      for (const rel of orderedDependsOn) {
        let entry = this.dependencyIndex.get(rel);
        if (!entry) {
          entry = {
            all: new Set(),
            byLevel: {
              never: new Set(),
              always: new Set(),
              requires: new Set(),
              when: new Set(),
              unless: new Set(),
              ordinary: new Set()
            }
          };
          this.dependencyIndex.set(rel, entry);
        }
        entry.all.add(relationName);
        if (ruleConfig.dependsByLevel) {
          for (const level of Object.keys(entry.byLevel)) {
            if (ruleConfig.dependsByLevel[level]?.includes(rel)) {
              entry.byLevel[level].add(relationName);
            }
          }
        }
      }
    }
  }

  getDependencyIndex() {
    return this.dependencyIndex;
  }

  /**
   * Build rule configuration from evidence
   * @param {EvidenceNode} evidence - Evidence to build config for
   * @returns {Object|null} Rule configuration or null
   */
  buildRuleConfig(evidence) {
    if (!evidence.body || !evidence.body.statements) {
      this.errors.push(`Evidence ${evidence.name} has no body`);
      return null;
    }

    const statements = evidence.body.statements;
    
    // Handle single statement evidence
    if (statements.length === 1) {
      return this.buildSingleStatementRule(statements[0]);
    }

    // Handle multiple statements with logical operators
    return this.buildLogicalRule(statements);
  }

  /**
   * Build rule configuration for a single statement
   * @param {BaseNode} statement - Statement to build rule for
   * @returns {Object|null} Rule configuration or null
   */
  buildSingleStatementRule(statement) {
    switch (statement.type) {
      case 'DirectEvidence':
        return this.buildDirectRule(statement);
      case 'PatternMatch':
        return this.buildPatternMatchRule(statement);
      case 'DefeasibleLogic':
        return this.buildDefeasibleRule(statement);
      case 'Fusion':
        return this.buildFusionRule(statement);
      case 'PredicateCall':
        return this.buildPredicateRule(statement);
      case 'UnaryExpression':
        return this.buildUnaryRule(statement);
      case 'BinaryExpression':
        // Top-level comparator — emit a relational_comparator rule. RF-24 closure.
        return this.buildRuleFromExpressionNode(statement);
      case 'Expression':
        // Handle expressions that might be predicate calls
        if (statement.type === 'PredicateCall') {
          return this.buildPredicateRule(statement);
        }
        return this.buildRuleFromExpressionNode(statement);
      default:
        this.errors.push(`Unsupported statement type: ${statement.type}`);
        return null;
    }
  }

  /**
   * Build rule for unary expression (NOT)
   * @param {Object} expression - Unary expression
   * @returns {Object|null} Rule configuration or null
   */
  buildUnaryRule(expression) {
    if (expression.operator !== 'NOT') {
      this.errors.push(`Unsupported unary operator: ${expression.operator}`);
      return null;
    }

    const innerRule = this.buildRuleFromExpression(expression.operand);
    if (!innerRule) {
      return null;
    }

    // NOT x: evaluation will negate the inner rule's possibility (1 - poss)
    return {
      type: 'logical',
      intersection: {
        rules: [innerRule],
        aggregator: 'min',
        negate: true
      }
    };
  }

  /**
   * Build logical rule configuration for multiple statements
   * @param {BaseNode[]} statements - Statements to combine
   * @returns {Object|null} Rule configuration or null
   */
  buildLogicalRule(statements) {
    const rules = [];
    
    statements.forEach(statement => {
      const rule = this.buildSingleStatementRule(statement);
      if (rule) {
        rules.push(rule);
      }
    });

    if (rules.length === 0) {
      this.errors.push('No valid rules found in evidence body');
      return null;
    }

    if (rules.length === 1) {
      return rules[0];
    }

    // Combine rules with intersection (AND) logic — REBAC default.
    // Use explicit fusion max { ... } in DSL for OR semantics.
    return {
      type: 'logical',
      intersection: {
        rules: rules,
        aggregator: 'min'
      }
    };
  }

  /**
   * Build direct rule configuration
   * @param {DirectEvidenceNode} directEvidence - Direct evidence statement
   * @returns {Object|null} Rule configuration or null
   */
  buildDirectRule(directEvidence) {
    if (!directEvidence.predicate) {
      this.errors.push('Direct evidence must have a predicate');
      return null;
    }

    const predicate = directEvidence.predicate;
    const relation = predicate.name;
    
    return {
      type: 'direct',
      relation: relation,
      reverse: false
    };
  }

  /**
   * Build pattern match rule configuration
   * @param {PatternMatchNode} patternMatch - Pattern match statement
   * @returns {Object|null} Rule configuration or null
   */
  buildPatternMatchRule(patternMatch) {
    if (!patternMatch.predicate) {
      this.errors.push('Pattern match must have a predicate');
      return null;
    }

    const predicate = patternMatch.predicate;
    const relation = predicate.name;

    // Membership/hierarchy predicates map to TupleToUsersetRule / ParentRule
    // regardless of body shape — those have priority over chain detection.
    if (this.isMembershipPredicate(predicate)) {
      return this.buildTupleToUsersetRule(patternMatch);
    }
    if (this.isHierarchyPredicate(predicate)) {
      return this.buildParentRule(patternMatch);
    }

    // Chain detection: ADR-000 ChainRule shape is "works_in(p, *d) { has_access(d, r) }".
    // The outer PatternMatch has a Wildcard binding, and its body contains a single
    // PredicateCall (no DefeasibleLogic wrapping, no nested PatternMatch). Treat that
    // as a chain: two-hop traversal through the wildcard intermediate. RF-24 closure
    // (parallel to RF-22/RF-23 — DSL→engine mapping gap surfaced by rigor coverage).
    if (this._isChainPattern(patternMatch)) {
      return this.buildChainRule(patternMatch);
    }

    return this.buildMultiHopRule(patternMatch);
  }

  /**
   * Detect the ChainRule shape: a PatternMatch whose body contains exactly one
   * PredicateCall and uses a Wildcard arg to bind the intermediate. The predicate
   * name itself is NOT in the membership/hierarchy lists (those map to TUS/Parent).
   */
  _isChainPattern(patternMatch) {
    if (!patternMatch.body || !Array.isArray(patternMatch.body.statements)) return false;
    const stmts = patternMatch.body.statements;
    if (stmts.length !== 1) return false;
    if (stmts[0].type !== 'PredicateCall') return false;
    const predicate = patternMatch.predicate;
    if (!predicate || !Array.isArray(predicate.args)) return false;
    // Must use at least one Wildcard (*var) to bind the intermediate
    return predicate.args.some(arg => arg && arg.type === 'Wildcard');
  }

  /**
   * Build chain rule configuration (ADR-000 ChainRule).
   * Compiles "works_in(p, *d) { has_access(d, doc) }" into
   *   { type: 'chain', steps: ['works_in', 'has_access'] }
   * The intermediate wildcard binds the two predicates' arguments.
   */
  buildChainRule(patternMatch) {
    const steps = [];
    steps.push(patternMatch.predicate.name);

    const inner = patternMatch.body.statements[0];
    if (inner && inner.type === 'PredicateCall' && inner.name) {
      steps.push(inner.name);
    }

    return {
      type: 'chain',
      steps,
      aggregator: 'max',
      collectValues: true
    };
  }

  /**
   * Build tuple-to-userset rule configuration
   * @param {PatternMatchNode} patternMatch - Pattern match statement
   * @returns {Object|null} Rule configuration or null
   */
  buildTupleToUsersetRule(patternMatch) {
    const predicate = patternMatch.predicate;
    const relation = predicate.name;
    
    return {
      type: 'tuple_to_userset',
      tuplesetRelation: 'owner', // Default, could be inferred from context
      computedRelation: relation,
      reverse: false,
      earlyExitThreshold: 0.95,
      maxIntermediates: patternMatch.limit || 10
    };
  }

  /**
   * Build parent rule configuration
   * @param {PatternMatchNode} patternMatch - Pattern match statement
   * @returns {Object|null} Rule configuration or null
   */
  buildParentRule(patternMatch) {
    const predicate = patternMatch.predicate;
    const relation = predicate.name;
    
    return {
      type: 'parent',
      parentRelation: 'parent', // Default, could be inferred from context
      relation: relation,
      reverse: false,
      aggregator: 'max'
    };
  }

  /**
   * Build multi-hop rule configuration
   * @param {PatternMatchNode} patternMatch - Pattern match statement
   * @returns {Object|null} Rule configuration or null
   */
  buildMultiHopRule(patternMatch) {
    const predicate = patternMatch.predicate;
    const relation = predicate.name;
    
    return {
      type: 'multi_hop',
      relation: relation,
      maxDepth: 3,
      pathAggregation: 'max',
      reverse: false,
      fallbackToBasicPaths: true,
      collectValues: false
    };
  }

  /**
   * Build defeasible rule configuration
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildDefeasibleRule(defeasibleLogic) {
    const logicType = defeasibleLogic.logicType;

    if (logicType === 'NEVER') {
      return this.buildNeverRule(defeasibleLogic);
    } else if (logicType === 'ALWAYS') {
      return this.buildStrictRule(defeasibleLogic);
    } else if (logicType === 'WHEN') {
      return this.buildDefeasibleRuleWithDefeater(defeasibleLogic);
    } else if (logicType === 'UNLESS') {
      return this.buildDefeaterRule(defeasibleLogic);
    } else if (logicType === 'REQUIRES') {
      return this.buildRequirementRule(defeasibleLogic);
    }

    this.errors.push(`Unsupported defeasible logic type: ${logicType}`);
    return null;
  }

  /**
   * Build NEVER rule configuration (absolute denial)
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildNeverRule(defeasibleLogic) {
    const condition = this.buildRuleFromExpression(defeasibleLogic.condition);

    return {
      type: 'logical',
      never: {
        union: {
          rules: [condition],
          aggregator: 'max'
        }
      }
    };
  }

  /**
   * Build strict rule configuration
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildStrictRule(defeasibleLogic) {
    const condition = this.buildRuleFromExpression(defeasibleLogic.condition);
    
    return {
      type: 'logical',
      always: {
        direct: condition,
        aggregator: 'min'
      }
    };
  }

  /**
   * Build defeasible rule with defeater
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildDefeasibleRuleWithDefeater(defeasibleLogic) {
    const condition = this.buildRuleFromExpression(defeasibleLogic.condition);
    const defeater = this.buildRuleFromExpression(defeasibleLogic.defeater);
    
    const rule = {
      type: 'logical',
      when: {
        intersection: {
          rules: [condition],
          aggregator: 'min'
        }
      }
    };

    if (defeater) {
      rule.unless = {
        union: {
          rules: [defeater],
          aggregator: 'max'
        }
      };
    }

    return rule;
  }

  /**
   * Build defeater rule configuration
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildDefeaterRule(defeasibleLogic) {
    const condition = this.buildRuleFromExpression(defeasibleLogic.condition);
    
    return {
      type: 'logical',
      unless: {
        union: {
          rules: [condition],
          aggregator: 'max'
        }
      }
    };
  }

  /**
   * Build requirement rule configuration
   * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement
   * @returns {Object|null} Rule configuration or null
   */
  buildRequirementRule(defeasibleLogic) {
    const condition = this.buildRuleFromExpression(defeasibleLogic.condition);
    
    return {
      type: 'logical',
      requires: {
        union: {
          rules: [condition],
          aggregator: 'min'
        }
      }
    };
  }

  /**
   * Build fusion rule configuration
   * @param {FusionNode} fusion - Fusion statement
   * @returns {Object|null} Rule configuration or null
   */
  buildFusionRule(fusion) {
    const rules = [];
    
    (fusion.expressions || fusion.evidence || []).forEach(evidence => {
      const rule = this.buildRuleFromExpression(evidence);
      if (rule) {
        rules.push(rule);
      }
    });

    if (rules.length === 0) {
      this.errors.push('Fusion has no valid evidence');
      return null;
    }

    if (fusion.weights && fusion.strategy !== 'custom') {
      this.errors.push(`Fusion weights require custom strategy, got: ${fusion.strategy}`);
      return null;
    }

    if (fusion.strategy === 'custom') {
      if (!fusion.weights || fusion.weights.length === 0) {
        this.errors.push('Custom fusion requires weights');
        return null;
      }
      const total = fusion.weights.reduce((sum, w) => sum + w, 0);
      if (Math.abs(total - 1.0) > 1e-6) {
        this.errors.push('Custom fusion weights must sum to 1.0');
        return null;
      }
    }

    const union = {
      rules: rules,
      aggregator: fusion.strategy
    };
    if (fusion.weights && fusion.weights.length > 0) {
      union.owaWeights = fusion.weights;
    }

    return {
      type: 'logical',
      union
    };
  }

  /**
   * Build rule from expression
   * @param {BaseNode} expression - Expression to build rule from
   * @returns {Object|null} Rule configuration or null
   */
  buildRuleFromExpression(expression) {
    if (!expression) {
      return null;
    }

    if (expression.type === 'Predicate') {
      return this.buildDirectRuleFromPredicate(expression);
    } else if (expression.type === 'Expression') {
      return this.buildRuleFromExpressionNode(expression);
    } else if (expression.type === 'PredicateCall') {
      return this.buildPredicateRule(expression);
    } else if (expression.type === 'UnaryExpression') {
      return this.buildUnaryRule(expression);
    }

    this.errors.push(`Unsupported expression type: ${expression.type}`);
    return null;
  }

  /**
   * Build direct rule from predicate
   * @param {PredicateNode} predicate - Predicate to build rule from
   * @returns {Object|null} Rule configuration or null
   */
  buildDirectRuleFromPredicate(predicate) {
    const expanded = this._expandPredicate(predicate.name);
    if (expanded) return expanded;

    return {
      type: 'direct',
      relation: predicate.name,
      reverse: false
    };
  }

  _expandPredicate(predicateName) {
    const existingConfig = this.generatedRules.get(predicateName) || this.arbiter?.relationConfigs?.get(predicateName);
    if (!existingConfig) return null;
    if (!existingConfig.union && !existingConfig.intersection && !existingConfig.exclusion) return null;

    const logicalKey = existingConfig.union ? 'union' : existingConfig.intersection ? 'intersection' : 'exclusion';
    const subRules = Array.isArray(existingConfig[logicalKey]?.rules)
      ? existingConfig[logicalKey].rules
      : Array.isArray(existingConfig[logicalKey]) ? existingConfig[logicalKey] : [];

    if (subRules.length === 0) return null;

    const expandedRules = subRules.map(r => {
      if (r && r.type === 'direct') return { type: 'direct', relation: r.relation, reverse: !!r.reverse };
      if (typeof r === 'string') return { type: 'direct', relation: r, reverse: false };
      return null;
    }).filter(Boolean);

    if (expandedRules.length === 0) return null;

    return {
      type: 'logical',
      [logicalKey]: {
        rules: expandedRules,
        aggregator: existingConfig[logicalKey]?.aggregator || 'min'
      },
      // Flag to tell the evaluator: this expanded sub-predicate is unary —
      // use the subject as the object instead of inheriting the parent's object.
      _subjectAsObject: true
    };
  }

  /**
   * Build rule from expression node
   * @param {ExpressionNode} expression - Expression to build rule from
   * @returns {Object|null} Rule configuration or null
   */
  buildRuleFromExpressionNode(expression) {
    if (expression.type === 'AttributeAccess') {
      return this.buildAttributeRule(expression);
    } else if (expression.type === 'PredicateCall') {
      return this.buildPredicateRule(expression);
    } else if (expression.type === 'BinaryExpression' && expression.operator === 'within') {
      return this.buildWithinRule(expression);
    } else if (expression.type === 'BinaryExpression' && this._isComparatorOperator(expression.operator)) {
      // ADR-000 RelationalComparatorRule shape: "userRisk(u) <= riskLimit(r)".
      // Route BinaryExpression with comparator operators here so the
      // evaluator can run a fuzzy interval comparison instead of treating
      // them as logical truth values. RF-24 closure.
      return this.buildRelationalComparatorRule(expression);
    }

    this.errors.push(`Unsupported expression type: ${expression.type}`);
    return null;
  }

  /**
   * Detect comparator operators per ADR-000. These are the operators that map
   * to RelationalComparatorRule (vs. the boolean operators like `&&`/`||` which
   * keep going through LogicalOperators).
   */
  _isComparatorOperator(operator) {
    return operator === '>' || operator === '>=' || operator === '<' ||
           operator === '<=' || operator === '==' || operator === '!=';
  }

  /**
   * Build relational comparator rule configuration (ADR-000 RelationalComparatorRule).
   * Compiles "personAge(p) >= docMinAge(d)" into
   *   {
   *     type: 'relational_comparator',
   *     left:  { rule: <predicate-call>, extractValue: true },
   *     right: { rule: <predicate-call>, extractValue: true },
   *     comparator: '>=',
   *     marginOfSafety: 1.0,
   *     fallbackBehavior: 'deny',
   *     minRulePossibility: 0
   *   }
   */
  buildRelationalComparatorRule(binaryExpression) {
    const comparator = binaryExpression.operator;
    const left = this._buildComparatorOperand(binaryExpression.left);
    const right = this._buildComparatorOperand(binaryExpression.right);
    if (!left || !right) {
      this.errors.push(`Comparator operands must resolve to predicate calls (operator=${comparator})`);
      return null;
    }

    return {
      type: 'relational_comparator',
      left,
      right,
      comparator,
      marginOfSafety: 1.0,
      fallbackBehavior: 'deny',
      minRulePossibility: 0
    };
  }

  /**
   * Wrap a BinaryExpression side into a relational_comparator operand. The
   * operand's `rule` field is the original predicate call (preserving reference
   * semantics so the inner rule's evaluator can resolve its values). `extractValue`
   * tells the evaluator to read the relation's `value` field rather than its
   * `possibility`, which is what `personAge(p)` / `docMinAge(d)` semantics require.
   */
  _buildComparatorOperand(side) {
    if (!side) return null;
    if (side.type === 'PredicateCall') {
      return {
        rule: side,
        extractValue: true,
        evaluatorFrom: 'auto'
      };
    }
    if (side.type === 'AttributeAccess') {
      // user.age — treat the attribute path as a "measure" reference
      return {
        rule: side,
        extractValue: true,
        evaluatorFrom: 'auto',
        attributePath: side.getAttributePath ? side.getAttributePath() : null
      };
    }
    return null;
  }

  /**
   * Build attribute rule configuration
   * @param {ExpressionNode} expression - Attribute expression
   * @returns {Object|null} Rule configuration or null
   */
  buildAttributeRule(expression) {
    const attributePath = expression.getAttributePath();
    
    return {
      type: 'direct',
      relation: attributePath,
      reverse: false
    };
  }

  /**
   * Build function rule configuration
   * @param {ExpressionNode} expression - Function expression
   * @returns {Object|null} Rule configuration or null
   */
  buildPredicateRule(expression) {
    const predicateName = expression.name;
    if (expression.challenge) {
      return this.buildChallengeRule(expression, null);
    }

    // Expand composite (logical) predicate references into their direct
    // leaf components so the optimizer can flatten to a correct direct_list.
    const expanded = this._expandPredicate(predicateName);
    if (expanded) return expanded;

    return {
      type: 'direct',
      relation: predicateName,
      reverse: false
    };
  }

  buildWithinRule(expression) {
    const left = expression.left;
    const right = expression.right;
    if (left && left.type === 'PredicateCall' && left.challenge) {
      this.errors.push('within must be applied to a challenge proof field, e.g. !mfa(user).issued_at within 10m');
      return null;
    }
    if (left && left.type === 'AttributeAccess' && left.object?.type === 'PredicateCall' && left.object?.challenge) {
      if (left.attribute !== 'issued_at' && left.attribute !== 'issuedAt') {
        this.errors.push('only issued_at is supported for challenge recency checks');
        return null;
      }
      return this.buildChallengeRule(left.object, right);
    }
    this.errors.push('within operator currently only supported with challenge predicates');
    return null;
  }

  buildChallengeRule(expression, duration) {
    const predicateName = expression.name;
    const subject = this._resolveChallengeSubject(expression.args || []);
    const withinMs = duration ? this._durationToMs(duration) : null;
    return {
      type: 'challenge',
      challenge: predicateName,
      subject,
      ...(withinMs !== null && { withinMs })
    };
  }

  _resolveChallengeSubject(args) {
    if (!args || !args.length) return 'user';
    const first = args[0];
    if (first?.type === 'Variable') {
      if (first.name === 'object') return 'object';
      if (first.name === 'session') return 'session';
      return 'user';
    }
    return 'user';
  }

  _durationToMs(duration) {
    if (!duration || duration.value === undefined) return null;
    const raw = typeof duration.value === 'string' ? duration.value : String(duration.value);
    const unit = duration.unit || raw.slice(-1);
    const numeric = parseFloat(raw);
    if (!Number.isFinite(numeric)) return null;
    switch (unit) {
      case 's':
        return numeric * 1000;
      case 'm':
        return numeric * 60 * 1000;
      case 'h':
        return numeric * 60 * 60 * 1000;
      case 'd':
        return numeric * 24 * 60 * 60 * 1000;
      case 'w':
        return numeric * 7 * 24 * 60 * 60 * 1000;
      default:
        return null;
    }
  }

  /**
   * Build measure rule configuration
   * @param {MeasureNode} measure - Measure to build rule for
   * @returns {Object|null} Rule configuration or null
   */
  buildMeasureRuleConfig(measure) {
    if (!measure.body) {
      this.errors.push(`Measure ${measure.name} has no body`);
      return null;
    }

    // Measures typically generate computed rules
    return {
      type: 'computed',
      relation: measure.name
    };
  }

  /**
   * Check if predicate is membership-based
   * @param {PredicateNode} predicate - Predicate to check
   * @returns {boolean} True if membership predicate
   */
  isMembershipPredicate(predicate) {
    const membershipPredicates = ['member', 'member_of', 'belongs_to', 'is_member'];
    return membershipPredicates.includes(predicate.name.toLowerCase());
  }

  /**
   * Check if predicate is hierarchy-based
   * @param {PredicateNode} predicate - Predicate to check
   * @returns {boolean} True if hierarchy predicate
   */
  isHierarchyPredicate(predicate) {
    const hierarchyPredicates = ['parent', 'parent_of', 'child', 'child_of', 'ancestor', 'descendant'];
    return hierarchyPredicates.includes(predicate.name.toLowerCase());
  }

  /**
   * Apply generated rules to arbiter
   */
  applyRulesToArbiter() {
    if (!this.arbiter || typeof this.arbiter.setRelationConfig !== 'function') {
      // Skip applying rules if arbiter is not available or doesn't support setRelationConfig
      return;
    }
    
    this.generatedRules.forEach((config, relation) => {
      try {
        this.arbiter.setRelationConfig(relation, config);
      } catch (error) {
        this.errors.push(`Failed to set relation config for ${relation}: ${error.message}`);
      }
    });

    if (typeof this.arbiter.registerDependencyIndex === 'function') {
      this.arbiter.registerDependencyIndex(this.dependencyIndex);
    }
  }

  /**
   * Get generated rules
   * @returns {Map} Map of generated rules
   */
  getGeneratedRules() {
    return this.generatedRules;
  }

  /**
   * Get generation errors
   * @returns {string[]} Array of error messages
   */
  getErrors() {
    return this.errors;
  }
}
