import { readFile } from "node:fs/promises";
import path from "node:path";

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 ATTESTATION_IMPOTS_TEMPLATE = path.join(
  process.cwd(),
  "public",
  "templates",
  "documents",
  "ATTESTATION_Impots_RH_Connect.docx",
);

export type DocumentGenerationErrorCode =
  | "EMPLOYE_NOT_FOUND"
  | "CONTRACT_NOT_FOUND"
  | "SIGNATAIRE_NOT_FOUND"
  | "MISSING_DOCUMENT_DATA";

export class DocumentGenerationError extends Error {
  constructor(
    public readonly code: DocumentGenerationErrorCode,
    public readonly missingFields: string[] = [],
  ) {
    super(code);
    this.name = "DocumentGenerationError";
  }
}

export type AttestationImpotsTemplateData = {
  formuleSoussignataire: string;
  signataireNom: string;
  signataireFonction: string;
  titreNomPrenom: string;
  domicilie: string;
  adresseLigne: string;
  typeContrat: string;
  dateEmbauche: string;
  poste: string;
  dateDocument: string;
};

export type AttestationImpotsPdfData = {
  formuleSoussignataire: string;
  signataireNom: string;
  signataireFonction: string;
  titreNomPrenom: string;
  domicilie: string;
  adresseLigne: string;
  entreprise: string;
  typeContrat: string;
  dateEmbauche: string;
  dateFinContrat: string;
  poste: string;
  dateDocument: string;
  lieuDocument: string;
  estToujoursDansEffectifs: boolean;
};

export type AttestationRetraitePdfData = Omit<
  AttestationImpotsPdfData,
  "dateFinContrat" | "estToujoursDansEffectifs"
> & {
  dateSortiePrevue: string;
  dateRetraite: string;
};

export type AttestationEmploiPdfData = Pick<
  AttestationImpotsPdfData,
  | "formuleSoussignataire"
  | "signataireNom"
  | "signataireFonction"
  | "titreNomPrenom"
  | "domicilie"
  | "adresseLigne"
  | "entreprise"
  | "dateDocument"
  | "lieuDocument"
> & {
  contratsTexte: string;
};

export type AttestationMutuellePdfData = AttestationImpotsPdfData & {
  dateEcheanceMutuelle: string;
};

export type AttestationProlongationMutuellePdfData = Omit<
  AttestationImpotsPdfData,
  "poste" | "dateFinContrat" | "estToujoursDansEffectifs"
> & {
  dateSortiePrevue: string;
  dateReprise: string;
};

export type AutorisationPrefecturePdfData = {
  correspondantNom: string;
  correspondantFonction: string;
  entreprise: string;
  adresseEntreprise: string;
  telephoneEntreprise: string;
  siret: string;
  urssaf: string;
  nomPrenom: string;
  adresseSalarie: string;
  dateNaissance: string;
  paysNaissance: string;
  nationalite: string;
  numeroSecuriteSociale: string;
  titreSejour: string;
  motifSejour: string;
  numeroCarteSejour: string;
  dateDelivrance: string;
  dateExpiration: string;
  dateDebutContrat: string;
  dureeContrat: string;
  typeContrat: string;
  poste: string;
  heuresHebdomadaires: string;
  lieuDocument: string;
  dateDocument: string;
};

function clean(value: string | null | undefined) {
  return (value ?? "").trim();
}

function formatFrenchDate(date: Date) {
  return new Intl.DateTimeFormat("fr-FR", {
    timeZone: "Europe/Paris",
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  }).format(date);
}

