import { createHash, randomUUID } from "node:crypto";

import { prisma } from "@/lib/prisma";
import type { AlertMailGroup } from "@/services/alertes-mail.service";

type PrismaErrorWithCode = {
  code?: string;
};

export type AlertMailClaim = {
  occurrenceKey: string;
  occurrenceHash: string;
  claimToken: string;
};

function hasPrismaCode(error: unknown, code: string) {
  return (
    typeof error === "object" &&
    error !== null &&
    "code" in error &&
    (error as PrismaErrorWithCode).code === code
  );
}

function buildOccurrenceHash(group: AlertMailGroup, occurrenceKey: string) {
  return createHash("sha256")
    .update(`${group}\u0000${occurrenceKey}`, "utf8")
    .digest("hex");
}

export async function claimAlertMailOccurrences(
  group: AlertMailGroup,
  occurrenceKeys: string[],
) {
  const claims: AlertMailClaim[] = [];

  for (const occurrenceKey of [...new Set(occurrenceKeys)]) {
    const occurrenceHash = buildOccurrenceHash(group, occurrenceKey);
    const claimToken = randomUUID();

    try {
      await prisma.alertes_mail_envois.create({
        data: {
          occurrence_hash: occurrenceHash,
          occurrence_key: occurrenceKey,
          groupe: group,
          statut: "PROCESSING",
          claim_token: claimToken,
        },
      });

      claims.push({ occurrenceKey, occurrenceHash, claimToken });
      continue;
    } catch (error) {
      if (!hasPrismaCode(error, "P2002")) {
        throw error;
      }
    }

    // Seuls les échecs explicitement enregistrés sont repris. Une ligne restée
    // PROCESSING après un arrêt brutal est volontairement bloquée afin d'éviter
    // de renvoyer un mail dont le succès SMTP serait incertain.
    const reclaimed = await prisma.alertes_mail_envois.updateMany({
      where: {
        occurrence_hash: occurrenceHash,
        statut: "FAILED",
      },
      data: {
        statut: "PROCESSING",
        claim_token: claimToken,
        tentatives: { increment: 1 },
        reserve_le: new Date(),
        echec_le: null,
        derniere_erreur: null,
      },
    });

    if (reclaimed.count === 1) {
      claims.push({ occurrenceKey, occurrenceHash, claimToken });
    }
  }

  return claims;
}

export async function markAlertMailClaimsSent(
  claims: AlertMailClaim[],
  details: {
    recipients: string[];
    messageId?: string | null;
  },
) {
  if (claims.length === 0) {
    return;
  }

  const sentAt = new Date();
  const recipients = details.recipients.join("; ");

  await prisma.$transaction(
    claims.map((claim) =>
      prisma.alertes_mail_envois.updateMany({
        where: {
          occurrence_hash: claim.occurrenceHash,
          claim_token: claim.claimToken,
          statut: "PROCESSING",
        },
        data: {
          statut: "SENT",
          destinataires: recipients,
          message_id: details.messageId ?? null,
          envoye_le: sentAt,
        },
      }),
    ),
  );
}

export async function markAlertMailClaimsFailed(
  claims: AlertMailClaim[],
  error: unknown,
) {
  if (claims.length === 0) {
    return;
  }

  const failedAt = new Date();
  const message =
    error instanceof Error ? error.message.slice(0, 4_000) : String(error).slice(0, 4_000);

  await prisma.$transaction(
    claims.map((claim) =>
      prisma.alertes_mail_envois.updateMany({
        where: {
          occurrence_hash: claim.occurrenceHash,
          claim_token: claim.claimToken,
          statut: "PROCESSING",
        },
        data: {
          statut: "FAILED",
          derniere_erreur: message,
          echec_le: failedAt,
        },
      }),
    ),
  );
}

