// Oracle backend — inference, fire/await, and session effects
// JOB-07: Added infer-fire/await/status + session-create/list/delete/reset
import type { EffectBackend } from "../../../../src/core/effects/backend";
import type { State } from "../../../../src/core/eval/machine";
import type { OpCall } from "../../../../src/core/effects/opcall";
import type { Val } from "../../../../src/core/eval/values";
import type { OracleAdapter } from "./adapter";
import type { SnapshotRepo } from "./snapshots";
import type { ReceiptStore } from "./receipts";
import { PortalImpl } from "./portalImpl";
import { runOracleSession } from "./driver";
import { type MeaningVal } from "./meaning";
import { distFrom, type DistVal } from "../../../../src/core/eval/dist";
import { computeSourceHash, type OracleEvidence } from "../../../../src/core/provenance/evidence";
import type { StoredReceipt } from "../../../../src/core/provenance/store/interface";
import { sha256JSON } from "../../../../src/core/artifacts/hash";
import type { Profile, BudgetTracker } from "../../../../src/package-bridge/governance";
import { capRequire } from "../../../../src/package-bridge/governance";
import { compileTextToExpr } from "../../../../src/core/pipeline/compileText";
import type { Runtime } from "../../../../src/core/eval/runtime";

// Driver system imports (JOB-04)
import type { DriverRegistry, InferenceDriver, InferRequest, InferResponse } from "@open-matrix/inference";
import { createBridgeSession } from "./protocolBridge";

type InferKind = "int" | "search" | "rewrite";

// ─── Pending Inference Table (fire/await state) ─────────────────────────────
// Stores unawaited Promises keyed by handle string.
// Lives in OracleBackend, NOT in drivers — per VISION Section 6.4.

type PendingEntry = {
  promise: Promise<InferResponse>;
  status: "pending" | "complete" | "failed";
  result?: InferResponse;
  error?: Error;
};

class PendingInferences {
  private counter = 0;
  private entries = new Map<string, PendingEntry>();

  store(promise: Promise<InferResponse>): string {
    const handle = `inf-${String(++this.counter).padStart(3, "0")}`;
    const entry: PendingEntry = { promise, status: "pending" };

    // Attach settlement tracking (non-blocking)
    promise.then(
      (resp) => { entry.status = "complete"; entry.result = resp; },
      (err) => { entry.status = "failed"; entry.error = err instanceof Error ? err : new Error(String(err)); },
    );

    this.entries.set(handle, entry);
    return handle;
  }

  async get(handle: string): Promise<InferResponse> {
    const entry = this.entries.get(handle);
    if (!entry) throw new Error(`No pending inference with handle "${handle}"`);
    if (entry.status === "complete") return entry.result!;
    if (entry.status === "failed") throw entry.error!;
    // Still pending — await the original promise
    return entry.promise;
  }

  status(handle: string): "pending" | "complete" | "failed" {
    const entry = this.entries.get(handle);
    if (!entry) throw new Error(`No pending inference with handle "${handle}"`);
    return entry.status;
  }
}

/**
 * Extract driver name from a Map-typed payload.
 * Returns undefined if payload is not a Map or has no driver entry.
 *
 * Checks (in order):
 *  1. `:driver` key — Sym(":driver"), Keyword("driver"), or Str(":driver")
 *  2. `"plugin"` key — Str("plugin"), as produced by `(with-plugin ...)` primitive
 */
function extractDriverName(payload: Val): string | undefined {
  if (payload.tag === "Map") {
    for (const [k, v] of payload.entries) {
      // Runtime keyword: Sym with ":driver"
      if (k.tag === "Sym" && k.name === ":driver" && v.tag === "Str") {
        return v.s;
      }
      // Pre-lowered keyword tag (used in some plugin code paths)
      if ((k as any).tag === "Keyword" && (k as any).name === "driver" && v.tag === "Str") {
        return v.s;
      }
      // String key ":driver" for programmatic construction
      if (k.tag === "Str" && k.s === ":driver" && v.tag === "Str") {
        return v.s;
      }
      // with-plugin creates maps with "plugin" key
      if (k.tag === "Str" && k.s === "plugin" && v.tag === "Str") {
        return v.s;
      }
    }
  }
  return undefined;
}

