/* Poids Plume — interface. Tout le calcul se fait côté serveur, sur cette machine. */

const $ = (id) => document.getElementById(id);
const CONCURRENCE = 3;

const etat = {
  profils: [],
  profilActif: 0,
  fichiers: [],
  selection: null,
  enCours: false,
  prochainId: 1,
};

/* ---------- utilitaires ---------- */

const ko = (o) => (o < 1024 * 1024 ? Math.round(o / 1024) + ' Ko' : (o / 1048576).toFixed(1) + ' Mo');

/** Écart de poids entre l'original et le fichier produit, signe compris. */
function gain(avant, apres) {
  const valeur = Math.round(100 - (apres / avant) * 100);
  return { valeur, texte: (valeur >= 0 ? '−' : '+') + Math.abs(valeur) + ' %' };
}

// Mêmes règles que lib/nommage.mjs, dupliquées pour l'aperçu instantané du nom.
function slug(t) {
  if (!t) return '';
  return String(t).toLowerCase()
    .replace(/œ/g, 'oe').replace(/æ/g, 'ae').replace(/ß/g, 'ss')
    .replace(/['’&]/g, '-')
    .normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}

function appliquerModele(modele, valeurs) {
  let nom = modele || '{original}';
  for (const [cle, brut] of Object.entries(valeurs)) {
    nom = nom.replaceAll(`{${cle}}`, cle === 'index' ? brut : slug(brut));
  }
  return nom.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/-{2,}/g, '-')
    .replace(/^-+|-+$/g, '').toLowerCase() || 'image';
}

function enBase64(objet) {
  const octets = new TextEncoder().encode(JSON.stringify(objet));
  let binaire = '';
  octets.forEach((o) => { binaire += String.fromCharCode(o); });
  return btoa(binaire);
}

async function fileDAttente(elements, largeur, tache) {
  let curseur = 0;
  const travailleurs = Array.from({ length: Math.min(largeur, elements.length) }, async () => {
    while (curseur < elements.length) {
      const rang = curseur;
      curseur += 1;
      await tache(elements[rang], rang);
    }
  });
  await Promise.all(travailleurs);
}

/* ---------- réglages <-> formulaire ---------- */

function lireOptions() {
  const mode = document.querySelector('.segment.actif').dataset.mode;
  return {
    format: $('format').value,
    contrainte: $('contrainte').value,
    largeurs: $('largeurs').value.split(',').map((v) => parseInt(v.trim(), 10)).filter(Boolean),
    budgetKo: mode === 'budget' ? Number($('budgetKo').value) || null : null,
    qualite: Number($('qualite').value),
    pasAgrandir: $('pasAgrandir').checked,
    nettete: $('nettete').checked,
    chroma444: $('chroma444').checked,
    transparence: $('transparence').value,
    couleurFond: $('couleurFond').value,
    modele: $('modele').value,
    motCle: $('motCle').value,
    detail: $('detail').value,
    ville: $('ville').value,
    departement: $('departement').value,
    alt: $('alt').value,
    dossierSortie: $('dossierSortie').value,
  };
}

/**
 * Empreinte des réglages qui influent sur les fichiers produits.
 * Le texte alternatif en est exclu : il ne change que le code à coller.
 */
function signatureOptions(o = lireOptions()) {
  const { alt, ...comptent } = o;
  return JSON.stringify(comptent);
}

/** Les réglages tels qu'ils seront écrits dans le profil. */
function profilDepuisOptions(o = lireOptions()) {
  return {
    format: o.format, contrainte: o.contrainte, largeurs: o.largeurs,
    budgetKo: o.budgetKo, qualite: o.qualite, pasAgrandir: o.pasAgrandir,
    nettete: o.nettete, chroma444: o.chroma444, transparence: o.transparence,
    couleurFond: o.couleurFond, modele: o.modele, motCle: o.motCle,
    detail: o.detail, ville: o.ville, departement: o.departement,
    dossierSortie: o.dossierSortie,
  };
}

/** Les réglages affichés diffèrent-ils de ceux enregistrés dans le profil ? */
function profilModifie() {
  const enregistre = etat.profils[etat.profilActif];
  if (!enregistre) return false;
  const actuel = profilDepuisOptions();
  return Object.keys(actuel).some((cle) => {
    const attendu = cle === 'dossierSortie'
      ? (enregistre.dossierSortie || etat.dossierDefaut || '')
      : enregistre[cle];
    return JSON.stringify(actuel[cle]) !== JSON.stringify(attendu);
  });
}

/**
 * Le bouton d'enregistrement nomme le profil concerné et signale visiblement
 * qu'il y a quelque chose à enregistrer : sans cela, on ne devine pas qu'il
 * sert à mémoriser les réglages du bouton actif.
 */
function majBoutonEnregistrer() {
  const profil = etat.profils[etat.profilActif];
  const bouton = $('enregistrerProfil');
  if (!profil) return;
  const modifie = profilModifie();
  bouton.textContent = modifie
    ? `Enregistrer ces réglages dans « ${profil.nom} »`
    : `Profil « ${profil.nom} » à jour`;
  bouton.classList.toggle('a-enregistrer', modifie);
}

/** Une photo est à traiter si elle ne l'a jamais été, ou si les réglages ont changé depuis. */
function aRefaire(f, signature) {
  return f.statut !== 'fait' || f.signature !== signature;
}

function ecrireProfil(p) {
  $('format').value = p.format ?? 'webp';
  $('contrainte').value = p.contrainte ?? 'largeur';
  $('largeurs').value = (p.largeurs ?? []).join(', ');
  $('pasAgrandir').checked = p.pasAgrandir !== false;
  $('nettete').checked = p.nettete !== false;
  $('chroma444').checked = Boolean(p.chroma444);
  $('transparence').value = p.transparence ?? 'conserver';
  $('couleurFond').value = p.couleurFond ?? '#ffffff';
  $('modele').value = p.modele ?? '{original}';
  $('motCle').value = p.motCle ?? '';
  $('detail').value = p.detail ?? '';
  $('ville').value = p.ville ?? '';
  $('departement').value = p.departement ?? '';
  $('qualite').value = p.qualite ?? 78;
  $('valeurQualite').textContent = p.qualite ?? 78;
  // Chaque profil retient son propre dossier de sortie.
  $('dossierSortie').value = p.dossierSortie || etat.dossierDefaut || '';

  const modeBudget = p.budgetKo != null;
  $('budgetKo').value = p.budgetKo ?? 150;
  basculerMode(modeBudget ? 'budget' : 'qualite');
  majFondVisible();
  majApercuNom();
  // Changer de profil modifie tous les réglages d'un coup, sans aucun événement.
  majBoutonConvertir();
}

function basculerMode(mode) {
  document.querySelectorAll('.segment').forEach((s) => s.classList.toggle('actif', s.dataset.mode === mode));
  $('ligneBudget').hidden = mode !== 'budget';
  $('ligneQualite').hidden = mode !== 'qualite';
  // Un clic sur un bouton ne déclenche ni « input » ni « change » :
  // il faut prévenir explicitement.
  majBoutonConvertir();
}

const majFondVisible = () => { $('ligneFond').hidden = $('transparence').value !== 'aplatir'; };

function majApercuNom() {
  const o = lireOptions();
  const base = appliquerModele(o.modele, {
    'mot-cle': o.motCle, detail: o.detail, ville: o.ville, departement: o.departement,
    marque: '', original: 'photo-source', index: '01',
  });
  const suffixe = o.largeurs.length > 1 ? `-${Math.max(...o.largeurs)}w` : '';
  $('apercuNom').textContent = `${base}${suffixe}.${o.format}`;
}

/* ---------- profils ---------- */

function rendreProfils() {
  const zone = $('profils');
  zone.textContent = '';
  etat.profils.forEach((p, i) => {
    const b = document.createElement('button');
    b.type = 'button';
    b.className = 'puce' + (i === etat.profilActif ? ' actif' : '');
    b.textContent = p.nom;
    b.addEventListener('click', () => {
      etat.profilActif = i;
      ecrireProfil(p);
      rendreProfils();
    });
    zone.append(b);
  });
}

async function enregistrerProfil() {
  const o = lireOptions();
  const p = etat.profils[etat.profilActif];
  Object.assign(p, {
    format: o.format, contrainte: o.contrainte, largeurs: o.largeurs,
    budgetKo: o.budgetKo, qualite: o.qualite, pasAgrandir: o.pasAgrandir,
    nettete: o.nettete, chroma444: o.chroma444, transparence: o.transparence,
    couleurFond: o.couleurFond, modele: o.modele, motCle: o.motCle,
    detail: o.detail, ville: o.ville, departement: o.departement,
    dossierSortie: o.dossierSortie,
  });
  await fetch('/api/profils', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ profils: etat.profils }),
  });
  $('etatTraitement').textContent = `Profil « ${p.nom} » enregistré`;
  setTimeout(() => { $('etatTraitement').textContent = ''; }, 2500);
  majBoutonEnregistrer();
}

