/**
 * MembraneStepper — lightweight CESK evaluator for per-actor membrane programs.
 *
 * This is NOT OmegaSandbox (too heavy — includes LLM oracle, full REPL state).
 * MembraneStepper is minimal: RuntimeImpl + MembraneEffectBackend + compile + run.
 * Target: <100μs per message for the default pass-through membrane.
 *
 * Usage:
 *   const stepper = MembraneStepper.create();
 *   const program = stepper.compile('(effect membrane.allow op payload)');
 *   const result = await stepper.run(program, { op: '$ping', payload: {}, sender: 'a', meta: {} });
 *   // result.action === 'allow'
 *
 * Programs are body expressions with free variables: op, payload, sender, meta.
 * compile() wraps the body in a lambda; run() applies it to Lit args.
 * This ensures the compiler handles hygiene and name binding correctly.
 */
import {
  compileTextToExpr,
  runToCompletionWithState,
  createKernelRuntime,
  createKernelPrimitives,
  type KernelRuntime,
} from '@open-matrix/omega-core';
import type { EffectBackend, Expr, Val, State, Env, Store } from '@open-matrix/omega-core';
import {
  MembraneEffectBackend,
  MembraneResultCollector,
  type MembraneResult,
  type CapabilityChecker,
} from './MembraneEffectBackend.js';

// ─── Default membrane source ────────────────────────────────────────────────

export const DEFAULT_MEMBRANE_SOURCE = '(effect membrane.allow op payload)';

// ─── MembraneStepper ─────────────────────────────────────────────────────────

export class MembraneStepper {
  private runtime: KernelRuntime;
  private baseEnv: Env;
  private baseStore: Store;
  private membraneBackend: MembraneEffectBackend;
  private maxSteps: number;
  /** Compiled default program — used for fast-path short-circuit */
  private _defaultProgram: Expr | null = null;

  private constructor(
    runtime: KernelRuntime,
    env: Env,
    store: Store,
    membraneBackend: MembraneEffectBackend,
    maxSteps: number,
  ) {
    this.runtime = runtime;
    this.baseEnv = env;
    this.baseStore = store;
    this.membraneBackend = membraneBackend;
    this.maxSteps = maxSteps;
  }

  /**
   * Create a new MembraneStepper with optional additional backends.
   * The MembraneEffectBackend is always registered.
   */
  static create(
    additionalBackends: EffectBackend[] = [],
    options?: { maxSteps?: number; capabilityChecker?: CapabilityChecker },
  ): MembraneStepper {
    // Initial collector — will be replaced per-run via setSink()
    const collector = new MembraneResultCollector();
    const membraneBackend = new MembraneEffectBackend(collector, options?.capabilityChecker);

    const runtime = createKernelRuntime({
      commit: { async commit() { return { tag: 'Unit' } as Val; } },
    });

    // Register membrane backend first, then additional
    runtime.registerBackend(membraneBackend);
    for (const b of additionalBackends) {
      runtime.registerBackend(b);
    }

    // Install primitive environment (needed for basic Lisp operations)
    const { env, store } = createKernelPrimitives();

    return new MembraneStepper(
      runtime,
      env,
      store,
      membraneBackend,
      options?.maxSteps ?? 10_000,
    );
  }

  /**
   * Pre-compile a Lisp program string to an Expr AST (a lambda).
   * Call once at actor init time — reuse the Expr for every message.
   *
   * The source is a BODY expression referencing free variables:
   *   op (string), payload (JSON string), sender (string), meta (JSON string).
   *
   * compile() wraps it in (lambda (op payload sender meta) <body>) and
   * runs it through the compiler for correct hygiene + lowering.
   */
  compile(source: string): Expr {
    // Wrap body in a lambda so the compiler handles name binding
    const lambdaSource = `(lambda (op payload sender meta) ${source})`;
    return compileTextToExpr(lambdaSource);
  }

  /**
   * Run the membrane program with message bindings.
   * Returns the membrane decision (allow/deny/mutate).
   *
   * CONCURRENCY FIX: Creates a fresh MembraneResultCollector per run and swaps
   * it onto the backend via setSink(). This prevents concurrent runs from
   * clobbering each other's results when async effects yield to the event loop.
   */
  async run(
    program: Expr,
    bindings: { op: string; payload: unknown; sender: string; meta: unknown },
  ): Promise<MembraneResult> {
    // Fast path: default pass-through membrane — skip CESK entirely
    if (this._defaultProgram && program === this._defaultProgram) {
      return { action: 'allow' };
    }

    // Per-run collector — prevents concurrent run() calls from clobbering results
    const collector = new MembraneResultCollector();
    this.membraneBackend.setSink(collector);

    // Build an App expression: (compiled-lambda op-lit payload-lit sender-lit meta-lit)
    // The lambda was compiled by compile(), so params are already hygienic.
    const appExpr: Expr = {
      tag: 'App',
      fn: program,
      args: [
        { tag: 'Lit', value: bindings.op },
        { tag: 'Lit', value: JSON.stringify(bindings.payload ?? null) },
        { tag: 'Lit', value: bindings.sender },
        { tag: 'Lit', value: JSON.stringify(bindings.meta ?? null) },
      ],
    };

    // Build initial CESK state
    const state0: State = {
      control: { tag: 'Expr', e: appExpr },
      env: this.baseEnv,
      store: this.baseStore,  // COWStore is immutable — writes create new instances
      kont: [],
      handlers: [],
    };

    try {
      await runToCompletionWithState(this.runtime, state0, { maxSteps: this.maxSteps });
      const result = collector.result;
      if (collector.logs.length > 0) {
        result.logs = collector.logs;
      }
      return result;
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : String(err);
      // Membrane errors never propagate — always converted to deny
      return {
        action: 'deny',
        error: `Membrane error: ${message}`,
      };
    }
  }

  /** Mark a compiled program as the default (enables fast-path short-circuit). */
  setDefaultProgram(program: Expr): void {
    this._defaultProgram = program;
  }

  /** Access the membrane backend (for testing/introspection). */
  getBackend(): MembraneEffectBackend {
    return this.membraneBackend;
  }

  /** Access registered backend ops (for testing). */
  getRuntime(): KernelRuntime {
    return this.runtime;
  }
}
