import { OPENAPI_ROUTE_DEFINITIONS } from "./openapi-routes.generated";

type JsonObject = Record<string, unknown>;

const PUBLIC_OPERATIONS = new Set([
  "GET /api/health",
  "GET /api/openapi",
  "POST /api/auth/login",
  "GET /api/auth/sso/login",
  "GET /api/auth/microsoft/callback",
]);

const TAGS = [
  ["/api/openapi", "Documentation"],
  ["/api/auth", "Authentification"],
  ["/api/employes", "Employés"],
  ["/api/contrats", "Contrats"],
  ["/api/avenants", "Avenants"],
  ["/api/absences", "Absences"],
  ["/api/alertes-mails", "Alertes e-mail"],
  ["/api/alertes", "Alertes"],
  ["/api/disciplinaires", "Sanctions et discipline"],
  ["/api/sanctions", "Sanctions et discipline"],
  ["/api/visites-medicales", "Visites médicales"],
  ["/api/fiches-paie", "Fiches de paie"],
  ["/api/impressions", "Impressions et documents"],
  ["/api/procedures", "Procédures"],
  ["/api/dashboard", "Tableau de bord"],
  ["/api/admin", "Administration"],
  ["/api/internal", "Interne"],
] as const;

const METHOD_LABELS: Record<string, string> = {
  GET: "Consulter",
  POST: "Créer ou exécuter",
  PUT: "Remplacer",
  PATCH: "Modifier",
  DELETE: "Supprimer",
};

const BODY_SCHEMAS: Record<string, string> = {
  "POST /api/auth/login": "LoginRequest",
  "POST /api/employes": "EmployeInput",
  "PUT /api/employes/{cos}": "EmployeInput",
  "POST /api/contrats": "ContratInput",
  "PUT /api/contrats/{id}": "ContratInput",
  "POST /api/avenants": "AvenantInput",
  "PUT /api/avenants/{id}": "AvenantInput",
  "POST /api/absences": "AbsenceInput",
  "PUT /api/absences/{id}": "AbsenceInput",
  "POST /api/employes/{cos}/procedures": "ProcedureRequest",
  "PUT /api/procedures/{id}": "ProcedureRequest",
  "POST /api/procedures/{id}/suite": "ProcedureFollowup",
  "POST /api/employes/{cos}/communications": "EmployeeEmailRequest",
};

const MULTIPART_OPERATIONS = new Set([
  "POST /api/admin/parametres/modeles-documents",
  "POST /api/admin/parametres/modeles-documents/bulk",
]);

const BINARY_RESPONSES = new Map<string, string>([
  ["GET /api/admin/parametres/modeles-documents/apercu", "application/pdf"],
  ["GET /api/fiches-paie/employe/{cos}", "application/pdf"],
  ["GET /api/procedures/{id}/pdf", "application/pdf"],
  ["GET /api/impressions/attestation-impots/pdf", "application/pdf"],
  ["GET /api/impressions/attestation-retraite/pdf", "application/pdf"],
  ["GET /api/impressions/{document}/pdf", "application/pdf"],
  [
    "GET /api/impressions/{document}/docx",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  ],
]);

const QUERY_PARAMETERS: Record<string, JsonObject[]> = {
  "GET /api/employes": [paginationParameter("limit", 1000), paginationParameter("offset")],
  "GET /api/contrats": [paginationParameter("limit", 100), paginationParameter("offset")],
  "GET /api/avenants": [paginationParameter("limit", 100), paginationParameter("offset")],
  "GET /api/absences": [paginationParameter("limit", 100), paginationParameter("offset")],
  "GET /api/fiches-paie/employe/{cos}": [
    queryParameter("etablissement", "string", true, "Établissement du fichier de paie"),
    queryParameter("annee", "integer", true, "Année sur quatre chiffres"),
    queryParameter("mois", "integer", true, "Mois compris entre 1 et 12"),
  ],
  "GET /api/admin/parametres/modeles-documents/apercu": [
    queryParameter("templateId", "integer", true, "Identifiant du modèle"),
    queryParameter("version", "integer", true, "Numéro de version"),
  ],
  "POST /api/impressions/modeles-documents/{id}/generate": [
    {
      name: "format",
      in: "query",
      required: true,
      description: "Format du document produit",
      schema: { type: "string", enum: ["pdf", "docx"] },
    },
  ],
};