function intSamplesFromPayload(payload: Val, fallback: number): number {
  try {
    if (payload.tag === "Map") {
      for (const [k, v] of payload.entries) {
        if (k.tag === "Str" && k.s === "n" && v.tag === "Num") return Math.max(1, Math.floor(v.n));
      }
    }
  } catch {}
  return fallback;
}

function needsProvenance(state: State): boolean {
  return !!state.provenanceGraph || !!state.provenanceStore;
}

async function attachOracleEvidence(meaning: MeaningVal, payload: Val, state: State): Promise<MeaningVal> {
  if (!needsProvenance(state)) return meaning;

  const timestamp = Date.now();
  const receiptId = sha256JSON({ payload, timestamp });
  const evidence: OracleEvidence = {
    tag: "OracleEvidence",
    receiptId,
    sourceHash: computeSourceHash(payload),
    timestamp,
  };

  state.provenanceGraph?.addNode(evidence);

  if (state.provenanceStore) {
    const receipt: StoredReceipt = { id: receiptId, timestamp, request: payload, response: meaning };
    await state.provenanceStore.storeReceipt(receipt);
  }

  const evidenceList = meaning.evidence ? meaning.evidence.concat([evidence]) : [evidence];
  return { ...meaning, evidence: evidenceList };
}

export class OracleBackend implements EffectBackend {
  readonly ops = [
    "infer.op", "int.op", "rewrite.op", "search.op", "oracle.apply.op",
    "infer-fire.op", "infer-await.op", "infer-status.op",
    "session-create.op", "session-list.op", "session-delete.op", "session-reset.op",
    "chatgpt.infer.op",
  ] as const;

  /** Optional DriverRegistry for named-driver routing (VISION 3.6) */
  private readonly registry: DriverRegistry | undefined;

  /** Default driver name when payload has no :driver (VISION 3.8) */
  private readonly defaultDriver: string | undefined;

  /** Pending inference table for fire/await concurrency (JOB-07) */
  private readonly pendingInferences = new PendingInferences();

  constructor(
    private readonly oracle: OracleAdapter | undefined,
    private readonly snapshots: SnapshotRepo,
    private readonly receipts: ReceiptStore,
    private readonly profile: Profile,
    private budget: BudgetTracker | undefined,
    private readonly runtimeRef: Runtime,
    registry?: DriverRegistry,
    defaultDriver?: string,
  ) {
    this.registry = registry;
    this.defaultDriver = defaultDriver;
  }

  setBudget(budget: BudgetTracker): void {
    this.budget = budget;
  }

  async handle(state: State, opcall: OpCall): Promise<State> {
    const opName = opcall.op;

    // Core inference effects
    if (opName === "int.op" || opName === "infer.op" || opName === "rewrite.op" || opName === "search.op") {
      return this.handleInference(state, opcall);
    }

    // ChatGPT browser automation effect — routes to "browser-agent-fast" driver
    // Usage from Lisp: (effect chatgpt.infer.op prompt model-name)
    if (opName === "chatgpt.infer.op") {
      return this.handleChatGPTInfer(state, opcall);
    }
    if (opName === "oracle.apply.op") {
      return this.handleOracleApply(state, opcall);
    }

    // Fire/await effects (JOB-07)
    if (opName === "infer-fire.op") return this.handleInferFire(state, opcall);
    if (opName === "infer-await.op") return this.handleInferAwait(state, opcall);
    if (opName === "infer-status.op") return this.handleInferStatus(state, opcall);

    // Session lifecycle effects (JOB-07)
    if (opName === "session-create.op") return this.handleSessionCreate(state, opcall);
    if (opName === "session-list.op") return this.handleSessionList(state, opcall);
    if (opName === "session-delete.op") return this.handleSessionDelete(state, opcall);
    if (opName === "session-reset.op") return this.handleSessionReset(state, opcall);

    throw new Error(`OracleBackend: unhandled op ${opName}`);
  }