/* ---------- licence ---------- */

/** Met à jour la pastille d'état, en haut à droite. */
function majEtatLicence(licence) {
  if (!licence) return;
  etat.licence = licence;
  const puce = $('etatLicence');
  puce.hidden = false;
  puce.className = 'puce-licence';

  if (licence.sousLicence) {
    puce.textContent = `Licence : ${licence.client}`;
    puce.title = licence.expire
      ? `Valable jusqu'au ${new Date(licence.expire).toLocaleDateString('fr-FR')}`
      : 'Licence perpétuelle';
    puce.classList.add('sous-licence');
    return;
  }

  if (licence.epuisee) {
    puce.textContent = 'Évaluation terminée';
    puce.title = 'Cliquez pour activer une licence';
    puce.classList.add('epuisee');
    return;
  }

  // On annonce la limite la plus proche : c'est elle qui arrivera en premier.
  const parJours = licence.joursRestants <= licence.conversionsRestantes;
  puce.textContent = parJours
    ? `Évaluation · ${licence.joursRestants} jour${licence.joursRestants > 1 ? 's' : ''}`
    : `Évaluation · ${licence.conversionsRestantes} photos`;
  puce.title = `Il vous reste ${licence.joursRestants} jour(s) `
    + `et ${licence.conversionsRestantes} conversion(s). Cliquez pour activer une licence.`;
  if (licence.joursRestants <= 3 || licence.conversionsRestantes <= 5) puce.classList.add('bientot');
}

