// src/core/oracle/trs.ts
// SOURCE: ARCHITECTURE/32-LANGUAGE-OFFICIAL-13-IMPLEMENTATION-16.md
// Patch Set 6-A2: Deterministic Term Rewriting System engine

import type { Val } from "../../../../src/core/eval/values";
import { matchAST, type Bindings } from "./match";

/**
 * A rewrite rule: pattern → template, optionally guarded by a predicate.
 */
export type Rule = {
  name: string;
  pattern: Val;
  template: Val;
  /** Optional predicate over bindings. Returns true if rule is applicable. */
  where?: (bindings: Bindings) => boolean;
};

/**
 * Traversal strategy for applying rewrite rules.
 */
export type Strategy = "topdown" | "bottomup";

/**
 * Result of a single rewrite step.
 */
export type RewriteResult = {
  changed: boolean;
  result: Val;
  ruleName?: string;
  position?: string;
};

/**
 * Conflict report from confluence analysis.
 */
export type ConflictReport = {
  rule1: string;
  rule2: string;
  overlap: "same-head" | "embeds" | "similar-shape";
  description: string;
};

// -----------------------------------------------------------------------------
// Pattern substitution: apply bindings to a template
// -----------------------------------------------------------------------------

function isObj(x: unknown): x is Record<string, unknown> {
  return !!x && typeof x === "object" && !Array.isArray(x);
}

function isVarRef(node: Val): string | null {
  // Template variable: {tag:"Sym", name:"?x"} or {tag:"Var", name:"?x"}
  if (isObj(node) && typeof (node as any).name === "string") {
    const name = (node as any).name as string;
    if (name.startsWith("?")) return name.slice(1);
  }
  return null;
}

/**
 * Substitute bindings into a template, producing a new Val.
 */
export function substitute(template: Val, bindings: Bindings): Val {
  const ref = isVarRef(template);
  if (ref !== null) {
    if (ref in bindings) {
      return bindings[ref] as Val;
    }
    // Unbound variable stays as-is (for partial templates)
    return template;
  }

  if (template === null || typeof template !== "object") {
    return template;
  }

  if (Array.isArray(template)) {
    return template.map(item => substitute(item as Val, bindings)) as unknown as Val;
  }

  // Object (Val node)
  const obj = template as Record<string, unknown>;
  const result: Record<string, unknown> = {};
  for (const k of Object.keys(obj)) {
    if (k === "loc" || k === "span") {
      // Don't recurse into source locations
      result[k] = obj[k];
    } else {
      result[k] = substitute(obj[k] as Val, bindings);
    }
  }
  return result as Val;
}

// -----------------------------------------------------------------------------
// Tree traversal helpers
// -----------------------------------------------------------------------------

function getChildren(node: Val): { key: string; child: Val }[] {
  if (node === null || typeof node !== "object") return [];
  if (Array.isArray(node)) {
    return node.map((item, i) => ({ key: String(i), child: item as Val }));
  }

  const obj = node as Record<string, unknown>;
  const children: { key: string; child: Val }[] = [];

  // Recurse into known Val structural fields
  switch (obj.tag) {
    case "Pair":
      children.push({ key: "car", child: obj.car as Val });
      children.push({ key: "cdr", child: obj.cdr as Val });
      break;
    case "Vector":
      (obj.items as Val[]).forEach((item, i) => {
        children.push({ key: `items[${i}]`, child: item });
      });
      break;
    case "Map":
      (obj.entries as Array<[Val, Val]>).forEach(([k, v], i) => {
        children.push({ key: `entries[${i}][0]`, child: k });
        children.push({ key: `entries[${i}][1]`, child: v });
      });
      break;
    case "Syntax":
      children.push({ key: "stx", child: obj.stx as Val });
      break;
    // For quoted Exprs, recurse into their structure
    default:
      // Generic object: recurse into non-special fields
      for (const k of Object.keys(obj)) {
        if (k === "tag" || k === "loc" || k === "span") continue;
        const v = obj[k];
        if (v && typeof v === "object") {
          children.push({ key: k, child: v as Val });
        }
      }
  }
  return children;
}

function setChild(node: Val, key: string, newChild: Val): Val {
  if (Array.isArray(node)) {
    const idx = parseInt(key, 10);
    const copy = [...node];
    copy[idx] = newChild;
    return copy as unknown as Val;
  }

  const obj = node as Record<string, unknown>;
  const copy = { ...obj };

  // Handle indexed keys like "items[0]" or "entries[0][1]"
  const arrayMatch = key.match(/^(\w+)\[(\d+)\](?:\[(\d+)\])?$/);
  if (arrayMatch) {
    const [, field, idx1, idx2] = arrayMatch;
    const arr = [...(copy[field] as unknown[])];
    if (idx2 !== undefined) {
      // entries[i][j]
      const inner = [...(arr[parseInt(idx1, 10)] as unknown[])];
      inner[parseInt(idx2, 10)] = newChild;
      arr[parseInt(idx1, 10)] = inner;
    } else {
      arr[parseInt(idx1, 10)] = newChild;
    }
    copy[field] = arr;
  } else {
    copy[key] = newChild;
  }

  return copy as Val;
}