  private async handleInference(state: State, opcall: OpCall): Promise<State> {
    const opName = opcall.op;
    const kind: InferKind = opName === "search.op" ? "search" : opName === "rewrite.op" ? "rewrite" : "int";
    const needProv = needsProvenance(state);

    this.budget?.consumeOracleTurn();

    capRequire(this.profile.caps, "eval", `start ${opName}`);
    capRequire(this.profile.caps, "apply", `start ${opName}`);
    capRequire(this.profile.caps, "observe", `start ${opName}`);

    const payload: Val = opcall.args[0] ?? ({ tag: "Unit" } as Val);

    // ─── VISION 3.6: Named-driver routing ───
    // If payload contains :driver "name", resolve from registry and use Protocol Bridge.
    // Falls back to defaultDriver if registry exists, else legacy OracleAdapter path.
    const explicitDriverName = extractDriverName(payload);
    const driverName = explicitDriverName ?? (this.oracle ? undefined : this.defaultDriver);

    if (driverName && this.registry) {
      // Governance: check allowedDrivers (VISION 6.5)
      const allowed = this.profile.allowedDrivers;
      if (allowed && !allowed.has(driverName)) {
        throw new Error(
          `Driver "${driverName}" not allowed by profile "${this.profile.name}". ` +
          `Allowed: ${[...allowed].join(", ")}`
        );
      }
      return this.handleDriverInference(state, opcall, driverName, payload, kind, needProv);
    }

    // ─── Legacy path: OracleAdapter (backward compatible) ───
    return this.handleLegacyInference(state, opcall, payload, kind, needProv);
  }

  /**
   * New path: route inference through a named InferenceDriver via the Protocol Bridge.
   * VISION Section 3.6 — the driver is resolved from the registry, translated
   * through the bridge into an OracleSession, and driven via the portal.
   */
  private async handleDriverInference(
    state: State,
    opcall: OpCall,
    driverName: string,
    payload: Val,
    kind: InferKind,
    needProv: boolean,
  ): Promise<State> {
    const opName = opcall.op;
    const driver: InferenceDriver = this.registry!.get(driverName);

    const requiresProtocol = kind === "int" || kind === "rewrite" || kind === "search";
    const driverSupportsProtocol = driver.capabilities.oracleProtocol.reentrant;

    // If the op requires the protocol but the driver can't do it, error clearly.
    if (requiresProtocol && !driverSupportsProtocol && kind !== "int") {
      throw new Error(
        `Effect "${opName}" requires a protocol-capable driver, ` +
        `but "${driverName}" doesn't support the oracle protocol. ` +
        `Use a reentrant driver like "inference-reentrant".`
      );
    }

    const envRef = this.snapshots.putEnv({ env: state.env, store: state.store });
    const stateRef = this.snapshots.putState({ state });

    const portal = new PortalImpl(this.runtimeRef as any, this.snapshots, this.receipts, {
      maxEvalSteps: 100_000,
      parseText: (src: string) => compileTextToExpr(src),
    });

    const init = { tag: "Infer" as const, payload, envRef, stateRef };

    // SEARCH: multi-shot sampling through the driver
    if (kind === "search") {
      const n = intSamplesFromPayload(payload, 8);
      const items: { v: Val; w: number }[] = [];

      for (let i = 0; i < n; i++) {
        const session = createBridgeSession(driver, init);
        const meaning = await runOracleSession(session, portal);
        const enriched = needProv ? await attachOracleEvidence(meaning as MeaningVal, payload, state) : meaning;
        items.push({ v: enriched as Val, w: 1 });
      }

      const d: DistVal = distFrom(items, { kind: "search", note: `n=${n}` });
      return opcall.resumption.invoke(d as Val) as State;
    }

    // Protocol path — driver supports the oracle protocol
    if (driverSupportsProtocol) {
      const session = createBridgeSession(driver, init);
      const meaning = await runOracleSession(session, portal);
      let enrichedMeaning = meaning as MeaningVal;
      if (needProv) {
        enrichedMeaning = await attachOracleEvidence(enrichedMeaning, payload, state);
      }

      const adoptEnv = portal.consumeAdoptEnvRef() ?? meaning.adoptEnvRef;
      let st2 = state;
      if (adoptEnv) {
        const snap = this.snapshots.getEnv(adoptEnv);
        st2 = { ...st2, env: snap.env, store: snap.store };
      }

      if (opName === "int.op" || opName === "rewrite.op") {
        return opcall.resumption.invoke(enrichedMeaning as Val) as State;
      }
      const resultVal = enrichedMeaning.denotation ?? ({ tag: "Unit" } as Val);
      return opcall.resumption.invoke(resultVal) as State;
    }

    // Simple path — non-protocol driver for infer.op only
    // Returns plain text, no MeaningVal richness.
    const session = createBridgeSession(driver, init);
    const meaning = await runOracleSession(session, portal);
    let enrichedMeaning = meaning as MeaningVal;
    if (needProv) {
      enrichedMeaning = await attachOracleEvidence(enrichedMeaning, payload, state);
    }
    const resultVal = enrichedMeaning.denotation ?? ({ tag: "Unit" } as Val);
    return opcall.resumption.invoke(resultVal) as State;
  }