function decrireLicence() {
  const l = etat.licence;
  const detail = $('detailLicence');
  detail.className = 'etat-licence';

  // Le bloc d'achat n'apparaît que si une page de vente est configurée, et
  // seulement tant que l'utilisateur n'a pas de licence.
  const boutique = etat.edition?.boutique;
  $('blocAchat').hidden = Boolean(l?.sousLicence) || !boutique;
  $('titreChampLicence').textContent = boutique && !l?.sousLicence
    ? 'Vous avez déjà une licence ? Collez-la ici'
    : 'Collez ici la licence qui vous a été transmise';

  const support = etat.edition?.support;
  $('piedLicence').innerHTML = l?.sousLicence
    ? "L'activation est locale : aucune donnée n'est transmise."
    : (boutique
      ? `Une question ? <b>${support}</b> — l'activation est locale, aucune donnée n'est transmise.`
      : `Pour obtenir une licence, écrivez à <b>${support}</b>.`
        + " L'activation est locale : aucune donnée n'est transmise.");

  if (!l) { detail.textContent = ''; return; }

  if (l.sousLicence) {
    detail.classList.add('active');
    detail.innerHTML = `Logiciel activé au nom de <b>${l.client}</b>.`
      + (l.expire ? ` Licence valable jusqu'au <b>${new Date(l.expire).toLocaleDateString('fr-FR')}</b>.` : ' Licence perpétuelle.');
    return;
  }
  if (l.epuisee) {
    detail.classList.add('epuisee');
    detail.innerHTML = "<b>Période d'évaluation terminée.</b> Les conversions sont suspendues "
      + "jusqu'à l'activation d'une licence.";
    return;
  }
  detail.innerHTML = `Version d'évaluation : il vous reste <b>${l.joursRestants} jour(s)</b> `
    + `et <b>${l.conversionsRestantes} conversion(s)</b>, selon ce qui arrive en premier.`
    + (l.refus ? `<br><span class="message-erreur">${l.refus}</span>` : '');
}

function installerLicence() {
  const voile = $('voileLicence');
  const fermer = () => { voile.hidden = true; };

  const ouvrir = () => {
    decrireLicence();
    $('messageLicence').hidden = true;
    voile.hidden = false;
    $('champLicence').focus();
  };

  $('etatLicence').addEventListener('click', ouvrir);
  etat.ouvrirLicence = ouvrir;

  $('obtenirLicence').addEventListener('click', () => {
    const boutique = etat.edition?.boutique;
    if (boutique) window.open(boutique, '_blank', 'noopener');
  });

  $('activerLicence').addEventListener('click', async () => {
    const message = $('messageLicence');
    const jeton = $('champLicence').value.trim();
    if (!jeton) {
      message.hidden = false;
      message.className = 'aide message-erreur';
      message.textContent = 'Collez d’abord votre licence dans le cadre ci-dessus.';
      return;
    }
    try {
      const reponse = await appeler('/api/licence', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ jeton }),
      });
      const donnees = await reponse.json();
      message.hidden = false;
      if (donnees.erreur) {
        message.className = 'aide message-erreur';
        message.textContent = donnees.erreur;
        return;
      }
      message.className = 'aide message-succes';
      message.textContent = 'Licence acceptée. Merci !';
      majEtatLicence(donnees.licence);
      decrireLicence();
      $('champLicence').value = '';
      majBoutonConvertir();
      setTimeout(fermer, 1600);
    } catch { /* le bandeau serveur s'en charge */ }
  });

  $('fermerLicence').addEventListener('click', fermer);
  voile.addEventListener('click', (e) => { if (e.target === voile) fermer(); });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !voile.hidden) fermer();
  });
}

/* ---------- didacticiel ---------- */

function installerAide() {
  const voile = $('voileAide');
  const fermer = () => { voile.hidden = true; };
  $('aideProfils').addEventListener('click', () => {
    voile.hidden = false;
    $('fermerAide').focus();
  });
  $('fermerAide').addEventListener('click', fermer);
  voile.addEventListener('click', (e) => { if (e.target === voile) fermer(); });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !voile.hidden) fermer();
  });
}