// -----------------------------------------------------------------------------
// Core rewrite engine
// -----------------------------------------------------------------------------

/**
 * Try to apply a single rule at the root of the node.
 * Returns the rewritten node if the rule matched, or null otherwise.
 */
function tryApplyRule(rule: Rule, node: Val): Val | null {
  const { ok, bindings } = matchAST(rule.pattern, node);
  if (!ok) return null;
  if (rule.where && !rule.where(bindings)) return null;
  return substitute(rule.template, bindings);
}

/**
 * Apply the first matching rule at the root.
 * Returns { changed: true, result, ruleName } if a rule matched,
 * or { changed: false, result: node } otherwise.
 */
function applyRulesAtRoot(rules: Rule[], node: Val): RewriteResult {
  for (const rule of rules) {
    const result = tryApplyRule(rule, node);
    if (result !== null) {
      return { changed: true, result, ruleName: rule.name };
    }
  }
  return { changed: false, result: node };
}

/**
 * Rewrite once using topdown strategy: try root first, then children.
 */
function rewriteOnceTopdown(rules: Rule[], node: Val, path: string = ""): RewriteResult {
  // Try root first
  const rootResult = applyRulesAtRoot(rules, node);
  if (rootResult.changed) {
    return { ...rootResult, position: path || "root" };
  }

  // Try children
  const children = getChildren(node);
  for (const { key, child } of children) {
    const childPath = path ? `${path}.${key}` : key;
    const childResult = rewriteOnceTopdown(rules, child, childPath);
    if (childResult.changed) {
      const newNode = setChild(node, key, childResult.result);
      return { changed: true, result: newNode, ruleName: childResult.ruleName, position: childResult.position };
    }
  }

  return { changed: false, result: node };
}

/**
 * Rewrite once using bottomup strategy: try children first, then root.
 */
function rewriteOnceBottomup(rules: Rule[], node: Val, path: string = ""): RewriteResult {
  // Try children first
  const children = getChildren(node);
  let currentNode = node;
  let anyChildChanged = false;

  for (const { key, child } of children) {
    const childPath = path ? `${path}.${key}` : key;
    const childResult = rewriteOnceBottomup(rules, child, childPath);
    if (childResult.changed) {
      currentNode = setChild(currentNode, key, childResult.result);
      anyChildChanged = true;
      // Return on first change (outermost bottomup)
      return { changed: true, result: currentNode, ruleName: childResult.ruleName, position: childResult.position };
    }
  }

  // Try root after children
  const rootResult = applyRulesAtRoot(rules, currentNode);
  if (rootResult.changed) {
    return { ...rootResult, position: path || "root" };
  }

  return { changed: anyChildChanged, result: currentNode };
}

/**
 * Apply rules once at the first matching position.
 */
export function rewriteOnce(rules: Rule[], expr: Val, strategy: Strategy = "topdown"): RewriteResult {
  if (strategy === "topdown") {
    return rewriteOnceTopdown(rules, expr);
  } else {
    return rewriteOnceBottomup(rules, expr);
  }
}

/**
 * Apply rules repeatedly until fixpoint or fuel exhausted.
 * @param fuel Maximum number of rewrite steps (default 100)
 */
export function rewriteFixpoint(
  rules: Rule[],
  expr: Val,
  strategy: Strategy = "topdown",
  fuel: number = 100
): { result: Val; steps: number; reachedFixpoint: boolean } {
  let current = expr;
  let steps = 0;

  while (steps < fuel) {
    const { changed, result } = rewriteOnce(rules, current, strategy);
    if (!changed) {
      return { result: current, steps, reachedFixpoint: true };
    }
    current = result;
    steps++;
  }

  return { result: current, steps, reachedFixpoint: false };
}

/**
 * Apply rules and return the full trace of intermediate results.
 */
export function rewriteTrace(
  rules: Rule[],
  expr: Val,
  strategy: Strategy = "topdown",
  fuel: number = 100
): { trace: Array<{ expr: Val; ruleName?: string; position?: string }>; reachedFixpoint: boolean } {
  const trace: Array<{ expr: Val; ruleName?: string; position?: string }> = [{ expr }];
  let current = expr;
  let steps = 0;

  while (steps < fuel) {
    const { changed, result, ruleName, position } = rewriteOnce(rules, current, strategy);
    if (!changed) {
      return { trace, reachedFixpoint: true };
    }
    current = result;
    trace.push({ expr: current, ruleName, position });
    steps++;
  }

  return { trace, reachedFixpoint: false };
}

// -----------------------------------------------------------------------------
// Confluence diagnostic: critical pair detection
// -----------------------------------------------------------------------------

/**
 * Extract the "head shape" of a pattern for overlap detection.
 * For Val nodes, this is the tag + direct structure.
 */
