// src/core/repl/OmegaRepl.ts
//
// Shared REPL engine extracted from the CLI monolith (bin/omega-repl.ts).
// Owns the define-accumulation / prefix-compilation / normalizeCtxBindings
// bookkeeping that makes definitions persist across eval() calls.
//
// Execution is pluggable via IReplExecutor — the CLI injects its strategy
// + middleware chain, the daemon injects runToCompletionWithState + MatrixEffectBackend.
//
// Lifecycle:
//   const repl = await OmegaRepl.create(executor);
//   const r1 = await repl.eval("(define x 42)");  // x persists
//   const r2 = await repl.eval("x");               // → 42
//   const saved = repl.serialize();                 // → JSON (disk/elastic)
//   const repl2 = OmegaRepl.deserialize(saved, executor);
//   const r3 = await repl2.eval("x");              // → 42 (restored)

import type { Expr } from "../ast";
import type { State } from "../eval/machine";
import type { Store } from "../eval/store";
import type { Env } from "../eval/env";
import type { Val } from "../eval/values";
import { COWStore } from "../eval/store";
import { installPrims } from "../prims";
import { compileTextToExpr } from "../pipeline/compileText";
import { buildNativeRegistry } from "../session/nativeRegistry";
import { buildSolverRegistry } from "../session/solverRegistry";
import {
  serializeState,
  deserializeState,
  type SerializedState,
} from "../session/serializer";

// ── IReplExecutor ────────────────────────────────────────────────────────────

/**
 * The one pluggable seam — how compiled AST gets executed.
 *
 * CLI provides a StrategyExecutor (strategy + middleware + driver registry).
 * Daemon provides a DirectExecutor (runToCompletionWithState + MatrixEffectBackend).
 */
export interface IReplExecutor {
  run(
    expr: Expr,
    state: State,
    maxSteps: number,
  ): Promise<{ value: Val; state: State }>;
}

// ── ReplCoreState ────────────────────────────────────────────────────────────

/** Core REPL state — no CLI concerns (no debug, breakpoints, readline, etc.) */
export interface ReplCoreState {
  env: Env;
  store: Store;
  baseState: State;
  defs: string[];                       // accumulated (define ...) source text
  syntaxDefs: string[];                 // accumulated (define-syntax ...) source text
  nativeRegistry: Map<string, Val>;     // for serialization (native fn name → val)
  solverRegistry: Map<string, Val>;     // for serialization (solver fn name → val)
  loadedPackages: string[];
  evalCount: number;
}

// ── SerializedReplState ──────────────────────────────────────────────────────

/** JSON-safe snapshot of the full REPL state (for disk or elastic). */
export interface SerializedReplState {
  ceskState: SerializedState;
  defs: string[];
  syntaxDefs: string[];
  evalCount: number;
  loadedPackages: string[];
}

// ── ReplEvalOpts / ReplEvalResult ────────────────────────────────────────────

export interface ReplEvalOpts {
  /** Override base state for this eval (e.g. from a checkpoint). */
  baseState?: State;
  /** Include prior defines in compile context (default: true). */
  includeDefineContext?: boolean;
  /** Persist new defines to defs[] (default: true). */
  persistDefinitions?: boolean;
  /** Max CESK steps (default: 50_000). */
  maxSteps?: number;
}

export interface ReplEvalResult {
  ok: boolean;
  value?: Val;
  display?: string;
  error?: string;
}

// ── OmegaRepl ────────────────────────────────────────────────────────────────

const DEFAULT_MAX_STEPS = 50_000;

export class OmegaRepl {
  private _state: ReplCoreState;
  private _executor: IReplExecutor;

  private constructor(state: ReplCoreState, executor: IReplExecutor) {
    this._state = state;
    this._executor = executor;
  }

  // ── Factory ──

  /** Create a fresh REPL with all 214+ primitives installed. */
  static async create(executor: IReplExecutor): Promise<OmegaRepl> {
    const store0 = new COWStore();
    const prim = installPrims(store0);
    const baseState: State = {
      control: { tag: "Val", v: { tag: "Unit" } as Val },
      env: prim.env,
      store: prim.store,
      kont: [],
      handlers: [],
    };
    const nativeRegistry = buildNativeRegistry(prim.store);
    const solverRegistry = buildSolverRegistry(prim.store);
    const state: ReplCoreState = {
      env: prim.env,
      store: prim.store,
      baseState,
      defs: [],
      syntaxDefs: [],
      nativeRegistry,
      solverRegistry,
      loadedPackages: [],
      evalCount: 0,
    };
    return new OmegaRepl(state, executor);
  }

