/**
 * Forge Management MCP — HTTP endpoint (Streamable HTTP transport).
 *
 * Hosts `createManagementMcpServer()` inside Next.js so it inherits Forge's
 * auth middleware and the nginx reverse proxy — no new port, no new auth surface.
 *
 * Uses the SDK's Web-standard transport, whose handleRequest(Request) → Response
 * fits an app-router handler directly. Stateful/session-keyed: the initialize
 * handshake mints a session id that ties later calls together. (Stateless mode
 * skips session validation but forbids transport reuse, so it can't hold a
 * multi-call session.) `enableJsonResponse` gives the stdio proxy plain JSON
 * back per request.
 *
 * The `forge mcp` stdio proxy (cli/mcp-proxy.ts) is the intended client: it
 * forwards JSON-RPC frames here and carries the Mcp-Session-Id header.
 */

import { isValidToken } from '@/app/api/auth/verify/route';
import { isAuthenticated } from '@/lib/api-auth';
import { createManagementMcpServer } from '@/mcp/server';
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
import { randomUUID } from 'node:crypto';

export const runtime = 'nodejs';

// A live session: the SDK transport plus when we last saw traffic (for idle eviction).
interface Session {
  transport: WebStandardStreamableHTTPServerTransport;
  lastSeen: number;
}

const SESSION_IDLE_MS = 30 * 60_000; // evict sessions idle longer than 30 min
const SESSION_MAX = 256; // hard cap; evict oldest beyond this

// Survive HMR / multiple route module evaluations (same pattern as the rest of Forge).
const KEY = Symbol.for('forge-management-mcp-sessions');
const g = globalThis as unknown as Record<symbol, Map<string, Session>>;
if (!g[KEY]) g[KEY] = new Map();
const sessions = g[KEY];

/** A real MCP client (not the proxy) typically closes its session with DELETE,
 *  but the stdio proxy may exit without one. Lazily reap idle/overflow sessions
 *  so the module-global Map can't grow unbounded on a long-running server. */
function reap(): void {
  const now = Date.now();
  for (const [id, s] of sessions) {
    if (now - s.lastSeen > SESSION_IDLE_MS) {
      try { s.transport.close(); } catch { /* best-effort */ }
      sessions.delete(id);
    }
  }
  if (sessions.size > SESSION_MAX) {
    const oldest = [...sessions.entries()].sort((a, b) => a[1].lastSeen - b[1].lastSeen);
    for (const [id, s] of oldest.slice(0, sessions.size - SESSION_MAX)) {
      try { s.transport.close(); } catch { /* best-effort */ }
      sessions.delete(id);
    }
  }
}

/** Returns a 401 Response if the request is not authorized, else null. */
async function denied(req: Request): Promise<Response | null> {
  const isDev = process.env.NODE_ENV !== 'production' || process.env.FORGE_DEV === '1';
  if (isDev) return null;
  // A valid API key (Bearer/x-forge-token) or validated browser session, or the
  // legacy admin-password-minted token.
  if ((await isAuthenticated(req)) || isValidToken(req)) return null;
  return Response.json({ error: 'unauthorized' }, { status: 401 });
}

/** Streamable-HTTP's signal for a stale/unknown session id: the client should
 *  discard it and re-initialize. Shaped as a JSON-RPC error so even a non-proxy
 *  client gets a usable body, with the 404 status the spec expects. */
function sessionNotFound(): Response {
  return Response.json(
    { jsonrpc: '2.0', error: { code: -32001, message: 'Session not found' }, id: null },
    { status: 404 },
  );
}

export async function POST(req: Request): Promise<Response> {
  const d = await denied(req);
  if (d) return d;
  reap();

  const sid = req.headers.get('mcp-session-id') || '';
  if (sid) {
    // A session id was supplied — it MUST already exist. If it doesn't (server
    // restarted, HMR reset, idle-evicted), reject with 404 instead of silently
    // building a fresh, uninitialized transport (which would surface a confusing
    // 400 "Server not initialized" and leak an un-stored transport).
    const s = sessions.get(sid);
    if (!s) return sessionNotFound();
    s.lastSeen = Date.now();
    return s.transport.handleRequest(req);
  }

  // No session id → this MUST be an `initialize` request. Anything else (a stale
  // client after a restart, a stray frame) would otherwise build a transport that
  // onsessioninitialized never stores — a leaked connected pair surfaced as a
  // confusing 400. Reject it with the same 404 the unknown-session path returns.
  let initBody: any;
  try { initBody = await req.clone().json(); } catch { initBody = null; }
  const isInit = Array.isArray(initBody)
    ? initBody.some((m) => m && m.method === 'initialize')
    : !!(initBody && initBody.method === 'initialize');
  if (!isInit) return sessionNotFound();

  let transport: WebStandardStreamableHTTPServerTransport;
  transport = new WebStandardStreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
    enableJsonResponse: true,
    onsessioninitialized: (id) => { sessions.set(id, { transport, lastSeen: Date.now() }); },
  });
  transport.onclose = () => {
    const id = transport.sessionId;
    if (id) sessions.delete(id);
  };
  const server = createManagementMcpServer();
  await server.connect(transport);
  return transport.handleRequest(req);
}

export async function GET(req: Request): Promise<Response> {
  const d = await denied(req);
  if (d) return d;
  reap();
  const sid = req.headers.get('mcp-session-id') || '';
  const s = sid ? sessions.get(sid) : undefined;
  if (!s) return sessionNotFound();
  s.lastSeen = Date.now();
  return s.transport.handleRequest(req);
}

export async function DELETE(req: Request): Promise<Response> {
  const d = await denied(req);
  if (d) return d;
  reap();
  const sid = req.headers.get('mcp-session-id') || '';
  const s = sid ? sessions.get(sid) : undefined;
  if (!s) return sessionNotFound();
  // handleRequest → transport.close() → onclose deletes the entry from `sessions`.
  return s.transport.handleRequest(req);
}