function getPatternHead(pattern: Val): string {
  if (pattern === null || typeof pattern !== "object") {
    return String(pattern);
  }
  if (Array.isArray(pattern)) {
    return `Array[${pattern.length}]`;
  }
  const obj = pattern as Record<string, unknown>;
  if (obj.tag) {
    return String(obj.tag);
  }
  return "Object";
}

/**
 * Check if two patterns have structural overlap that could cause non-confluence.
 */
function patternsOverlap(p1: Val, p2: Val): "same-head" | "embeds" | "similar-shape" | null {
  const head1 = getPatternHead(p1);
  const head2 = getPatternHead(p2);

  // Different head tags - no overlap
  if (head1 !== head2) return null;

  // Same head tag - need deeper analysis
  if (isObj(p1) && isObj(p2)) {
    const obj1 = p1 as Record<string, unknown>;
    const obj2 = p2 as Record<string, unknown>;

    // For App nodes, check if the function names differ
    if (obj1.tag === "App" && obj2.tag === "App") {
      const fn1 = obj1.fn as Record<string, unknown> | undefined;
      const fn2 = obj2.fn as Record<string, unknown> | undefined;

      // If both have concrete (non-variable) function names that differ, no overlap
      if (fn1 && fn2 &&
          fn1.tag === "Var" && fn2.tag === "Var" &&
          typeof fn1.name === "string" && typeof fn2.name === "string" &&
          !fn1.name.startsWith("?") && !fn2.name.startsWith("?") &&
          fn1.name !== fn2.name) {
        return null;
      }
    }

    // For Var nodes with different concrete names, no overlap
    if (obj1.tag === "Var" && obj2.tag === "Var") {
      const name1 = obj1.name as string | undefined;
      const name2 = obj2.name as string | undefined;
      if (name1 && name2 &&
          !name1.startsWith("?") && !name2.startsWith("?") &&
          name1 !== name2) {
        return null;
      }
    }

    // For Num nodes with different values, no overlap
    if (obj1.tag === "Num" && obj2.tag === "Num") {
      if (obj1.n !== obj2.n) {
        return null;
      }
    }

    // For Sym nodes with different concrete names, no overlap
    if (obj1.tag === "Sym" && obj2.tag === "Sym") {
      const name1 = obj1.name as string | undefined;
      const name2 = obj2.name as string | undefined;
      if (name1 && name2 &&
          !name1.startsWith("?") && !name2.startsWith("?") &&
          name1 !== name2) {
        return null;
      }
    }

    // Count pattern variables to check for embedding
    const vars1 = countPatternVars(p1);
    const vars2 = countPatternVars(p2);

    // If one has more concrete structure (fewer vars), it might embed
    if (Math.abs(vars1 - vars2) > 0) {
      return "embeds";
    }

    // Same structure - potential conflict
    return "same-head";
  }

  // Primitive types with same head
  if (head1 !== "Object") {
    return "same-head";
  }

  return null;
}

function countPatternVars(node: unknown): number {
  if (node === null) return 0;
  if (typeof node === "string") return node.startsWith("?") ? 1 : 0;
  if (typeof node !== "object") return 0;

  if (Array.isArray(node)) {
    return node.reduce((acc, item) => acc + countPatternVars(item), 0);
  }

  const obj = node as Record<string, unknown>;
  if (typeof obj.name === "string" && obj.name.startsWith("?")) return 1;

  let count = 0;
  for (const k of Object.keys(obj)) {
    if (k !== "tag" && k !== "loc" && k !== "span") {
      count += countPatternVars(obj[k]);
    }
  }
  return count;
}

/**
 * Detect potential non-confluence in a rule set.
 * Returns conflict reports for pairs of rules that might produce different results.
 */
export function detectConflicts(rules: Rule[]): ConflictReport[] {
  const conflicts: ConflictReport[] = [];

  for (let i = 0; i < rules.length; i++) {
    for (let j = i + 1; j < rules.length; j++) {
      const r1 = rules[i];
      const r2 = rules[j];
      const overlap = patternsOverlap(r1.pattern, r2.pattern);

      if (overlap) {
        conflicts.push({
          rule1: r1.name,
          rule2: r2.name,
          overlap,
          description: `Rules '${r1.name}' and '${r2.name}' have overlapping patterns (${overlap}). ` +
            `This may cause non-confluent rewriting depending on rule order.`
        });
      }
    }
  }

  return conflicts;
}

// -----------------------------------------------------------------------------
// Rule construction helpers
// -----------------------------------------------------------------------------

/**
 * Create a rewrite rule.
 */
export function rule(name: string, pattern: Val, template: Val, where?: (bindings: Bindings) => boolean): Rule {
  return { name, pattern, template, where };
}

/**
 * Create a rule from pattern/template Val objects.
 */
export function makeRule(
  name: string,
  pattern: Val,
  template: Val,
  wherePred?: Val
): Rule {
  // wherePred would need to be evaluated in context; for now, leave it as optional JS function
  return { name, pattern, template };
}
