/**
 * Pure helper functions for the DSL workflow interpreter.
 *
 * These functions are deterministic and have no side effects,
 * making them safe to import in Temporal workflow code and easy to unit test.
 */
import type { WorkflowDSL, DslNode } from './types';

// ─── Template Resolution ─────────────────────────────────────────────────────

/**
 * Recursively resolve {{path}} template variables in an object.
 */
export function resolveTemplates(obj: any, ctx: Record<string, any>): any {
  if (typeof obj === 'string') {
    return obj.replace(/\{\{(.+?)\}\}/g, (_, path) => {
      const val = getNestedValue(ctx, path.trim());
      return val !== undefined ? String(val) : '';
    });
  }
  if (Array.isArray(obj)) return obj.map((v) => resolveTemplates(v, ctx));
  if (obj && typeof obj === 'object') {
    const result: Record<string, any> = {};
    for (const [k, v] of Object.entries(obj)) {
      result[k] = resolveTemplates(v, ctx);
    }
    return result;
  }
  return obj;
}

/**
 * Resolve a simple path expression against the context.
 * If the path resolves to a value, return it; otherwise return the raw string.
 */
export function resolveExpression(expr: string, ctx: Record<string, any>): any {
  const value = getNestedValue(ctx, expr);
  if (value !== undefined) return value;
  return expr;
}

// ─── Object Path Access ──────────────────────────────────────────────────────

/**
 * Get a nested value from an object using dot notation.
 * e.g. getNestedValue({ a: { b: 1 } }, "a.b") → 1
 */
export function getNestedValue(obj: Record<string, any>, path: string): any {
  const parts = path.split('.');
  let current: any = obj;
  for (const part of parts) {
    if (current == null) return undefined;
    current = current[part];
  }
  return current;
}

// ─── Condition Evaluation ────────────────────────────────────────────────────

/**
 * Evaluate multiple condition rules with AND/OR combining.
 */
export function evaluateConditions(
  conditions: Array<{ field: string; operator: string; value: string }>,
  ctx: Record<string, any>,
  combineOp: 'AND' | 'OR',
): boolean {
  if (conditions.length === 0) return true;
  const results = conditions.map((rule) => {
    const fieldValue = getNestedValue(ctx, rule.field);
    return evaluateRule(fieldValue, rule.operator, rule.value);
  });
  return combineOp === 'AND' ? results.every(Boolean) : results.some(Boolean);
}

/**
 * Evaluate a single condition rule.
 */
export function evaluateRule(fieldValue: any, operator: string, compareValue: string): boolean {
  switch (operator) {
    case 'equals':
    case '==':
      return String(fieldValue) === compareValue;
    case 'notEquals':
    case '!=':
      return String(fieldValue) !== compareValue;
    case 'contains':
      return String(fieldValue).includes(compareValue);
    case 'notContains':
      return !String(fieldValue).includes(compareValue);
    case 'greaterThan':
    case '>':
      return Number(fieldValue) > Number(compareValue);
    case 'lessThan':
    case '<':
      return Number(fieldValue) < Number(compareValue);
    case 'greaterThanOrEqual':
    case '>=':
      return Number(fieldValue) >= Number(compareValue);
    case 'lessThanOrEqual':
    case '<=':
      return Number(fieldValue) <= Number(compareValue);
    case 'isEmpty':
      return fieldValue == null || fieldValue === '';
    case 'isNotEmpty':
      return fieldValue != null && fieldValue !== '';
    default:
      return false;
  }
}

// ─── Graph Navigation ────────────────────────────────────────────────────────

/**
 * Get successor node IDs from a list of source node IDs.
 */
export function getSuccessorsFromNodes(dsl: WorkflowDSL, nodeIds: string[]): string[] {
  return dsl.edges.filter((e) => nodeIds.includes(e.source)).map((e) => e.target);
}

/**
 * Determine next nodes to execute based on the current node and optional branch.
 * For condition nodes, follows only the matching branch edge.
 */
export function getNextNodes(dsl: WorkflowDSL, node: DslNode, branch?: string): string[] {
  if (branch !== undefined) {
    const branchEdges = dsl.edges.filter(
      (e) => e.source === node.id && e.sourceHandle === branch,
    );
    if (branchEdges.length > 0) return branchEdges.map((e) => e.target);
  }
  return dsl.edges.filter((e) => e.source === node.id).map((e) => e.target);
}
