import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";

import Docxtemplater from "docxtemplater";
import PizZip from "pizzip";

import { prisma } from "@/lib/prisma";
import type { EmployeeAccessScope } from "@/services/employes.service";
import { getEmployeByCos } from "@/services/employes.service";

const execFileAsync = promisify(execFile);

export type DynamicTemplateField = {
  key: string;
  label: string;
  type: "text" | "date" | "number" | "boolean";
  source: "manual" | "employee" | "contract" | "signatory" | "establishment";
  sourcePath: string | null;
  required: boolean;
};

type TemplateValue = string | number | boolean;

export class DynamicDocumentError extends Error {
  constructor(
    public readonly code:
      | "TEMPLATE_NOT_FOUND"
      | "EMPLOYE_NOT_FOUND"
      | "SIGNATAIRE_NOT_FOUND"
      | "INVALID_VALUES"
      | "PDF_CONVERSION_FAILED",
    public readonly missingFields: string[] = [],
  ) {
    super(code);
    this.name = "DynamicDocumentError";
  }
}

function storageRoot() {
  return process.env.DOCUMENT_TEMPLATES_ROOT || "/data/document-templates";
}

function clean(value: unknown) {
  return value == null ? "" : String(value).trim();
}

function isoDate(value: Date | null | undefined) {
  if (!value) return "";
  return new Intl.DateTimeFormat("fr-CA", {
    timeZone: "Europe/Paris",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(value);
}

function frenchDate(value: Date | null | undefined) {
  if (!value) return "";
  return new Intl.DateTimeFormat("fr-FR", {
    timeZone: "Europe/Paris",
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  }).format(value);
}

function addOneDay(value: Date | null | undefined) {
  if (!value) return "";
  const next = new Date(value);
  next.setUTCDate(next.getUTCDate() + 1);
  return isoDate(next);
}

function frenchDateFromIso(value: string) {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
  return match ? `${match[3]}/${match[2]}/${match[1]}` : value;
}

function frenchDateTimeFromLocal(value: string) {
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value);
  return match
    ? `${match[3]}/${match[2]}/${match[1]} \u00e0 ${match[4]}h${match[5]}`
    : value;
}

function normalizeKey(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9]/g, "")
    .toLowerCase();
}

function normalizeCivilite(value: string | null | undefined) {
  const normalized = normalizeKey(clean(value));
  if (["mme", "madame", "mlle", "mademoiselle"].includes(normalized)) {
    return {
      label: ["mlle", "mademoiselle"].includes(normalized)
        ? "Mademoiselle"
        : "Madame",
      domicilie: "domiciliée",
    };
  }
  if (["m", "mr", "monsieur"].includes(normalized)) {
    return { label: "Monsieur", domicilie: "domicilié" };
  }
  return { label: clean(value), domicilie: "domicilié(e)" };
}

function aliases(entries: Array<[string[], TemplateValue]>) {
  const result = new Map<string, TemplateValue>();
  for (const [keys, value] of entries) {
    for (const key of keys) result.set(normalizeKey(key), value);
  }
  return result;
}

async function activeTemplate(templateId: number) {
  const template = await prisma.modeles_documents.findFirst({
    where: { id: templateId, actif: true, version_active: { not: null } },
    include: {
      versions: {
        where: { statut: "ACTIVE" },
        orderBy: { numero: "desc" },
        take: 1,
      },
    },
  });
  const version = template?.versions[0];
  if (!template || !version || version.numero !== template.version_active) {
    throw new DynamicDocumentError("TEMPLATE_NOT_FOUND");
  }
  return { template, version };
}

export async function listActiveDynamicTemplates() {
  const templates = await prisma.modeles_documents.findMany({
    where: { actif: true, version_active: { not: null } },
    orderBy: [{ categorie: "asc" }, { libelle: "asc" }],
    include: {
      versions: {
        where: { statut: "ACTIVE" },
        orderBy: { numero: "desc" },
        take: 1,
        select: { numero: true, champs: true },
      },
    },
  });

  return templates
    .filter((template) => template.versions[0]?.numero === template.version_active)
    .map((template) => ({
      id: template.id,
      code: template.code,
      libelle: template.libelle,
      description: template.description,
      categorie: template.categorie,
      etablissement: template.etablissement,
      version: template.version_active!,
      champs: template.versions[0].champs as unknown as DynamicTemplateField[],
    }));
}

