import { prisma } from "@/lib/prisma";

export const ALERT_MAIL_GROUPS = [
  "direction",
  "adm",
  "cisp",
  "logistique",
  "atelier",
] as const;

export type AlertMailGroup = (typeof ALERT_MAIL_GROUPS)[number];

type AlertRule = {
  id: number;
  ordre: number | null;
  libelle: string | null;
  direction: boolean | null;
  adm: boolean | null;
  cisp: boolean | null;
  logistique: boolean | null;
  atelier: boolean | null;
};

type MailConfiguration = {
  id: number;
  adm_mail: string | null;
  cisp_mail: string | null;
  direction_mail: string | null;
  logistique_mail: string | null;
  atelier_mail: string | null;
};

export type AlertMailSectionItem = {
  id: number;
  occurrenceKey: string;
  message: string;
  urgent: boolean;
};

export type AlertMailSection = {
  category: string;
  order: number;
  messages: AlertMailSectionItem[];
};

type ActiveEmployee = {
  cos: number;
  lastName: string;
  firstName: string;
  birthDate: Date | null;
  residencePermitExpiry: Date | null;
  healthCoverageEnd: Date | null;
  mutualExemptionEnd: Date | null;
  sector: string;
  contractStart: Date | null;
  trialPeriodEnd: Date | null;
  contractEnd: Date | null;
  medicalVisits: Array<{
    id: number;
    scheduledAt: Date | null;
    purpose: string | null;
    fitness: string | null;
  }>;
};

type ParisCalendarDate = {
  year: number;
  month: number;
  day: number;
};

const GROUP_LABELS: Record<AlertMailGroup, string> = {
  direction: "Direction",
  adm: "Administratif",
  cisp: "CISP",
  logistique: "Logistique",
  atelier: "Atelier",
};

const BIRTHDAY_TODAY_CATEGORY = "Anniversaires aujourd'hui";
const BIRTHDAY_NEXT_MONTH_CATEGORY = "Anniversaires le mois prochain";
const RESIDENCE_PERMIT_LAST_DAY_CATEGORY =
  "Carte de séjour : dernier jour";
const RESIDENCE_PERMIT_EXPIRED_CATEGORY = "Carte de séjour : périmée";
const RESIDENCE_PERMIT_ONE_WEEK_CATEGORY = "Carte de séjour : - 1semaine";
const MEDICAL_VISITS_TOMORROW_CATEGORY = "Visites Médicales demain";
const HEALTH_COVERAGE_NEXT_MONTH_CATEGORY = "Fin de CMU mois prochain";
const TRIAL_PERIOD_CATEGORY = "Fin de Période d'essai";
const TRIAL_PERIOD_MUTUAL_CATEGORY =
  "Fin de période d'essai : Mutuelle ?";
const RENEWALS_CATEGORY = "Renouvellements";
const ECHELON_B_CATEGORY = "Passage échelon B";
const MUTUAL_EXEMPTION_END_CATEGORY = "Fin dérogation mutuelle";
const NIGHT_TEAM_END_CATEGORY = "Fin équipe de nuit";

// Ces deux horizons reproduisent le fonctionnement du relevé Access :
// les échéances des deux prochains mois sont affichées.
const TRIAL_PERIOD_LOOKAHEAD_DAYS = 60;
const RENEWAL_LOOKAHEAD_DAYS = 60;
const MUTUAL_EXEMPTION_LOOKAHEAD_DAYS = 10;
const NIGHT_TEAM_END_LOOKAHEAD_DAYS = 60;

const MONTH_LABELS = [
  "janvier",
  "février",
  "mars",
  "avril",
  "mai",
  "juin",
  "juillet",
  "août",
  "septembre",
  "octobre",
  "novembre",
  "décembre",
] as const;

export function isAlertMailGroup(value: string): value is AlertMailGroup {
  return ALERT_MAIL_GROUPS.includes(value as AlertMailGroup);
}

export function normalizeAlertCategory(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/\uFFFD/g, "")
    .toLocaleLowerCase("fr")
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