/* ---------- liste des photos ---------- */

function ajouterFichiers(entrees) {
  for (const e of entrees) {
    etat.fichiers.push({
      id: etat.prochainId, nom: e.nom, taille: e.taille,
      fichier: e.fichier ?? null, chemin: e.chemin ?? null,
      statut: 'attente', resultat: null, erreur: null,
    });
    etat.prochainId += 1;
  }
  rendreListe();
}

/** Petit bouton d'action posé sur une ligne de la liste. */
function boutonItem(signe, intitule, action) {
  const b = document.createElement('button');
  b.type = 'button';
  b.className = 'action-item';
  b.textContent = signe;
  b.title = intitule;
  b.setAttribute('aria-label', intitule);
  b.addEventListener('click', (e) => {
    e.stopPropagation();
    action();
  });
  return b;
}

function retirer(id) {
  etat.fichiers = etat.fichiers.filter((f) => f.id !== id);
  if (etat.selection === id) {
    etat.selection = null;
    selectionner(null);
  }
  rendreListe();
}

function rendreListe() {
  const liste = $('liste');
  liste.textContent = '';

  for (const f of etat.fichiers) {
    const li = document.createElement('li');
    li.className = 'item' + (etat.selection === f.id ? ' actif' : '');
    li.tabIndex = 0;
    li.setAttribute('role', 'button');

    const vignette = document.createElement('img');
    vignette.className = 'vignette damier';
    vignette.alt = '';
    if (f.resultat?.apercuApres) vignette.src = f.resultat.apercuApres;

    const bloc = document.createElement('div');
    const nom = document.createElement('div');
    nom.className = 'nom';
    nom.textContent = f.nom;
    const sous = document.createElement('div');
    sous.className = 'sous';

    if (f.statut === 'fait') {
      // On compare l'original à la variante qui le remplace, pas à la somme
      // des tailles produites : un jeu responsive pèse forcément plus au total.
      const principale = f.resultat.fichiers[0];
      const g = gain(f.resultat.avant.taille, principale.taille);
      sous.textContent = `${ko(f.resultat.avant.taille)} -> ${ko(principale.taille)}  ${g.texte}`;
      sous.classList.add(g.valeur >= 0 ? 'gain' : 'souci');
      if (f.resultat.fichiers.some((x) => !x.budgetTenu)) {
        sous.classList.add('souci');
        sous.textContent += '  budget dépassé';
      }
    } else if (f.statut === 'erreur') {
      sous.textContent = f.erreur;
      sous.classList.add('souci');
    } else {
      sous.textContent = ko(f.taille);
    }

    bloc.append(nom, sous);

    const actions = document.createElement('div');
    actions.className = 'actions-item';

    if (f.statut === 'fait') {
      actions.append(boutonItem('↻', `Convertir de nouveau ${f.nom}`, () => {
        f.statut = 'attente';
        rendreListe();
      }));
    }
    actions.append(boutonItem('✕', `Retirer ${f.nom} de la liste`, () => retirer(f.id)));

    const pastille = document.createElement('span');
    pastille.className = 'pastille ' + ({ attente: '', encours: 'encours', fait: 'fait', erreur: 'erreur' }[f.statut]);
    actions.append(pastille);

    li.append(vignette, bloc, actions);
    const choisir = () => selectionner(f.id);
    li.addEventListener('click', choisir);
    li.addEventListener('keydown', (ev) => {
      if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); choisir(); }
    });
    liste.append(li);
  }

  $('compteur').textContent = etat.fichiers.length;
  $('viderListe').hidden = etat.fichiers.length === 0;
  majBoutonConvertir();
}

/**
 * Le bouton annonce ce qui sera réellement traité. Si les réglages ont changé
 * depuis la dernière conversion, il propose de refaire les photos concernées.
 */
function majBoutonConvertir() {
  majBoutonEnregistrer();
  const bouton = $('convertir');

  if (!etat.fichiers.length) {
    bouton.disabled = true;
    bouton.textContent = 'Convertir';
    return;
  }

  // Dès qu'il y a des photos, le bouton reste actionnable : l'étiquette
  // renseigne sur le travail à faire, elle n'interdit jamais de le relancer.
  bouton.disabled = etat.enCours;

  const aFaire = etat.fichiers.filter((f) => aRefaire(f, signatureOptions())).length;
  if (aFaire === 0) {
    bouton.textContent = etat.fichiers.length > 1 ? 'Tout reconvertir' : 'Reconvertir';
    return;
  }
  const verbe = etat.fichiers.every((f) => f.statut === 'fait') ? 'Reconvertir' : 'Convertir';
  bouton.textContent = aFaire > 1 ? `${verbe} ${aFaire} photos` : verbe;
}

/* ---------- aperçu ---------- */

