/**
 * Streamable HTTP MCP protocol endpoint.
 *
 * Exposes the McpRouter over HTTP at /mcp so Claude Code can connect
 * via `{ "type": "http", "url": "http://localhost:3200/mcp" }` in .mcp.json.
 *
 * Each client session gets its own StreamableHTTPServerTransport, but all
 * share the same McpRouter (and therefore the same upstream connections).
 */
import { randomUUID } from 'node:crypto';
import type { FastifyInstance } from 'fastify';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
import type { McpRouter } from '../router.js';
import type { JsonRpcRequest } from '../types.js';

interface SessionEntry {
  transport: StreamableHTTPServerTransport;
}

export function registerMcpEndpoint(app: FastifyInstance, router: McpRouter): void {
  const sessions = new Map<string, SessionEntry>();

  // POST /mcp — JSON-RPC requests (initialize, tools/call, etc.)
  app.post('/mcp', async (request, reply) => {
    const sessionId = request.headers['mcp-session-id'] as string | undefined;

    if (sessionId && sessions.has(sessionId)) {
      // Existing session
      const session = sessions.get(sessionId)!;
      await session.transport.handleRequest(request.raw, reply.raw, request.body);
      // Fastify must not send its own response — the transport already did
      reply.hijack();
      return;
    }

    if (sessionId && !sessions.has(sessionId)) {
      // Unknown session
      reply.code(404).send({ error: 'Session not found' });
      return;
    }

    // New session — no session ID header
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (id) => {
        sessions.set(id, { transport });
      },
    });

    // Wire transport messages to the router
    transport.onmessage = async (message: JSONRPCMessage) => {
      // The transport sends us JSON-RPC messages; route them through McpRouter
      if ('method' in message && 'id' in message) {
        const response = await router.route(message as unknown as JsonRpcRequest);
        await transport.send(response as unknown as JSONRPCMessage);
      }
      // Notifications (no id) are ignored — router doesn't handle inbound notifications
    };

    transport.onclose = () => {
      const id = transport.sessionId;
      if (id) {
        sessions.delete(id);
      }
    };

    await transport.handleRequest(request.raw, reply.raw, request.body);
    reply.hijack();
  });

  // GET /mcp — SSE stream for server-initiated notifications
  app.get('/mcp', async (request, reply) => {
    const sessionId = request.headers['mcp-session-id'] as string | undefined;

    if (!sessionId || !sessions.has(sessionId)) {
      reply.code(400).send({ error: 'Invalid or missing session ID' });
      return;
    }

    const session = sessions.get(sessionId)!;
    await session.transport.handleRequest(request.raw, reply.raw);
    reply.hijack();
  });

  // DELETE /mcp — Session cleanup
  app.delete('/mcp', async (request, reply) => {
    const sessionId = request.headers['mcp-session-id'] as string | undefined;

    if (!sessionId || !sessions.has(sessionId)) {
      reply.code(400).send({ error: 'Invalid or missing session ID' });
      return;
    }

    const session = sessions.get(sessionId)!;
    await session.transport.handleRequest(request.raw, reply.raw);
    sessions.delete(sessionId);
    reply.hijack();
  });
}
