// ── MCP-uitvoeringslaag ──────────────────────────────────────────────────────
// Bouwt de toolcatalogus uit de OpenAPI-spec en voert toolaanroepen uit door de
// bijbehorende interne REST-endpoint te bevragen. Zo blijft er één bron van
// waarheid (de datalaag via lib/openapi.ts) en hoeft er per nieuwe endpoint
// niets aan de MCP-server te wijzigen.

import { bouwOpenApiSpec } from "@/lib/openapi";
import {
  openApiNaarToolOperaties,
  type McpTool,
  type ToolOperatie,
} from "@/lib/mcp/openapi-naar-tools";

export const MCP_SERVER_INFO = {
  name: "notary-ai",
  version: "1.0",
  title: "Notary AI - notariskantoor Tervuren",
} as const;

/** Cache van de toolcatalogus (de spec is binnen één proces stabiel). */
let operatiesCache: ToolOperatie[] | null = null;

function toolOperaties(): ToolOperatie[] {
  if (!operatiesCache) operatiesCache = openApiNaarToolOperaties(bouwOpenApiSpec());
  return operatiesCache;
}

/** Lijst van MCP-tooldefinities (voor tools/list). */
export function lijstMcpTools(): McpTool[] {
  return toolOperaties().map((op) => op.tool);
}

/** Bestaat een tool met deze naam? (Voor de -32602-fout in het protocol.) */
export function bestaatMcpTool(naam: string): boolean {
  return toolOperaties().some((op) => op.tool.name === naam);
}

/** Foutboodschap voor een onbekende toolnaam, met "bedoelde je …?"-hint. */
export function onbekendeToolBoodschap(naam: string): string {
  const hint = gelijkendeNamen(naam, toolOperaties().map((op) => op.tool.name));
  return (
    `Onbekende tool "${naam}".` +
    (hint.length ? ` Bedoelde je: ${hint.join(", ")}?` : "") +
    ` Roep tools/list aan voor de volledige catalogus.`
  );
}

/** Downloadbaar bestand in het toolresultaat → resource_link-contentblok (MCP). */
export interface ResourceLink {
  uri: string;
  name: string;
  mimeType?: string;
  description?: string;
}

export interface ToolResultaat {
  /** JSON-tekst van het REST-antwoord (of een foutboodschap). */
  tekst: string;
  isFout: boolean;
  /** Geparste JSON-body als object, voor structuredContent (MCP 2025-06-18). */
  gestructureerd?: Record<string, unknown>;
  /** Downloadbare bestanden (bv. het gegenereerde .docx) als resource_link. */
  links?: ResourceLink[];
}

type ToolArgumenten = Record<string, unknown>;

function alsTekst(waarde: unknown): string {
  return typeof waarde === "string" ? waarde : String(waarde);
}

function fout(boodschap: string): ToolResultaat {
  return { tekst: boodschap, isFout: true };
}

/**
 * Corrigeerbare INVOERfout (typfout in een argument, ontbrekende verplichte
 * parameter, ongeldige enum, of een 4xx-antwoord van de endpoint): bewust als
 * GEWOON resultaat (isError: false) met een "fout"-veld, niet als
 * toolfout. Copilot Studio toont de inhoud van een isError-resultaat namelijk
 * niet aan het model ("Tool did not respond with success", zonder de body) —
 * waarmee alle zelfcorrectie-hints ("bedoelde je …?", "zie /api/… voor de
 * lijst") verloren gaan die precies voor het model geschreven zijn. Als
 * gewoon resultaat leest élke client ze en corrigeert de agent zichzelf.
 * Echte uitvoeringsfouten (transport, 5xx) blijven isError.
 */
function invoerfout(boodschap: string): ToolResultaat {
  return { tekst: JSON.stringify({ fout: boodschap }), isFout: false };
}

