// Generated by `doctrine boot install` — do not edit.

/**
 * SL-120: Doctrine MCP Bridge pi Extension
 *
 * Spawns `doctrine serve --mcp --path <cwd>` once per session, negotiates the
 * MCP handshake (initialize → tools/list), and registers each discovered MCP
 * tool as a pi tool with `pi.registerTool()`.
 *
 * Tool names are prefixed: `review_raise` → `doctrine_review_raise`.
 * Params use pass-through TypeBox schema (Type.Object({}, {additionalProperties: true})).
 *
 * Graceful shutdown on session_shutdown: MCP shutdown → exit notification → kill fallback.
 */

import { spawn, ChildProcess } from 'child_process';
import * as readline from 'readline';
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';

// ── BIN_PATH ──────────────────────────────────────────────────────────────────
// Baked at install time by `doctrine boot install` (SL-119 extended pattern).
// Fallback in development: environment variable or hardcoded path.

declare const BIN_PATH: string;

// ── State ─────────────────────────────────────────────────────────────────────

let proc: ChildProcess | null = null;
let procDead = false;
let procExitCode: number | null = null;
let stderrBytes = '';
let requestId = 0;
let started = false;

const MAX_STDERR_BYTES = 16384;

// Pending response handlers keyed by request id.
const pending = new Map<
  number,
  {
    resolve: (r: JsonRpcResponse) => void;
    reject: (e: Error) => void;
  }
>();

// ── Types ─────────────────────────────────────────────────────────────────────

interface JsonRpcResponse {
  jsonrpc: '2.0';
  id?: number;
  result?: unknown;
  error?: { code: number; message: string; data?: unknown };
}

interface McpTool {
  name: string;
  description: string;
  inputSchema: unknown;
}

interface ToolsListResult {
  tools: McpTool[];
}

interface McpContent {
  type: string;
  text?: string;
  data?: string;
}

interface McpToolCallResult {
  content: McpContent[];
  isError?: boolean;
}

// ── Error classes ─────────────────────────────────────────────────────────────

class TimeoutError extends Error {
  constructor(operation: string, ms: number) {
    super(`${operation} timed out after ${ms}ms`);
    this.name = 'TimeoutError';
  }
}

class ProcessDeadError extends Error {
  constructor(exitCode: number | null, stderr: string) {
    super(
      `doctrine process exited with code ${exitCode}${stderr ? '\nstderr:\n' + stderr : ''}`
    );
    this.name = 'ProcessDeadError';
  }
}

// ── Pure functions ────────────────────────────────────────────────────────────

/** Build a JSON-RPC request line (compact JSON + "\n"). */
function buildRequest(id: number, method: string, params?: unknown): string {
  return JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
}

/** Parse one JSON-RPC response line. Throws on malformed JSON. */
function parseResponse(line: string): JsonRpcResponse {
  return JSON.parse(line) as JsonRpcResponse;
}

/** "review_raise" → "doctrine_review_raise" */
function toolPiName(mcpName: string): string {
  return `doctrine_${mcpName}`;
}

/** Extract text from MCP content items, filtering non-text. */
function extractText(content: McpContent[]): string {
  return content
    .filter(
      (c): c is { type: 'text'; text: string } =>
        c.type === 'text' && typeof c.text === 'string'
    )
    .map((c) => c.text)
    .join('\n');
}

/** Format an MCP error response for the LLM. */
function formatMcpError(err: {
  code: number;
  message: string;
  data?: unknown;
}): string {
  const base = `MCP error ${err.code}: ${err.message}`;
  return err.data ? `${base}\ndata: ${JSON.stringify(err.data)}` : base;
}

/** Race a promise against a timeout. */
function withTimeout<T>(
  promise: Promise<T>,
  ms: number,
  label?: string
): Promise<T> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(
      () => reject(new TimeoutError(label ?? 'operation', ms)),
      ms
    );
    promise.then(
      (v) => {
        clearTimeout(timer);
        resolve(v);
      },
      (e) => {
        clearTimeout(timer);
        reject(e);
      }
    );
  });
}

// ── Process lifecycle ─────────────────────────────────────────────────────────

function spawnDoctrine(binPath: string, cwd: string): ChildProcess {
  const p = spawn(binPath, ['serve', '--mcp', '--path', cwd], {
    stdio: ['pipe', 'pipe', 'pipe'],
    env: { ...process.env, DOCTRINE_RESERVATION_FALLBACK: '1' }
  });

  p.on('error', () => {
    procDead = true;
    const deadErr = new ProcessDeadError(null, stderrBytes);
    for (const [, { reject }] of pending) reject(deadErr);
    pending.clear();
  });

  p.on('exit', (code) => {
    procDead = true;
    procExitCode = code;
    const deadErr = new ProcessDeadError(code, stderrBytes);
    for (const [, { reject }] of pending) reject(deadErr);
    pending.clear();
  });

  // Persistent stdout line reader — one readline for process lifetime
  const rl = readline.createInterface({ input: p.stdout! });
  rl.on('line', (line: string) => {
    if (!line.trim()) return;
    let resp: JsonRpcResponse;
    try {
      resp = parseResponse(line);
    } catch {
      return; // malformed line, skip
    }
    if (resp.id === undefined) {
      // Notification — ignore (MCP allows server→client notifications)
      return;
    }
    const handler = pending.get(resp.id);
    if (handler) {
      pending.delete(resp.id);
      handler.resolve(resp);
    }
  });

  // Stderr byte-bounded ring buffer
  p.stderr!.on('data', (chunk: Buffer) => {
    stderrBytes = (stderrBytes + chunk.toString()).slice(-MAX_STDERR_BYTES);
  });

  return p;
}

