// ── MCP JSON-RPC 2.0-protocol ────────────────────────────────────────────────
// Stateless implementatie van het Model Context Protocol over de Streamable
// HTTP-transport. We ondersteunen wat een tools-only server nodig heeft:
// initialize, ping, tools/list en tools/call (+ notifications, die geen
// antwoord krijgen). Geen externe SDK nodig — MCP is JSON-RPC 2.0.

import {
  lijstMcpTools,
  roepMcpToolAan,
  bestaatMcpTool,
  onbekendeToolBoodschap,
  MCP_SERVER_INFO,
} from "@/lib/mcp/server";

/** Protocolversie waarop wij standaard antwoorden als de client er geen vraagt. */
const STANDAARD_PROTOCOLVERSIE = "2025-06-18";
/** Versies die we expliciet ondersteunen (we spiegelen de gevraagde versie). */
const ONDERSTEUNDE_VERSIES = new Set(["2025-06-18", "2025-03-26", "2024-11-05"]);

type JsonRpcId = string | number | null;

export interface JsonRpcBericht {
  jsonrpc?: string;
  id?: JsonRpcId;
  method?: string;
  params?: Record<string, unknown>;
}

export interface JsonRpcAntwoord {
  jsonrpc: "2.0";
  id: JsonRpcId;
  result?: unknown;
  error?: { code: number; message: string; data?: unknown };
}

function resultaat(id: JsonRpcId, result: unknown): JsonRpcAntwoord {
  return { jsonrpc: "2.0", id, result };
}

function fout(id: JsonRpcId, code: number, message: string): JsonRpcAntwoord {
  return { jsonrpc: "2.0", id, error: { code, message } };
}

function kiesProtocolversie(params: Record<string, unknown> | undefined): string {
  const gevraagd = params?.protocolVersion;
  if (typeof gevraagd === "string" && ONDERSTEUNDE_VERSIES.has(gevraagd)) return gevraagd;
  return STANDAARD_PROTOCOLVERSIE;
}

/**
 * Verwerkt één JSON-RPC-bericht. Retourneert het antwoord, of `null` voor
 * notificaties (berichten zonder `id`) — die krijgen volgens de spec geen
 * antwoord.
 */
