/**
 * # mcp/stdio — the thin stdio adapter over the MCP dispatcher (kestrel-djm.7)
 *
 * The MCP wire is newline-delimited JSON-RPC 2.0 over stdin/stdout. This module is a THIN ADAPTER: it frames
 * stdin into lines, hands each raw frame to {@link KestrelMcpServer.handleFrame} (a malformed frame fails
 * closed as a JSON-RPC parse error — the transport-layer signal), and writes each JSON-RPC response as one
 * line to stdout. A NOTIFICATION frame (no `id` member) yields `undefined` from the dispatcher and NOTHING is
 * written for it — JSON-RPC 2.0 and MCP both forbid answering a notification, and every real MCP client sends
 * `notifications/initialized` as its third handshake step. It holds NO Kestrel semantics — the dispatcher
 * (`./server.ts`) is the contract; this is only the pipe. Responses are serialized in arrival order (a
 * per-frame async chain), so a slow verb never reorders the stream.
 *
 * RUN IT: `bun src/mcp/stdio.ts` (the `import.meta.main` guard below constructs a default LOCAL server —
 * `createSdk(localTransport())` — and serves stdio). An embedder builds its own transport CHOICE and calls
 * `serveStdio(createKestrelMcpServer({ transport }))`.
 *
 * ── DESIGN FORK RESOLVED (kestrel-0f7i; platform kestrel-markets-ff9m) — a node-runnable entry EXISTS ──
 * This header once logged "NO node bin, on purpose": the LOCAL transport pulls the Bun-hosted engine
 * (`Bun.CryptoHasher` et al.), so a plain-`node` bin over `localTransport()` would fault. The resolution is
 * the `kestrel mcp` verb (src/cli/commands/mcp.ts): the published bin serves this SAME adapter with the
 * REMOTE transport by DEFAULT (pure fetch — node-runnable end-to-end; the `npx kestrel.markets mcp` stdio
 * drop-in an MCP client configures), while `--local` opts into the in-process engine behind the launcher's
 * heavy gate (bun-less ⇒ loud exit 4 RUNTIME_UNAVAILABLE, never a raw Bun fault on the MCP wire). The
 * dispatcher — not any bin — is still what the contract pins; `bun src/mcp/stdio.ts` stays the direct local
 * entry below.
 */

import { localTransport } from "../sdk/index.ts";
import { createKestrelMcpServer, type KestrelMcpServer } from "./server.ts";

/**
 * Serve one MCP server over stdin/stdout as newline-delimited JSON-RPC 2.0. Resolves when stdin ends. Each
 * frame is dispatched through {@link KestrelMcpServer.handleFrame}; responses are written in arrival order.
 */
export function serveStdio(server: KestrelMcpServer): Promise<void> {
  const input = process.stdin;
  const output = process.stdout;
  input.setEncoding("utf8");

  let buffer = "";
  let chain: Promise<void> = Promise.resolve();

  const enqueue = (line: string): void => {
    chain = chain.then(async () => {
      const response = await server.handleFrame(line);
      // `undefined` ⇒ the frame was a NOTIFICATION — the wire carries NO response for it (JSON-RPC 2.0 §4.1).
      if (response !== undefined) output.write(JSON.stringify(response) + "\n");
    });
  };

  return new Promise<void>((resolve, reject) => {
    input.on("data", (chunk: Buffer | string) => {
      buffer += String(chunk);
      let nl: number;
      while ((nl = buffer.indexOf("\n")) >= 0) {
        const line = buffer.slice(0, nl).trim();
        buffer = buffer.slice(nl + 1);
        if (line !== "") enqueue(line);
      }
    });
    input.on("end", () => {
      const tail = buffer.trim();
      if (tail !== "") enqueue(tail);
      chain.then(() => resolve(), reject);
    });
    input.on("error", reject);
  });
}

// Runnable entry: `bun src/mcp/stdio.ts` → a default LOCAL MCP server over stdio (does not fire when this
// module is merely imported as the library face).
if (import.meta.main) {
  serveStdio(createKestrelMcpServer({ transport: localTransport() })).catch((e: unknown) => {
    process.stderr.write(`kestrel-mcp: fatal ${e instanceof Error ? e.message : String(e)}\n`);
    process.exitCode = 1;
  });
}