/** Levenshtein-afstand, voor "bedoelde je …?"-suggesties bij typfouten. */
function bewerkAfstand(a: string, b: string): number {
  const vorige = Array.from({ length: b.length + 1 }, (_, i) => i);
  for (let i = 1; i <= a.length; i++) {
    let diagonaal = vorige[0];
    vorige[0] = i;
    for (let j = 1; j <= b.length; j++) {
      const nieuw = Math.min(
        vorige[j] + 1,
        vorige[j - 1] + 1,
        diagonaal + (a[i - 1] === b[j - 1] ? 0 : 1),
      );
      diagonaal = vorige[j];
      vorige[j] = nieuw;
    }
  }
  return vorige[b.length];
}

/** De best gelijkende namen (max 3), voor foutboodschappen met een hint. */
function gelijkendeNamen(gezocht: string, kandidaten: string[]): string[] {
  return kandidaten
    .map((naam) => ({ naam, afstand: bewerkAfstand(gezocht.toLowerCase(), naam.toLowerCase()) }))
    .filter((k) => k.afstand <= Math.max(3, Math.floor(gezocht.length / 3)))
    .sort((a, b) => a.afstand - b.afstand)
    .slice(0, 3)
    .map((k) => k.naam);
}

/**
 * Valideert de aangeleverde argumenten tegen het toolschema vóór de REST-call:
 * onbekende argumentnamen (typfouten — het schema is gesloten), ontbrekende
 * verplichte parameters en ongeldige enum-waarden krijgen zo een duidelijke,
 * onmiddellijke foutboodschap in plaats van een verwarrend endpoint-antwoord.
 */
function valideerArgumenten(operatie: ToolOperatie, argumenten: ToolArgumenten): string | null {
  const schema = operatie.tool.inputSchema;
  const bekend = Object.keys(schema.properties);

  for (const naam of Object.keys(argumenten)) {
    if (!bekend.includes(naam)) {
      const hint = gelijkendeNamen(naam, bekend);
      return (
        `Onbekend argument "${naam}" voor tool "${operatie.tool.name}".` +
        (hint.length ? ` Bedoelde je: ${hint.join(", ")}?` : "") +
        ` Geldige argumenten: ${bekend.join(", ")}.`
      );
    }
  }

  for (const naam of schema.required ?? []) {
    const waarde = argumenten[naam];
    if (waarde === undefined || waarde === null || waarde === "") {
      return `Ontbrekende verplichte parameter "${naam}" voor tool "${operatie.tool.name}".`;
    }
  }

  for (const [naam, prop] of Object.entries(schema.properties)) {
    const waarde = argumenten[naam];
    if (waarde === undefined || waarde === null) continue;
    if (prop.enum && !prop.enum.includes(alsTekst(waarde))) {
      return (
        `Ongeldige waarde "${alsTekst(waarde)}" voor "${naam}" van tool "${operatie.tool.name}". ` +
        `Toegelaten waarden: ${prop.enum.join(", ")}.`
      );
    }
    if (prop.type === "object" && (typeof waarde !== "object" || Array.isArray(waarde))) {
      return `Argument "${naam}" van tool "${operatie.tool.name}" moet een JSON-object zijn.`;
    }
  }

  return null;
}

/** Haalt een downloadbaar bestand (downloadUrl + bestandsnaam) uit het antwoord. */
function vindResourceLinks(gestructureerd: Record<string, unknown> | undefined): ResourceLink[] {
  if (!gestructureerd) return [];
  const { downloadUrl, bestandsnaam, mimeType } = gestructureerd;
  if (typeof downloadUrl !== "string" || !downloadUrl) return [];
  const link: ResourceLink = {
    uri: downloadUrl,
    name: typeof bestandsnaam === "string" && bestandsnaam ? bestandsnaam : "document.docx",
    description: "Gegenereerd werkdocument — downloadlink 24 uur geldig.",
  };
  if (typeof mimeType === "string" && mimeType) link.mimeType = mimeType;
  return [link];
}

/** Parseert een JSON-object uit de responstekst (voor structuredContent). */
function parseGestructureerd(tekst: string): Record<string, unknown> | undefined {
  try {
    const parsed: unknown = JSON.parse(tekst);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      return parsed as Record<string, unknown>;
    }
  } catch {
    // Geen JSON-object: enkel tekst teruggeven.
  }
  return undefined;
}

