// Importation d'un jeu de démonstration (`jeu-demo.json`).
//
// ADDITIF ET IDEMPOTENT, sans exception. Aucun `delete`, aucune remise à zéro :
// cet import peut être déclenché depuis l'Administration d'une installation qui
// contient déjà des données réelles. Une seule ligne de production détruite par
// un chargement de démo serait un défaut inacceptable, alors qu'un doublon se
// corrige.
//
// Les clés de rejeu sont NATURELLES, jamais les identifiants de base :
//   · organisation → `code`      · utilisateur → `email`
//   · baie/salle   → `code` + parent  · intervention → `numero` + organisation
// Relancer l'import complète ce qui manque au lieu de dupliquer.
//
// DATES : le fichier porte des décalages en jours ; ils sont résolus ici contre
// le jour de l'import. Le jeu est donc toujours « frais », quelle que soit son
// ancienneté — voir l'en-tête de `format.ts`.

import type { Prisma, PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";

import { calculerRecette } from "@/lib/verdict-optique";
import { construireFichePdf } from "@/server/services/fiche-pdf";
import { construireRapportRecettePdf } from "@/server/services/rapport-recette-pdf";
import { enregistrer } from "@/server/storage/storage";

import type { JeuDemo, InterventionDemo } from "./format";

const JOUR_MS = 86_400_000;
const MINUTE_MS = 60_000;

export type BilanImport = {
  clients: number;
  utilisateurs: number;
  fournisseurs: number;
  equipements: number;
  interventions: number;
  livrables: number;
  /** Liens optiques mesurés et évalués (module de recette I2). */
  recettesOptiques: number;
  /** Interventions rangées hors vue opérationnelle. */
  archivees: number;
  /** Éléments déjà présents et laissés intacts — la mesure de l'idempotence. */
  ignores: number;
  /** Anomalies non bloquantes (référence inconnue, PDF non généré…). */
  avertissements: string[];
};

export type OptionsImport = {
  /**
   * Compte qui déclenche l'import. Il est rattaché à toutes les organisations de
   * démonstration : sans cela, l'administrateur importerait des données qu'il ne
   * pourrait pas voir, le guard tenant les lui masquant intégralement.
   */
  adminId: string;
  /** Jour de référence des décalages. Injectable pour rendre les tests déterministes. */
  maintenant?: Date;
};

/** Minuit du jour de référence : les décalages en jours doivent partir d'une base stable. */
function jourZero(maintenant: Date): number {
  const d = new Date(maintenant);
  d.setHours(0, 0, 0, 0);
  return d.getTime();
}

/**
 * Crée un lien optique, ses mesures OTDR et le résultat de recette.
 *
 * ⚠️ Les VERDICTS ne viennent PAS du fichier : ils sont recalculés ici par
 * `calculerRecette`, la fonction de production. Un verdict figé dans le JSON
 * pourrait contredire les seuils normatifs de l'application — et une
 * démonstration qui affiche « conforme » sur un lien hors norme démontre
 * l'inverse de ce que le module sert à prouver.
 *
 * Renvoie le nombre de livrables produits (rapports de mesure PDF).
 */
async function creerRecetteOptique(o: {
  prisma: PrismaClient;
  tenantId: string;
  ticketId: string;
  tenantNom: string;
  recette: NonNullable<InterventionDemo["recetteOptique"]>;
  operateur: string;
  dateMesure: Date;
  auteurId: string;
}): Promise<number> {
  const { prisma, tenantId, ticketId, recette } = o;

  const lien = await prisma.lienOptique.create({
    data: {
      tenantId,
      ticketId,
      reference: recette.reference,
      typeFibre: recette.typeFibre,
      connectique: recette.connectique,
      polissage: recette.polissage,
      longueurM: recette.longueurM,
      nbConnecteurs: recette.nbConnecteurs,
      nbEpissures: recette.nbEpissures,
      baieDepart: recette.baieDepart,
      uDepart: recette.uDepart,
      tiroirDepart: "Tiroir optique",
      baieArrivee: recette.baieArrivee,
      uArrivee: recette.uArrivee,
      tiroirArrivee: "Tiroir optique",
    },
  });

  let livrables = 0;

  for (const m of recette.mesures) {
    const mesure = await prisma.mesureOptique.create({
      data: {
        tenantId,
        lienId: lien.id,
        longueurOnde: m.longueurOnde,
        sens: m.sens,
        attenuationTotaleDb: m.attenuationTotaleDb,
        appareil: m.appareil,
        operateur: o.operateur,
        dateMesure: o.dateMesure,
      },
    });

    const resultat = calculerRecette(
      {
        typeFibre: recette.typeFibre,
        polissage: recette.polissage,
        nbConnecteurs: recette.nbConnecteurs,
        nbEpissures: recette.nbEpissures,
        longueurM: recette.longueurM,
      },
      m.evenements.map((e) => ({
        ordre: e.ordre,
        type: e.type,
        perteDb: e.perteDb,
        returnLossDb: e.returnLossDb,
      })),
      m.attenuationTotaleDb,
      m.longueurOnde,
    );

    const verdictParOrdre = new Map(resultat.evenements.map((v) => [v.ordre, v]));

    for (const e of m.evenements) {
      await prisma.evenementOTDR.create({
        data: {
          tenantId,
          mesureId: mesure.id,
          ordre: e.ordre,
          distanceM: e.distanceM,
          type: e.type,
          perteDb: e.perteDb,
          returnLossDb: e.returnLossDb,
          verdictEvenement: verdictParOrdre.get(e.ordre)?.conformeNorme ? "conforme" : "non_conforme",
        },
      });
    }

    await prisma.recetteResultat.create({
      data: {
        tenantId,
        lienId: lien.id,
        mesureId: mesure.id,
        verdictGlobal: resultat.verdictGlobal,
        verdictObjectifInterne: resultat.verdictObjectifInterne,
        perteMaxConnecteur: resultat.perteMaxConnecteur,
        rlMin: resultat.rlMin,
        attenuationMesuree: resultat.attenuationMesuree,
        budgetTheorique: resultat.budgetTheorique,
        margeBudgetDb: resultat.margeBudgetDb,
        evenementsFautifs: JSON.stringify(resultat.evenementsFautifs),
        dateCalcul: o.dateMesure,
      },
    });

    // Rapport de mesure : produit par le service de production, comme la fiche.
    const pdf = await construireRapportRecettePdf({
      tenantNom: o.tenantNom,
      reference: recette.reference,
      typeFibre: recette.typeFibre,
      connectique: recette.connectique,
      polissage: recette.polissage,
      longueurM: recette.longueurM,
      nbConnecteurs: recette.nbConnecteurs,
      baieDepart: recette.baieDepart,
      uDepart: recette.uDepart,
      baieArrivee: recette.baieArrivee,
      uArrivee: recette.uArrivee,
      longueurOnde: m.longueurOnde,
      sens: m.sens,
      appareil: m.appareil,
      operateur: o.operateur,
      dateMesure: o.dateMesure,
      attenuationMesuree: resultat.attenuationMesuree,
      budgetTheorique: resultat.budgetTheorique,
      margeBudgetDb: resultat.margeBudgetDb,
      perteMaxConnecteur: resultat.perteMaxConnecteur,
      rlMin: resultat.rlMin,
      verdictGlobal: resultat.verdictGlobal,
      verdictObjectifInterne: resultat.verdictObjectifInterne,
      evenements: m.evenements.map((e) => ({
        ordre: e.ordre,
        distanceM: e.distanceM,
        type: e.type,
        perteDb: e.perteDb,
        returnLossDb: e.returnLossDb,
        verdictEvenement: verdictParOrdre.get(e.ordre)?.conformeNorme ? "conforme" : "non_conforme",
      })),
    });

    const nomFichier = `Recette_${recette.reference.replace(/[^A-Za-z0-9-]/g, "_")}_${m.longueurOnde}nm.pdf`;
    const { chemin, taille } = await enregistrer(tenantId, nomFichier, Buffer.from(pdf), "application/pdf");
    await prisma.livrable.create({
      data: {
        tenantId,
        ticketId,
        type: "rapport_mesure",
        nomFichier,
        cheminStockage: chemin,
        taille,
        mime: "application/pdf",
        auteurId: o.auteurId,
        horodatage: o.dateMesure,
      },
    });
    livrables += 1;
  }

  return livrables;
}

export async function importerJeuDemo(
  prisma: PrismaClient,
  jeu: JeuDemo,
  { adminId, maintenant = new Date() }: OptionsImport,
): Promise<BilanImport> {
  const bilan: BilanImport = {
    clients: 0,
    utilisateurs: 0,
    fournisseurs: 0,
    equipements: 0,
    interventions: 0,
    livrables: 0,
    recettesOptiques: 0,
    archivees: 0,
    ignores: 0,
    avertissements: [],
  };

  const base = jourZero(maintenant);
  const dateDe = (jours: number, heure = 9, minute = 0) =>
    new Date(base + jours * JOUR_MS + heure * 3_600_000 + minute * MINUTE_MS);

  const hash = await bcrypt.hash(jeu.motDePasse, 10);

  // ── 1. Fournisseurs ───────────────────────────────────────────
  // Clés naturelles RÉELLEMENT créées — pas celles simplement réutilisées.
  // C'est ce qui rend la purge sûre : sans cette distinction, supprimer « la
  // démo » effacerait une organisation réelle qui partagerait un code, ou un
  // fournisseur que l'exploitant utilisait déjà.
  const creesTenants: string[] = [];
  const creesUtilisateurs: string[] = [];
  const creesFournisseurs: string[] = [];

  const fournisseurParNom = new Map<string, string>();
  for (const f of jeu.fournisseurs) {
    const existe = await prisma.fournisseur.findFirst({ where: { nom: f.nom } });
    if (existe) {
      fournisseurParNom.set(f.nom, existe.id);
      bilan.ignores += 1;
      continue;
    }
    const cree = await prisma.fournisseur.create({ data: { ...f, actif: true } });
    fournisseurParNom.set(f.nom, cree.id);
    creesFournisseurs.push(f.nom);
    bilan.fournisseurs += 1;
  }

  // ── 2. Organisations, sites, salles, baies, équipements, SLA ──
  const tenantParCode = new Map<string, string>();
  const baieParCle = new Map<string, { id: string; rangeeAllee: string | null; zoneCage: string | null }>();
  const equipementParCle = new Map<string, string>();

  for (const c of jeu.clients) {
    let tenant = await prisma.tenant.findUnique({ where: { code: c.code } });
    if (tenant) bilan.ignores += 1;
    else {
      tenant = await prisma.tenant.create({ data: { code: c.code, nom: c.nom, statut: "actif" } });
      creesTenants.push(c.code);
      bilan.clients += 1;
    }
    tenantParCode.set(c.code, tenant.id);

    let site = await prisma.site.findFirst({ where: { tenantId: tenant.id, code: c.site.code } });
    site ??= await prisma.site.create({ data: { tenantId: tenant.id, code: c.site.code, nom: c.site.nom } });

    for (const s of c.salles) {
      let salle = await prisma.salle.findFirst({ where: { tenantId: tenant.id, code: s.code } });
      salle ??= await prisma.salle.create({
        data: { tenantId: tenant.id, siteId: site.id, code: s.code, nom: s.nom },
      });

      for (const b of s.baies) {
        let baie = await prisma.baie.findFirst({ where: { tenantId: tenant.id, code: b.code } });
        baie ??= await prisma.baie.create({
          data: {
            tenantId: tenant.id,
            salleId: salle.id,
            code: b.code,
            rangeeAllee: b.rangeeAllee,
            zoneCage: b.zoneCage,
            hauteurU: b.hauteurU,
          },
        });
        baieParCle.set(`${c.code}/${b.code}`, {
          id: baie.id,
          rangeeAllee: baie.rangeeAllee,
          zoneCage: baie.zoneCage,
        });
      }
    }

    for (const e of c.equipements) {
      const baie = baieParCle.get(`${c.code}/${e.baie}`);
      if (!baie) {
        bilan.avertissements.push(`${c.code} : baie « ${e.baie} » introuvable pour ${e.hostnameAsset}.`);
        continue;
      }
      let eq = await prisma.equipement.findFirst({
        where: { tenantId: tenant.id, hostnameAsset: e.hostnameAsset },
      });
      if (eq) bilan.ignores += 1;
      else {
        eq = await prisma.equipement.create({
          data: {
            tenantId: tenant.id,
            baieId: baie.id,
            nature: e.nature,
            type: e.type,
            marqueModele: e.marqueModele,
            numeroSerie: e.numeroSerie,
            hostnameAsset: e.hostnameAsset,
            ipIloIdrac: e.ipIloIdrac,
            environnement: e.environnement,
            criticite: e.criticite,
            sousContratGarantie: e.sousContratGarantie,
            positionU: e.positionU,
          },
        });
        bilan.equipements += 1;
      }
      equipementParCle.set(`${c.code}/${e.hostnameAsset}`, eq.id);
    }

    for (const sla of c.contratSla ?? []) {
      await prisma.contratSla.upsert({
        where: { tenantId_priorite: { tenantId: tenant.id, priorite: sla.priorite } },
        update: { gtiMinutes: sla.gtiMinutes },
        create: { tenantId: tenant.id, priorite: sla.priorite, gtiMinutes: sla.gtiMinutes },
      });
    }
  }

  const tousTenants = [...tenantParCode.values()];

  // L'administrateur qui importe doit VOIR ce qu'il vient de charger.
  for (const tenantId of tousTenants) {
    await prisma.userTenant.upsert({
      where: { userId_tenantId: { userId: adminId, tenantId } },
      update: {},
      create: { userId: adminId, tenantId },
    });
  }

  // ── 3. Contacts clients ───────────────────────────────────────
  const userParEmail = new Map<string, { id: string; nom: string; prenom: string | null }>();

  async function poserUtilisateur(
    email: string,
    data: Omit<Prisma.UserUncheckedCreateInput, "email" | "passwordHash">,
  ) {
    const existant = await prisma.user.findUnique({ where: { email } });
    if (existant) {
      userParEmail.set(email, existant);
      bilan.ignores += 1;
      return existant;
    }
    const cree = await prisma.user.create({
      data: {
        ...data,
        email,
        passwordHash: hash,
        // ⚠️ `false` volontairement : `true` déclencherait l'overlay bloquant de
        // changement de mot de passe à la première connexion, et un prospect
        // resterait coincé sur un formulaire au lieu de voir l'application.
        doitChangerMdp: false,
      },
    });
    userParEmail.set(email, cree);
    creesUtilisateurs.push(email);
    bilan.utilisateurs += 1;
    return cree;
  }

  for (const c of jeu.clients) {
    const tenantId = tenantParCode.get(c.code)!;
    for (const ct of c.contacts) {
      const u = await poserUtilisateur(ct.email, {
        nom: ct.nom,
        prenom: ct.prenom,
        role: "client",
        actif: true,
        fonction: ct.fonction,
        telephone: ct.telephone,
      });
      // Un contact client n'est rattaché qu'à SON organisation — c'est
      // l'invariant d'isolation, pas un détail de confort.
      await prisma.userTenant.upsert({
        where: { userId_tenantId: { userId: u.id, tenantId } },
        update: {},
        create: { userId: u.id, tenantId },
      });
    }
  }

  // ── 4. Techniciens, compétences, absences ─────────────────────
  const listeCompetences = await prisma.listeDeroulante.findUnique({ where: { code: "competences" } });

  for (const t of jeu.techniciens) {
    const u = await poserUtilisateur(t.email, {
      nom: t.nom,
      prenom: t.prenom,
      role: "technicien",
      actif: true,
      fonction: t.fonction,
      telephone: t.telephone,
      horaires: t.horaires,
      heuresHebdo: t.heuresHebdo,
    });

    // Le personnel exploitant intervient chez tous les clients.
    for (const tenantId of tousTenants) {
      await prisma.userTenant.upsert({
        where: { userId_tenantId: { userId: u.id, tenantId } },
        update: {},
        create: { userId: u.id, tenantId },
      });
    }

    if (listeCompetences) {
      for (const valeur of t.competences) {
        let item = await prisma.itemListe.findFirst({ where: { listeId: listeCompetences.id, valeur } });
        // Compétence absente du référentiel : on l'ajoute plutôt que de perdre
        // l'information. La liste est éditable, l'admin pourra l'ajuster.
        item ??= await prisma.itemListe.create({
          data: { listeId: listeCompetences.id, valeur, ordre: 99 },
        });
        await prisma.userCompetence.upsert({
          where: { userId_itemId: { userId: u.id, itemId: item.id } },
          update: {},
          create: { userId: u.id, itemId: item.id },
        });
      }
    }

    for (const a of t.absences) {
      const deja = await prisma.absence.findFirst({ where: { userId: u.id, motif: a.motif } });
      if (deja) {
        bilan.ignores += 1;
        continue;
      }
      await prisma.absence.create({
        data: {
          userId: u.id,
          type: a.type,
          dateDebut: dateDe(a.jourDebut, 0),
          dateFin: dateDe(a.jourFin, 23, 59),
          motif: a.motif,
        },
      });
    }
  }

  // ── 4 bis. Encadrement et lecture seule ───────────────────────
  // Manager et superviseur : sans eux, la démonstration n'expose aucun rôle
  // autre que technicien et client, et le contrôle d'accès reste invisible.
  for (const e of jeu.exploitants ?? []) {
    const u = await poserUtilisateur(e.email, {
      nom: e.nom,
      prenom: e.prenom,
      role: e.role,
      actif: true,
      fonction: e.fonction,
      telephone: e.telephone,
    });
    for (const tenantId of tousTenants) {
      await prisma.userTenant.upsert({
        where: { userId_tenantId: { userId: u.id, tenantId } },
        update: {},
        create: { userId: u.id, tenantId },
      });
    }
  }

  // ── 5. Interventions ──────────────────────────────────────────
  const prestations = await prisma.prestationCatalogue.findMany({ select: { id: true, ref: true } });
  const prestationParRef = new Map(prestations.map((p) => [p.ref, p.id]));

  const modeles = await prisma.modeleChecklist.findMany({ select: { id: true, code: true } });
  const modeleParCode = new Map(modeles.map((m) => [m.code, m.id]));

  if (prestationParRef.size === 0) {
    bilan.avertissements.push(
      "Catalogue de prestations vide : les interventions seront créées sans ligne de prestation.",
    );
  }

  for (const it of jeu.interventions) {
    const tenantId = tenantParCode.get(it.client);
    if (!tenantId) {
      bilan.avertissements.push(`Intervention ${it.numero} : organisation « ${it.client} » inconnue.`);
      continue;
    }

    // Clé de rejeu : le numéro est unique par organisation.
    const deja = await prisma.ticket.findFirst({
      where: { tenantId, numero: it.numero },
      select: { id: true },
    });
    if (deja) {
      bilan.ignores += 1;
      continue;
    }

    const baie = baieParCle.get(`${it.client}/${it.baie}`);
    const technicien = it.technicien ? userParEmail.get(it.technicien) : null;
    const demandeur = userParEmail.get(it.demandeur);
    if (!demandeur) {
      bilan.avertissements.push(`Intervention ${it.numero} : demandeur « ${it.demandeur} » introuvable.`);
      continue;
    }

    const dateIntervention = dateDe(it.jourIntervention, it.heureIntervention, it.minuteIntervention);
    const dateCreation = new Date(dateIntervention.getTime() - it.joursAvantCreation * JOUR_MS);
    const clos = it.statut === "clôturé";
    const fait = clos || it.statut === "terminé";
    const heureArrivee = it.statut === "nouveau" || it.statut === "planifié" ? null : dateIntervention;
    const heureDepart =
      fait && it.dureeMinutes ? new Date(dateIntervention.getTime() + it.dureeMinutes * MINUTE_MS) : null;

    const ticket = await prisma.ticket.create({
      data: {
        tenantId,
        numero: it.numero,
        baieId: baie?.id ?? null,
        rangeeAllee: it.rangeeAllee,
        zoneCage: it.zoneCage,
        contactSurSite: it.contactSurSite,
        demandeurId: demandeur.id,
        technicienId: technicien?.id ?? null,
        fournisseurId: it.fournisseur ? (fournisseurParNom.get(it.fournisseur) ?? null) : null,
        priorite: it.priorite,
        typeIntervention: it.typeIntervention,
        criticite: it.criticite,
        statut: it.statut,
        description: it.description,
        diagnosticMotif: it.diagnosticMotif,
        actionsCorrectives: it.actionsCorrectives,
        heureArrivee,
        heureDepart,
        dureeTotale: it.dureeMinutes
          ? `${Math.floor(it.dureeMinutes / 60)}h${String(it.dureeMinutes % 60).padStart(2, "0")}`
          : null,
        resultat: it.resultat,
        equipementRemisProd: it.equipementRemisProd,
        interruptionService: it.interruptionService,
        dureeIndisponibilite: it.dureeIndisponibilite,
        prochaineEcheance: it.prochaineEcheanceJour === null ? null : dateDe(it.prochaineEcheanceJour),
        dateCreation,
        datePriseEnCompte:
          it.statut === "nouveau" ? null : new Date(dateCreation.getTime() + 30 * MINUTE_MS),
        dateIntervention,
        dateCloture: clos && heureDepart ? new Date(heureDepart.getTime() + 2 * 3_600_000) : null,
        validationTechnicien:
          fait && technicien ? `${technicien.prenom ?? ""} ${technicien.nom}`.trim() : null,
        validationTechnicienDate: fait ? heureDepart : null,
        validationClient: clos ? it.contactSurSite : null,
        validationClientDate: clos && heureDepart ? new Date(heureDepart.getTime() + 3_600_000) : null,
        // Paraphes manuscrits : c'est ce qui donne sa valeur probante au bon
        // d'intervention. Un index hors bornes ne doit pas casser l'import.
        signatureTechnicien:
          it.signatureTechnicien === null ? null : (jeu.signatures[it.signatureTechnicien] ?? null),
        signatureClient: it.signatureClient === null ? null : (jeu.signatures[it.signatureClient] ?? null),
        // ⚠️ Posé à la création, pas par `ticket.archiver` : cette mutation
        // refuserait une intervention non clôturée et journaliserait 80 fois.
        archivedAt: it.archivee ? new Date((heureDepart ?? dateIntervention).getTime() + 30 * JOUR_MS) : null,
      },
    });
    // Équipe affectée : le référent démo devient membre de l'équipe (table de liaison).
    if (technicien) {
      await prisma.ticketTechnicien.create({ data: { ticketId: ticket.id, userId: technicien.id } });
    }
    bilan.interventions += 1;
    if (it.archivee) bilan.archivees += 1;

    const equipementId = it.equipement ? equipementParCle.get(`${it.client}/${it.equipement}`) : null;
    if (equipementId) {
      await prisma.ticketEquipement.create({ data: { ticketId: ticket.id, equipementId } });
    }

    for (const l of it.lignes) {
      const prestationId = prestationParRef.get(l.ref);
      if (!prestationId) {
        bilan.avertissements.push(`Intervention ${it.numero} : prestation « ${l.ref} » absente du catalogue.`);
        continue;
      }
      await prisma.lignePrestation.create({
        data: {
          tenantId,
          ticketId: ticket.id,
          prestationId,
          quantite: l.quantite,
          statut: l.statut,
          observations: l.observations,
        },
      });
    }

    for (const p of it.pieces) {
      await prisma.pieceRemplacee.create({
        data: {
          tenantId,
          ticketId: ticket.id,
          composant: p.composant,
          fournisseurId: fournisseurParNom.get(p.fournisseur) ?? null,
          serieRetiree: p.serieRetiree,
          serieInstallee: p.serieInstallee,
        },
      });
    }

    if (it.checklist) {
      const modeleId = modeleParCode.get(it.checklist.code);
      if (modeleId) {
        const checklist = await prisma.checklistIntervention.create({
          data: { tenantId, ticketId: ticket.id, modeleId },
        });
        for (const pt of it.checklist.points) {
          await prisma.pointControle.create({
            data: {
              tenantId,
              checklistId: checklist.id,
              ordre: pt.ordre,
              libelle: pt.libelle,
              statut: pt.statut,
              observations: pt.observations,
            },
          });
        }
      } else {
        bilan.avertissements.push(
          `Intervention ${it.numero} : modèle de checklist « ${it.checklist.code} » introuvable.`,
        );
      }
    }

    for (const e of it.journal) {
      await prisma.journalEvenement.create({
        data: {
          tenantId,
          entite: "Ticket",
          entiteId: ticket.id,
          typeAction: e.action,
          auteurId: technicien?.id ?? adminId,
          detail: JSON.stringify(e.detail),
          horodatage: new Date(dateCreation.getTime() + e.minutes * MINUTE_MS),
        },
      });
    }

    // ── Recette optique (module I2) ─────────────────────────────
    if (it.recetteOptique) {
      try {
        const nb = await creerRecetteOptique({
          prisma,
          tenantId,
          ticketId: ticket.id,
          tenantNom: jeu.clients.find((c) => c.code === it.client)?.nom ?? it.client,
          recette: it.recetteOptique,
          operateur: technicien ? `${technicien.prenom ?? ""} ${technicien.nom}`.trim() : "Technicien",
          dateMesure: heureDepart ?? dateIntervention,
          auteurId: technicien?.id ?? adminId,
        });
        bilan.recettesOptiques += 1;
        bilan.livrables += nb;
      } catch (e) {
        bilan.avertissements.push(
          `Intervention ${it.numero} : recette optique non créée (${e instanceof Error ? e.message : e}).`,
        );
      }
    }

    // ── Livrable : fiche PDF produite par le SERVICE DE PRODUCTION ──
    // Générer le PDF à l'import plutôt que de l'embarquer dans le JSON évite un
    // fichier de plusieurs dizaines de mégaoctets, et garantit que la fiche
    // reflète le rendu réel de l'application, pas une capture figée.
    if (it.livrable) {
      try {
        const complet = await prisma.ticket.findUniqueOrThrow({
          where: { id: ticket.id },
          include: {
            demandeur: { select: { nom: true } },
            technicien: { select: { nom: true } },
            fournisseur: { select: { nom: true } },
            baie: { select: { code: true, salle: { select: { code: true } } } },
            lignes: { include: { prestation: { select: { ref: true, libelle: true, uniteOeuvreLabel: true } } } },
            pieces: true,
            checklists: {
              include: { modele: { select: { nom: true } }, points: { orderBy: { ordre: "asc" } } },
            },
          },
        });
        const client = jeu.clients.find((c) => c.code === it.client);
        const pdf = await construireFichePdf({ ...complet, tenantNom: client?.nom ?? it.client });
        const nomFichier = `Fiche_${it.numero}.pdf`;
        const { chemin, taille } = await enregistrer(tenantId, nomFichier, Buffer.from(pdf), "application/pdf");
        const livrable = await prisma.livrable.create({
          data: {
            tenantId,
            ticketId: ticket.id,
            type: it.livrable,
            nomFichier,
            cheminStockage: chemin,
            taille,
            mime: "application/pdf",
            auteurId: technicien?.id ?? adminId,
            horodatage: heureDepart ?? dateIntervention,
          },
        });
        await prisma.journalEvenement.create({
          data: {
            tenantId,
            entite: "Livrable",
            entiteId: livrable.id,
            typeAction: "ajout_livrable",
            auteurId: technicien?.id ?? adminId,
            detail: JSON.stringify({ type: livrable.type, nomFichier, source: "fiche_pdf" }),
            horodatage: livrable.horodatage,
          },
        });
        bilan.livrables += 1;
      } catch (e) {
        // Un PDF qui échoue ne doit pas faire perdre les 80 interventions déjà
        // écrites : on consigne et on continue.
        bilan.avertissements.push(
          `Intervention ${it.numero} : fiche PDF non générée (${e instanceof Error ? e.message : e}).`,
        );
      }
    }
  }

  // ── 6. Trace de l'import ──────────────────────────────────────
  // Écrite en dernier, et seulement s'il y a eu création : un rejeu qui n'ajoute
  // rien ne doit pas laisser une trace vide, que la purge prendrait ensuite pour
  // un jeu à supprimer.
  if (creesTenants.length || creesUtilisateurs.length || creesFournisseurs.length) {
    const auteur = await prisma.user.findUnique({
      where: { id: adminId },
      select: { nom: true, prenom: true },
    });
    await prisma.importDemo.create({
      data: {
        libelle: jeu.libelle,
        genereLe: jeu.genereLe,
        auteurId: adminId,
        auteurNom: auteur ? `${auteur.prenom ?? ""} ${auteur.nom}`.trim() : null,
        tenants: JSON.stringify(creesTenants),
        utilisateurs: JSON.stringify(creesUtilisateurs),
        fournisseurs: JSON.stringify(creesFournisseurs),
      },
    });
  }

  return bilan;
}