  // ── Core eval ──

  /**
   * Evaluate Omega Lisp source with persistent define accumulation.
   *
   * Phases:
   *   PREPARE   — extract define name, dedup defs, build prefix, compile, skip
   *   EXECUTE   — delegate to this._executor.run()
   *   POST-PROC — normalizeCtxBindings, persist defs, update state
   */
  async eval(src: string, opts?: ReplEvalOpts): Promise<ReplEvalResult> {
    try {
      // ── Strip (require 'package) forms ──
      // In the Omega compiler, `require` is an amb guard (if pred unit (effect amb.fail ...)),
      // NOT a package import.  The CLI REPL intercepts require expressions before compilation
      // and loads package .lisp files from disk.  In the daemon, all 214+ primitives are
      // already installed via installPrims() — no file-based package loading is needed.
      // We strip require forms from ANYWHERE in the source (standalone or inside begin blocks)
      // so agents can freely write (require 'strings) without error.
      const { cleaned, packages } = stripRequireForms(src);
      for (const pkg of packages) {
        if (!this._state.loadedPackages.includes(pkg)) {
          this._state.loadedPackages.push(pkg);
        }
      }
      // If stripping requires left nothing meaningful, return early
      if (!cleaned.trim() || cleaned.trim() === '(begin)') {
        if (packages.length > 0) {
          this._state.evalCount++;
          return {
            ok: true,
            value: { tag: "Sym", name: packages[packages.length - 1] } as Val,
            display: packages.join(', '),
          };
        }
      }

      const includeDefineContext = opts?.includeDefineContext ?? true;
      const persistDefinitions = opts?.persistDefinitions ?? true;
      const maxSteps = opts?.maxSteps ?? DEFAULT_MAX_STEPS;

      // ── PREPARE ──
      // Use `cleaned` (require-stripped) source from here on
      const evalSrc = cleaned;

      // Check if this is a define — extract the name
      const defineName = extractDefName(evalSrc);
      const isDefine = !!defineName;

      // Check if this is a define-syntax
      const defineSyntaxMatch = evalSrc.trim().match(/^\(define-syntax\s+([^\s()]+)/);
      const isDefineSyntax = !!defineSyntaxMatch;
      const defineSyntaxName = defineSyntaxMatch?.[1];

      // Build the define prefix (all prior defines, deduped)
      let defsToUse = includeDefineContext ? this._state.defs : [];
      if (isDefine && defineName) {
        defsToUse = defsToUse.filter(d => extractDefName(d) !== defineName);
      }

      // Prepend syntax defs so macros are available at compile time.
      // These vanish during compilation (don't produce output exprs), so don't count in skipCount.
      const plainDefs = includeDefineContext
        ? defsToUse.filter(d => d.trim().startsWith("(define "))
        : [];
      const allForms = [...this._state.syntaxDefs, ...plainDefs, evalSrc];
      const baseState = opts?.baseState ?? this._state.baseState;
      const programSrc = `(begin\n${allForms.join("\n")}\n)`;
      const expr = compileTextToExpr(programSrc);

      // Skip only the plain-defs prefix (already in env/store).
      // define-syntax forms disappear at compile time, so only count plain define forms.
      const skipCount = plainDefs.length;
      let activeExpr: Expr;
      if (expr.tag === "Begin" && skipCount > 0 && expr.exprs.length > skipCount) {
        const remaining = expr.exprs.slice(skipCount);
        activeExpr = remaining.length === 1
          ? remaining[0]
          : { ...expr, exprs: remaining };
      } else {
        activeExpr = expr;
      }

      // Build initial CESK state
      const state0: State = {
        control: { tag: "Expr", e: activeExpr },
        env: baseState?.env ?? this._state.env,
        store: baseState?.store ?? this._state.store,
        kont: [],
        handlers: baseState?.handlers ?? [],
      };

      // ── EXECUTE ──

      const { value, state: finalState } = await this._executor.run(activeExpr, state0, maxSteps);

      // ── POST-PROCESS ──

      normalizeCtxBindings(finalState.env);

      // Detect macro-expanded defines: a custom macro that compiled to a Define node
      let macroDefName: string | undefined;
      if (!isDefine && !isDefineSyntax && (activeExpr as any).tag === "Define") {
        macroDefName = ((activeExpr as any).name as string).replace(/\$bid#\d+$/, "");
      }

      if (persistDefinitions) {
        if (isDefine && defineName) {
          this._state.defs = this._state.defs.filter(d => extractDefName(d) !== defineName);
          this._state.defs.push(evalSrc);
        } else if (macroDefName) {
          this._state.defs = this._state.defs.filter(d => extractDefName(d) !== macroDefName);
          this._state.defs.push(evalSrc);
        }
      }

      // Accumulate define-syntax forms for compile-time macros
      if (persistDefinitions && isDefineSyntax && defineSyntaxName) {
        this._state.syntaxDefs = this._state.syntaxDefs.filter(d => {
          const prev = d.trim().match(/^\(define-syntax\s+([^\s()]+)/);
          return prev?.[1] !== defineSyntaxName;
        });
        this._state.syntaxDefs.push(evalSrc);
      }

      // Update persistent state
      this._state.baseState = finalState;
      this._state.env = finalState.env;
      this._state.store = finalState.store;
      this._state.evalCount++;

      // For standalone defines, return the symbol name (Lisp convention)
      const hasMultipleExprs = extractSexpressions(evalSrc.trim()).length > 1;
      let displayValue = value;
      if (isDefine && defineName && !hasMultipleExprs) {
        displayValue = { tag: "Sym", name: defineName } as Val;
      } else if (isDefineSyntax && defineSyntaxName) {
        displayValue = { tag: "Sym", name: defineSyntaxName } as Val;
      } else if (macroDefName && !hasMultipleExprs) {
        displayValue = { tag: "Sym", name: macroDefName } as Val;
      }

      return {
        ok: true,
        value: displayValue,
        display: valToDisplay(displayValue),
      };
    } catch (e) {
      return { ok: false, error: String(e) };
    }
  }

  // ── Accessors ──

  getState(): Readonly<ReplCoreState> { return this._state; }
  getDefs(): readonly string[] { return this._state.defs; }
  getSyntaxDefs(): readonly string[] { return this._state.syntaxDefs; }
  get evalCount(): number { return this._state.evalCount; }

  // ── Persistence ──

  /** Serialize full REPL state to JSON (for disk / elastic). */
  serialize(): SerializedReplState {
    return {
      ceskState: serializeState(this._state.baseState),
      defs: [...this._state.defs],
      syntaxDefs: [...this._state.syntaxDefs],
      evalCount: this._state.evalCount,
      loadedPackages: [...this._state.loadedPackages],
    };
  }

  /** Restore a REPL from serialized state. */
  static deserialize(saved: SerializedReplState, executor: IReplExecutor): OmegaRepl {
    // Rebuild registries from a fresh primed store (native fns are singletons)
    const store0 = new COWStore();
    const prim = installPrims(store0);
    const nativeRegistry = buildNativeRegistry(prim.store);
    const solverRegistry = buildSolverRegistry(prim.store);

    // Restore CESK state (env + store + closures + native fn references)
    const restoredState = deserializeState(saved.ceskState, nativeRegistry, solverRegistry);

    const state: ReplCoreState = {
      env: restoredState.env,
      store: restoredState.store,
      baseState: restoredState,
      defs: [...saved.defs],
      syntaxDefs: [...saved.syntaxDefs],
      nativeRegistry,
      solverRegistry,
      loadedPackages: [...saved.loadedPackages],
      evalCount: saved.evalCount,
    };
    return new OmegaRepl(state, executor);
  }

  /** Reset to fresh state — all prior definitions are lost. */
  reset(): void {
    const store0 = new COWStore();
    const prim = installPrims(store0);
    const baseState: State = {
      control: { tag: "Val", v: { tag: "Unit" } as Val },
      env: prim.env,
      store: prim.store,
      kont: [],
      handlers: [],
    };
    this._state = {
      env: prim.env,
      store: prim.store,
      baseState,
      defs: [],
      syntaxDefs: [],
      nativeRegistry: buildNativeRegistry(prim.store),
      solverRegistry: buildSolverRegistry(prim.store),
      loadedPackages: [],
      evalCount: 0,
    };
  }
}

// ── Pure helper functions ────────────────────────────────────────────────────

/**
 * Extract the name being defined from a source form.
 * Handles standard (define ...) and common macros (defun, defn, defvar, defparameter).
 */
export function extractDefName(src: string): string | undefined {
  const s = src.trim();
  const dm = s.match(/^\(define\s+(?:\(([^\s()]+)|([^\s()]+))/);
  if (dm) return dm[1] || dm[2];
  const mm = s.match(/^\((?:defun|defn|defvar|defparameter)\s+([^\s()]+)/);
  if (mm) return mm[1];
  return undefined;
}

/**
 * Clean up compiler-generated binding name suffixes.
 * After compilation, names like `foo$bid#7` get an alias `foo` in the env
 * so REPL users see readable names and subsequent evals can resolve them.
 */
export function normalizeCtxBindings(env?: any): void {
  let ctx = env;
  while (ctx) {
    if (ctx.frame && ctx.frame instanceof Map) {
      for (const [name, addr] of Array.from((ctx.frame as Map<string, number>).entries())) {
        const plain = typeof name === "string" ? name.replace(/\$bid#\d+$/, "") : name;
        if (typeof plain === "string" && plain !== name && !ctx.frame.has(plain)) {
          ctx.frame.set(plain, addr);
        }
      }
    }
    ctx = ctx.parent;
  }
}

/**
 * Split a string into top-level s-expressions.
 * Used to detect whether input has multiple top-level forms.
 */
function extractSexpressions(text: string): string[] {
  const results: string[] = [];
  let current = "";
  let depth = 0;
  let inString = false;
  let escape = false;

  for (const char of text) {
    if (escape) { current += char; escape = false; continue; }
    if (char === "\\") { current += char; escape = true; continue; }
    if (char === '"') { inString = !inString; current += char; continue; }
    if (inString) { current += char; continue; }

    if (char === "(") {
      if (depth === 0 && current.trim()) {
        results.push(current.trim());
        current = "";
      }
      depth++;
      current += char;
    } else if (char === ")") {
      depth--;
      current += char;
      if (depth === 0) {
        results.push(current.trim());
        current = "";
      }
    } else {
      current += char;
    }
  }

  if (current.trim()) results.push(current.trim());
  return results;
}

/**
 * Convert a Val to a human-readable display string.
 * Used for LLM consumption and REPL output.
 */
function valToDisplay(v: Val): string {
  if (!v) return "()";
  switch (v.tag) {
    case "Num": return String((v as any).n);
    case "Str": return `"${(v as any).s}"`;
    case "Bool": return (v as any).b ? "#t" : "#f";
    case "Unit": return "()";
    case "Sym": return (v as any).name;
    case "Vector": {
      const items = (v as any).items as Val[];
      if (items.length === 2 && items[1]?.tag === "Unit") {
        return `(${valToDisplay(items[0])})`;
      }
      if (items.length === 2) {
        const elements: string[] = [];
        let cur: Val = v;
        while (cur?.tag === "Vector") {
          elements.push(valToDisplay((cur as any).items[0]));
          cur = (cur as any).items[1];
        }
        if (cur?.tag === "Unit") return `(${elements.join(" ")})`;
        return `(${elements.join(" ")} . ${valToDisplay(cur)})`;
      }
      return `#(${items.map(valToDisplay).join(" ")})`;
    }
    case "Map": {
      const entries = (v as any).entries as [Val, Val][];
      const parts = entries.map(
        ([k, val]: [Val, Val]) => `${valToDisplay(k)}: ${valToDisplay(val)}`,
      );
      return `{${parts.join(", ")}}`;
    }
    case "Closure": return "<closure>";
    case "Native": return "<native>";
    default: return `<${v.tag}>`;
  }
}

/**
 * Strip all (require 'package) forms from source code — handles both standalone
 * and embedded-in-begin cases.  Returns the cleaned source and a list of
 * package names that were stripped.
 *
 * Examples:
 *   "(require 'strings)"                          → { cleaned: "", packages: ["strings"] }
 *   "(begin (require 'strings) (define x 1))"     → { cleaned: "(begin  (define x 1))", packages: ["strings"] }
 *   "(define x 1)"                                → { cleaned: "(define x 1)", packages: [] }
 */
function stripRequireForms(src: string): { cleaned: string; packages: string[] } {
  const packages: string[] = [];
  const cleaned = src.replace(
    /\(\s*require\s+'([a-zA-Z0-9_\-\/]+)\s*\)/g,
    (_match, pkg) => {
      packages.push(pkg);
      return '';
    },
  );
  return { cleaned, packages };
}