  /**
   * Legacy path: route through OracleAdapter (existing behavior, backward compatible).
   * This is the original handleInference logic, preserved exactly.
   */
  private async handleLegacyInference(
    state: State,
    opcall: OpCall,
    payload: Val,
    kind: InferKind,
    needProv: boolean,
  ): Promise<State> {
    if (!this.oracle) {
      throw new Error(
        `No OracleAdapter configured. Provide an adapter via OmegaConfig, ` +
        `or use (with-plugin "driver-name" ...) with a DriverRegistry.`
      );
    }
    const opName = opcall.op;

    const envRef = this.snapshots.putEnv({ env: state.env, store: state.store });
    const stateRef = this.snapshots.putState({ state });

    const portal = new PortalImpl(this.runtimeRef as any, this.snapshots, this.receipts, {
      maxEvalSteps: 100_000,
      parseText: (src: string) => compileTextToExpr(src),
    });

    // SEARCH: multi-shot oracle sampling
    if (kind === "search") {
      const n = intSamplesFromPayload(payload, 8);
      const items: { v: Val; w: number }[] = [];

      for (let i = 0; i < n; i++) {
        const session = this.oracle.startSession({
          tag: "Infer",
          payload,
          envRef,
          stateRef,
        });

        const meaning = await runOracleSession(session, portal);
        const enriched = needProv ? await attachOracleEvidence(meaning as MeaningVal, payload, state) : meaning;
        items.push({ v: enriched as Val, w: 1 });
      }

      const d: DistVal = distFrom(items, { kind: "search", note: `n=${n}` });
      return opcall.resumption.invoke(d as Val) as State;
    }

    // int.op, infer.op, rewrite.op: single oracle session
    const session = this.oracle.startSession({
      tag: "Infer",
      payload,
      envRef,
      stateRef,
    });

    const meaning = await runOracleSession(session, portal);
    let enrichedMeaning = meaning as MeaningVal;
    if (needProv) {
      enrichedMeaning = await attachOracleEvidence(enrichedMeaning, payload, state);
    }

    const adoptEnv = portal.consumeAdoptEnvRef() ?? meaning.adoptEnvRef;
    let st2 = state;
    if (adoptEnv) {
      const snap = this.snapshots.getEnv(adoptEnv);
      st2 = { ...st2, env: snap.env, store: snap.store };
    }

    if (opName === "int.op" || opName === "rewrite.op") {
      return opcall.resumption.invoke(enrichedMeaning as Val) as State;
    }

    const resultVal = enrichedMeaning.denotation ?? ({ tag: "Unit" } as Val);
    return opcall.resumption.invoke(resultVal) as State;
  }

