// src/core/oracle/protocolBridge.ts
// Protocol Bridge: wraps InferenceDriver → OracleSession
//
// The bridge sits between two independent type systems:
//   TRANSPORT LEVEL: InferenceDriver (prompt in, text out)
//   CESK LEVEL: OracleSession (coroutine yielding OracleReq/OracleResp)
//
// The driver NEVER sees CESK types (Val, State, EnvRef, etc.)
// The bridge is the thin translation layer that connects them.
//
// Source of truth: VISION-FIXED-POINT.md Section 3.5

import type { InferenceDriver, InferRequest, InferResponse } from "@open-matrix/inference";
import type { OracleSession, OracleReq, OracleResp, OracleReturn, Meaning } from "./protocol";
import type { OracleInit, InferInit, ApplyInit } from "./adapter";
import type { Val } from "../../../../src/core/eval/values";
import { meaning as mkMeaning } from "./meaning";

export interface ProtocolBridgeOptions {
  /** Max depth for reentrant protocol exchanges (default: 4) */
  maxDepth?: number;
}

/**
 * Create an OracleSession from an InferenceDriver.
 *
 * Two paths:
 * 1. Simple path: Driver is not reentrant → single infer() call, wrap result as Meaning
 * 2. Reentrant path: Driver claims reentrant capability → multi-turn protocol
 *    (Currently uses simple path even for reentrant drivers; full reentrant
 *     protocol bridging is a JOB-04 enhancement when we wire actual driver registry)
 */
export function createBridgeSession(
  driver: InferenceDriver,
  init: OracleInit,
  opts?: ProtocolBridgeOptions,
): OracleSession {
  const isReentrant = driver.capabilities.oracleProtocol.reentrant;

  if (isReentrant) {
    return createReentrantSession(driver, init, opts);
  } else {
    return createSimpleSession(driver, init);
  }
}

/**
 * Simple path: single infer() call, wrap result as Meaning.
 * Used for drivers that don't support the oracle protocol.
 */
async function* createSimpleSession(
  driver: InferenceDriver,
  init: OracleInit,
): OracleSession {
  // Build InferRequest from OracleInit (CESK → transport translation)
  const request = oracleInitToInferRequest(init);

  // Call the driver (CESK-unaware)
  const response = await driver.infer(request);

  // Wrap the response as a Meaning (transport → CESK translation)
  const meaning = inferResponseToMeaning(response);

  // Terminal: return the Meaning (the generator is done)
  return meaning;
}

/**
 * Reentrant path: multi-turn protocol exchange.
 * The driver supports the coroutine protocol through repeated infer() calls.
 *
 * Flow:
 * 1. Send initial prompt (from OracleInit)
 * 2. Parse LLM response for OracleReq instructions
 * 3. Yield the OracleReq to the portal
 * 4. Get the OracleResp back
 * 5. Format the response as a follow-up prompt
 * 6. Send follow-up to the driver
 * 7. Repeat until LLM returns a ReqReturn
 */
async function* createReentrantSession(
  driver: InferenceDriver,
  init: OracleInit,
  opts?: ProtocolBridgeOptions,
): OracleSession {
  const maxDepth = opts?.maxDepth ?? 4;

  // Build initial prompt
  const request = oracleInitToInferRequest(init);

  // First exchange
  let response = await driver.infer(request);
  let depth = 0;

  while (depth < maxDepth) {
    // Try to parse the response as an OracleReq
    const parsed = tryParseOracleReq(response.content);

    if (!parsed) {
      // No structured request found → treat as terminal response
      return inferResponseToMeaning(response);
    }

    if (parsed.tag === "ReqReturn") {
      return parsed.result;
    }

    if (parsed.tag === "ReqFail") {
      throw new Error(`Oracle failed: ${parsed.reason}`);
    }

    // Yield the request to the portal and get a response
    const portalResp: OracleResp = yield parsed;

    // Format the portal response as a follow-up prompt
    const followUp = formatPortalResponse(portalResp);

    // Send follow-up to driver
    response = await driver.infer({
      prompt: followUp,
      conversationRef: response.conversationRef,
      model: request.model,
      systemPrompt: request.systemPrompt,
    });

    depth++;
  }

  // Exceeded max depth — return whatever we have as a Meaning
  return inferResponseToMeaning(response);
}

