/**
 * Expression Interpreter
 * 
 * Interprets inline DSL expressions by evaluating them against existing
 * compiled DSL rules in the graph. No temporary rules are created.
 */

import { PredicateResolver } from './PredicateResolver.js';

export class ExpressionInterpreter {
  constructor(context, options = {}) {
    this.context = context;
    this.graphStores = context.graphStores;
    this.resolver = new PredicateResolver(context);
    this.customBuiltIns = options.customBuiltIns || {};
  }

  /**
   * Interpret an expression AST against the graph
   * 
   * @param {Object} ast - Parsed expression AST
   * @param {Object} bindings - Variable bindings
   * @returns {Object} Interpretation result
   */
  async interpret(ast, bindings) {
    switch (ast.type) {
      case 'Fusion':
        return this.interpretFusion(ast, bindings);
      case 'Or':
        return this.interpretOr(ast, bindings);
      case 'And':
        return this.interpretAnd(ast, bindings);
      case 'Not':
        return this.interpretNot(ast, bindings);
      case 'Defeasible':
        return this.interpretDefeasible(ast, bindings);
      case 'Predicate':
        return this.interpretPredicate(ast, bindings);
      default:
        throw new Error(`Unknown AST node type: ${ast.type}`);
    }
  }

  /**
   * Interpret FUSION block - ALL expressions must be true
   * Uses OWA semantics: minimum possibility across all expressions
   */
  async interpretFusion(ast, bindings) {
    const results = await Promise.all(
      ast.expressions.map(expr => this.interpret(expr, bindings))
    );

    const allAllowed = results.every(r => r.allowed);
    const minPossibility = results.length > 0 
      ? Math.min(...results.map(r => r.possibility || 0))
      : 0;

    return {
      allowed: allAllowed,
      possibility: minPossibility,
      type: 'Fusion',
      details: results
    };
  }

  /**
   * Interpret OR - ANY expression can be true (short-circuited)
   */
  async interpretOr(ast, bindings) {
    for (const operand of ast.operands) {
      const result = await this.interpret(operand, bindings);
      if (result.allowed) {
        return {
          allowed: true,
          possibility: result.possibility,
          type: 'Or',
          satisfiedBy: operand
        };
      }
    }

    return {
      allowed: false,
      possibility: 0,
      type: 'Or'
    };
  }

  /**
   * Interpret AND - ALL expressions must be true
   */
  async interpretAnd(ast, bindings) {
    const results = [];
    let minPossibility = 1;

    for (const operand of ast.operands) {
      const result = await this.interpret(operand, bindings);
      results.push(result);
      minPossibility = Math.min(minPossibility, result.possibility || 0);
      
      if (!result.allowed) {
        return {
          allowed: false,
          possibility: 0,
          type: 'And',
          failedAt: operand,
          details: results
        };
      }
    }

    return {
      allowed: true,
      possibility: minPossibility,
      type: 'And',
      details: results
    };
  }

  /**
   * Interpret NOT - negate the operand result
   */
  async interpretNot(ast, bindings) {
    const result = await this.interpret(ast.operand, bindings);
    
    return {
      allowed: !result.allowed,
      possibility: result.allowed ? 0 : 1,
      type: 'Not',
      inner: result
    };
  }

  /**
   * Interpret Defeasible - primary UNLESS exception
   * If exception is true, primary is defeated
   */
  async interpretDefeasible(ast, bindings) {
    // Check exception first (short-circuit if possible)
    const exceptionResult = await this.interpret(ast.exception, bindings);
    
    if (exceptionResult.allowed) {
      return {
        allowed: false,
        possibility: 0,
        type: 'Defeasible',
        reason: 'Defeated by exception',
        defeatedBy: exceptionResult
      };
    }

    // Exception is false, evaluate primary
    const primaryResult = await this.interpret(ast.primary, bindings);
    
    return {
      ...primaryResult,
      type: 'Defeasible',
      primary: primaryResult,
      exception: exceptionResult
    };
  }