function assertProcAlive(): ChildProcess {
  if (!proc || procDead) {
    throw new ProcessDeadError(procExitCode, stderrBytes);
  }
  return proc;
}

async function sendRequest(
  method: string,
  params?: unknown,
  signal?: AbortSignal
): Promise<JsonRpcResponse> {
  const p = assertProcAlive();
  const id = ++requestId;
  const request = buildRequest(id, method, params);

  return new Promise<JsonRpcResponse>((resolve, reject) => {
    const onAbort = () => {
      pending.delete(id);
      reject(new Error('aborted'));
    };
    signal?.addEventListener('abort', onAbort, { once: true });

    pending.set(id, {
      resolve: (resp: JsonRpcResponse) => {
        signal?.removeEventListener('abort', onAbort);
        if (resp.error) reject(new Error(formatMcpError(resp.error)));
        else resolve(resp);
      },
      reject: (err: Error) => {
        signal?.removeEventListener('abort', onAbort);
        reject(err);
      }
    });

    p.stdin!.write(request, (err) => {
      if (err) {
        pending.delete(id);
        reject(new Error(`write failed: ${err.message}`));
      }
    });
  });
}

// ── Handshake ─────────────────────────────────────────────────────────────────

async function initialize(signal?: AbortSignal): Promise<void> {
  const initResp = await withTimeout(
    sendRequest(
      'initialize',
      {
        protocolVersion: '2024-11-05',
        capabilities: {},
        clientInfo: { name: 'doctrine-pi-mcp', version: '1.0.0' }
      },
      signal
    ),
    2000,
    'initialize'
  );

  const result = initResp.result as
    | { capabilities?: { tools?: unknown } }
    | undefined;
  if (!result?.capabilities?.tools) {
    throw new Error('MCP server does not advertise tools capability');
  }

  // Send initialized notification (fire-and-forget, no response expected)
  assertProcAlive().stdin!.write(
    buildRequest(0, 'notifications/initialized')
  );
}

// ── Tool discovery ────────────────────────────────────────────────────────────

async function discoverTools(signal?: AbortSignal): Promise<McpTool[]> {
  const resp = await withTimeout(
    sendRequest('tools/list', undefined, signal),
    5000,
    'tools/list'
  );
  const result = resp.result as ToolsListResult | undefined;
  return result?.tools ?? [];
}

// ── Tool call ─────────────────────────────────────────────────────────────────

async function callTool(
  toolName: string,
  args: unknown,
  signal?: AbortSignal
): Promise<McpToolCallResult> {
  const resp = await withTimeout(
    sendRequest('tools/call', { name: toolName, arguments: args }, signal),
    30_000,
    `tools/call ${toolName}`
  );
  return resp.result as McpToolCallResult;
}

// ── Graceful shutdown ─────────────────────────────────────────────────────────

async function shutdown(procToKill: ChildProcess, timeoutMs = 2000): Promise<void> {
  if (procDead) return;
  try {
    await withTimeout(sendRequest('shutdown'), timeoutMs, 'shutdown');
  } catch {
    // timeout — force kill below
  }
  // Send exit notification (fire-and-forget)
  try {
    procToKill.stdin!.write(buildRequest(0, 'notifications/exit'));
  } catch {
    // pipe already closed
  }
  if (!procToKill.killed) procToKill.kill();
}

// ── Extension entry point ─────────────────────────────────────────────────────

export default function doctrineMcpExtension(pi: ExtensionAPI) {
  pi.on('session_start', async (_event, ctx) => {
    // Guard idempotency — session_start may fire more than once
    if (started) return;
    started = true;

    // Resolve BIN_PATH: use baked constant if available (from --define),
    // otherwise fall back to env or hardcoded path
    const binPath =
      typeof BIN_PATH !== 'undefined'
        ? BIN_PATH
        : process.env.DOCTRINE_BIN || 'doctrine';

    try {
      proc = spawnDoctrine(binPath, ctx.cwd);
      await initialize();
      const tools = await discoverTools();

      for (const tool of tools) {
        pi.registerTool({
          name: toolPiName(tool.name),
          label: `Doctrine ${tool.name}`,
          description: `${tool.description}\n\nParameters (JSON Schema):\n${JSON.stringify(tool.inputSchema, null, 2)}`,
          parameters: Type.Object({}, { additionalProperties: true }),
          async execute(_toolCallId, params, signal) {
            const result = await callTool(tool.name, params, signal);
            return {
              content: [{ type: 'text', text: extractText(result.content) }],
              details: {}
            };
          }
        });
      }
    } catch (err) {
      if (ctx.hasUI) {
        ctx.ui.notify(
          `doctrine-mcp: ${(err as Error).message}`,
          'warning'
        );
      }
    }
  });

  pi.on('session_shutdown', async () => {
    if (proc) {
      await shutdown(proc);
      proc = null;
      procDead = false;
      started = false;
    }
  });
}
