/**
 * `forge mcp` — thin stdio↔HTTP proxy for the Forge Management MCP.
 *
 * THE CONTRACT (see mcp/README.md): transport-only. No database driver, no SQL,
 * no `lib/` business imports, no secrets. It reads newline-delimited JSON-RPC
 * frames from stdin, forwards each to the Forge server's /api/mcp endpoint over
 * HTTP, and writes responses back to stdout. All logic lives behind the service.
 *
 * stdout is the protocol channel — diagnostics MUST go to stderr only, and only
 * well-formed JSON-RPC frames are ever written to stdout (never a raw HTTP error
 * body, which would corrupt the client's stream).
 *
 * Robustness (so a real client never hangs or sees garbage):
 *  - transport failure / non-2xx → a JSON-RPC error keyed to the frame's id is
 *    emitted, so the client's pending request resolves instead of hanging.
 *  - a dead session (server restarted → 404) is healed transparently: the cached
 *    `initialize` frame is replayed to mint a new session, then the failed frame
 *    is retried once.
 *  - on stdin EOF the session is released with a best-effort DELETE.
 *
 * Config (env):
 *   MW_URL          base URL of the Forge server (default http://localhost:8403)
 *   FORGE_API_KEY   persistent API key (sent as Authorization: Bearer). Generate
 *                   one in Settings → API Key. Falls back to a Forge-owned
 *                   ~/.forge/mcp-key file. Preferred over the legacy token below.
 *   FORGE_API_TOKEN legacy admin-minted token (sent as x-forge-token). Ephemeral
 *                   in-memory/session-scoped — mint one via POST /api/auth/verify
 *                   with the admin password. Prefer FORGE_API_KEY.
 *
 * Session plumbing: the Streamable-HTTP endpoint is stateful, so this captures
 * the Mcp-Session-Id from the initialize response and the negotiated
 * MCP-Protocol-Version, and replays both on later requests.
 */