  /**
   * Interpret Predicate - call existing DSL rule via graph.check()
   * OR evaluate built-in function
   * 
   * This is where we use the ALREADY COMPILED DSL rules.
   * We do NOT create temporary rules.
   */
  async interpretPredicate(ast, bindings) {
    // Check for built-in functions first (ip_in_cidr, etc.)
    const { isBuiltInFunction, evaluateBuiltIn } = await import('./BuiltInFunctions.js');
    const resolvedArgs = ast.args.map(arg => this.resolveArgument(arg, bindings));
    
    // Check custom built-ins first (e.g., PriceOps predicates)
    if (this.customBuiltIns[ast.name]) {
      const result = await this.customBuiltIns[ast.name](...resolvedArgs);
      return {
        allowed: result === true || result === 1,
        possibility: result === true || result === 1 ? 1 : 0,
        type: 'BuiltInFunction',
        function: ast.name,
        args: resolvedArgs,
        result
      };
    }
    
    if (isBuiltInFunction(ast.name)) {
      const result = evaluateBuiltIn(ast.name, resolvedArgs);
      
      return {
        allowed: result === true || result === 1,
        possibility: result === true || result === 1 ? 1 : 0,
        type: 'BuiltInFunction',
        function: ast.name,
        args: resolvedArgs,
        result
      };
    }
    
    // Resolve predicate to existing rule
    const rule = this.resolver.resolve(ast.name);
    if (!rule) {
      throw new Error(`Unknown predicate: ${ast.name} - must be defined in compiled DSL`);
    }

    // Extract subject (user) and optional object
    const subject = resolvedArgs[0];  // First arg is always the subject
    // For single-argument predicates, use subject as object to avoid "missing_node" errors
    const object = resolvedArgs[1] || subject;  // Second arg is optional object

    // Get the appropriate graph store
    const graphStore = this.context.getGraphStore
      ? this.context.getGraphStore(rule.scope, {
        tenantId: bindings.tenant,
        applicationId: bindings.applicationId
      })
      : this.graphStores[rule.scope];
    if (!graphStore) {
      throw new Error(`Graph store not found for scope: ${rule.scope}`);
    }

    // Execute check using EXISTING compiled rule
    // The graphStore.check() will use the pre-compiled DSL rule config
    const checkOptions = {};
    if (bindings.partialGraph) {
      checkOptions.partialGraph = bindings.partialGraph;
    }

    const result = graphStore.check(subject, ast.name, object, checkOptions);

    return {
      allowed: result?.allowed || result?.possibility === 1,
      possibility: result?.possibility || 0,
      type: 'Predicate',
      predicate: ast.name,
      scope: rule.scope,
      subject,
      object,
      rawResult: result
    };
  }

  /**
   * Resolve an argument to its actual value
   */
  resolveArgument(arg, bindings) {
    switch (arg.type) {
      case 'Variable':
        return this.resolveVariable(arg, bindings);
      case 'Reference':
        return this.resolveReference(arg, bindings);
      case 'Literal':
        return arg.value;
      default:
        throw new Error(`Unknown argument type: ${arg.type}`);
    }
  }

  /**
   * Resolve a variable binding
   * :user -> bindings.user
   * :params.id -> bindings.params.id
   */
  resolveVariable(variable, bindings) {
    let value = bindings[variable.name];
    
    // Handle nested paths: :params.id
    if (variable.path && variable.path.length > 0) {
      for (const key of variable.path) {
        if (value === undefined || value === null) {
          return undefined;
        }
        value = value[key];
      }
    }
    
    return value;
  }

  /**
   * Resolve a typed reference
   * document::params.id -> bindings.params.id with type info
   */
  resolveReference(ref, bindings) {
    // Typed references like document::params.id
    // The type (document) is metadata, the value comes from the path
    let value = bindings;
    
    for (const key of ref.path) {
      if (value === undefined || value === null) {
        return undefined;
      }
      value = value[key];
    }
    
    return value;
  }
}

/**
 * Utility to collect all predicates from an AST
 * Used for validation before interpretation
 */
export function collectPredicates(ast, predicates = []) {
  if (ast.type === 'Predicate') {
    predicates.push(ast);
  }
  
  // Recursively collect from child nodes
  const childKeys = ['expressions', 'operands', 'operand', 'primary', 'exception', 'inner'];
  for (const key of childKeys) {
    if (ast[key]) {
      if (Array.isArray(ast[key])) {
        ast[key].forEach(child => collectPredicates(child, predicates));
      } else {
        collectPredicates(ast[key], predicates);
      }
    }
  }
  
  return predicates;
}