  private async handleOracleApply(state: State, opcall: OpCall): Promise<State> {
    if (!this.oracle) {
      throw new Error(
        `No OracleAdapter configured for oracle.apply.op. Provide an adapter via OmegaConfig.`
      );
    }
    this.budget?.consumeOracleTurn();
    const needProv = needsProvenance(state);

    capRequire(this.profile.caps, "eval", "oracle.apply.op");
    capRequire(this.profile.caps, "apply", "oracle.apply.op");
    capRequire(this.profile.caps, "observe", "oracle.apply.op");

    const proc = opcall.args[0];
    const argVec = opcall.args[1];
    if (argVec.tag !== "Vector") throw new Error("oracle.apply.op expects (OracleProc, Vector)");

    const envRef = this.snapshots.putEnv({ env: state.env, store: state.store });
    const stateRef = this.snapshots.putState({ state });

    const portal = new PortalImpl(this.runtimeRef as any, this.snapshots, this.receipts, {
      maxEvalSteps: 100_000,
      parseText: (src: string) => compileTextToExpr(src),
    });

    const session = this.oracle.startSession({
      tag: "Apply",
      proc,
      args: argVec.items,
      envRef,
      stateRef,
    });

    const meaning = await runOracleSession(session, portal);

    let enrichedMeaning = meaning as MeaningVal;
    if (needProv) {
      const payload: Val = {
        tag: "Vector",
        items: [((proc as any).spec ?? ({ tag: "Unit" } as Val)) as Val, argVec as Val],
      };
      enrichedMeaning = await attachOracleEvidence(enrichedMeaning, payload, state);
    }

    const v = enrichedMeaning.denotation ?? ({ tag: "Unit" } as Val);

    const adoptEnv = portal.consumeAdoptEnvRef() ?? meaning.adoptEnvRef;
    let st2 = state;
    if (adoptEnv) {
      const snap = this.snapshots.getEnv(adoptEnv);
      st2 = { ...st2, env: snap.env, store: snap.store };
    }

    return opcall.resumption.invoke(v) as State;
  }

  // ─── Fire/Await Effects (JOB-07, VISION 6.4) ──────────────────────────────

  /**
   * infer-fire.op: Non-blocking inference — starts driver.infer() without awaiting,
   * stores the Promise, returns a handle string to the CESK machine immediately.
   *
   * Usage: (define h (infer-fire "prompt" :driver "claude-code-cli"))
   */
  private handleInferFire(state: State, opcall: OpCall): State {
    if (!this.registry) {
      throw new Error("infer-fire.op requires a DriverRegistry (no registry configured)");
    }

    const payload: Val = opcall.args[0] ?? ({ tag: "Unit" } as Val);
    const driverName = extractDriverName(payload);
    if (!driverName) {
      throw new Error("infer-fire.op requires :driver in payload");
    }

    const driver = this.registry.get(driverName);

    // Build InferRequest from payload
    // with-plugin stores the prompt under "payload" key; direct maps use "prompt" key
    const prompt = payload.tag === "Str" ? payload.s
      : payload.tag === "Map" ? (extractStringField(payload, "prompt") ?? extractStringField(payload, "payload") ?? "")
      : "";
    const model = payload.tag === "Map" ? (extractStringField(payload, ":model") ?? extractStringField(payload, "model")) : undefined;
    const conversationRef = payload.tag === "Map" ? extractStringField(payload, ":conversation") : undefined;

    const request: InferRequest = { prompt, model: model ?? undefined, conversationRef: conversationRef ?? undefined };

    // Fire — NOT awaited
    this.budget?.consumeOracleTurn();
    const promise = driver.infer(request);
    const handle = this.pendingInferences.store(promise);

    return opcall.resumption.invoke({ tag: "Str", s: handle } as Val) as State;
  }