/**
 * Voert een toolaanroep uit: bouwt de REST-call (methode, pad, query, body) op
 * basis van de operatiemetadata en haalt het antwoord op tegen `origin` (de
 * eigen host). Ondersteunt zowel GET als POST, zodat toekomstige
 * schrijf-/genereerfuncties automatisch werken.
 */
export async function roepMcpToolAan(
  naam: string,
  argumenten: ToolArgumenten,
  origin: string,
): Promise<ToolResultaat> {
  const operatie = toolOperaties().find((op) => op.tool.name === naam);
  if (!operatie) return fout(onbekendeToolBoodschap(naam));

  // Argumenten valideren vóór de REST-call: duidelijke, door het model
  // leesbare boodschap i.p.v. een verwarrend endpoint-antwoord bij typfouten
  // of ontbrekende parameters (invoerfout: geen isError, zie hierboven).
  const validatiefout = valideerArgumenten(operatie, argumenten);
  if (validatiefout) return invoerfout(validatiefout);

  // Path-parameters substitueren (vereist-zijn is hierboven al gevalideerd).
  let pad = operatie.padSjabloon;
  for (const param of operatie.padParameters) {
    pad = pad.replace(`{${param}}`, encodeURIComponent(alsTekst(argumenten[param])));
  }

  // Queryparameters opbouwen (gedeclareerd + eventueel vrije invulparameters).
  const query = new URLSearchParams();
  for (const param of operatie.queryParameters) {
    const waarde = argumenten[param];
    if (waarde !== undefined && waarde !== null) query.set(param, alsTekst(waarde));
  }
  if (operatie.vrijeQueryEigenschap) {
    const vrij = argumenten[operatie.vrijeQueryEigenschap];
    if (vrij && typeof vrij === "object") {
      for (const [sleutel, waarde] of Object.entries(vrij as Record<string, unknown>)) {
        if (waarde !== undefined && waarde !== null) query.set(sleutel, alsTekst(waarde));
      }
    }
  }

  // JSON request body opbouwen voor POST-operaties. Een ruime timeout voorkomt
  // dat een hangende interne fetch de hele MCP-aanroep eeuwig blokkeert
  // (Word-generatie met afbeeldingen kan even duren; 60 s is ruim voldoende).
  const init: RequestInit = {
    method: operatie.methode,
    headers: { Accept: "application/json" },
    signal: AbortSignal.timeout(60_000),
  };
  if (operatie.methode === "POST" && operatie.bodyParameters.length > 0) {
    const body: Record<string, unknown> = {};
    for (const param of operatie.bodyParameters) {
      if (argumenten[param] !== undefined) body[param] = argumenten[param];
    }
    init.headers = { ...init.headers, "Content-Type": "application/json" };
    init.body = JSON.stringify(body);
  }

  const querystring = query.toString();
  const url = `${origin}${pad}${querystring ? `?${querystring}` : ""}`;

  try {
    const res = await fetch(url, init);
    const tekst = await res.text();
    const gestructureerd = parseGestructureerd(tekst);
    if (res.ok) {
      const resultaat: ToolResultaat = { tekst, isFout: false, gestructureerd };
      const links = vindResourceLinks(gestructureerd);
      if (links.length) resultaat.links = links;
      return resultaat;
    }
    // 4xx met een leesbare JSON-foutbody: de endpoints schrijven die bewust
    // instructief ("zie /api/modeldocumenten voor de lijst", geldige waarden,
    // …) zodat het model zichzelf corrigeert — als gewoon resultaat doorgeven
    // (zie invoerfout), zónder gestructureerd: een foutbody voldoet niet aan
    // het beloofde outputSchema en hoort dus niet in structuredContent.
    if (res.status < 500 && gestructureerd) {
      return { tekst, isFout: false };
    }
    // 5xx of een onleesbare body (bv. een HTML-foutpagina): echte toolfout,
    // mét de body zodat een client die wél toont er iets aan heeft.
    return { tekst, isFout: true };
  } catch (err) {
    const boodschap = err instanceof Error ? err.message : String(err);
    return fout(`Kon endpoint niet bereiken: ${boodschap}`);
  }
}
