// ── OpenAPI → MCP-tools ──────────────────────────────────────────────────────
// Pure vertaling van de gedeelde OpenAPI-spec (lib/openapi.ts) naar de
// tooldefinities van het Model Context Protocol (spec-revisie 2025-06-18).
// Elke GET- én POST-operatie wordt één MCP-tool; nieuwe of gewijzigde endpoints
// in de spec verschijnen automatisch als tool, zonder de MCP-server te
// herconfigureren. Naast de tooldefinitie houden we per tool de
// uitvoeringsmetadata bij (methode, pad, path-/query-/bodyparameters) zodat de
// server de juiste REST-call kan opbouwen.

import type {
  OpenApiSpec,
  OpenApiOperation,
  OpenApiParameterSchema,
  OpenApiObjectSchema,
} from "@/lib/openapi";

/** JSON Schema-eigenschap zoals MCP-clients ze verwachten in input/outputSchema. */
export interface JsonSchemaProperty {
  type: string;
  description?: string;
  enum?: readonly string[];
  default?: string | number | boolean;
  additionalProperties?: { type: string };
  /** Geneste eigenschappen wanneer type "object" is (bv. het kenmerken-blok). */
  properties?: Record<string, JsonSchemaProperty>;
}

export interface JsonSchemaObject {
  type: "object";
  properties: Record<string, JsonSchemaProperty>;
  required?: string[];
  /**
   * Inputschema's zijn gesloten (false: onbekende argumenten zijn typfouten);
   * outputschema's blijven open (weggelaten), zodat een strikte client niet
   * struikelt over extra velden die de endpoint legitiem meegeeft.
   */
  additionalProperties?: false;
}

/**
 * Tool-annotaties (MCP 2025-06-18): hints over het gedrag van een tool zodat
 * clients ze veilig kunnen presenteren/uitvoeren. Voor een GET (read-only,
 * idempotent) zetten we de overeenkomstige vlaggen; een POST is niet read-only.
 */
export interface ToolAnnotations {
  title?: string;
  readOnlyHint?: boolean;
  idempotentHint?: boolean;
  destructiveHint?: boolean;
  openWorldHint?: boolean;
}

/** MCP-tooldefinitie (tools/list-formaat). */
export interface McpTool {
  name: string;
  title?: string;
  description: string;
  inputSchema: JsonSchemaObject;
  /**
   * Schema van het structuredContent-resultaat (MCP 2025-06-18), gegenereerd
   * uit het 200-responsschema van de OpenAPI-spec wanneer dat er is. Clients
   * kunnen het resultaat dan getypt lezen i.p.v. de tekst te parsen.
   */
  outputSchema?: JsonSchemaObject;
  annotations?: ToolAnnotations;
}

export type HttpMethode = "GET" | "POST";

/** MCP-tool + alles wat nodig is om de onderliggende REST-call uit te voeren. */
export interface ToolOperatie {
  tool: McpTool;
  methode: HttpMethode;
  /** Padsjabloon met {param}-plaatshouders, bv. "/api/modelbrieven/{id}". */
  padSjabloon: string;
  /** Namen van de path-parameters (verplicht, in het pad te substitueren). */
  padParameters: string[];
  /** Namen van de gewone (gedeclareerde) queryparameters. */
  queryParameters: string[];
  /** Namen van de top-level eigenschappen die naar de JSON request body gaan. */
  bodyParameters: string[];
  /**
   * Naam van de gegenereerde object-eigenschap voor vrije queryparameters
   * (afgeleid van een <plaatshouder>-parameter in de spec), of null.
   */
  vrijeQueryEigenschap: string | null;
}

/** Naam waaronder vrije (dynamische) queryparameters worden gebundeld. */
const VRIJE_QUERY_EIGENSCHAP = "invulParameters";

function isPlaatshouder(naam: string): boolean {
  return /^<.+>$/.test(naam);
}

function schemaNaarProperty(
  schema: OpenApiParameterSchema,
  beschrijving?: string,
): JsonSchemaProperty {
  const prop: JsonSchemaProperty = { type: schema.type };
  if (beschrijving) prop.description = beschrijving;
  if (schema.enum) prop.enum = schema.enum;
  if (schema.default !== undefined) prop.default = schema.default;
  // Geneste object-eigenschappen (bv. het kenmerken-blok) mee doorgeven, zodat
  // de agent de deelvelden en hun omschrijvingen in het toolschema ziet in
  // plaats van een ondoorzichtig "object".
  if (schema.properties) {
    prop.properties = Object.fromEntries(
      Object.entries(schema.properties).map(([naam, sub]) => [
        naam,
        schemaNaarProperty(sub, sub.description),
      ]),
    );
  }
  return prop;
}

/** Zet het 200-responsschema (indien aanwezig) om naar een MCP-outputSchema. */
function responsNaarOutputSchema(operatie: OpenApiOperation): JsonSchemaObject | undefined {
  const schema: OpenApiObjectSchema | undefined =
    operatie.responses?.["200"]?.content?.["application/json"]?.schema;
  if (!schema) return undefined;
  const properties = Object.fromEntries(
    Object.entries(schema.properties).map(([naam, sub]) => [
      naam,
      schemaNaarProperty(sub, sub.description),
    ]),
  );
  const output: JsonSchemaObject = { type: "object", properties };
  if (schema.required?.length) output.required = [...schema.required];
  return output;
}

