// Format du fichier de jeu de démonstration (`jeu-demo.json`).
//
// SOURCE UNIQUE du contrat : le générateur (`scripts/generer-jeu-demo.ts`) et
// l'importateur (`src/server/demo/importer.ts`) s'y réfèrent tous les deux. Un
// champ ajouté d'un seul côté ne compile pas.
//
// Deux partis pris structurants :
//
// 1. AUCUN IDENTIFIANT DE BASE. Le fichier ne référence que des CLÉS NATURELLES
//    (code client, e-mail, référence de prestation, code de baie). Un fichier
//    contenant des `id` cuid serait inutilisable sur une autre installation, et
//    l'import ne serait pas rejouable.
//
// 2. DATES RELATIVES, pas absolues. Chaque intervention porte un décalage en
//    jours par rapport au jour de l'import. Un jeu à dates figées vieillit :
//    importé six mois plus tard, il n'aurait plus une seule intervention dans le
//    mois courant et le tableau de bord s'afficherait vide sur sa période par
//    défaut — exactement l'inverse de ce qu'une démo doit montrer.

import { z } from "zod";

export const FORMAT_JEU_DEMO = "cyceo-proximite-datacenter/jeu-demo";
export const VERSION_JEU_DEMO = 1;

const texte = z.string().min(1);
const optionnel = z.string().min(1).nullable();

/** Décalage en jours par rapport au jour de l'import. Négatif = passé. */
const jour = z.number().int().min(-400).max(400);

const zBaie = z.object({
  code: texte,
  rangeeAllee: optionnel,
  zoneCage: optionnel,
  hauteurU: z.number().int().positive(),
});

const zSalle = z.object({ code: texte, nom: texte, baies: z.array(zBaie).min(1) });

const zEquipement = z.object({
  baie: texte,
  nature: texte,
  type: texte,
  marqueModele: texte,
  numeroSerie: texte,
  hostnameAsset: texte,
  ipIloIdrac: texte,
  environnement: texte,
  criticite: texte,
  sousContratGarantie: texte,
  positionU: texte,
});

const zContact = z.object({
  email: z.string().email(),
  nom: texte,
  prenom: texte,
  fonction: texte,
  telephone: texte,
});

const zClient = z.object({
  code: z.string().regex(/^[A-Z]{4}$/, "code client : 4 majuscules"),
  nom: texte,
  ville: texte,
  site: z.object({ code: texte, nom: texte }),
  salles: z.array(zSalle).min(1),
  equipements: z.array(zEquipement).min(1),
  /** Absent = le client est au barème standard, ce qui doit rester le cas courant. */
  contratSla: z.array(z.object({ priorite: texte, gtiMinutes: z.number().int().positive() })).optional(),
  contacts: z.array(zContact).min(1),
});

const zAbsence = z.object({ type: texte, jourDebut: jour, jourFin: jour, motif: texte });

const zTechnicien = z.object({
  email: z.string().email(),
  nom: texte,
  prenom: texte,
  fonction: texte,
  telephone: texte,
  horaires: texte,
  heuresHebdo: z.number().int().positive(),
  competences: z.array(texte),
  absences: z.array(zAbsence),
});

/**
 * Personnel d'exploitation non technicien : encadrement et lecture seule.
 *
 * Sans eux, la démonstration ne contenait que des techniciens et des contacts
 * clients — impossible de montrer le contrôle d'accès par rôle, qui est
 * pourtant l'un des arguments de l'application. Le rôle `admin` est absent
 * volontairement : un seul administrateur par société, c'est le compte de la
 * personne qui importe le jeu.
 */
const zExploitant = z.object({
  email: z.string().email(),
  nom: texte,
  prenom: texte,
  role: z.enum(["manager", "superviseur"]),
  fonction: texte,
  telephone: texte,
});

// ── Recette optique (module I2) ──────────────────────────────────
// Les VERDICTS ne sont pas stockés : ils sont recalculés à l'import par
// `calculerRecette`, la fonction de production. Un verdict figé dans le fichier
// pourrait contredire les seuils normatifs de l'application — c'est-à-dire
// démontrer exactement l'inverse de ce que le module sert à prouver.
const zEvenementOtdr = z.object({
  ordre: z.number().int().positive(),
  distanceM: z.number().nonnegative(),
  type: z.enum(["connecteur_depart", "connecteur_traversant", "connecteur_arrivee", "epissure", "fibre"]),
  perteDb: z.number().nullable(),
  returnLossDb: z.number().nullable(),
});

const zMesureOptique = z.object({
  longueurOnde: z.number().int().positive(),
  sens: texte,
  attenuationTotaleDb: z.number().nonnegative(),
  appareil: texte,
  evenements: z.array(zEvenementOtdr).min(2),
});

const zRecetteOptique = z.object({
  reference: texte,
  typeFibre: z.enum(["OS2", "OM3", "OM4"]),
  connectique: texte,
  polissage: z.enum(["apc", "upc"]),
  longueurM: z.number().positive(),
  nbConnecteurs: z.number().int().nonnegative(),
  nbEpissures: z.number().int().nonnegative(),
  baieDepart: texte,
  uDepart: texte,
  baieArrivee: texte,
  uArrivee: texte,
  mesures: z.array(zMesureOptique).min(1),
});

const zLigne = z.object({
  ref: texte,
  quantite: z.number().int().positive(),
  statut: texte,
  observations: optionnel,
});

const zPiece = z.object({
  composant: texte,
  fournisseur: texte,
  serieRetiree: optionnel,
  serieInstallee: optionnel,
});