export async function buildDynamicDocumentDraft(input: {
  templateId: number;
  cos: number;
  signataireId?: number | null;
  scope: EmployeeAccessScope;
  now?: Date;
}) {
  const { template, version } = await activeTemplate(input.templateId);
  const employe = await getEmployeByCos(input.cos, input.scope);
  if (!employe) throw new DynamicDocumentError("EMPLOYE_NOT_FOUND");

  const contrat = await prisma.contrats.findFirst({
    where: { Id_Salarie: input.cos },
    orderBy: [{ DAE: "desc" }, { id: "desc" }],
  });
  const [contratsHistorique, etablissement] = await Promise.all([
    prisma.contrats.findMany({
      where: { Id_Salarie: input.cos },
      orderBy: [{ DAE: "asc" }, { id: "asc" }],
      select: { DAE: true, DSP: true, DSR: true, TCS: true, QUA: true, POS: true },
    }),
    contrat?.ETB
      ? prisma.etablissement.findFirst({
          where: { etb: contrat.ETB },
          select: { entreprise: true, adresseetb: true, adressesiege: true },
        })
      : null,
  ]);
  const signataire = input.signataireId
    ? await prisma.ref_signataires.findFirst({
        where: { id: input.signataireId, actif: true },
      })
    : null;
  if (input.signataireId && !signataire) {
    throw new DynamicDocumentError("SIGNATAIRE_NOT_FOUND");
  }

  const civilite = normalizeCivilite(employe.TIT);
  const nom = clean(employe.NSA);
  const prenom = clean(employe.PRE);
  const nomPrenom = [nom, prenom].filter(Boolean).join(" ");
  const prenomNom = [prenom, nom].filter(Boolean).join(" ");
  const titreNomPrenom = [civilite.label, prenomNom].filter(Boolean).join(" ");
  const adresse = [
    clean(employe.ADR),
    [clean(employe.COP), clean(employe.VIL)].filter(Boolean).join(" "),
  ]
    .filter(Boolean)
    .join(", ");
  const poste = clean(contrat?.POS) || clean(contrat?.QUA);
  const now = input.now ?? new Date();
  const isFeminine = ["mme", "madame", "mlle", "mademoiselle"].includes(
    normalizeKey(clean(employe.TIT)),
  );
  const contratsTexte = contratsHistorique
    .map((item) => {
      const dateDebut = frenchDate(item.DAE);
      const dateFin = frenchDate(item.DSP || item.DSR);
      const periode = dateDebut
        ? dateFin
          ? `du ${dateDebut} au ${dateFin}`
          : `depuis le ${dateDebut}`
        : "";
      const emploi = clean(item.QUA) || clean(item.POS);
      return [clean(item.TCS), periode, emploi].filter(Boolean).join(" - ");
    })
    .filter(Boolean)
    .join("\n");
  const companyName = clean(etablissement?.entreprise) || clean(contrat?.ETB);
  const companyAddress =
    clean(etablissement?.adresseetb) || clean(etablissement?.adressesiege);

  const employeeValues = aliases([
    [["cos", "matricule", "numeroSalarie"], clean(employe.COS)],
    [["civilite", "titre"], civilite.label],
    [["nom", "nomSalarie"], nom],
    [["prenom", "prenomSalarie"], prenom],
    [["nomPrenom", "salarieNomPrenom"], nomPrenom],
    [["prenomNom"], prenomNom],
    [["titreNomPrenom", "identiteSalarie"], titreNomPrenom],
    [["domicilie", "formuleDomiciliation"], civilite.domicilie],
    [["adresse", "adresseLigne", "adresseSalarie"], adresse],
    [["rue", "adresseRue"], clean(employe.ADR)],
    [["codePostal"], clean(employe.COP)],
    [["ville", "villeSalarie"], clean(employe.VIL)],
    [["telephone", "telephoneSalarie"], clean(employe.TEL) || clean(employe.GSM)],
    [["email", "emailSalarie"], clean(employe.EmailE2e) || clean(employe.EmailPerso)],
    [["dateNaissance"], isoDate(employe.DAN)],
    [["lieuNaissance", "paysNaissance"], clean(employe.LNA)],
    [["nationalite"], clean(employe.NAT)],
    [["numeroSecuriteSociale", "numeroSecu", "nss"], clean(employe.NSS)],
    [["genreAccord", "genre"], isFeminine ? "e" : ""],
    [["genrePronom", "genre2"], isFeminine ? "elle" : "il"],
    [["titreSejour"], clean(employe.TypeCS)],
    [["numeroCarteSejour"], clean(employe.NCJ)],
    [["dateExpirationTitreSejour", "dateExpiration"], isoDate(employe.EXJ)],
    [["dateFinCmu"], isoDate(employe.DateFinCMU)],
    [["dateFinDerogationMutuelle", "dateEcheanceMutuelle"], isoDate(employe.DateFinDerogationMutuelle)],
    [["motifDerogationMutuelle"], clean(employe.MotifDerogationMutuelle)],
  ]);

  const contractValues = aliases([
    [["typeContrat", "contrat", "tcs"], clean(contrat?.TCS)],
    [["motifContrat"], clean(contrat?.MotifContrat)],
    [["dateEmbauche", "dateDebutContrat"], isoDate(contrat?.DAE)],
    [["dateFinContrat", "dateSortiePrevue"], isoDate(contrat?.DSP)],
    [["dateSortieReelle"], isoDate(contrat?.DSR)],
    [["poste", "emploi", "qualification"], poste],
    [["etablissement"], clean(contrat?.ETB)],
    [["secteur"], clean(contrat?.Secteur) || clean(contrat?.SEC)],
    [["categorie", "classification"], clean(contrat?.CAT)],
    [["niveau"], clean(contrat?.NIV)],
    [["echelon"], clean(contrat?.ECH)],
    [["coefficient"], clean(contrat?.COE)],
    [["salaireMensuel", "salaireMensuelBrut"], clean(contrat?.SMBE)],
    [["heuresHebdomadaires", "nbHeuresHebdo"], clean(contrat?.NBHRHEB)],
    [["heuresMensuelles", "mensu"], clean(contrat?.MEN)],
    [["dateReprise"], addOneDay(contrat?.DSP)],
    [["dureeContrat"], contrat?.DSP || contrat?.DSR
      ? `Du ${frenchDate(contrat?.DAE)} au ${frenchDate(contrat?.DSP || contrat?.DSR)}`
      : "Durée indéterminée"],
    [["contratsTexte"], contratsTexte],
  ]);

  const signatoryValues = aliases([
    [["signataireNom", "nomSignataire", "correspondantNom"], clean(signataire?.prenomnom)],
    [["signataireFonction", "fonctionSignataire", "correspondantFonction"], clean(signataire?.fonction)],
    [["formuleSoussignataire"], "soussigné(e)"],
  ]);

  const generalValues = aliases([
    [["dateDocument", "dateDuDocument", "dateEdition"], isoDate(now)],
    [["lieuDocument", "lieuSignature"], clean(contrat?.ETB) || "Lesquin"],
    [["entreprise", "societe"], companyName],
    [["adresseEntreprise"], companyAddress],
    [["telephoneEntreprise"], process.env.DOCUMENT_COMPANY_PHONE || "03.20.30.76.16"],
    [["siret"], process.env.DOCUMENT_COMPANY_SIRET || "484 873 955 00021"],
    [["urssaf"], process.env.DOCUMENT_COMPANY_URSSAF || "590 1503390465"],
  ]);

  const fields = version.champs as unknown as DynamicTemplateField[];
  const values: Record<string, TemplateValue> = {};
  const resolvedFields = fields.map((field) => {
    const normalized = normalizeKey(field.key);
    const candidates: Array<[DynamicTemplateField["source"], Map<string, TemplateValue>]> = [
      ["employee", employeeValues],
      ["contract", contractValues],
      ["signatory", signatoryValues],
      ["establishment", generalValues],
    ];
    const found = candidates.find(([, map]) => map.has(normalized));
    const value = found?.[1].get(normalized) ?? "";
    values[field.key] = value;
    return {
      ...field,
      source: found?.[0] ?? "manual",
      sourcePath: found ? normalized : null,
    };
  });

  return {
    template: {
      id: template.id,
      code: template.code,
      libelle: template.libelle,
      version: version.numero,
    },
    fields: resolvedFields,
    values,
  };
}