function selectionner(id) {
  etat.selection = id;
  rendreListe();
  const f = etat.fichiers.find((x) => x.id === id);

  if (!f?.resultat) {
    $('apercu').hidden = true;
    $('apercuVide').hidden = false;
    $('apercuVide').firstElementChild.textContent = f
      ? 'Cette photo n\'a pas encore été convertie.'
      : 'Sélectionnez une photo pour la comparer avant et après.';
    return;
  }

  const r = f.resultat;
  $('apercuVide').hidden = true;
  $('apercu').hidden = false;
  $('imgAvant').src = r.apercuAvant;
  $('imgApres').src = r.apercuApres;

  const principale = r.fichiers[0];
  const totalJeu = r.fichiers.reduce((s, x) => s + x.taille, 0);
  const g = gain(r.avant.taille, principale.taille);
  const budgetDepasse = r.fichiers.some((x) => !x.budgetTenu);

  const cases = [
    ['Avant', `${ko(r.avant.taille)}<small>${r.avant.largeur} × ${r.avant.hauteur} · ${r.avant.format}</small>`, ''],
    ['Après', `${ko(principale.taille)}<small>${principale.largeur} × ${principale.hauteur} · jeu complet ${ko(totalJeu)}</small>`, 'bien'],
    ['Écart de poids', `${g.texte}<small>qualité ${r.fichiers.map((x) => x.qualite).join(' / ')}</small>`,
      g.valeur < 0 || budgetDepasse ? 'souci' : 'bien'],
    ['Transparence', r.transparente ? (r.alphaConservee ? 'conservée' : 'aplatie') : 'aucune',
      r.transparente && r.alphaConservee ? 'bien' : ''],
    ['Profil couleur', r.avant.profilIcc ? 'conservé' : 'aucun', ''],
    ['Décodage', r.decodeurSecours ? 'secours HEIC' : 'direct', ''],
  ];

  const details = $('details');
  details.textContent = '';
  for (const [titre, valeur, classe] of cases) {
    const div = document.createElement('div');
    const dt = document.createElement('dt');
    dt.textContent = titre;
    const dd = document.createElement('dd');
    dd.className = classe;
    dd.innerHTML = valeur;
    div.append(dt, dd);
    details.append(div);
  }

  const tailles = document.createElement('div');
  const dt = document.createElement('dt');
  dt.textContent = 'Fichiers écrits';
  const dd = document.createElement('dd');
  dd.style.fontSize = '11px';
  dd.textContent = r.fichiers.map((x) => `${x.nom} (${ko(x.taille)})`).join('\n');
  dd.style.whiteSpace = 'pre-line';
  tailles.append(dt, dd);
  details.append(tailles);

  $('blocHtml').hidden = r.fichiers.length < 2;
  $('codeHtml').textContent = r.html;
  appliquerComparaison($('curseurComparaison').value);
}

function appliquerComparaison(valeur) {
  const position = Math.min(100, Math.max(0, Number(valeur)));
  $('comparateur').style.setProperty('--coupe', position + '%');
  if ($('curseurComparaison').value !== String(position)) {
    $('curseurComparaison').value = position;
  }
}

/** On peut aussi attraper la séparation directement sur l'image. */
function installerGlissement() {
  const cadre = $('comparateur');
  let actif = false;

  const depuisPointeur = (e) => {
    const boite = cadre.getBoundingClientRect();
    appliquerComparaison(Math.round(((e.clientX - boite.left) / boite.width) * 100));
  };

  cadre.addEventListener('pointerdown', (e) => {
    actif = true;
    cadre.setPointerCapture(e.pointerId);
    depuisPointeur(e);
  });
  cadre.addEventListener('pointermove', (e) => { if (actif) depuisPointeur(e); });
  cadre.addEventListener('pointerup', () => { actif = false; });
  cadre.addEventListener('pointercancel', () => { actif = false; });
}

/* ---------- traitement ---------- */

const ERREUR_SERVEUR = 'serveur arrêté — relancez demarrer.cmd';

/** fetch, mais un serveur injoignable devient un message compréhensible. */
async function appeler(url, init) {
  try {
    return await fetch(url, init);
  } catch {
    signalerServeurAbsent();
    throw new Error(ERREUR_SERVEUR);
  }
}

function signalerServeurAbsent() {
  const banniere = $('banniere');
  if (!banniere.hidden) return;
  banniere.hidden = false;
  $('convertir').disabled = true;
}

async function serveurRepond() {
  try {
    const r = await fetch('/api/etat', { cache: 'no-store' });
    return r.ok;
  } catch {
    return false;
  }
}