// ─── Translation helpers ─────────────────────────────────────────────────────

function oracleInitToInferRequest(init: OracleInit): InferRequest {
  if (init.tag === "Infer") {
    let payload = init.payload;
    let model: string | undefined;

    // with-plugin / with-plugin-cwd creates Maps with "plugin", "model",
    // optional "cwd", and "payload" keys.
    // Extract the actual prompt, model, and metadata.
    let cwd: string | undefined;
    if (payload.tag === "Map") {
      for (const [k, v] of payload.entries) {
        if (k.tag === "Str" && (k.s === "payload" || k.s === "prompt") && v.tag === "Str") {
          payload = v; // unwrap to the actual prompt string
        }
        if (k.tag === "Str" && k.s === "model" && v.tag === "Str") {
          model = v.s;
        }
        if (k.tag === "Str" && k.s === "cwd" && v.tag === "Str") {
          cwd = v.s;
        }
      }
    }

    const prompt = valToPromptString(payload);
    return {
      prompt,
      model,
      systemPrompt: "You are an oracle in a CESK machine. Respond with the result.",
      ...(cwd ? { metadata: { cwd } } : {}),
    };
  }

  if (init.tag === "Apply") {
    const fnStr = valToString(init.proc);
    const argsStr = init.args.map(valToString).join(", ");
    return {
      prompt: `Apply function ${fnStr} to arguments: (${argsStr})`,
      systemPrompt: "You are an oracle in a CESK machine. Apply the function and return the result.",
    };
  }

  throw new Error(`Unknown OracleInit tag: ${(init as any).tag}`);
}

function inferResponseToMeaning(response: InferResponse): Meaning {
  const denotation: Val = { tag: "Str", s: response.content };
  return mkMeaning({ denotation });
}

function valToPromptString(v: Val): string {
  if (v.tag === "Str") return v.s;
  if (v.tag === "Num") return String(v.n);
  if (v.tag === "Bool") return v.b ? "#t" : "#f";
  if (v.tag === "Unit") return "(unit)";
  if (v.tag === "Vector") return `(${v.items.map(valToString).join(" ")})`;
  if (v.tag === "Sym") return v.name;
  // For complex vals, use a simplified representation
  return JSON.stringify(v);
}

function valToString(v: Val): string {
  return valToPromptString(v);
}

/**
 * Try to parse an LLM response as a structured OracleReq.
 * Returns undefined if the response is plain text (no protocol).
 *
 * Looks for JSON-encoded OracleReq in the response, wrapped in
 * markers like ```oracle-req ... ``` or {"tag":"Req..."}.
 */
function tryParseOracleReq(content: string): OracleReq | undefined {
  // Look for JSON block with OracleReq structure
  const jsonMatch = content.match(/```oracle-req\s*([\s\S]*?)```/);
  if (jsonMatch) {
    try {
      const parsed = JSON.parse(jsonMatch[1].trim());
      if (parsed.tag && parsed.tag.startsWith("Req")) {
        return parsed as OracleReq;
      }
    } catch {}
  }

  // Look for inline JSON with Req tag
  const inlineMatch = content.match(/\{"tag"\s*:\s*"Req\w+"[^}]*\}/);
  if (inlineMatch) {
    try {
      const parsed = JSON.parse(inlineMatch[0]);
      if (parsed.tag && parsed.tag.startsWith("Req")) {
        return parsed as OracleReq;
      }
    } catch {}
  }

  return undefined;
}

function formatPortalResponse(resp: OracleResp): string {
  if (resp.tag === "RespVal") {
    return `Result: ${valToString(resp.value)}`;
  }
  if (resp.tag === "RespObs") {
    return `Observation: ${JSON.stringify(resp.data)}`;
  }
  if (resp.tag === "RespTool") {
    return `Tool result: ${JSON.stringify(resp.result)}`;
  }
  if (resp.tag === "RespTest") {
    return `Test ${resp.passed ? "passed" : "failed"}: ${JSON.stringify(resp.report)}`;
  }
  if (resp.tag === "RespAck") {
    return "Acknowledged.";
  }
  if (resp.tag === "RespError") {
    return `Error: ${resp.message}`;
  }
  return JSON.stringify(resp);
}