function formatIsoDate(date: Date | null | undefined) {
  if (!date) return "";

  return new Intl.DateTimeFormat("fr-CA", {
    timeZone: "Europe/Paris",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(date);
}

function addOneDay(value: string) {
  const [year, month, day] = value.split("-").map(Number);

  if (!year || !month || !day) return "";

  const nextDay = new Date(Date.UTC(year, month - 1, day + 1, 12));
  return nextDay.toISOString().slice(0, 10);
}

function normalizeCivilite(value: string | null | undefined) {
  const normalized = clean(value)
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[.\s]/g, "")
    .toLowerCase();

  if (["mme", "madame", "mlle", "mademoiselle"].includes(normalized)) {
    return { label: normalized === "mlle" || normalized === "mademoiselle" ? "Mademoiselle" : "Madame", domicilie: "domiciliée" };
  }

  if (["m", "mr", "monsieur"].includes(normalized)) {
    return { label: "Monsieur", domicilie: "domicilié" };
  }

  return { label: clean(value), domicilie: "domicilié(e)" };
}

export async function listActiveSignataires() {
  return prisma.ref_signataires.findMany({
    where: { actif: true },
    orderBy: [{ prenomnom: "asc" }, { id: "asc" }],
    select: {
      id: true,
      prenomnom: true,
      fonction: true,
    },
  });
}

export async function buildAttestationImpotsData(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationImpotsTemplateData> {
  const employe = await getEmployeByCos(cos, scope);

  if (!employe) {
    throw new DocumentGenerationError("EMPLOYE_NOT_FOUND");
  }

  const contrat = await prisma.contrats.findFirst({
    where: {
      Id_Salarie: cos,
    },
    orderBy: [{ DAE: "desc" }, { id: "desc" }],
    select: {
      DAE: true,
      TCS: true,
      POS: true,
      QUA: true,
    },
  });

  if (!contrat) {
    throw new DocumentGenerationError("CONTRACT_NOT_FOUND");
  }

  const signataire = await prisma.ref_signataires.findFirst({
    where: { id: signataireId, actif: true },
    select: { prenomnom: true, fonction: true },
  });

  if (!signataire) {
    throw new DocumentGenerationError("SIGNATAIRE_NOT_FOUND");
  }

  const civilite = normalizeCivilite(employe.TIT);
  const nomComplet = [clean(employe.PRE), clean(employe.NSA)]
    .filter(Boolean)
    .join(" ");
  const titreNomPrenom = [civilite.label, nomComplet].filter(Boolean).join(" ");
  const adresseLigne = [
    clean(employe.ADR),
    [clean(employe.COP), clean(employe.VIL)].filter(Boolean).join(" "),
  ]
    .filter(Boolean)
    .join(", ");
  const poste = clean(contrat.QUA) || clean(contrat.POS);

  const values = {
    signataireNom: clean(signataire.prenomnom),
    signataireFonction: clean(signataire.fonction),
    titreNomPrenom,
    adresseLigne,
    typeContrat: clean(contrat.TCS),
    poste,
  };
  const missingFields = Object.entries(values)
    .filter(([, value]) => !value)
    .map(([field]) => field);

  if (!contrat.DAE) missingFields.push("dateEmbauche");

  if (missingFields.length > 0) {
    throw new DocumentGenerationError("MISSING_DOCUMENT_DATA", missingFields);
  }

  return {
    formuleSoussignataire: "soussigné(e)",
    signataireNom: values.signataireNom,
    signataireFonction: values.signataireFonction,
    titreNomPrenom: values.titreNomPrenom,
    domicilie: civilite.domicilie,
    adresseLigne: values.adresseLigne,
    typeContrat: values.typeContrat,
    dateEmbauche: formatFrenchDate(contrat.DAE!),
    poste: values.poste,
    dateDocument: formatFrenchDate(now),
  };
}

export async function buildAttestationImpotsPdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationImpotsPdfData> {
  const employe = await getEmployeByCos(cos, scope);

  if (!employe) {
    throw new DocumentGenerationError("EMPLOYE_NOT_FOUND");
  }

  const contrat = await prisma.contrats.findFirst({
    where: { Id_Salarie: cos },
    orderBy: [{ DAE: "desc" }, { id: "desc" }],
    select: {
      DAE: true,
      DSP: true,
      DSR: true,
      TCS: true,
      POS: true,
      QUA: true,
      ETB: true,
    },
  });

  if (!contrat) {
    throw new DocumentGenerationError("CONTRACT_NOT_FOUND");
  }

  const signataire = await prisma.ref_signataires.findFirst({
    where: { id: signataireId, actif: true },
    select: { prenomnom: true, fonction: true },
  });

  if (!signataire) {
    throw new DocumentGenerationError("SIGNATAIRE_NOT_FOUND");
  }

  const etablissement = contrat.ETB
    ? await prisma.etablissement.findFirst({
        where: { etb: contrat.ETB },
        select: { entreprise: true },
      })
    : null;
  const civilite = normalizeCivilite(employe.TIT);
  const nomComplet = [clean(employe.PRE), clean(employe.NSA)]
    .filter(Boolean)
    .join(" ");
  const adresseLigne = [
    clean(employe.ADR),
    [clean(employe.COP), clean(employe.VIL)].filter(Boolean).join(" "),
  ]
    .filter(Boolean)
    .join(", ");
  const estToujoursDansEffectifs =
    contrat.DSR === null &&
    (contrat.DSP === null || formatIsoDate(contrat.DSP) >= formatIsoDate(now));

  return {
    formuleSoussignataire: "soussigné(e)",
    signataireNom: clean(signataire.prenomnom),
    signataireFonction: clean(signataire.fonction),
    titreNomPrenom: [civilite.label, nomComplet].filter(Boolean).join(" "),
    domicilie: civilite.domicilie,
    adresseLigne,
    entreprise: clean(etablissement?.entreprise) || "S.A.S. ENVIE 2E NORD",
    typeContrat: clean(contrat.TCS),
    dateEmbauche: formatIsoDate(contrat.DAE),
    dateFinContrat: formatIsoDate(contrat.DSR ?? contrat.DSP),
    poste: clean(contrat.QUA) || clean(contrat.POS),
    dateDocument: formatIsoDate(now),
    lieuDocument: "Lesquin",
    estToujoursDansEffectifs,
  };
}

export async function buildAttestationRetraitePdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationRetraitePdfData> {
  const impotsDraft = await buildAttestationImpotsPdfDraft(
    cos,
    signataireId,
    scope,
    now,
  );
  const dateSortiePrevue = impotsDraft.dateFinContrat;

  return {
    formuleSoussignataire: impotsDraft.formuleSoussignataire,
    signataireNom: impotsDraft.signataireNom,
    signataireFonction: impotsDraft.signataireFonction,
    titreNomPrenom: impotsDraft.titreNomPrenom,
    domicilie: impotsDraft.domicilie,
    adresseLigne: impotsDraft.adresseLigne,
    entreprise: impotsDraft.entreprise,
    typeContrat: impotsDraft.typeContrat,
    dateEmbauche: impotsDraft.dateEmbauche,
    dateSortiePrevue,
    dateRetraite: addOneDay(dateSortiePrevue),
    poste: impotsDraft.poste,
    dateDocument: impotsDraft.dateDocument,
    lieuDocument: impotsDraft.lieuDocument,
  };
}

export async function buildAttestationEmploiPdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationEmploiPdfData> {
  const common = await buildAttestationImpotsPdfDraft(
    cos,
    signataireId,
    scope,
    now,
  );
  const contrats = await prisma.contrats.findMany({
    where: { Id_Salarie: cos },
    orderBy: [{ DAE: "asc" }, { id: "asc" }],
    select: { DAE: true, DSP: true, DSR: true, TCS: true, POS: true, QUA: true },
  });
  const contratsTexte = contrats
    .map((contrat) => {
      const periode = [
        formatIsoDate(contrat.DAE),
        formatIsoDate(contrat.DSR ?? contrat.DSP),
      ]
        .filter(Boolean)
        .join(" au ");
      const poste = clean(contrat.QUA) || clean(contrat.POS);
      return [clean(contrat.TCS), periode && `du ${periode}`, poste && `- ${poste}`]
        .filter(Boolean)
        .join(" ");
    })
    .filter(Boolean)
    .join("\n");

  return {
    formuleSoussignataire: common.formuleSoussignataire,
    signataireNom: common.signataireNom,
    signataireFonction: common.signataireFonction,
    titreNomPrenom: common.titreNomPrenom,
    domicilie: common.domicilie,
    adresseLigne: common.adresseLigne,
    entreprise: common.entreprise,
    contratsTexte,
    dateDocument: common.dateDocument,
    lieuDocument: common.lieuDocument,
  };
}

export async function buildAttestationMutuellePdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationMutuellePdfData> {
  const common = await buildAttestationImpotsPdfDraft(
    cos,
    signataireId,
    scope,
    now,
  );
  const employe = await getEmployeByCos(cos, scope);

  return {
    ...common,
    dateEcheanceMutuelle: formatIsoDate(employe?.DateFinDerogationMutuelle),
  };
}

export async function buildAttestationProlongationMutuellePdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AttestationProlongationMutuellePdfData> {
  const common = await buildAttestationImpotsPdfDraft(
    cos,
    signataireId,
    scope,
    now,
  );
  const dateSortiePrevue = common.dateFinContrat;

  return {
    formuleSoussignataire: common.formuleSoussignataire,
    signataireNom: common.signataireNom,
    signataireFonction: common.signataireFonction,
    titreNomPrenom: common.titreNomPrenom,
    domicilie: common.domicilie,
    adresseLigne: common.adresseLigne,
    entreprise: common.entreprise,
    typeContrat: common.typeContrat,
    dateEmbauche: common.dateEmbauche,
    dateSortiePrevue,
    dateReprise: addOneDay(dateSortiePrevue),
    dateDocument: common.dateDocument,
    lieuDocument: common.lieuDocument,
  };
}

export async function buildAutorisationPrefecturePdfDraft(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
  now = new Date(),
): Promise<AutorisationPrefecturePdfData> {
  const common = await buildAttestationImpotsPdfDraft(
    cos,
    signataireId,
    scope,
    now,
  );
  const employe = await getEmployeByCos(cos, scope);
  const contrat = await prisma.contrats.findFirst({
    where: { Id_Salarie: cos },
    orderBy: [{ DAE: "desc" }, { id: "desc" }],
    select: { DAE: true, DSP: true, DSR: true, TCS: true, POS: true, QUA: true, NBHRHEB: true, ETB: true },
  });
  const etablissement = contrat?.ETB
    ? await prisma.etablissement.findFirst({
        where: { etb: contrat.ETB },
        select: { adresseetb: true, adressesiege: true },
      })
    : null;

  return {
    correspondantNom: common.signataireNom,
    correspondantFonction: common.signataireFonction,
    entreprise: common.entreprise,
    adresseEntreprise:
      clean(etablissement?.adresseetb) ||
      clean(etablissement?.adressesiege) ||
      "CS 60500 - 2 boulevard Thomson, 59815 LESQUIN CEDEX",
    telephoneEntreprise: "03.20.30.76.16",
    siret: "484 873 955 00021",
    urssaf: "590 1503390465",
    nomPrenom: common.titreNomPrenom,
    adresseSalarie: common.adresseLigne,
    dateNaissance: formatIsoDate(employe?.DAN),
    paysNaissance: clean(employe?.LNA),
    nationalite: clean(employe?.NAT),
    numeroSecuriteSociale: clean(employe?.NSS),
    titreSejour: clean(employe?.TypeCS),
    motifSejour: "",
    numeroCarteSejour: clean(employe?.NCJ),
    dateDelivrance: "",
    dateExpiration: formatIsoDate(employe?.EXJ),
    dateDebutContrat: formatIsoDate(contrat?.DAE),
    dureeContrat:
      contrat?.DSP || contrat?.DSR ? "Contrat à durée déterminée" : "Durée indéterminée",
    typeContrat: clean(contrat?.TCS),
    poste: clean(contrat?.QUA) || clean(contrat?.POS),
    heuresHebdomadaires: contrat?.NBHRHEB?.toString() ?? "",
    lieuDocument: common.lieuDocument,
    dateDocument: common.dateDocument,
  };
}

export async function renderAttestationImpots(
  data: AttestationImpotsTemplateData,
) {
  const template = await readFile(ATTESTATION_IMPOTS_TEMPLATE);
  const zip = new PizZip(template);
  const document = new Docxtemplater(zip, {
    paragraphLoop: true,
    linebreaks: true,
    nullGetter: () => "",
  });

  document.render(data);

  return document.getZip().generate({
    type: "nodebuffer",
    compression: "DEFLATE",
  }) as Buffer;
}

export async function generateAttestationImpots(
  cos: number,
  signataireId: number,
  scope: EmployeeAccessScope,
) {
  const data = await buildAttestationImpotsData(cos, signataireId, scope);
  return renderAttestationImpots(data);
}