function bouwBeschrijving(operatie: OpenApiOperation): string {
  const basis = operatie.description ?? operatie.summary;
  const respons = operatie.responses?.["200"]?.description;
  return respons ? `${basis} Antwoord: ${respons}` : basis;
}

/**
 * OperationId's die de gedeelde kantoorbibliotheek muteren. Per het
 * twee-regimes-principe (AGENTS.md) is dit het ENIGE schrijfpad: enkel de
 * wijzigingsvoorstel-endpoints wijzigen gedeelde data. Alle andere operaties —
 * ook de POST-en die louter afleiden (bepaalAkteType, bepaalModelmailCategorieen),
 * transformeren (genereerOntwerpUitKenmerken) of een werkdocument genereren
 * (genereerWerkdossierAlsWord) — muteren niets en zijn read-only. Voeg hier elke
 * toekomstige schrijvende endpoint toe, zodat MCP-clients (Claude.ai, Claude
 * Code, …) ze correct als 'write' i.p.v. read-only classificeren.
 */
const MUTERENDE_OPERATIES = new Set<string>([
  "voegModelonderdeelVoorstelToe",
  "voegModeldocumentVoorstelToe",
]);

function bouwAnnotaties(operationId: string, titel: string): ToolAnnotations {
  // Read-only = wijzigt geen (gedeelde) data. De HTTP-methode is hiervoor GEEN
  // betrouwbare maatstaf: verschillende read-only tools zijn POST omdat ze een
  // JSON-body nemen (opzoeken/berekenen/genereren), niet omdat ze schrijven.
  const readOnly = !MUTERENDE_OPERATIES.has(operationId);
  return {
    title: titel,
    readOnlyHint: readOnly,
    // Read-only tools zijn idempotent; een wijzigingsvoorstel niet (elke aanroep
    // maakt een nieuw voorstel).
    idempotentHint: readOnly,
    // Nooit destructief: read-only tools wijzigen niets, en een voorstel muteert
    // geen gedeelde data — de notaris valideert in 'Te valideren' (AGENTS.md).
    destructiveHint: false,
    // Alle endpoints zijn intern (eigen datalaag), geen open-world interactie.
    openWorldHint: false,
  };
}

/**
 * Zet één OpenAPI-operatie om naar een MCP-tool met uitvoeringsmetadata.
 * Path-parameters en als `required` gemarkeerde parameters komen in
 * `required`; een <plaatshouder>-parameter wordt een vrij object met
 * stringwaarden, waarvan de sleutels bij uitvoering als queryparameters worden
 * meegegeven; een eventuele JSON request body wordt vlak mee opgenomen.
 */
function operatieNaarTool(
  padSjabloon: string,
  methode: HttpMethode,
  operatie: OpenApiOperation,
): ToolOperatie {
  const properties: Record<string, JsonSchemaProperty> = {};
  const required: string[] = [];
  const padParameters: string[] = [];
  const queryParameters: string[] = [];
  const bodyParameters: string[] = [];
  let vrijeQueryEigenschap: string | null = null;

  for (const param of operatie.parameters ?? []) {
    if (isPlaatshouder(param.name)) {
      vrijeQueryEigenschap = VRIJE_QUERY_EIGENSCHAP;
      properties[VRIJE_QUERY_EIGENSCHAP] = {
        type: "object",
        description: param.description,
        additionalProperties: { type: "string" },
      };
      continue;
    }

    properties[param.name] = schemaNaarProperty(param.schema, param.description);

    if (param.in === "path") {
      padParameters.push(param.name);
      required.push(param.name);
    } else {
      queryParameters.push(param.name);
      if (param.required) required.push(param.name);
    }
  }

  // JSON request body (POST): eigenschappen vlak mee opnemen in het inputSchema.
  const bodySchema = operatie.requestBody?.content["application/json"].schema;
  if (bodySchema) {
    for (const [naam, propSchema] of Object.entries(bodySchema.properties)) {
      properties[naam] = schemaNaarProperty(propSchema, propSchema.description);
      bodyParameters.push(naam);
    }
    if (operatie.requestBody?.required && bodySchema.required) {
      required.push(...bodySchema.required);
    }
  }

  const inputSchema: JsonSchemaObject = {
    type: "object",
    properties,
    additionalProperties: false,
  };
  if (required.length > 0) inputSchema.required = required;

  const titel = operatie.summary;
  const tool: McpTool = {
    name: operatie.operationId,
    title: titel,
    description: bouwBeschrijving(operatie),
    inputSchema,
    annotations: bouwAnnotaties(operatie.operationId, titel),
  };
  const outputSchema = responsNaarOutputSchema(operatie);
  if (outputSchema) tool.outputSchema = outputSchema;

  return {
    tool,
    methode,
    padSjabloon,
    padParameters,
    queryParameters,
    bodyParameters,
    vrijeQueryEigenschap,
  };
}

/**
 * Vertaalt de volledige OpenAPI-spec naar een lijst tool-operaties. Behoudt de
 * volgorde van de paden in de spec; per pad worden GET en POST (in die
 * volgorde) omgezet.
 */
export function openApiNaarToolOperaties(spec: OpenApiSpec): ToolOperatie[] {
  const operaties: ToolOperatie[] = [];
  for (const [pad, item] of Object.entries(spec.paths)) {
    if (item.get) operaties.push(operatieNaarTool(pad, "GET", item.get));
    if (item.post) operaties.push(operatieNaarTool(pad, "POST", item.post));
  }
  return operaties;
}