export async function verwerkBericht(
  bericht: JsonRpcBericht,
  origin: string,
): Promise<JsonRpcAntwoord | null> {
  const isNotificatie = bericht.id === undefined;
  const id = bericht.id ?? null;
  const method = bericht.method;

  switch (method) {
    case "initialize":
      return resultaat(id, {
        protocolVersion: kiesProtocolversie(bericht.params),
        capabilities: { tools: { listChanged: false } },
        serverInfo: MCP_SERVER_INFO,
        instructions:
          "Notary.AI is een autonome notariële medewerker voor het kantoor te " +
          "Tervuren. Einddoel: stel op basis van de bronstukken zelfstandig " +
          "ontwerpdocumenten (akte of onderhands document) op, maak de " +
          "bijbehorende modelmails klaar en bereken de afrekeningen " +
          "(aktekosten/ereloon én schenk-/erfbelasting), telkens als " +
          "werkdocument dat de notaris naleest en valideert. " +
          "ZELF-ONTDEKKING: roep bij je EERSTE gebruik (en bij elke twijfel " +
          "over volgorde of samenhang) haalWerkwijze op — dat stappenplan dekt " +
          "gegarandeerd élke tool van deze server en leidt je van oriëntatie " +
          "(kantoorkennis via lijstKennisbank, gewogen akte-checklists via " +
          "haalAkteChecklists) over opzoekingen, genereren, afrekenen en " +
          "termijnbewaking (berekenDossierTermijnen) tot de Word-oplevering, " +
          "de lokale dossierimport (haalIntakeInstructies) en de kwaliteitslus. " +
          "Twee regimes: (1) GENEREREN/BEREKENEN/OPZOEKEN — modeldocumenten en " +
          "modelbrieven opvragen én laten invullen, aktekosten/belastingen " +
          "berekenen, termijnen bewaken, vastgoedopzoekingen/kennisbank/" +
          "checklists raadplegen — wijzigt geen gedeelde data; werk hier " +
          "volledig vrij, autonoom en proactief. " +
          "(2) MUTEREN VAN DE KANTOORBIBLIOTHEEK EN -KENNIS — uitsluitend via " +
          "een voorstel (voegModelonderdeelVoorstelToe / " +
          "voegModeldocumentVoorstelToe / voegKennisVoorstelToe), nooit " +
          "rechtstreeks; de notaris keurt goed. " +
          "Privacy: stuur nooit dossierspecifieke persoonsgegevens naar de server; " +
          "de invoer geldt als geanonimiseerd en je herstelt persoonsgegevens pas " +
          "lokaal in het eindresultaat. (Het intake-JSON van haalIntakeInstructies " +
          "is de omgekeerde flow: dat blijft volledig lokaal bij de notaris.) " +
          "Alle bedragen zijn indicatief (BETA) en moeten door de notaris " +
          "gevalideerd worden. " +
          "Determinisme: geef bij genereerOntwerpUitKenmerken en " +
          "genereerWerkdossierAlsWord ALLE kenmerken mee die je met zekerheid uit " +
          "de stukken kent (goedType, metKrediet, epcAanwezig, syndicusInfo, " +
          "fiscaalRegime, ... — zie het toolschema): elk aangeleverd feit kiest " +
          "automatisch de juiste hypothese; laat een feit weg als je twijfelt. " +
          "Gegenereerde .docx-bestanden: de downloadUrl blijft 24 uur geldig — " +
          "deel ze als klikbare Markdown-link, nooit als kale URL.",
      });

    case "ping":
      return resultaat(id, {});

    case "tools/list":
      // We leveren alle tools in één pagina; een meegegeven `cursor` wordt
      // genegeerd en we laten `nextCursor` weg (einde van de lijst).
      return resultaat(id, { tools: lijstMcpTools() });

    case "tools/call": {
      const params = bericht.params ?? {};
      const naam = params.name;
      if (typeof naam !== "string") {
        return fout(id, -32602, "Ongeldige parameters: 'name' (string) is vereist.");
      }
      // Onbekende toolnaam is per de MCP-spec een protocolfout (-32602), geen
      // toolresultaat — mét een "bedoelde je …?"-hint voor de client.
      if (!bestaatMcpTool(naam)) {
        return fout(id, -32602, onbekendeToolBoodschap(naam));
      }
      if (
        params.arguments !== undefined &&
        (typeof params.arguments !== "object" ||
          params.arguments === null ||
          Array.isArray(params.arguments))
      ) {
        return fout(id, -32602, "Ongeldige parameters: 'arguments' moet een JSON-object zijn.");
      }
      const argumenten = (params.arguments as Record<string, unknown> | undefined) ?? {};
      const { tekst, isFout, gestructureerd, links } = await roepMcpToolAan(
        naam,
        argumenten,
        origin,
      );
      type ContentBlok =
        | { type: "text"; text: string }
        | { type: "resource_link"; uri: string; name: string; mimeType?: string; description?: string };
      const content: ContentBlok[] = [{ type: "text", text: tekst }];
      // Downloadbare bestanden (bv. het gegenereerde .docx) ook als
      // resource_link aanbieden, zodat MCP-clients ze rechtstreeks kunnen
      // ophalen/tonen zonder de JSON-tekst te parsen.
      for (const link of links ?? []) {
        content.push({ type: "resource_link", ...link });
      }
      const result: {
        content: ContentBlok[];
        isError: boolean;
        structuredContent?: Record<string, unknown>;
      } = { content, isError: isFout };
      // structuredContent (MCP 2025-06-18): machineleesbaar JSON-resultaat naast
      // de tekst, voor clients die het ondersteunen. Enkel bij succes: een tool
      // met outputSchema belooft een conform resultaat, en een foutbody voldoet
      // daar niet aan. Tekst blijft altijd de fallback.
      if (gestructureerd && !isFout) result.structuredContent = gestructureerd;
      return resultaat(id, result);
    }

    default:
      // Onbekende notificatie (bv. notifications/initialized): stil negeren.
      if (isNotificatie) return null;
      return fout(id, -32601, `Onbekende methode: ${method ?? "(geen)"}.`);
  }
}