function validateValues(
  fields: DynamicTemplateField[],
  submitted: Record<string, TemplateValue>,
) {
  const allowed = new Set(fields.map((field) => field.key));
  const values: Record<string, TemplateValue> = {};
  for (const field of fields) {
    const value = submitted[field.key] ?? "";
    if (typeof value === "string" && value.length > 10_000) {
      throw new DynamicDocumentError("INVALID_VALUES", [field.key]);
    }
    values[field.key] = value;
  }
  const unexpected = Object.keys(submitted).filter((key) => !allowed.has(key));
  if (unexpected.length > 0) {
    throw new DynamicDocumentError("INVALID_VALUES", unexpected);
  }
  const missing = fields
    .filter((field) => field.required)
    .filter((field) => {
      const value = values[field.key];
      return value == null || (typeof value === "string" && !value.trim());
    })
    .map((field) => field.key);
  if (missing.length > 0) {
    throw new DynamicDocumentError("INVALID_VALUES", missing);
  }
  return values;
}

function safeFilename(value: string) {
  const normalized = value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .toLowerCase();
  return normalized || "document";
}

export async function generateDynamicDocument(input: {
  templateId: number;
  cos: number;
  signataireId?: number | null;
  scope: EmployeeAccessScope;
  values: Record<string, TemplateValue>;
  format: "docx" | "pdf";
}) {
  const draft = await buildDynamicDocumentDraft(input);
  const { template, version } = await activeTemplate(input.templateId);
  const values = validateValues(draft.fields, input.values);
  const renderedValues = Object.fromEntries(
    draft.fields.map((field) => {
      const value = values[field.key];
      const renderedValue =
        typeof value === "string" && normalizeKey(field.key).includes("dateheure")
          ? frenchDateTimeFromLocal(value)
          : field.type === "date" && typeof value === "string"
            ? frenchDateFromIso(value)
            : value;
      return [field.key, renderedValue];
    }),
  );
  const source = await readFile(path.join(storageRoot(), version.nom_fichier_stockage));
  const document = new Docxtemplater(new PizZip(source), {
    paragraphLoop: true,
    linebreaks: true,
    nullGetter: () => "",
  });
  document.render(renderedValues);
  const docx = document.getZip().generate({
    type: "nodebuffer",
    compression: "DEFLATE",
  }) as Buffer;
  const filename = `${safeFilename(template.code)}-${input.cos}`;
  if (input.format === "docx") return { buffer: docx, filename };

  const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "rh-connect-doc-"));
  const docxPath = path.join(temporaryDirectory, `${filename}.docx`);
  const pdfPath = path.join(temporaryDirectory, `${filename}.pdf`);
  try {
    await writeFile(docxPath, docx);
    await execFileAsync(
      "soffice",
      [
        "--headless",
        "--nologo",
        "--nodefault",
        "--nolockcheck",
        "--convert-to",
        "pdf",
        "--outdir",
        temporaryDirectory,
        docxPath,
      ],
      {
        timeout: 60_000,
        env: { ...process.env, HOME: temporaryDirectory, SAL_USE_VCLPLUGIN: "svp" },
      },
    );
    return { buffer: await readFile(pdfPath), filename };
  } catch {
    throw new DynamicDocumentError("PDF_CONVERSION_FAILED");
  } finally {
    await rm(temporaryDirectory, { recursive: true, force: true });
  }
}