  /**
   * infer-await.op: Block on an inference handle until it resolves.
   *
   * Usage: (infer-await h) → returns the response text
   */
  private async handleInferAwait(state: State, opcall: OpCall): Promise<State> {
    const handleVal = opcall.args[0];
    if (!handleVal || handleVal.tag !== "Str") {
      throw new Error("infer-await.op expects a string handle argument");
    }

    const response = await this.pendingInferences.get(handleVal.s);
    return opcall.resumption.invoke({ tag: "Str", s: response.content } as Val) as State;
  }

  /**
   * infer-status.op: Non-blocking status check on an inference handle.
   * Returns a symbol: 'pending, 'complete, or 'failed.
   *
   * Usage: (infer-status h) → 'pending | 'complete | 'failed
   */
  private handleInferStatus(state: State, opcall: OpCall): State {
    const handleVal = opcall.args[0];
    if (!handleVal || handleVal.tag !== "Str") {
      throw new Error("infer-status.op expects a string handle argument");
    }

    const status = this.pendingInferences.status(handleVal.s);
    return opcall.resumption.invoke({ tag: "Sym", name: status } as Val) as State;
  }

  // ─── Session Lifecycle Effects (JOB-07, VISION 3.11) ──────────────────────

  /** Resolve driver and check it supports managed sessions */
  private getSessionDriver(driverName: string): InferenceDriver {
    if (!this.registry) {
      throw new Error("session-*.op requires a DriverRegistry (no registry configured)");
    }
    const driver = this.registry.get(driverName);
    if (!driver.sessions) {
      throw new Error(
        `Driver "${driverName}" does not support managed sessions. ` +
        `Check driver.capabilities.session.managedSessions.`
      );
    }
    return driver;
  }

  /**
   * session-create.op: Create a managed session for a driver.
   * Args: [driverName: Str, options?: Map with :name, :model]
   * Returns: session ref string
   */
  private async handleSessionCreate(state: State, opcall: OpCall): Promise<State> {
    const driverNameVal = opcall.args[0];
    if (!driverNameVal || driverNameVal.tag !== "Str") {
      throw new Error("session-create.op expects (driverName: string, opts?: map)");
    }
    const driver = this.getSessionDriver(driverNameVal.s);

    const optsVal = opcall.args[1];
    const name = optsVal?.tag === "Map" ? extractStringField(optsVal, ":name") : undefined;
    const model = optsVal?.tag === "Map" ? extractStringField(optsVal, ":model") : undefined;

    const ref = await driver.sessions!.create({ name: name ?? undefined, model: model ?? undefined });
    return opcall.resumption.invoke({ tag: "Str", s: ref } as Val) as State;
  }

  /**
   * session-list.op: List all managed sessions for a driver.
   * Args: [driverName: Str]
   * Returns: Vector of Maps with ref, name, model, lastUsed, messageCount
   */
  private async handleSessionList(state: State, opcall: OpCall): Promise<State> {
    const driverNameVal = opcall.args[0];
    if (!driverNameVal || driverNameVal.tag !== "Str") {
      throw new Error("session-list.op expects (driverName: string)");
    }
    const driver = this.getSessionDriver(driverNameVal.s);

    const sessions = await driver.sessions!.list();
    const items: Val[] = sessions.map(s => ({
      tag: "Map",
      entries: [
        [{ tag: "Str", s: "ref" } as Val, { tag: "Str", s: s.ref } as Val] as [Val, Val],
        [{ tag: "Str", s: "name" } as Val, s.name ? { tag: "Str", s: s.name } as Val : { tag: "Unit" } as Val] as [Val, Val],
        [{ tag: "Str", s: "model" } as Val, { tag: "Str", s: s.model } as Val] as [Val, Val],
        [{ tag: "Str", s: "lastUsed" } as Val, { tag: "Str", s: s.lastUsed } as Val] as [Val, Val],
        [{ tag: "Str", s: "messageCount" } as Val, { tag: "Num", n: s.messageCount } as Val] as [Val, Val],
      ],
    } as Val));

    return opcall.resumption.invoke({ tag: "Vector", items } as Val) as State;
  }