async function traiterUn(f, rang, options) {
  f.statut = 'encours';
  rendreListe();

  // On indique les fichiers déjà produits pour cette photo : le serveur
  // réutilise les mêmes noms et efface ce qui ne sert plus.
  const parametres = {
    options,
    nom: f.nom,
    index: rang + 1,
    anciens: f.resultat?.fichiers?.map((x) => x.chemin) ?? [],
  };
  const reponse = f.fichier
    ? await appeler('/api/traiter', {
      method: 'POST',
      headers: { 'Content-Type': 'application/octet-stream', 'x-parametres': enBase64(parametres) },
      body: f.fichier,
    })
    : await appeler('/api/traiter', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...parametres, chemin: f.chemin }),
    });

  const donnees = await reponse.json();
  if (donnees.licence) majEtatLicence(donnees.licence);
  if (donnees.licenceRequise) {
    etat.licenceBloquante = true;
    throw new Error(donnees.erreur);
  }
  if (!reponse.ok || donnees.erreur) throw new Error(donnees.erreur || 'échec du traitement');
  return donnees;
}

async function convertirTout() {
  const signature = signatureOptions();
  // Rien n'a changé depuis la dernière fois : c'est donc une demande
  // explicite de tout refaire.
  const aFaire = etat.fichiers.filter((f) => aRefaire(f, signature));
  const lot = aFaire.length ? aFaire : [...etat.fichiers];
  if (!lot.length) return;

  // On s'assure que le serveur répond avant de lancer le lot : sans cela,
  // chaque photo échouerait l'une après l'autre sans raison lisible.
  if (!(await serveurRepond())) {
    signalerServeurAbsent();
    return;
  }

  etat.enCours = true;
  const options = lireOptions();
  let termines = 0;
  let octetsAvant = 0;
  let octetsApres = 0;
  let octetsEcrits = 0;
  let fichiersEcrits = 0;
  let erreurs = 0;

  $('progression').hidden = false;
  $('bilan').hidden = true;
  $('etatTraitement').textContent = `0 / ${lot.length}`;
  rendreListe();

  const debut = performance.now();

  await fileDAttente(lot, CONCURRENCE, async (f, rang) => {
    try {
      f.resultat = await traiterUn(f, rang, options);
      f.statut = 'fait';
      f.signature = signature;
      octetsAvant += f.resultat.avant.taille;
      octetsApres += f.resultat.fichiers[0].taille;
      octetsEcrits += f.resultat.fichiers.reduce((s, x) => s + x.taille, 0);
      fichiersEcrits += f.resultat.fichiers.length;
    } catch (e) {
      f.statut = 'erreur';
      f.erreur = e.message;
      erreurs += 1;
    }
    termines += 1;
    $('jauge').style.width = (termines / lot.length) * 100 + '%';
    $('etatTraitement').textContent = `${termines} / ${lot.length}`;
    rendreListe();
    if (etat.selection === f.id) selectionner(f.id);
  });

  etat.enCours = false;

  // L'évaluation s'est épuisée en cours de lot : on propose l'activation.
  if (etat.licenceBloquante) {
    etat.licenceBloquante = false;
    etat.ouvrirLicence?.();
  }

  const secondes = ((performance.now() - debut) / 1000).toFixed(1);
  const g = octetsAvant ? gain(octetsAvant, octetsApres) : { texte: '—', valeur: 0 };

  $('etatTraitement').textContent = '';
  $('progression').hidden = true;
  $('bilan').hidden = false;
  $('bilan').innerHTML = `${ko(octetsAvant)} → <b>${ko(octetsApres)}</b> · poids <b>${g.texte}</b>`
    + ` · ${fichiersEcrits} fichiers écrits (${ko(octetsEcrits)}) · ${secondes} s`
    + (erreurs ? ` · <span style="color:var(--erreur)">${erreurs} en échec</span>` : '');

  rendreListe();
  const premier = etat.fichiers.find((f) => f.statut === 'fait');
  if (premier && !etat.selection) selectionner(premier.id);
}

/* ---------- dépôt de fichiers ---------- */

const EXTENSIONS = /\.(jpe?g|png|webp|avif|tiff?|bmp|gif|heic|heif)$/i;

function depuisFileList(liste) {
  return [...liste]
    .filter((f) => EXTENSIONS.test(f.name))
    .map((f) => ({ nom: f.name, taille: f.size, fichier: f }));
}