const zPointControle = z.object({
  ordre: z.number().int().positive(),
  libelle: texte,
  statut: texte,
  observations: optionnel,
});

const zEvenementJournal = z.object({
  action: texte,
  /** Minutes écoulées depuis la création de l'intervention. */
  minutes: z.number().int().min(0),
  detail: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
});

const zIntervention = z.object({
  client: texte,
  numero: texte,
  salle: texte,
  baie: texte,
  /** `hostnameAsset` de l'équipement concerné, ou null si l'intervention n'en vise aucun. */
  equipement: optionnel,
  priorite: texte,
  /** ⚠️ LIBELLÉ, jamais un slug — voir CLAUDE.md. */
  typeIntervention: texte,
  criticite: texte,
  statut: texte,
  description: texte,
  diagnosticMotif: optionnel,
  actionsCorrectives: optionnel,
  /** ⚠️ SLUG ici (énum métier), contrairement aux champs adossés à une liste. */
  resultat: optionnel,
  technicien: optionnel,
  demandeur: z.string().email(),
  fournisseur: optionnel,
  contactSurSite: texte,
  rangeeAllee: optionnel,
  zoneCage: optionnel,
  jourIntervention: jour,
  heureIntervention: z.number().int().min(0).max(23),
  minuteIntervention: z.number().int().min(0).max(59),
  /** Délai entre création et intervention, en jours. */
  joursAvantCreation: z.number().int().min(0),
  dureeMinutes: z.number().int().positive().nullable(),
  equipementRemisProd: z.boolean(),
  interruptionService: z.boolean(),
  dureeIndisponibilite: optionnel,
  prochaineEcheanceJour: jour.nullable(),
  lignes: z.array(zLigne),
  pieces: z.array(zPiece),
  checklist: z.object({ code: texte, points: z.array(zPointControle).min(1) }).nullable(),
  journal: z.array(zEvenementJournal),
  /** Type de livrable à produire (fiche PDF générée à l'import), ou null. */
  livrable: optionnel,

  /**
   * Interventions rangées hors vue opérationnelle. Sans elles, le filtre
   * « archivées » de la liste ne renvoie jamais rien et la fonction paraît
   * inexistante. Seules des interventions clôturées ou annulées le sont.
   */
  archivee: z.boolean(),

  /**
   * Index dans le tableau `signatures` du fichier, ou null.
   * Un index plutôt que l'image : les mêmes paraphes servent plusieurs fois,
   * les dupliquer multiplierait le poids du fichier sans rien apporter.
   */
  signatureTechnicien: z.number().int().nonnegative().nullable(),
  signatureClient: z.number().int().nonnegative().nullable(),

  /** Recette optique attachée à l'intervention (module I2), ou null. */
  recetteOptique: zRecetteOptique.nullable(),
});

export const schemaJeuDemo = z.object({
  format: z.literal(FORMAT_JEU_DEMO),
  version: z.literal(VERSION_JEU_DEMO),
  libelle: texte,
  genereLe: texte,
  motDePasse: z.string().min(8),
  fournisseurs: z
    .array(
      z.object({
        nom: texte,
        specialite: texte,
        contact: texte,
        telephone: texte,
        email: z.string().email(),
        adresse: texte,
      }),
    )
    .min(1),
  clients: z.array(zClient).min(1),
  techniciens: z.array(zTechnicien).min(1),
  /** Encadrement et lecture seule — voir `zExploitant`. */
  exploitants: z.array(zExploitant),
  /** Paraphes manuscrits en data URL PNG, référencés par index. */
  signatures: z.array(z.string().regex(/^data:image\/png;base64,/)),
  interventions: z.array(zIntervention).min(1),
});

export type JeuDemo = z.infer<typeof schemaJeuDemo>;
export type InterventionDemo = z.infer<typeof zIntervention>;
export type RecetteOptiqueDemo = z.infer<typeof zRecetteOptique>;
export type ClientDemo = z.infer<typeof zClient>;
export type TechnicienDemo = z.infer<typeof zTechnicien>;

/**
 * Analyse un contenu JSON et renvoie un jeu validé.
 *
 * Les messages d'erreur sont volontairement explicites : ce fichier est destiné
 * à être choisi à la main par un administrateur, qui doit comprendre pourquoi il
 * est refusé sans avoir accès aux journaux du serveur.
 */
export function lireJeuDemo(contenu: unknown): JeuDemo {
  const brut =
    typeof contenu === "string"
      ? (() => {
          try {
            return JSON.parse(contenu);
          } catch {
            throw new Error("Fichier illisible : ce n'est pas du JSON valide.");
          }
        })()
      : contenu;

  const r = schemaJeuDemo.safeParse(brut);
  if (r.success) return r.data;

  const premier = r.error.issues[0];
  const champ = premier?.path.join(".") || "(racine)";

  // Cas fréquent et diagnostiquable : un fichier d'un autre outil, ou un export
  // de données confondu avec un jeu de démonstration.
  if (
    typeof brut === "object" &&
    brut !== null &&
    (brut as { format?: unknown }).format !== FORMAT_JEU_DEMO
  ) {
    throw new Error(
      "Ce fichier n'est pas un jeu de démonstration CYCEO Proximité Datacenter " +
        `(champ « format » attendu : « ${FORMAT_JEU_DEMO} »).`,
    );
  }

  throw new Error(`Fichier invalide — ${champ} : ${premier?.message ?? "structure inattendue"}.`);
}