function isValidEmail(value: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

export function parseMailRecipients(value: string | null | undefined) {
  const uniqueRecipients = new Set<string>();
  const invalidRecipients = new Set<string>();

  for (const rawRecipient of value?.split(/[;,\r\n]+/) ?? []) {
    const recipient = rawRecipient.trim().toLocaleLowerCase("fr");

    if (!recipient) {
      continue;
    }

    if (!isValidEmail(recipient)) {
      invalidRecipients.add(recipient);
      continue;
    }

    uniqueRecipients.add(recipient);
  }

  return {
    recipients: [...uniqueRecipients],
    invalidRecipients: [...invalidRecipients],
  };
}

function isRuleEnabledForGroup(rule: AlertRule, group: AlertMailGroup) {
  switch (group) {
    case "direction":
      return rule.direction === true;
    case "adm":
      return rule.adm === true;
    case "cisp":
      return rule.cisp === true;
    case "logistique":
      return rule.logistique === true;
    case "atelier":
      return rule.atelier === true;
  }
}

function getParisCalendarDate(date = new Date()): ParisCalendarDate {
  const parts = new Intl.DateTimeFormat("fr-FR", {
    timeZone: "Europe/Paris",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).formatToParts(date);

  const values = new Map(
    parts.map((part) => [part.type, Number(part.value)]),
  );

  const year = values.get("year");
  const month = values.get("month");
  const day = values.get("day");

  if (!year || !month || !day) {
    throw new Error("UNABLE_TO_RESOLVE_PARIS_DATE");
  }

  return { year, month, day };
}

export function parseAlertMailReferenceDate(
  value: string | null | undefined,
): ParisCalendarDate | null {
  const normalizedValue = value?.trim();

  if (!normalizedValue) {
    return null;
  }

  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalizedValue);

  if (!match) {
    return null;
  }

  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const date = new Date(Date.UTC(year, month - 1, day));

  if (
    date.getUTCFullYear() !== year ||
    date.getUTCMonth() + 1 !== month ||
    date.getUTCDate() !== day
  ) {
    return null;
  }

  return { year, month, day };
}

function formatCalendarDate(date: ParisCalendarDate) {
  return (
    `${String(date.day).padStart(2, "0")}/` +
    `${String(date.month).padStart(2, "0")}/${date.year}`
  );
}

function formatOccurrenceDate(date: ParisCalendarDate) {
  return (
    `${String(date.year).padStart(4, "0")}-` +
    `${String(date.month).padStart(2, "0")}-` +
    `${String(date.day).padStart(2, "0")}`
  );
}

function addCalendarDays(date: ParisCalendarDate, numberOfDays: number) {
  const result = new Date(
    Date.UTC(date.year, date.month - 1, date.day + numberOfDays),
  );

  return {
    year: result.getUTCFullYear(),
    month: result.getUTCMonth() + 1,
    day: result.getUTCDate(),
  };
}

function addCalendarYears(date: ParisCalendarDate, numberOfYears: number) {
  const result = new Date(
    Date.UTC(date.year + numberOfYears, date.month - 1, date.day),
  );

  return {
    year: result.getUTCFullYear(),
    month: result.getUTCMonth() + 1,
    day: result.getUTCDate(),
  };
}

function formatDatabaseTime(date: Date) {
  return (
    `${String(date.getUTCHours()).padStart(2, "0")}:` +
    `${String(date.getUTCMinutes()).padStart(2, "0")}`
  );
}

function isCalendarDateBetween(
  value: ParisCalendarDate,
  start: ParisCalendarDate,
  end: ParisCalendarDate,
) {
  return (
    compareCalendarDates(value, start) >= 0 &&
    compareCalendarDates(value, end) <= 0
  );
}

function compareCalendarDates(
  left: ParisCalendarDate,
  right: ParisCalendarDate,
) {
  const leftValue = left.year * 10_000 + left.month * 100 + left.day;
  const rightValue = right.year * 10_000 + right.month * 100 + right.day;

  return leftValue - rightValue;
}

function getDatabaseCalendarDate(date: Date): ParisCalendarDate {
  // Les dates RH sont stockées comme des dates civiles à minuit dans MySQL.
  // Les lire en UTC évite qu'un décalage de fuseau change leur jour.
  return {
    year: date.getUTCFullYear(),
    month: date.getUTCMonth() + 1,
    day: date.getUTCDate(),
  };
}

function isActiveContract(
  contract: {
    DAE: Date | null;
    DSP: Date | null;
    DSR: Date | null;
    ETB: string | null;
  },
  today: ParisCalendarDate,
) {
  if (!contract.DAE || contract.DSR) {
    return false;
  }

  const establishment = contract.ETB?.trim().toLocaleUpperCase("fr");

  if (establishment !== "LESQUIN" && establishment !== "ENVIE LESQUIN") {
    return false;
  }

  if (
    compareCalendarDates(getDatabaseCalendarDate(contract.DAE), today) > 0
  ) {
    return false;
  }

  return (
    !contract.DSP ||
    compareCalendarDates(getDatabaseCalendarDate(contract.DSP), today) >= 0
  );
}

type DynamicAlertKind =
  | "birthday-today"
  | "birthday-next-month"
  | "residence-permit-last-day"
  | "residence-permit-expired"
  | "residence-permit-one-week"
  | "medical-visits-tomorrow"
  | "health-coverage-next-month"
  | "trial-period"
  | "trial-period-mutual"
  | "renewals"
  | "echelon-b"
  | "mutual-exemption-end"
  | "night-team-end"
  | "convocations-tomorrow"
  | "news-request-sent"
  | "meal-tickets-after-two-months"
  | "contract-suspension-end";

function getDynamicAlertKind(category: string): DynamicAlertKind | null {
  const normalizedCategory = normalizeAlertCategory(category);

  if (
    normalizedCategory === "anniversaire aujourd hui" ||
    normalizedCategory === "anniversaires aujourd hui"
  ) {
    return "birthday-today";
  }

  if (
    normalizedCategory === "anniversaire le mois prochain" ||
    normalizedCategory === "anniversaires le mois prochain"
  ) {
    return "birthday-next-month";
  }

  if (
    normalizedCategory.includes("carte de s") &&
    normalizedCategory.includes("jour") &&
    normalizedCategory.includes("1semaine")
  ) {
    return "residence-permit-one-week";
  }

  if (
    normalizedCategory.includes("carte de s") &&
    normalizedCategory.includes("dernier jour")
  ) {
    return "residence-permit-last-day";
  }

  if (
    normalizedCategory.includes("carte de s") &&
    (normalizedCategory.includes("perime") ||
      normalizedCategory.includes("prime"))
  ) {
    return "residence-permit-expired";
  }

  if (
    normalizedCategory.includes("visite") &&
    normalizedCategory.includes("demain") &&
    (normalizedCategory.includes("medical") ||
      normalizedCategory.includes("mdical"))
  ) {
    return "medical-visits-tomorrow";
  }

  if (
    normalizedCategory.includes("fin de cmu") &&
    normalizedCategory.includes("mois prochain")
  ) {
    return "health-coverage-next-month";
  }

  if (
    normalizedCategory.startsWith("fin de p") &&
    normalizedCategory.includes("d essai") &&
    normalizedCategory.includes("mutuelle")
  ) {
    return "trial-period-mutual";
  }

  if (
    normalizedCategory.startsWith("fin de p") &&
    normalizedCategory.endsWith("d essai")
  ) {
    return "trial-period";
  }

  if (
    normalizedCategory === "renouvellement" ||
    normalizedCategory === "renouvellements"
  ) {
    return "renewals";
  }

  if (
    normalizedCategory.startsWith("passage") &&
    normalizedCategory.endsWith("chelon b")
  ) {
    return "echelon-b";
  }

  if (
    normalizedCategory.startsWith("fin d") &&
    normalizedCategory.includes("rogation mutuelle")
  ) {
    return "mutual-exemption-end";
  }

  if (
    normalizedCategory.startsWith("fin") &&
    normalizedCategory.endsWith("quipe de nuit")
  ) {
    return "night-team-end";
  }

  // Ces catégories Access n'ont actuellement aucune date exploitable dans
  // les tables migrées. On ignore leurs anciens instantanés plutôt que
  // d'envoyer des informations périmées.
  if (
    normalizedCategory.includes("convocation") &&
    normalizedCategory.includes("lendemain")
  ) {
    return "convocations-tomorrow";
  }

  if (
    normalizedCategory.startsWith("demande de nouvelles") &&
    normalizedCategory.includes("envoy")
  ) {
    return "news-request-sent";
  }

  if (normalizedCategory.includes("titres restaurant")) {
    return "meal-tickets-after-two-months";
  }

  if (normalizedCategory.includes("suspension de contrat")) {
    return "contract-suspension-end";
  }

  return null;
}

function findDynamicAlertRule(
  rules: AlertRule[],
  kind: DynamicAlertKind,
) {
  const exactRule = rules.find((rule) => {
    if (!rule.libelle) {
      return false;
    }

    return getDynamicAlertKind(rule.libelle) === kind;
  });

  if (exactRule) {
    return exactRule;
  }

  if (
    kind === "birthday-today" ||
    kind === "birthday-next-month"
  ) {
    // L'ancienne base ne contient parfois qu'une règle générique
    // "Anniversaires aujourd'hui". Elle pilote alors les deux sections.
    return rules.find((rule) =>
      normalizeAlertCategory(rule.libelle ?? "").includes("anniversaire"),
    );
  }

  return undefined;
}

async function getActiveEmployees(referenceDate: ParisCalendarDate) {
  const employees = await prisma.employes.findMany({
    where: {
      Actif: true,
    },
    select: {
      COS: true,
      NSA: true,
      PRE: true,
      DAN: true,
      EXJ: true,
      DateFinCMU: true,
      DateFinDerogationMutuelle: true,
      visites_medicales: {
        select: {
          id: true,
          dvp: true,
          obv: true,
          apt: true,
        },
      },
      contrats: {
        where: {
          DAE: {
            not: null,
          },
        },
        orderBy: [{ DAE: "desc" }, { id: "desc" }],
        select: {
          DAE: true,
          DSP: true,
          DSR: true,
          ETB: true,
          SEC: true,
          Secteur: true,
          FPE: true,
        },
      },
    },
  });

  const activeEmployees: ActiveEmployee[] = [];

  for (const employee of employees) {
    if (employee.COS === null || !employee.NSA || !employee.PRE) {
      continue;
    }

    // On retient le dernier contrat commencé, puis on vérifie qu'il est
    // encore actif. Un ancien contrat sans DSR mais avec une DSP passée
    // n'est donc pas considéré comme actif.
    const latestStartedContract = employee.contrats.find((contract) => {
      if (!contract.DAE) {
        return false;
      }

      return (
        compareCalendarDates(
          getDatabaseCalendarDate(contract.DAE),
          referenceDate,
        ) <= 0
      );
    });

    if (
      !latestStartedContract ||
      !isActiveContract(latestStartedContract, referenceDate)
    ) {
      continue;
    }

    activeEmployees.push({
      cos: employee.COS,
      lastName: employee.NSA.trim(),
      firstName: employee.PRE.trim(),
      birthDate: employee.DAN,
      residencePermitExpiry: employee.EXJ,
      healthCoverageEnd: employee.DateFinCMU,
      mutualExemptionEnd: employee.DateFinDerogationMutuelle,
      sector:
        latestStartedContract.Secteur?.trim() ||
        latestStartedContract.SEC?.trim() ||
        "Secteur non renseigné",
      contractStart: latestStartedContract.DAE,
      trialPeriodEnd: latestStartedContract.FPE,
      contractEnd: latestStartedContract.DSP,
      medicalVisits: employee.visites_medicales.map((visit) => ({
        id: visit.id,
        scheduledAt: visit.dvp,
        purpose: visit.obv,
        fitness: visit.apt,
      })),
    });
  }

  return activeEmployees.sort(
    (left, right) =>
      left.lastName.localeCompare(right.lastName, "fr") ||
      left.firstName.localeCompare(right.firstName, "fr"),
  );
}

function buildBirthdayMessages(
  employees: ActiveEmployee[],
  referenceDate: ParisCalendarDate,
) {
  const nextMonth =
    referenceDate.month === 12 ? 1 : referenceDate.month + 1;
  const nextMonthYear =
    referenceDate.month === 12
      ? referenceDate.year + 1
      : referenceDate.year;

  const todayMessages: AlertMailSectionItem[] = [];
  const nextMonthMessages: AlertMailSectionItem[] = [];

  for (const employee of employees) {
    if (!employee.birthDate) {
      continue;
    }

    const birthDate = getDatabaseCalendarDate(employee.birthDate);
    const identity =
      `${employee.lastName} ${employee.firstName} (${employee.sector})`;

    if (
      birthDate.month === referenceDate.month &&
      birthDate.day === referenceDate.day
    ) {
      todayMessages.push({
        id: -(employee.cos * 10 + 1),
        occurrenceKey: `birthday-today:${employee.cos}:${referenceDate.year}`,
        message:
          `${identity} : Anniversaire ${
            referenceDate.year - birthDate.year
          } ans`,
        urgent: false,
      });
    }

    if (birthDate.month === nextMonth) {
      const day = String(birthDate.day).padStart(2, "0");

      nextMonthMessages.push({
        id: -(employee.cos * 10 + 2),
        occurrenceKey:
          `birthday-next-month:${employee.cos}:` +
          `${nextMonthYear}-${String(nextMonth).padStart(2, "0")}`,
        message:
          `${identity} : Anniversaire le ${day} ${MONTH_LABELS[nextMonth - 1]}`,
        urgent: false,
      });
    }
  }

  return {
    todayMessages,
    nextMonthMessages,
    nextMonthYear,
  };
}

function buildOperationalAlertMessages(
  employees: ActiveEmployee[],
  referenceDate: ParisCalendarDate,
) {
  const healthCoverageMonth =
    referenceDate.month === 12 ? 1 : referenceDate.month + 1;
  const healthCoverageYear =
    referenceDate.month === 12
      ? referenceDate.year + 1
      : referenceDate.year;
  const residencePermitTargetDate = addCalendarDays(referenceDate, 7);
  const tomorrow = addCalendarDays(referenceDate, 1);
  const trialPeriodEndDate = addCalendarDays(
    referenceDate,
    TRIAL_PERIOD_LOOKAHEAD_DAYS,
  );
  const renewalEndDate = addCalendarDays(
    referenceDate,
    RENEWAL_LOOKAHEAD_DAYS,
  );
  const mutualExemptionEndDate = addCalendarDays(
    referenceDate,
    MUTUAL_EXEMPTION_LOOKAHEAD_DAYS,
  );
  const nextMonth =
    referenceDate.month === 12 ? 1 : referenceDate.month + 1;
  const nextMonthYear =
    referenceDate.month === 12
      ? referenceDate.year + 1
      : referenceDate.year;

  const residencePermitLastDayMessages: AlertMailSectionItem[] = [];
  const residencePermitExpiredMessages: AlertMailSectionItem[] = [];
  const residencePermitMessages: AlertMailSectionItem[] = [];
  const medicalVisitMessages: AlertMailSectionItem[] = [];
  const healthCoverageMessages: AlertMailSectionItem[] = [];
  const trialPeriodMessages: AlertMailSectionItem[] = [];
  const trialPeriodMutualMessages: AlertMailSectionItem[] = [];
  const renewalMessages: AlertMailSectionItem[] = [];
  const echelonBMessages: AlertMailSectionItem[] = [];
  const mutualExemptionMessages: AlertMailSectionItem[] = [];

  for (const employee of employees) {
    const identity =
      `${employee.lastName} ${employee.firstName} (${employee.sector})`;

    if (employee.residencePermitExpiry) {
      const expiryDate = getDatabaseCalendarDate(
        employee.residencePermitExpiry,
      );

      if (compareCalendarDates(expiryDate, residencePermitTargetDate) === 0) {
        residencePermitMessages.push({
          id: -(employee.cos * 10 + 3),
          occurrenceKey:
            `residence-permit-one-week:${employee.cos}:` +
            formatOccurrenceDate(expiryDate),
          message:
            `${identity} : Carte de séjour expire dans 1 semaine ` +
            `(${formatCalendarDate(expiryDate)})`,
          urgent: false,
        });
      }

      if (compareCalendarDates(expiryDate, referenceDate) === 0) {
        residencePermitLastDayMessages.push({
          id: -(employee.cos * 100 + 10),
          occurrenceKey:
            `residence-permit-last-day:${employee.cos}:` +
            formatOccurrenceDate(expiryDate),
          message:
            `${identity} : Carte de séjour expire aujourd'hui ` +
            `(${formatCalendarDate(expiryDate)})`,
          urgent: true,
        });
      }

      if (compareCalendarDates(expiryDate, referenceDate) < 0) {
        residencePermitExpiredMessages.push({
          id: -(employee.cos * 100 + 11),
          occurrenceKey:
            `residence-permit-expired:${employee.cos}:` +
            formatOccurrenceDate(expiryDate),
          message:
            `${identity} : Carte de séjour périmée depuis le ` +
            formatCalendarDate(expiryDate),
          urgent: true,
        });
      }
    }

    for (const visit of employee.medicalVisits) {
      if (!visit.scheduledAt) {
        continue;
      }

      const fitness = normalizeAlertCategory(visit.fitness ?? "");

      if (fitness && fitness !== "en attente") {
        continue;
      }

      const scheduledDate = getDatabaseCalendarDate(visit.scheduledAt);

      if (compareCalendarDates(scheduledDate, tomorrow) !== 0) {
        continue;
      }

      const purpose = visit.purpose?.trim();

      medicalVisitMessages.push({
        id: -(visit.id * 100 + 12),
        occurrenceKey:
          `medical-visit-tomorrow:${visit.id}:` +
          formatOccurrenceDate(scheduledDate),
        message:
          `${identity} : visite médicale prévue le ` +
          `${formatCalendarDate(scheduledDate)} à ` +
          `${formatDatabaseTime(visit.scheduledAt)}` +
          (purpose ? ` (${purpose})` : ""),
        urgent: false,
      });
    }

    if (employee.healthCoverageEnd) {
      const healthCoverageEndDate = getDatabaseCalendarDate(
        employee.healthCoverageEnd,
      );

      if (
        healthCoverageEndDate.year === healthCoverageYear &&
        healthCoverageEndDate.month === healthCoverageMonth
      ) {
        healthCoverageMessages.push({
          id: -(employee.cos * 10 + 6),
          occurrenceKey:
            `health-coverage-next-month:${employee.cos}:` +
            formatOccurrenceDate(healthCoverageEndDate),
          message:
            `${identity} : fin de CMU le ` +
            formatCalendarDate(healthCoverageEndDate),
          urgent: false,
        });
      }
    }

    if (employee.trialPeriodEnd) {
      const trialDate = getDatabaseCalendarDate(employee.trialPeriodEnd);

      if (
        isCalendarDateBetween(
          trialDate,
          referenceDate,
          trialPeriodEndDate,
        )
      ) {
        trialPeriodMessages.push({
          id: -(employee.cos * 10 + 4),
          occurrenceKey:
            `trial-period:${employee.cos}:` +
            formatOccurrenceDate(trialDate),
          message:
            `${identity} : fin période d'essai le ` +
            formatCalendarDate(trialDate),
          urgent: false,
        });
        trialPeriodMutualMessages.push({
          id: -(employee.cos * 10 + 7),
          occurrenceKey:
            `trial-period-mutual:${employee.cos}:` +
            formatOccurrenceDate(trialDate),
          message:
            `${identity} fin de PE le ` +
            `${formatCalendarDate(trialDate)} - Mutuelle ?`,
          urgent: false,
        });
      }
    }

    if (employee.contractEnd) {
      const contractEndDate = getDatabaseCalendarDate(employee.contractEnd);

      if (
        isCalendarDateBetween(
          contractEndDate,
          referenceDate,
          renewalEndDate,
        )
      ) {
        renewalMessages.push({
          id: -(employee.cos * 10 + 5),
          occurrenceKey:
            `renewal:${employee.cos}:` +
            formatOccurrenceDate(contractEndDate),
          message:
            `${identity} : renouvellement le ` +
            formatCalendarDate(contractEndDate),
          urgent: false,
        });
      }
    }

    if (employee.contractStart) {
      const echelonDate = addCalendarYears(
        getDatabaseCalendarDate(employee.contractStart),
        1,
      );

      if (
        echelonDate.year === nextMonthYear &&
        echelonDate.month === nextMonth
      ) {
        echelonBMessages.push({
          id: -(employee.cos * 100 + 13),
          occurrenceKey:
            `echelon-b:${employee.cos}:` +
            formatOccurrenceDate(echelonDate),
          message:
            `${identity} : Passage échelon B le ` +
            formatCalendarDate(echelonDate),
          urgent: false,
        });
      }
    }

    if (employee.mutualExemptionEnd) {
      const exemptionEndDate = getDatabaseCalendarDate(
        employee.mutualExemptionEnd,
      );

      if (
        isCalendarDateBetween(
          exemptionEndDate,
          referenceDate,
          mutualExemptionEndDate,
        )
      ) {
        mutualExemptionMessages.push({
          id: -(employee.cos * 100 + 14),
          occurrenceKey:
            `mutual-exemption-end:${employee.cos}:` +
            formatOccurrenceDate(exemptionEndDate),
          message:
            `${identity} : fin de dérogation mutuelle le ` +
            formatCalendarDate(exemptionEndDate),
          urgent: false,
        });
      }
    }
  }

  return {
    residencePermitLastDayMessages,
    residencePermitExpiredMessages,
    residencePermitMessages,
    medicalVisitMessages,
    healthCoverageMessages,
    trialPeriodMessages,
    trialPeriodMutualMessages,
    renewalMessages,
    echelonBMessages,
    mutualExemptionMessages,
    healthCoverageMonth,
    healthCoverageYear,
    residencePermitTargetDate,
    tomorrow,
    trialPeriodEndDate,
    renewalEndDate,
    mutualExemptionEndDate,
  };
}

async function buildNightTeamEndMessages(
  employees: ActiveEmployee[],
  referenceDate: ParisCalendarDate,
) {
  const windowEnd = addCalendarDays(
    referenceDate,
    NIGHT_TEAM_END_LOOKAHEAD_DAYS,
  );
  const activeEmployeeIds = new Set(employees.map((employee) => employee.cos));
  const events = await prisma.equipenuit.findMany({
    where: {
      equipenuit_fin: {
        not: null,
      },
    },
    select: {
      id: true,
      cos: true,
      equipenuit_fin: true,
    },
    orderBy: [{ equipenuit_fin: "asc" }, { id: "asc" }],
  });
  const dates = new Map<string, ParisCalendarDate>();

  for (const event of events) {
    if (!event.equipenuit_fin) {
      continue;
    }

    const employeeId = Number(event.cos);

    if (!Number.isInteger(employeeId) || !activeEmployeeIds.has(employeeId)) {
      continue;
    }

    const endDate = getDatabaseCalendarDate(event.equipenuit_fin);

    if (!isCalendarDateBetween(endDate, referenceDate, windowEnd)) {
      continue;
    }

    dates.set(formatCalendarDate(endDate), endDate);
  }

  return {
    messages: [...dates.values()].map((date, index) => ({
      id: -(9_000_000 + index),
      occurrenceKey: `night-team-end:${formatOccurrenceDate(date)}`,
      message: `Fin de l'équipe de nuit le ${formatCalendarDate(date)}`,
      urgent: false,
    })),
    windowEnd,
  };
}

function getConfiguredRecipients(
  configuration: MailConfiguration | null,
  group: AlertMailGroup,
) {
  if (!configuration) {
    return null;
  }

  switch (group) {
    case "direction":
      return configuration.direction_mail;
    case "adm":
      return configuration.adm_mail;
    case "cisp":
      return configuration.cisp_mail;
    case "logistique":
      return configuration.logistique_mail;
    case "atelier":
      return configuration.atelier_mail;
  }
}

function escapeHtml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

export function buildAlertMailContent(
  subject: string,
  groupLabel: string,
  sections: AlertMailSection[],
) {
  const sectionsHtml = sections
    .map((section) => {
      const messagesHtml = section.messages
        .map((item) => {
          const urgentStyle = item.urgent
            ? "color:#b91c1c;font-weight:700;"
            : "color:#475569;";

          return `
            <li style="margin:0 0 8px 0;${urgentStyle}">
              ${escapeHtml(item.message)}
            </li>
          `;
        })
        .join("");

      return `
        <section style="margin:0 0 24px 0;">
          <h2 style="margin:0 0 10px 0;color:#0f172a;font-size:20px;">
            ${escapeHtml(section.category)}
          </h2>
          <ul style="margin:0;padding-left:24px;">
            ${messagesHtml}
          </ul>
        </section>
      `;
    })
    .join("");

  return `<!doctype html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <title>${escapeHtml(subject)}</title>
  </head>
  <body style="margin:0;background:#f8fafc;font-family:Arial,sans-serif;">
    <div style="max-width:900px;margin:0 auto;padding:24px;">
      <div style="background:#ffffff;border:1px solid #cbd5e1;padding:24px;">
        <h1 style="margin:0 0 6px 0;color:#0369a1;font-size:26px;">
          Alertes Ressources Humaines
        </h1>
        <p style="margin:0 0 28px 0;color:#64748b;">
          Groupe destinataire : ${escapeHtml(groupLabel)}
        </p>
        ${
          sectionsHtml ||
          '<p style="color:#64748b;">Aucune alerte à afficher.</p>'
        }
      </div>
    </div>
  </body>
</html>`;
}

export async function getAlertMailPreview(
  group: AlertMailGroup,
  requestedReferenceDate?: ParisCalendarDate,
) {
  const referenceDate = requestedReferenceDate ?? getParisCalendarDate();
  const [sourceMessages, rules, mailConfiguration] =
    await prisma.$transaction([
      prisma.alertes_mail_liste.findMany({
        orderBy: [{ rang: "asc" }, { id: "asc" }],
      }),
      prisma.listes_alertes.findMany({
        select: {
          id: true,
          ordre: true,
          libelle: true,
          direction: true,
          adm: true,
          cisp: true,
          logistique: true,
          atelier: true,
        },
        orderBy: [{ ordre: "asc" }, { id: "asc" }],
      }),
      prisma.mails_alertes.findFirst({
        select: {
          id: true,
          adm_mail: true,
          cisp_mail: true,
          direction_mail: true,
          logistique_mail: true,
          atelier_mail: true,
        },
        orderBy: {
          id: "desc",
        },
      }),
    ]);
  const activeEmployees = await getActiveEmployees(referenceDate);

  const birthdayMessages = buildBirthdayMessages(
    activeEmployees,
    referenceDate,
  );
  const operationalMessages = buildOperationalAlertMessages(
    activeEmployees,
    referenceDate,
  );
  const nightTeamEndMessages = await buildNightTeamEndMessages(
    activeEmployees,
    referenceDate,
  );

  const rulesByCategory = new Map<string, AlertRule>();

  for (const rule of rules) {
    if (!rule.libelle) {
      continue;
    }

    rulesByCategory.set(normalizeAlertCategory(rule.libelle), rule);
  }

  const sectionsByCategory = new Map<string, AlertMailSection>();
  const unmatchedCategories = new Set<string>();
  let matchedMessageCount = 0;

  for (const sourceMessage of sourceMessages) {
    const category = sourceMessage.categoriemessage?.trim();
    const message = sourceMessage.message?.trim();

    if (!category || !message) {
      continue;
    }

    // Ces anciennes lignes sont des instantanés Access. Elles sont
    // remplacées plus bas par un calcul effectué à la date demandée.
    if (getDynamicAlertKind(category)) {
      continue;
    }

    const rule = rulesByCategory.get(normalizeAlertCategory(category));

    if (!rule) {
      unmatchedCategories.add(category);
      continue;
    }

    matchedMessageCount += 1;

    if (!isRuleEnabledForGroup(rule, group)) {
      continue;
    }

    const sectionKey = normalizeAlertCategory(category);
    const existingSection = sectionsByCategory.get(sectionKey);
    const sectionOrder =
      sourceMessage.rang ?? rule.ordre ?? Number.MAX_SAFE_INTEGER;

    if (existingSection) {
      existingSection.order = Math.min(existingSection.order, sectionOrder);
      existingSection.messages.push({
        id: sourceMessage.id,
        occurrenceKey:
          `source:${sourceMessage.id}:` +
          `${sourceMessage.dateextraction?.toISOString() ?? "no-date"}:` +
          `${normalizeAlertCategory(category)}:${message}`,
        message,
        urgent: sourceMessage.urgent === true,
      });
      continue;
    }

    sectionsByCategory.set(sectionKey, {
      category,
      order: sectionOrder,
      messages: [
        {
          id: sourceMessage.id,
          occurrenceKey:
            `source:${sourceMessage.id}:` +
            `${sourceMessage.dateextraction?.toISOString() ?? "no-date"}:` +
            `${normalizeAlertCategory(category)}:${message}`,
          message,
          urgent: sourceMessage.urgent === true,
        },
      ],
    });
  }

  const dynamicSections: Array<{
    kind: DynamicAlertKind;
    category: string;
    messages: AlertMailSectionItem[];
  }> = [
    {
      kind: "birthday-today",
      category: BIRTHDAY_TODAY_CATEGORY,
      messages: birthdayMessages.todayMessages,
    },
    {
      kind: "birthday-next-month",
      category: BIRTHDAY_NEXT_MONTH_CATEGORY,
      messages: birthdayMessages.nextMonthMessages,
    },
    {
      kind: "residence-permit-last-day",
      category: RESIDENCE_PERMIT_LAST_DAY_CATEGORY,
      messages: operationalMessages.residencePermitLastDayMessages,
    },
    {
      kind: "residence-permit-expired",
      category: RESIDENCE_PERMIT_EXPIRED_CATEGORY,
      messages: operationalMessages.residencePermitExpiredMessages,
    },
    {
      kind: "residence-permit-one-week",
      category: RESIDENCE_PERMIT_ONE_WEEK_CATEGORY,
      messages: operationalMessages.residencePermitMessages,
    },
    {
      kind: "medical-visits-tomorrow",
      category: MEDICAL_VISITS_TOMORROW_CATEGORY,
      messages: operationalMessages.medicalVisitMessages,
    },
    {
      kind: "health-coverage-next-month",
      category: HEALTH_COVERAGE_NEXT_MONTH_CATEGORY,
      messages: operationalMessages.healthCoverageMessages,
    },
    {
      kind: "trial-period",
      category: TRIAL_PERIOD_CATEGORY,
      messages: operationalMessages.trialPeriodMessages,
    },
    {
      kind: "trial-period-mutual",
      category: TRIAL_PERIOD_MUTUAL_CATEGORY,
      messages: operationalMessages.trialPeriodMutualMessages,
    },
    {
      kind: "renewals",
      category: RENEWALS_CATEGORY,
      messages: operationalMessages.renewalMessages,
    },
    {
      kind: "echelon-b",
      category: ECHELON_B_CATEGORY,
      messages: operationalMessages.echelonBMessages,
    },
    {
      kind: "mutual-exemption-end",
      category: MUTUAL_EXEMPTION_END_CATEGORY,
      messages: operationalMessages.mutualExemptionMessages,
    },
    {
      kind: "night-team-end",
      category: NIGHT_TEAM_END_CATEGORY,
      messages: nightTeamEndMessages.messages,
    },
  ];

  for (const dynamicSection of dynamicSections) {
    const rule = findDynamicAlertRule(rules, dynamicSection.kind);

    if (!rule) {
      unmatchedCategories.add(dynamicSection.category);
      continue;
    }

    matchedMessageCount += dynamicSection.messages.length;

    if (
      !isRuleEnabledForGroup(rule, group) ||
      dynamicSection.messages.length === 0
    ) {
      continue;
    }

    sectionsByCategory.set(
      normalizeAlertCategory(dynamicSection.category),
      {
        category: dynamicSection.category,
        order: rule.ordre ?? Number.MAX_SAFE_INTEGER,
        messages: dynamicSection.messages,
      },
    );
  }

  const sections = [...sectionsByCategory.values()].sort(
    (left, right) =>
      left.order - right.order ||
      left.category.localeCompare(right.category, "fr"),
  );

  const rawRecipients = getConfiguredRecipients(mailConfiguration, group);
  const { recipients, invalidRecipients } =
    parseMailRecipients(rawRecipients);

  const groupLabel = GROUP_LABELS[group];
  const subject = `Alertes Ressources Humaines - ${groupLabel}`;
  const warnings: string[] = [];

  if (!mailConfiguration) {
    warnings.push("Aucune configuration mails_alertes n'a été trouvée.");
  }

  if (recipients.length === 0) {
    warnings.push(
      `Aucun destinataire valide n'est configuré pour le groupe ${groupLabel}.`,
    );
  }

  if (invalidRecipients.length > 0) {
    warnings.push(
      `${invalidRecipients.length} adresse(s) invalide(s) ont été ignorée(s).`,
    );
  }

  if (unmatchedCategories.size > 0) {
    warnings.push(
      `${unmatchedCategories.size} catégorie(s) ne correspondent à aucune règle de listes_alertes.`,
    );
  }

  if (sections.length === 0) {
    warnings.push(
      `Aucun message n'est activé pour le groupe ${groupLabel}.`,
    );
  }

  return {
    subject,
    group,
    groupLabel,
    recipients,
    sections,
    html: buildAlertMailContent(subject, groupLabel, sections),
    canSend:
      process.env.ALERT_MAIL_GRAPH_TEST_ENABLED?.trim().toLowerCase() ===
      "true",
    generatedAt: new Date().toISOString(),
    source: "alertes_mail_liste + échéances RH dynamiques",
    stats: {
      sourceMessages: sourceMessages.length,
      matchedMessages: matchedMessageCount,
      includedMessages: sections.reduce(
        (total, section) => total + section.messages.length,
        0,
      ),
      includedSections: sections.length,
      unmatchedCategories: [...unmatchedCategories],
      invalidRecipientCount: invalidRecipients.length,
      activeEmployeesChecked: activeEmployees.length,
      referenceDate: formatCalendarDate(referenceDate),
      birthdayNextMonth:
        `${
          MONTH_LABELS[
            (referenceDate.month === 12 ? 1 : referenceDate.month + 1) - 1
          ]
        } ` +
        `${birthdayMessages.nextMonthYear}`,
      residencePermitTargetDate: formatCalendarDate(
        operationalMessages.residencePermitTargetDate,
      ),
      medicalVisitsTargetDate: formatCalendarDate(
        operationalMessages.tomorrow,
      ),
      trialPeriodWindowEnd: formatCalendarDate(
        operationalMessages.trialPeriodEndDate,
      ),
      renewalWindowEnd: formatCalendarDate(
        operationalMessages.renewalEndDate,
      ),
      mutualExemptionWindowEnd: formatCalendarDate(
        operationalMessages.mutualExemptionEndDate,
      ),
      nightTeamEndWindowEnd: formatCalendarDate(
        nightTeamEndMessages.windowEnd,
      ),
    },
    warnings,
  };
}