function installerDepot() {
  const depot = $('depot');

  ['dragenter', 'dragover'].forEach((t) => depot.addEventListener(t, (e) => {
    e.preventDefault();
    depot.classList.add('survol');
  }));
  ['dragleave', 'drop'].forEach((t) => depot.addEventListener(t, (e) => {
    e.preventDefault();
    depot.classList.remove('survol');
  }));

  depot.addEventListener('drop', async (e) => {
    const fichiers = [];
    const entrees = [...(e.dataTransfer.items ?? [])]
      .map((i) => (i.webkitGetAsEntry ? i.webkitGetAsEntry() : null))
      .filter(Boolean);

    if (entrees.length) {
      const parcourir = async (entree, prefixe = '') => {
        if (entree.isFile) {
          const f = await new Promise((r) => entree.file(r));
          if (EXTENSIONS.test(f.name)) fichiers.push({ nom: prefixe + f.name, taille: f.size, fichier: f });
        } else if (entree.isDirectory) {
          const lecteur = entree.createReader();
          const lot = await new Promise((r) => lecteur.readEntries(r));
          for (const sous of lot) await parcourir(sous, `${prefixe}${entree.name}/`);
        }
      };
      for (const entree of entrees) await parcourir(entree);
      ajouterFichiers(fichiers);
    } else {
      ajouterFichiers(depuisFileList(e.dataTransfer.files));
    }
  });

  $('choisirFichiers').addEventListener('click', () => $('entreeFichiers').click());
  $('entreeFichiers').addEventListener('change', (e) => {
    ajouterFichiers(depuisFileList(e.target.files));
    e.target.value = '';
  });

  $('choisirDossier').addEventListener('click', () => { $('scanDossier').hidden = false; $('cheminDossier').focus(); });
  $('annulerScan').addEventListener('click', () => { $('scanDossier').hidden = true; });
  $('lancerScan').addEventListener('click', async () => {
    const reponse = await fetch('/api/scan', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ dossier: $('cheminDossier').value.trim() }),
    });
    const donnees = await reponse.json();
    if (donnees.erreur) { $('etatTraitement').textContent = donnees.erreur; return; }
    ajouterFichiers(donnees.fichiers.map((f) => ({ nom: f.nom, taille: f.taille, chemin: f.chemin })));
    $('scanDossier').hidden = true;
  });
}

/* ---------- sélecteur de dossier ---------- */
// Construit dans la page plutôt que confié à une boîte de dialogue du système :
// celle-ci pouvait s'ouvrir derrière le navigateur, ou ne pas s'ouvrir du tout.

async function ouvrirDossier(chemin) {
  const reponse = await appeler('/api/lister-dossiers', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chemin }),
  });
  const etatDossier = await reponse.json();

  $('cheminCourant').value = etatDossier.courant;
  $('allerParent').disabled = !etatDossier.parent;
  $('allerParent').dataset.cible = etatDossier.parent ?? '';

  const remplir = (zone, entrees) => {
    zone.textContent = '';
    for (const { nom, chemin: cible } of entrees) {
      const b = document.createElement('button');
      b.type = 'button';
      b.className = 'lecteur' + (cible === etatDossier.courant ? ' actif' : '');
      b.textContent = nom;
      b.title = cible;
      b.addEventListener('click', () => ouvrirDossier(cible));
      zone.append(b);
    }
  };

  remplir($('raccourcis'), etatDossier.raccourcis ?? []);
  remplir($('lecteurs'), (etatDossier.lecteurs ?? []).map((l) => ({ nom: l, chemin: l })));

  const liste = $('listeDossiers');
  liste.textContent = '';
  if (!etatDossier.dossiers.length) {
    const vide = document.createElement('li');
    vide.className = 'vide-dossier';
    vide.textContent = 'Aucun sous-dossier ici. Vous pouvez choisir ce dossier tel quel.';
    liste.append(vide);
  }
  for (const nom of etatDossier.dossiers) {
    const li = document.createElement('li');
    const b = document.createElement('button');
    b.type = 'button';
    b.textContent = nom;
    b.addEventListener('click', () => ouvrirDossier(`${etatDossier.courant.replace(/\\$/, '')}\\${nom}`));
    li.append(b);
    liste.append(li);
  }

  $('avertissementDossier').hidden = !etatDossier.avertissement;
  $('avertissementDossier').textContent = etatDossier.avertissement ?? '';
}

function installerSelecteurDossier() {
  const voile = $('voileDossier');
  const fermer = () => { voile.hidden = true; };

  $('parcourirSortie').addEventListener('click', async () => {
    voile.hidden = false;
    await ouvrirDossier($('dossierSortie').value);
    $('cheminCourant').focus();
  });

  $('allerParent').addEventListener('click', (e) => {
    const cible = e.currentTarget.dataset.cible;
    if (cible) ouvrirDossier(cible);
  });

  $('cheminCourant').addEventListener('keydown', (e) => {
    if (e.key === 'Enter') { e.preventDefault(); ouvrirDossier($('cheminCourant').value.trim()); }
  });

  $('creerDossier').addEventListener('click', async () => {
    const nom = $('nomNouveauDossier').value.trim();
    if (!nom) return;
    const reponse = await appeler('/api/creer-dossier', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ parent: $('cheminCourant').value, nom }),
    });
    const { dossier, erreur } = await reponse.json();
    if (erreur) { $('avertissementDossier').hidden = false; $('avertissementDossier').textContent = erreur; return; }
    $('nomNouveauDossier').value = '';
    await ouvrirDossier(dossier);
  });

  $('validerDossier').addEventListener('click', () => {
    $('dossierSortie').value = $('cheminCourant').value;
    majBoutonConvertir();
    fermer();
  });

  $('annulerDossier').addEventListener('click', fermer);
  voile.addEventListener('click', (e) => { if (e.target === voile) fermer(); });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !voile.hidden) fermer();
  });
}