  /**
   * session-delete.op: Delete a managed session.
   * Args: [driverName: Str, sessionRef: Str]
   */
  private async handleSessionDelete(state: State, opcall: OpCall): Promise<State> {
    const driverNameVal = opcall.args[0];
    const refVal = opcall.args[1];
    if (!driverNameVal || driverNameVal.tag !== "Str" || !refVal || refVal.tag !== "Str") {
      throw new Error("session-delete.op expects (driverName: string, sessionRef: string)");
    }
    const driver = this.getSessionDriver(driverNameVal.s);

    await driver.sessions!.delete(refVal.s);
    return opcall.resumption.invoke({ tag: "Unit" } as Val) as State;
  }

  /**
   * session-reset.op: Reset a session (edit first message).
   * Args: [driverName: Str, sessionRef: Str, newPrompt: Str]
   * Returns: response text from the reset
   */
  private async handleSessionReset(state: State, opcall: OpCall): Promise<State> {
    const driverNameVal = opcall.args[0];
    const refVal = opcall.args[1];
    const promptVal = opcall.args[2];
    if (!driverNameVal || driverNameVal.tag !== "Str" || !refVal || refVal.tag !== "Str") {
      throw new Error("session-reset.op expects (driverName: string, sessionRef: string, newPrompt: string)");
    }
    const driver = this.getSessionDriver(driverNameVal.s);

    if (!driver.sessions!.reset) {
      throw new Error(`Driver "${driverNameVal.s}" does not support session reset`);
    }

    const newPrompt = promptVal?.tag === "Str" ? promptVal.s : "";
    const response = await driver.sessions!.reset(refVal.s, newPrompt);
    return opcall.resumption.invoke({ tag: "Str", s: response.content } as Val) as State;
  }

  /**
   * chatgpt.infer.op: Route inference to the ChatGPT browser automation driver.
   * Lisp usage: (effect chatgpt.infer.op prompt-string model-string)
   * Routes through "browser-agent-fast" driver in the registry.
   */
  private async handleChatGPTInfer(state: State, opcall: OpCall): Promise<State> {
    if (!this.registry || !this.registry.has("browser-agent-fast")) {
      throw new Error(
        "chatgpt.infer.op: No ChatGPT driver registered. " +
        "Set CHATGPT_CHROME_PORT=9222 in .env and ensure Chrome is running."
      );
    }

    this.budget?.consumeOracleTurn();

    const promptVal = opcall.args[0];
    const modelVal = opcall.args[1];
    const prompt = promptVal?.tag === "Str" ? promptVal.s : "";
    const model = modelVal?.tag === "Str" ? modelVal.s : "instant";

    const driver = this.registry.get("browser-agent-fast");
    const response = await driver.infer({ prompt, model });

    return opcall.resumption.invoke({ tag: "Str", s: response.content } as Val) as State;
  }
}

// ─── Helpers ────────────────────────────────────────────────────────────────

/** Extract a string field from a Map Val by key (supports :key and key forms) */
function extractStringField(map: Val, key: string): string | null {
  if (map.tag !== "Map") return null;
  for (const [k, v] of map.entries) {
    if (k.tag === "Str" && k.s === key && v.tag === "Str") return v.s;
    if (k.tag === "Sym" && k.name === key && v.tag === "Str") return v.s;
    // Handle keyword form: :name → Keyword with name stripped of colon
    if ((k as any).tag === "Keyword" && (k as any).name === key.replace(/^:/, "") && v.tag === "Str") return v.s;
  }
  return null;
}