import { createInterface } from 'node:readline';
import { readFileSync, existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

/** Resolve the API credential: env first, then a Forge-owned key file
 *  (~/.forge/mcp-key). Never read from ~/.claude or other prompt infrastructure. */
function resolveCredential(): string {
  const env = process.env.FORGE_API_KEY || process.env.FORGE_API_TOKEN;
  if (env) return env.trim();
  try {
    const f = join(homedir(), '.forge', 'mcp-key');
    if (existsSync(f)) return readFileSync(f, 'utf-8').trim();
  } catch { /* ignore */ }
  return '';
}

/** Injectable IO + fetch, so the recovery logic can be tested without a real
 *  server or stdio. Defaults wire up process.stdin/stdout/stderr and global fetch. */
export interface ProxyDeps {
  input?: NodeJS.ReadableStream;
  writeOut?: (s: string) => void;
  logErr?: (s: string) => void;
  fetchImpl?: typeof fetch;
}

export async function mcpProxyCommand(_args: string[], deps: ProxyDeps = {}): Promise<void> {
  const input = deps.input ?? process.stdin;
  const out = deps.writeOut ?? ((s: string) => { process.stdout.write(s); });
  const logErr = deps.logErr ?? ((s: string) => { process.stderr.write(s); });
  const doFetch = deps.fetchImpl ?? fetch;

  const base = (process.env.MW_URL || 'http://localhost:8403').replace(/\/$/, '');
  const endpoint = `${base}/api/mcp`;
  const credential = resolveCredential();
  // forge_sk_ keys go in Authorization: Bearer; legacy admin-minted tokens stay
  // on x-forge-token. Applied to every request.
  const applyAuth = (headers: Record<string, string>) => {
    if (!credential) return;
    if (credential.startsWith('forge_sk_')) headers['authorization'] = `Bearer ${credential}`;
    else headers['x-forge-token'] = credential;
  };

  const err = (m: string) => logErr(`[forge mcp] ${m}\n`);
  err(`proxy → ${endpoint}`);

  let sessionId = '';
  let protocolVersion = '';
  let initFrame = ''; // cached initialize request, for transparent session recovery

  // ── stdout helpers (protocol channel) ──────────────────
  const writeFrame = (text: string) => out(text.endsWith('\n') ? text : text + '\n');
  const emit = (obj: unknown) => out(JSON.stringify(obj) + '\n');
  const rpcError = (id: unknown, message: string, code = -32000) =>
    emit({ jsonrpc: '2.0', id, error: { code, message } });

  // ── HTTP helpers ───────────────────────────────────────
  const post = (frame: string, useSession: boolean): Promise<Response> => {
    const headers: Record<string, string> = {
      'content-type': 'application/json',
      accept: 'application/json, text/event-stream',
    };
    applyAuth(headers);
    if (useSession && sessionId) headers['mcp-session-id'] = sessionId;
    if (protocolVersion) headers['mcp-protocol-version'] = protocolVersion;
    return doFetch(endpoint, { method: 'POST', headers, body: frame });
  };
  const adoptSession = (res: Response) => {
    const sid = res.headers.get('mcp-session-id');
    if (sid) sessionId = sid;
  };
  const learnProtocol = (text: string) => {
    try {
      const v = JSON.parse(text)?.result?.protocolVersion;
      if (typeof v === 'string') protocolVersion = v;
    } catch { /* not an initialize result — ignore */ }
  };
  // Never surface a raw HTTP body (could be HTML) onto the protocol channel.
  const errorMessage = (status: number, text: string): string => {
    try {
      const m = JSON.parse(text)?.error?.message;
      if (typeof m === 'string') return m;
    } catch { /* not JSON */ }
    return `Forge server returned HTTP ${status}`;
  };
  const sessionDead = (status: number, text: string) =>
    status === 404 || (status === 400 && /not initialized|session not found/i.test(text));

  /** Replay initialize on a fresh session, then retry the failed frame once.
   *  Always "handles" the frame (writes output or a JSON-RPC error). */
  async function recover(frame: string, id: unknown, hasId: boolean): Promise<void> {
    err('session expired — re-initializing');
    sessionId = '';
    protocolVersion = '';
    let initRes: Response;
    try { initRes = await post(initFrame, false); }
    catch (e) { if (hasId) rpcError(id, `forge mcp: re-init failed: ${(e as Error).message}`); return; }
    adoptSession(initRes);
    if (!initRes.ok) { if (hasId) rpcError(id, 'forge mcp: re-initialization failed'); return; }
    learnProtocol(await initRes.text());
    try { await post('{"jsonrpc":"2.0","method":"notifications/initialized"}', true); } catch { /* best-effort */ }

    let retry: Response;
    try { retry = await post(frame, true); }
    catch (e) { if (hasId) rpcError(id, `forge mcp: ${(e as Error).message}`); return; }
    adoptSession(retry);
    if (retry.status === 202) return;
    const rtext = await retry.text();
    if (retry.ok) { if (rtext) writeFrame(rtext); return; }
    if (hasId) rpcError(id, errorMessage(retry.status, rtext));
  }

  // ── main loop ──────────────────────────────────────────
  const rl = createInterface({ input, crlfDelay: Infinity });

  for await (const line of rl) {
    const frame = line.trim();
    if (!frame) continue;

    let msg: { id?: unknown; method?: string } | undefined;
    try { msg = JSON.parse(frame); }
    catch { err('dropping non-JSON stdin line'); continue; }
    const hasId = !!msg && 'id' in msg && msg.id !== null && msg.id !== undefined;
    const id = hasId ? msg!.id : undefined;
    if (msg?.method === 'initialize') initFrame = frame;

    let res: Response;
    try { res = await post(frame, true); }
    catch (e) {
      err(`request failed: ${(e as Error).message}`);
      if (hasId) rpcError(id, `forge mcp: ${(e as Error).message}`);
      continue;
    }

    adoptSession(res);
    if (res.status === 202) continue; // notification/response accepted, no body

    const text = await res.text();
    if (res.ok) {
      learnProtocol(text);
      if (text) writeFrame(text);
      continue;
    }

    if (sessionDead(res.status, text) && initFrame && msg?.method !== 'initialize') {
      await recover(frame, id, hasId);
      continue;
    }
    if (hasId) rpcError(id, errorMessage(res.status, text));
    else err(`server ${res.status} on notification (dropped)`);
  }

  // stdin closed → release the server-side session (best-effort).
  if (sessionId) {
    try {
      const headers: Record<string, string> = { 'mcp-session-id': sessionId };
      applyAuth(headers);
      if (protocolVersion) headers['mcp-protocol-version'] = protocolVersion;
      await doFetch(endpoint, { method: 'DELETE', headers });
    } catch { /* best-effort */ }
  }
}