/* ---------- démarrage ---------- */

async function demarrer() {
  let donnees;
  try {
    donnees = await (await fetch('/api/etat', { cache: 'no-store' })).json();
  } catch {
    signalerServeurAbsent();
    return;
  }
  etat.profils = donnees.profils ?? [];
  etat.dossierDefaut = donnees.dossierSortieDefaut;

  etat.edition = donnees.edition;
  const e = donnees.edition;
  if (e?.editeur) {
    $('edition').textContent = `Éditeur ${e.editeur} · Version ${e.version} · ${e.annee}`;
  }
  $('dossierSortie').value = donnees.dossierSortieDefaut;
  rendreProfils();
  if (etat.profils.length) ecrireProfil(etat.profils[0]);

  installerDepot();

  document.querySelectorAll('.segment').forEach((s) => {
    s.addEventListener('click', () => basculerMode(s.dataset.mode));
  });
  $('qualite').addEventListener('input', (e) => { $('valeurQualite').textContent = e.target.value; });
  $('transparence').addEventListener('change', majFondVisible);
  ['modele', 'motCle', 'detail', 'ville', 'departement', 'largeurs', 'format']
    .forEach((id) => $(id).addEventListener('input', majApercuNom));

  $('curseurComparaison').addEventListener('input', (e) => appliquerComparaison(e.target.value));
  installerGlissement();

  // Tout changement de réglage peut rendre des photos « à refaire » :
  // le bouton doit s'en apercevoir immédiatement.
  const reglages = document.querySelector('.colonne-reglages');
  reglages.addEventListener('input', majBoutonConvertir);
  reglages.addEventListener('change', majBoutonConvertir);
  $('dossierSortie').addEventListener('input', majBoutonConvertir);

  // Arrêt depuis l'interface : il n'y a plus de fenêtre noire à fermer.
  // Deux clics sont demandés, pour qu'un clic malheureux ne coupe pas l'outil.
  let arretDemande = false;
  $('quitter').addEventListener('click', async () => {
    const bouton = $('quitter');
    if (!arretDemande) {
      arretDemande = true;
      bouton.classList.add('confirme');
      bouton.textContent = 'Confirmer l\'arrêt';
      setTimeout(() => {
        arretDemande = false;
        bouton.classList.remove('confirme');
        bouton.textContent = 'Quitter';
      }, 4000);
      return;
    }
    bouton.textContent = 'Arrêt…';
    try {
      await fetch('/api/quitter', { method: 'POST' });
    } catch { /* le serveur peut couper avant de répondre */ }
    setTimeout(signalerServeurAbsent, 600);
  });

  $('rechargerPage').addEventListener('click', () => window.location.reload());
  $('convertir').addEventListener('click', convertirTout);
  $('enregistrerProfil').addEventListener('click', enregistrerProfil);
  $('viderListe').addEventListener('click', () => {
    etat.fichiers = [];
    etat.selection = null;
    $('bilan').hidden = true;
    rendreListe();
    selectionner(null);
  });
  installerSelecteurDossier();
  installerAide();
  installerLicence();
  majEtatLicence(donnees.licence);

  $('ouvrirSortie').addEventListener('click', async () => {
    const bouton = $('ouvrirSortie');
    const libelle = bouton.textContent;
    try {
      const reponse = await appeler('/api/ouvrir-dossier', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ dossier: $('dossierSortie').value }),
      });
      const donnees = await reponse.json();
      // Windows peut refuser le premier plan à une fenêtre lancée en arrière-plan :
      // on confirme dans l'interface pour que le clic ne semble jamais sans effet.
      bouton.textContent = donnees.erreur ? 'Introuvable' : 'Ouvert ↗';
      bouton.classList.toggle('bouton-alerte', Boolean(donnees.erreur));
    } catch {
      bouton.textContent = 'Échec';
    }
    setTimeout(() => {
      bouton.textContent = libelle;
      bouton.classList.remove('bouton-alerte');
    }, 2200);
  });
  $('copierHtml').addEventListener('click', async () => {
    await navigator.clipboard.writeText($('codeHtml').textContent);
    $('copierHtml').textContent = 'Copié';
    setTimeout(() => { $('copierHtml').textContent = 'Copier'; }, 1500);
  });

  // Raccourcis : Entrée lance la conversion, Espace compare.
  document.addEventListener('keydown', (e) => {
    if (e.target.matches('input, select, textarea')) return;
    if (e.key === 'Enter' && !etat.enCours) { e.preventDefault(); convertirTout(); }
    if (e.key === ' ') {
      e.preventDefault();
      const c = $('curseurComparaison');
      c.value = c.value > 50 ? 0 : 100;
      appliquerComparaison(c.value);
    }
  });

  rendreListe();
}

demarrer();