function queryParameter(name: string, type: string, required: boolean, description: string) {
  return { name, in: "query", required, description, schema: { type } };
}

function paginationParameter(name: "limit" | "offset", maximum?: number) {
  return {
    name,
    in: "query",
    required: false,
    description: name === "limit" ? "Nombre maximal de résultats" : "Nombre de résultats à ignorer",
    schema: {
      type: "integer",
      minimum: name === "limit" ? 1 : 0,
      ...(maximum ? { maximum } : {}),
    },
  };
}

function tagFor(path: string) {
  if (path.includes("/procedures")) return "Procédures";
  if (path.includes("/communications")) return "Communications salariés";
  const match = TAGS.find(([prefix]) => path.startsWith(prefix));
  if (match) return match[1];
  return "Référentiels";
}

function readableResource(path: string) {
  return path
    .replace(/^\/api\//, "")
    .replace(/\{[^}]+\}/g, "")
    .replace(/\//g, " · ")
    .replace(/-/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function pathParameters(path: string) {
  return Array.from(path.matchAll(/\{([^}]+)\}/g)).map((match) => ({
    name: match[1],
    in: "path",
    required: true,
    description: `Paramètre ${match[1]}`,
    schema: {
      type: match[1] === "table" || match[1] === "document" ? "string" : "integer",
    },
  }));
}

function requestBodyFor(key: string, method: string) {
  if (MULTIPART_OPERATIONS.has(key)) {
    return {
      required: true,
      content: {
        "multipart/form-data": {
          schema: {
            type: "object",
            required: ["file"],
            properties: {
              file: { type: "string", format: "binary" },
              templateId: { type: "integer", nullable: true },
              code: { type: "string" },
              libelle: { type: "string" },
              description: { type: "string" },
              categorie: { type: "string" },
              etablissement: { type: "string" },
            },
          },
        },
      },
    };
  }

  if (!["POST", "PUT", "PATCH"].includes(method)) return undefined;
  const schemaName = BODY_SCHEMAS[key];
  return {
    required: true,
    content: {
      "application/json": {
        schema: schemaName
          ? { $ref: `#/components/schemas/${schemaName}` }
          : { type: "object", additionalProperties: true },
      },
    },
  };
}

function responsesFor(key: string, method: string) {
  const binaryType = BINARY_RESPONSES.get(key);
  const success = binaryType
    ? {
        description: "Document généré",
        content: { [binaryType]: { schema: { type: "string", format: "binary" } } },
      }
    : {
        description: "Opération réussie",
        content: { "application/json": { schema: { $ref: "#/components/schemas/ApiResponse" } } },
      };

  const successResponses: Record<string, JsonObject> = { "200": success };
  if (method === "POST") {
    successResponses["201"] = { ...success, description: "Ressource créée" };
  }
  if (method === "DELETE") {
    successResponses["204"] = { description: "Suppression effectuée sans contenu" };
  }

  return {
    ...successResponses,
    "400": { $ref: "#/components/responses/BadRequest" },
    "401": { $ref: "#/components/responses/Unauthorized" },
    "403": { $ref: "#/components/responses/Forbidden" },
    "404": { $ref: "#/components/responses/NotFound" },
    "500": { $ref: "#/components/responses/InternalError" },
  };
}

function operationFor(method: string, path: string) {
  const key = `${method} ${path}`;
  const parameters = [...pathParameters(path), ...(QUERY_PARAMETERS[key] ?? [])];
  const operation: JsonObject = {
    tags: [tagFor(path)],
    summary: `${METHOD_LABELS[method] ?? method} ${readableResource(path)}`,
    operationId: `${method.toLowerCase()}_${path
      .replace(/^\/api\//, "")
      .replace(/[{}]/g, "")
      .replace(/[^a-zA-Z0-9]+/g, "_")}`,
    ...(parameters.length ? { parameters } : {}),
    responses: responsesFor(key, method),
  };

  const requestBody = requestBodyFor(key, method);
  if (requestBody) operation.requestBody = requestBody;

  if (PUBLIC_OPERATIONS.has(key)) operation.security = [];
  else if (path.startsWith("/api/internal/")) operation.security = [{ cronSecret: [] }];
  else operation.security = [{ bearerAuth: [] }, { cookieAuth: [] }];

  return operation;
}

function buildPaths() {
  const paths: Record<string, Record<string, JsonObject>> = {};
  for (const definition of OPENAPI_ROUTE_DEFINITIONS) {
    paths[definition.path] ??= {};
    paths[definition.path][definition.method.toLowerCase()] = operationFor(
      definition.method,
      definition.path,
    );
  }
  return paths;
}

const nullableString = (maxLength: number) => ({
  type: "string",
  maxLength,
  nullable: true,
});

const schemas = {
  ApiResponse: {
    type: "object",
    additionalProperties: true,
    description: "Réponse JSON dont le contenu dépend de l’opération.",
  },
  Error: {
    type: "object",
    required: ["message"],
    properties: {
      message: { type: "string" },
      issues: { type: "object", additionalProperties: true },
      code: { type: "string" },
    },
  },
  LoginRequest: {
    type: "object",
    required: ["username", "password"],
    properties: {
      username: { type: "string", example: "admin" },
      password: { type: "string", format: "password", writeOnly: true },
    },
  },
  EmployeInput: {
    type: "object",
    description: "Création ou modification d’un salarié. Les champs non requis peuvent être nuls.",
    properties: {
      TIT: nullableString(150),
      NSA: nullableString(150),
      NJF: nullableString(150),
      PRE: nullableString(150),
      ADR: nullableString(255),
      COP: nullableString(50),
      VIL: nullableString(150),
      TEL: nullableString(50),
      GSM: nullableString(50),
      DAN: { type: "string", format: "date-time", nullable: true },
      Matricule: { type: "integer", nullable: true },
      EmailPerso: { type: "string", format: "email", nullable: true },
      EmailE2e: { type: "string", format: "email", nullable: true },
      Actif: { type: "boolean", nullable: true },
      Commentaire: nullableString(5000),
    },
    additionalProperties: true,
  },
  ContratInput: {
    type: "object",
    required: ["id_salarie"],
    properties: {
      id_salarie: { type: "integer" },
      TCS: nullableString(150),
      MotifContrat: nullableString(255),
      DAE: { type: "string", format: "date", nullable: true },
      DSP: { type: "string", format: "date", nullable: true },
      DSR: { type: "string", format: "date", nullable: true },
      POS: nullableString(150),
      QUA: nullableString(150),
      CAT: nullableString(150),
      ETB: nullableString(150),
      Secteur: nullableString(150),
      SMBE: { type: "number", nullable: true },
    },
    additionalProperties: true,
  },
  AvenantInput: {
    type: "object",
    required: ["id_contrat"],
    properties: {
      id_contrat: { type: "integer" },
      avenant_type: nullableString(150),
      avenant_datedu: { type: "string", format: "date", nullable: true },
      avenant_dateau: { type: "string", format: "date", nullable: true },
      commentaire: nullableString(5000),
    },
  },
  AbsenceInput: {
    type: "object",
    required: ["num_salarie"],
    properties: {
      num_salarie: { type: "integer" },
      typeabsence: nullableString(150),
      datedu: { type: "string", format: "date", nullable: true },
      dateau: { type: "string", format: "date", nullable: true },
      nbheures: { type: "number", nullable: true },
      commentaire: nullableString(5000),
      revenu: { type: "boolean", nullable: true },
    },
  },
  ProcedureRequest: {
    type: "object",
    required: [
      "managerName",
      "requestType",
      "factsDate",
      "factsTime",
      "observedBy",
      "factsLocation",
      "factsDescription",
    ],
    properties: {
      managerName: { type: "string", maxLength: 255 },
      requestType: {
        type: "string",
        enum: [
          "DEMANDE_NOUVELLES",
          "LETTRE_RECADRAGE",
          "AVERTISSEMENT_DIRECT",
          "CONVOCATION_SANCTION",
          "CONVOCATION_LICENCIEMENT_RUPTURE",
          "AUTRE",
        ],
      },
      otherType: nullableString(255),
      factsDate: { type: "string", format: "date" },
      factsTime: { type: "string", pattern: "^([01]\\d|2[0-3]):[0-5]\\d$" },
      observedBy: { type: "string", maxLength: 255 },
      factsLocation: { type: "string", maxLength: 255 },
      factsDescription: { type: "string", minLength: 10, maxLength: 20000 },
      witnesses: nullableString(10000),
      writtenStatements: { type: "boolean", nullable: true },
    },
  },
  ProcedureFollowup: {
    type: "object",
    required: [
      "interviewAt",
      "interviewerName",
      "employeePresent",
      "decisionType",
      "decisionReasons",
      "factsAcknowledged",
      "decisionMakerName",
    ],
    properties: {
      interviewAt: { type: "string", format: "date-time" },
      interviewerName: { type: "string", maxLength: 255 },
      employeePresent: { type: "boolean" },
      accompaniedBy: nullableString(255),
      decisionType: { type: "string" },
      decisionReasons: { type: "string", minLength: 10, maxLength: 20000 },
      factsAcknowledged: { type: "boolean" },
      employeeStatement: nullableString(20000),
      decisionMakerName: { type: "string", maxLength: 255 },
    },
    additionalProperties: true,
  },
  EmployeeEmailRequest: {
    type: "object",
    required: ["recipient", "subject", "bodyText"],
    properties: {
      recipient: { type: "string", format: "email", maxLength: 255 },
      subject: { type: "string", minLength: 1, maxLength: 255 },
      bodyText: { type: "string", minLength: 1, maxLength: 20000 },
      procedureId: { type: "integer", nullable: true, minimum: 1 },
    },
  },
};

function errorResponse(description: string) {
  return {
    description,
    content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
  };
}

export function isSwaggerEnabled() {
  const configured = process.env.SWAGGER_ENABLED?.trim().toLowerCase();
  if (configured) return configured === "true";
  return process.env.NODE_ENV !== "production";
}

export function createOpenApiDocument(origin?: string) {
  const configuredServer = process.env.OPENAPI_SERVER_URL?.trim();
  const serverUrl = configuredServer || origin || "http://localhost:8000";

  return {
    openapi: "3.0.4",
    info: {
      title: "RH Connect — API",
      version: "1.0.0",
      description:
        "Documentation interactive de l’API RH Connect pour Envie2e Nord. Les routes protégées nécessitent un JWT ou la session SSO.",
    },
    servers: [{ url: serverUrl, description: "Serveur courant" }],
    tags: [
      "Documentation",
      "Authentification",
      "Employés",
      "Contrats",
      "Avenants",
      "Absences",
      "Alertes",
      "Alertes e-mail",
      "Sanctions et discipline",
      "Visites médicales",
      "Fiches de paie",
      "Impressions et documents",
      "Procédures",
      "Communications salariés",
      "Tableau de bord",
      "Administration",
      "Référentiels",
      "Interne",
    ].map((name) => ({ name })),
    paths: buildPaths(),
    components: {
      securitySchemes: {
        bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
        cookieAuth: { type: "apiKey", in: "cookie", name: "auth_token" },
        cronSecret: { type: "apiKey", in: "header", name: "x-cron-secret" },
      },
      schemas,
      responses: {
        BadRequest: errorResponse("Requête ou paramètres invalides"),
        Unauthorized: errorResponse("Authentification requise"),
        Forbidden: errorResponse("Droits insuffisants"),
        NotFound: errorResponse("Ressource introuvable"),
        InternalError: errorResponse("Erreur interne du serveur"),
      },
    },
  };
}
