/* global React */
// ─── FloodsFlow — les statistiques des comptes branchés ───────────────────────────────
//
// D'OÙ VIENNENT CES CHIFFRES. bundle.social relève chaque compte connecté : abonnés, vues,
// impressions, j'aime, commentaires, nombre de publications. Il le fait tout seul UNE FOIS
// PAR 24 H — d'où le forçage, et d'où le bouton « Relever maintenant ».
//
// CE QUI FAISAIT CROIRE À UNE FAUSSE DONNÉE, et qui n'en était pas une. Les tuiles du haut
// donnent le TOTAL DU PARC (16 publications) pendant que la courbe donnait le détail PAR
// COMPTE (8 au plus haut). Deux chiffres justes, jamais présentés comme différents — on lisait
// donc un graphique qui contredisait son propre en-tête. D'où le sélecteur « Total du parc /
// Par compte » : à tout instant, l'écran dit lequel des deux il montre.
//
// (Et non, `@chlosasa_b` ne baisse pas par erreur : le fournisseur rend bien 3 → 8 → 7
// publications. Une publication a été supprimée. On affiche ce qu'il dit.)
//
// UNE MESURE À LA FOIS, ET C'EST UNE RÈGLE. Les abonnés se comptent en dizaines, les
// impressions en centaines de milliers. Les mettre sur le même graphique demanderait deux axes
// verticaux — la faute la plus classique de la lecture de données : deux échelles côte à côte
// laissent croire à une corrélation que le tracé a fabriquée.
//
// LES VUES UNIQUES NE SONT PAS PROPOSÉES : Instagram les renvoie toujours à zéro (vérifié sur
// les quatre comptes du parc). Un zéro constant se lit comme une mesure ; c'est une absence.

// Palette catégorielle de référence, validée pour les DEUX surfaces de l'application
// (#121826 en sombre, #ffffff en clair) : bande de clarté, plancher de chroma, séparation pour
// les daltonismes et contraste. L'ordre des teintes EST le mécanisme de sécurité — on assigne
// dans cet ordre, on ne recycle jamais.
const FS_SERIES_SOMBRE = ["#3987e5", "#d95926", "#199e70", "#c98500",
                          "#d55181", "#008300", "#9085e9", "#e66767"];
const FS_SERIES_CLAIR = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100",
                         "#e87ba4", "#008300", "#4a3aa7", "#e34948"];

const FS_PLAGES = [{ id: 1, label: "24 h" }, { id: 7, label: "7 jours" },
                   { id: 14, label: "14 jours" }, { id: 30, label: "30 jours" }];

const fsBase = () => (window.Backend && window.Backend.base) || "";

// 1 284 · 12,9 k · 4,2 M — un compteur d'impressions à sept chiffres ne se lit pas.
const fsCompact = (n) => {
  const v = Number(n) || 0;
  const s = v < 0 ? "−" : "";
  const a = Math.abs(v);
  if (a >= 1e6) return s + (a / 1e6).toFixed(1).replace(".", ",") + " M";
  if (a >= 1e4) return s + Math.round(a / 1e3) + " k";
  return s + a.toLocaleString("fr-FR");
};
const fsEntier = (n) => (Number(n) || 0).toLocaleString("fr-FR");
const fsJourCourt = (iso) => iso.slice(8) + "/" + iso.slice(5, 7);
const fsJourLong = (iso) => {
  try {
    return new Date(iso + "T12:00:00Z").toLocaleDateString("fr-FR",
      { weekday: "long", day: "numeric", month: "long" });
  } catch (e) { return iso; }
};

const fsSombre = () => {
  try {
    const t = document.documentElement.getAttribute("data-theme");
    if (t) return t !== "light";
    return !window.matchMedia || window.matchMedia("(prefers-color-scheme: dark)").matches;
  } catch (e) { return true; }
};

// Des graduations rondes : 0 / 20 000 / 40 000, jamais 0 / 17 384 / 34 768.
function fsEchelle(min, max) {
  const bas = Math.min(0, min);
  const etendue = Math.max(1, max - bas);
  const brut = etendue / 4;
  const ordre = Math.pow(10, Math.floor(Math.log10(brut)));
  const pas = [1, 2, 2.5, 5, 10].map((m) => m * ordre).find((p) => p >= brut) || ordre * 10;
  const plancher = Math.floor(bas / pas) * pas;
  const haut = Math.ceil(max / pas) * pas || pas;
  const ticks = [];
  for (let v = plancher; v <= haut + 1e-9; v += pas) ticks.push(v);
  return { bas: plancher, haut, ticks };
}

// Une trace de 12 points, dans la tuile. Elle ne porte pas de valeur : elle dit la FORME.
function FsTrace({ valeurs, couleur }) {
  const v = (valeurs || []).slice(-12);
  if (v.length < 2) return null;
  const mn = Math.min(...v), mx = Math.max(...v), amp = mx - mn || 1;
  const d = v.map((y, i) => (i ? "L" : "M") + (i / (v.length - 1)) * 62 + " "
    + (16 - ((y - mn) / amp) * 14)).join(" ");
  return (
    <svg viewBox="0 0 62 18" width="62" height="18" style={{ display: "block", marginTop: 3 }}>
      <path d={d} fill="none" stroke={couleur} strokeWidth="1.5" strokeLinejoin="round"
        strokeLinecap="round" opacity=".85" />
    </svg>
  );
}

// ── LA JOURNÉE ────────────────────────────────────────────────────────────────────────
//
// CE QUE LES COURBES NE DIRONT JAMAIS. Elles viennent du fournisseur, ne bougent qu'une fois
// par 24 h, et racontent la CONSÉQUENCE : des vues, des abonnés. Ce bandeau raconte la CAUSE,
// et il vient de nos propres plannings : combien de publications doivent sortir aujourd'hui,
// combien sont réellement sorties, et sur quels comptes ça a raté.
//
// LA FORME EST UNE JAUGE, pas un graphique. Un rapport unique contre une limite — livrées sur
// prévues — se lit d'un coup sur une barre et jamais sur une courbe.

// Une jauge : la part faite, sur la même piste que le reste. Les segments sont séparés de 2 px
// de surface, sinon deux couleurs voisines se lisent comme une seule.
function FsJauge({ parts, total }) {
  const t = Math.max(1, total || 0);
  return (
    <div style={{ display: "flex", gap: 2, height: 10, borderRadius: 99, overflow: "hidden",
      background: "var(--surface-3, rgba(255,255,255,.06))" }}>
      {parts.filter((p) => p.n > 0).map((p) => (
        <i key={p.cle} title={p.n + " " + p.titre}
          style={{ flex: p.n / t, background: p.teinte, display: "block" }} />
      ))}
    </div>
  );
}

function FsNombre({ valeur, libelle, teinte, note, gros }) {
  return (
    <div style={{ minWidth: 96 }}>
      <div style={{ fontSize: gros ? 30 : 21, fontWeight: 800, lineHeight: 1.1,
        color: teinte || "var(--text)", fontVariantNumeric: "tabular-nums" }}>{valeur}</div>
      <div style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".05em",
        color: "var(--muted)", marginTop: 2 }}>{libelle}</div>
      {note ? <div style={{ fontSize: 10.5, color: "var(--faint)", marginTop: 1 }}>{note}</div> : null}
    </div>
  );
}

function FsBandeauJournee({ j }) {
  if (!j || !j.ok) return null;
  const t = j.totaux || {};
  const rien = !t.a_livrer;
  return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 12, padding: "13px 15px",
      background: "var(--surface-2, rgba(255,255,255,.02))", marginBottom: 12 }}>
      <div style={{ display: "flex", alignItems: "flex-end", gap: 22, flexWrap: "wrap" }}>
        <FsNombre gros valeur={t.livrees + " / " + t.a_livrer} libelle="livrées aujourd'hui"
          teinte={t.livrees ? "var(--accent)" : "var(--faint)"}
          note={rien ? "rien de programmé pour aujourd'hui" : null} />
        {/* « VALIDÉES » ET « LIVRÉES » NE SONT PAS LE MÊME NOMBRE, et c'est la distinction que
            l'opérateur demandait. Validée = acceptée par bundle.social, elle partira toute
            seule. Livrée = son heure est passée, elle est sortie. */}
        <FsNombre valeur={t.acceptees} libelle="validées"
          teinte={t.acceptees ? "var(--accent)" : "var(--faint)"}
          note="acceptées par bundle.social" />
        <FsNombre valeur={t.a_envoyer} libelle="à envoyer"
          teinte={t.a_envoyer ? "var(--busy, #f5b942)" : "var(--faint)"}
          note={t.a_envoyer ? "rien n'est parti" : "tout est parti"} />
        {t.bloquees > 0 && (
          <FsNombre valeur={t.bloquees} libelle="bloquées" teinte="var(--danger, #ff5c6c)"
            note="le fournisseur n'a pas répondu" />
        )}
        <div style={{ flex: 1, minWidth: 150 }}>
          <FsJauge total={t.a_livrer}
            parts={[{ cle: "l", n: t.livrees, teinte: "var(--accent)", titre: "livrées" },
                    { cle: "v", n: t.acceptees - t.livrees, teinte: "rgba(0,224,138,.42)",
                      titre: "validées, heure pas encore venue" },
                    { cle: "e", n: t.a_envoyer, teinte: "var(--busy, #f5b942)",
                      titre: "à envoyer" },
                    { cle: "b", n: t.bloquees, teinte: "var(--danger, #ff5c6c)",
                      titre: "bloquées" }]} />
          <div style={{ fontSize: 10.5, color: "var(--faint)", marginTop: 5 }}>
            {t.comptes_actifs} compte(s) sur {t.branches} branché(s) publient aujourd'hui
            {t.comptes_ko ? " · " + t.comptes_ko + " demandent une décision" : ""}
          </div>
        </div>
      </div>
    </div>
  );
}

// ── LES COMPTES, SOUS LE GRAPHIQUE ────────────────────────────────────────────────────
//
// « il faut que je sache aussi les comptes qui ont échoué et si on a la data sur la
// problématique ». On l'a : quand un envoi est refusé, le motif du fournisseur est écrit sur
// la ligne du planning, et un envoi réussi l'efface. Un motif non vide veut donc dire : la
// dernière tentative a échoué, et voici pourquoi.
//
// LA FORME EST UN TABLEAU. Au-delà de sept lignes qui portent toutes du sens, un graphique
// cesse d'aider — et ici chaque ligne porte un pseudo, quatre nombres et une phrase.
function FsComptes({ j }) {
  const [tout, setTout] = React.useState(false);
  if (!j || !j.ok || !(j.comptes || []).length) return null;
  const souci = (f) => f.refus || f.souci;
  const aVoir = j.comptes.filter((f) => souci(f) || f.a_livrer || f.echecs);
  const calmes = j.comptes.filter((f) => aVoir.indexOf(f) < 0);
  const lignes = tout ? j.comptes : aVoir;

  const cell = { padding: "7px 9px", fontSize: 12, borderTop: "1px solid var(--border)" };
  const tete = { padding: "6px 9px", fontSize: 10, textTransform: "uppercase",
    letterSpacing: ".05em", color: "var(--faint)", textAlign: "left", fontWeight: 600 };
  const nb = { ...cell, textAlign: "right", fontVariantNumeric: "tabular-nums", width: 74 };

  return (
    <div style={{ marginTop: 16 }}>
      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: ".06em",
        textTransform: "uppercase", color: "var(--muted)", marginBottom: 6 }}>
        Compte par compte, aujourd'hui
      </div>
      <div style={{ border: "1px solid var(--border)", borderRadius: 11, overflow: "hidden" }}>
        <table style={{ width: "100%", borderCollapse: "collapse" }}>
          <thead>
            <tr style={{ background: "var(--surface-2, rgba(255,255,255,.03))" }}>
              <th style={tete}>compte</th>
              <th style={{ ...tete, textAlign: "right" }}>à livrer</th>
              <th style={{ ...tete, textAlign: "right" }}>livrées</th>
              <th style={{ ...tete, textAlign: "right" }}>échecs</th>
              <th style={tete}>état</th>
            </tr>
          </thead>
          <tbody>
            {lignes.map((f) => {
              const mal = !!souci(f);
              return (
                <tr key={f.compte}>
                  <td style={{ ...cell, maxWidth: 190, overflow: "hidden",
                    textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {f.compte}
                    {f.concept ? <span style={{ color: "var(--faint)", fontSize: 10.5 }}>
                      {" · " + f.concept}</span> : null}
                  </td>
                  <td style={nb}>{f.a_livrer || "—"}</td>
                  <td style={{ ...nb, color: f.livrees ? "var(--accent)" : "var(--faint)" }}>
                    {f.livrees || "—"}
                  </td>
                  <td style={{ ...nb, color: f.echecs ? "var(--danger, #ff5c6c)" : "var(--faint)" }}>
                    {f.echecs || "—"}
                  </td>
                  {/* L'ÉTAT NE PASSE JAMAIS PAR LA SEULE COULEUR : un signe, un mot, et la
                      cause en toutes lettres quand on l'a. */}
                  <td style={{ ...cell, color: mal ? "var(--danger, #ff5c6c)" : "var(--muted)" }}>
                    <span title={souci(f) || ""}
                      style={{ display: "inline-block", maxWidth: 330, overflow: "hidden",
                        textOverflow: "ellipsis", whiteSpace: "nowrap", verticalAlign: "bottom" }}>
                      {mal ? "⨯ " + souci(f)
                        : !f.branche ? "· pas branché"
                        : f.a_livrer ? "✓ rien ne s'y oppose"
                        : "· rien de prévu aujourd'hui"}
                    </span>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
        {calmes.length > 0 && (
          <div onClick={() => setTout(!tout)}
            style={{ padding: "8px 10px", fontSize: 11.5, color: "var(--faint)",
              borderTop: "1px solid var(--border)", cursor: "pointer" }}>
            {tout ? "▾ tous les comptes sont affichés — n'afficher que ceux à traiter"
              : "▸ et " + calmes.length + " compte(s) sans rien à signaler"}
          </div>
        )}
      </div>
    </div>
  );
}

function VueStats() {
  const [jours, setJours] = React.useState(7);
  // CE QUE L'ECRAN MONTRE EN S'OUVRANT. Il ouvrait sur « Abonnes, par compte » : quatre
  // courbes dont deux plates au ras de l'axe, sur un parc de douze a trente-six abonnes.
  // C'est vrai, et c'est illisible. « Vues, tout le parc » raconte quelque chose des le
  // premier coup d'oeil ; le detail par compte reste a un clic.
  const [mesure, setMesure] = React.useState("views");
  const [vue, setVue] = React.useState("total");        // comptes | total
  const [pas, setPas] = React.useState("cumul");        // cumul | jour
  const [donnees, setDonnees] = React.useState(null);
  const [erreur, setErreur] = React.useState("");
  const [occupe, setOccupe] = React.useState("");
  const [avis, setAvis] = React.useState(null);
  const [tableau, setTableau] = React.useState(false);
  const [survol, setSurvol] = React.useState(null);
  const [jrn, setJrn] = React.useState(null);   // la journee, cote plannings
  const [isole, setIsole] = React.useState(null);       // compte mis en avant à la demande
  const svg = React.useRef(null);

  const charger = React.useCallback(async (n) => {
    try {
      const r = await fetch(fsBase() + "/api/floodsflow/stats/" + (n || jours),
        { cache: "no-store" });
      const d = await r.json();
      if (d && d.ok) { setDonnees(d); setErreur(""); }
      else setErreur((d && d.error) || "statistiques indisponibles");
    } catch (e) { setErreur("backend injoignable — " + e.message); }
  }, [jours]);

  React.useEffect(() => { charger(jours); }, [jours, charger]);

  // LA JOURNEE SE LIT A PART, ET C'EST VOULU. Elle vient de NOS plannings, pas du
  // fournisseur : elle est fraiche a la seconde, alors que les releves ont jusqu'a 24 h de
  // retard. Les melanger dans un seul appel ferait attendre l'un pour l'autre.
  const chargerJournee = React.useCallback(async () => {
    try {
      const r = await fetch(fsBase() + "/api/floodsflow/journee", { cache: "no-store" });
      const d = await r.json();
      if (d && d.ok) setJrn(d);
    } catch (e) { /* le reste du panneau reste utilisable */ }
  }, []);
  React.useEffect(() => {
    chargerJournee();
    const surEnvoi = () => setTimeout(chargerJournee, 1500);
    window.addEventListener("floodsflow:envoi", surEnvoi);
    return () => window.removeEventListener("floodsflow:envoi", surEnvoi);
  }, [chargerJournee]);

  const relever = async () => {
    if (occupe) return;
    setOccupe("relève"); setAvis(null);
    try {
      const r = await fetch(fsBase() + "/api/floodsflow/stats", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ jours: jours, forcer: true }),
      });
      const d = await r.json();
      if (!d.ok) { setAvis({ ton: "ko", texte: d.error || "relève refusée" }); return; }
      if (d.historique) setDonnees(d.historique);
      setAvis({ ton: (d.erreurs || []).length ? "ko" : "ok",
        texte: d.comptes.length + " compte(s) relevé(s) · " + d.releves_neufs
          + " nouveau(x) relevé(s) · " + d.forcages + " forçage(s)"
          + ((d.erreurs || []).length ? " — " + d.erreurs.length + " refus : "
            + d.erreurs.slice(0, 2).join(" · ") : "") });
    } catch (e) {
      setAvis({ ton: "ko", texte: "relève interrompue — " + e.message });
    } finally { setOccupe(""); }
  };

  const noirs = fsSombre();
  const teintes = noirs ? FS_SERIES_SOMBRE : FS_SERIES_CLAIR;
  const parCompte = (donnees && donnees.par_compte) || {};
  const libelles = (donnees && donnees.libelles) || {};
  const cumul = (donnees && donnees.cumul) || [];
  const pouls = donnees && donnees.pouls;   // ce que NOS plannings savent de la journee
  // L'ORDRE DES SÉRIES EST STABLE, ALPHABÉTIQUE. La couleur suit le compte, jamais son rang :
  // isoler une série ne doit pas repeindre les autres.
  const noms = Object.keys(parCompte).sort();
  const couleur = (u) => teintes[noms.indexOf(u) % teintes.length];
  const NEUTRE = noirs ? "#3987e5" : "#2a78d6";

  const dates = [...new Set(noms.flatMap((u) => (parCompte[u].points || []).map((p) => p.jour)))]
    .sort();

  // ── les séries à tracer ────────────────────────────────────────────────────────────
  // « Par jour » soustrait le jour précédent : pour des abonnés ou des vues, c'est ce qui
  // s'est passé CE jour-là, et c'est presque toujours la question. Le cumul, lui, dit où on
  // en est. Les deux sont vrais ; ils ne répondent pas à la même chose.
  const valeurAu = (u, j) => {
    const pts = parCompte[u] ? parCompte[u].points || [] : [];
    let v = null;
    for (const p of pts) { if (p.jour <= j) v = Number(p[mesure]) || 0; }
    return v;
  };
  const brut = (u, j) => {
    const p = (parCompte[u].points || []).find((x) => x.jour === j);
    return p ? Number(p[mesure]) || 0 : null;
  };
  const serie = (u) => dates.map((j, i) => {
    const v = pas === "cumul" ? brut(u, j) : (() => {
      const a = valeurAu(u, j), b = i ? valeurAu(u, dates[i - 1]) : null;
      return brut(u, j) === null || b === null ? null : a - b;
    })();
    return { jour: j, v };
  });
  const serieTotal = dates.map((j, i) => {
    const c = cumul.find((x) => x.jour === j);
    const a = c ? Number(c[mesure]) || 0 : null;
    if (pas === "cumul") return { jour: j, v: a };
    const cp = i ? cumul.find((x) => x.jour === dates[i - 1]) : null;
    return { jour: j, v: cp && a !== null ? a - (Number(cp[mesure]) || 0) : null };
  });

  const series = vue === "total"
    ? [{ cle: "__total", nom: "Tout le parc", couleur: NEUTRE, points: serieTotal, muet: false }]
    : noms.map((u) => ({ cle: u, nom: "@" + u, couleur: couleur(u), points: serie(u),
                         muet: !!parCompte[u].muet, retard: parCompte[u].retard_jours }));

  const toutes = series.flatMap((s) => s.points.map((p) => p.v)).filter((v) => v !== null);
  const ech = fsEchelle(toutes.length ? Math.min(...toutes) : 0,
                        toutes.length ? Math.max(...toutes) : 1);

  // ── géométrie ──────────────────────────────────────────────────────────────────────
  const L = 900, H = 300, mg = { g: 58, d: 118, h: 18, b: 34 };
  const px = (i) => mg.g + (dates.length <= 1 ? (L - mg.g - mg.d) / 2
    : (i / (dates.length - 1)) * (L - mg.g - mg.d));
  const py = (v) => H - mg.b - (((Number(v) || 0) - ech.bas) / (ech.haut - ech.bas || 1))
    * (H - mg.h - mg.b);

  const der = cumul.length ? cumul[cumul.length - 1] : null;
  const prem = cumul.length ? cumul[0] : null;
  // LE TEMPS D'ENGAGEMENT A ETE DEMANDE, ET LE FOURNISSEUR NE LE DONNE PAS.
  // `GET /analytics/social-account` rend followers, following, views, viewsUnique,
  // impressions, impressionsUnique, likes, comments, postCount -- et rien sur la duree de
  // visionnage. Fabriquer une moyenne a partir des vues serait un chiffre invente. Ce qui
  // s'en approche honnetement, c'est le taux de reaction : on le calcule, on le nomme, et on
  // ecrit l'absence de l'autre a cote.
  // L'ÂGE DE LA DONNÉE, EN JOURS. On prend la relève la plus RÉCENTE du parc : si un seul
  // compte répond encore, les courbes ne sont pas mortes — elles sont incomplètes, et c'est
  // une autre phrase.
  const retard = donnees && donnees.collecte_le
    ? Math.max(0, (Date.now() - Date.parse(donnees.collecte_le)) / 86400000)
    : 0;
  const muets = Object.keys(parCompte).filter((u) => parCompte[u].muet).sort();
  const engagement = der && der.views
    ? (((der.likes + der.comments) / der.views) * 100).toFixed(1).replace(".", ",") + " %"
    : "—";

  const surSouris = (e) => {
    if (!svg.current || dates.length < 1) return;
    const b = svg.current.getBoundingClientRect();
    const x = ((e.clientX - b.left) / b.width) * L;
    const t = dates.length <= 1 ? 0
      : Math.round(((x - mg.g) / (L - mg.g - mg.d)) * (dates.length - 1));
    setSurvol(Math.max(0, Math.min(dates.length - 1, t)));
  };

  const cadre = { padding: "16px 18px", background: "var(--surface)",
    border: "1px solid var(--border)", borderRadius: 13, marginBottom: 16 };
  const onglet = (actif) => ({
    padding: "5px 11px", borderRadius: 8, fontSize: 12, fontWeight: 600, cursor: "pointer",
    font: "inherit", border: "1px solid " + (actif ? "var(--accent)" : "var(--border)"),
    background: actif ? "var(--accent-soft, rgba(0,224,138,.12))" : "transparent",
    color: actif ? "var(--accent)" : "var(--muted)",
  });
  const segment = (actif, premier, dernier) => ({
    padding: "5px 10px", fontSize: 12, fontWeight: 600, cursor: "pointer", font: "inherit",
    border: "1px solid " + (actif ? "var(--accent)" : "var(--border)"),
    borderRadius: (premier ? "8px 0 0 8px" : dernier ? "0 8px 8px 0" : 0),
    marginLeft: premier ? 0 : -1,
    background: actif ? "var(--accent-soft, rgba(0,224,138,.12))" : "transparent",
    color: actif ? "var(--accent)" : "var(--muted)", position: "relative",
    zIndex: actif ? 1 : 0,
  });

  const titreMesure = (libelles[mesure] || mesure)
    + (pas === "jour" ? " — évolution par jour" : "")
    + (vue === "total" ? " · tout le parc" : " · par compte");

  return (
    <div style={cadre}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12,
        flexWrap: "wrap" }}>
        <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: ".06em",
          textTransform: "uppercase", color: "var(--muted)" }}>
          Comptes branchés — statistiques
        </div>
        <div style={{ fontSize: 11, color: "var(--faint)" }}>
          {donnees && donnees.collecte_le
            ? "dernière relève " + new Date(donnees.collecte_le).toLocaleString("fr-FR",
                { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })
            : "jamais relevé"}
          {" · automatique toutes les 4 h"}
        </div>
        <button onClick={relever} disabled={!!occupe}
          title="Demande un relevé immédiat à bundle.social (budget : teams × 5 par jour)"
          style={{ ...onglet(false), marginLeft: "auto", opacity: occupe ? .5 : 1 }}>
          {occupe ? "relève…" : "↻ Relever maintenant"}
        </button>
      </div>

      {/* LES FILTRES AU-DESSUS DU GRAPHIQUE, sur deux lignes : d'abord QUAND, puis QUOI. */}
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 7,
        flexWrap: "wrap" }}>
        {FS_PLAGES.map((p) => (
          <button key={p.id} onClick={() => setJours(p.id)} style={onglet(jours === p.id)}>
            {p.label}
          </button>
        ))}
        <span style={{ width: 10 }} />
        <div style={{ display: "flex" }}>
          <button onClick={() => setVue("total")} style={segment(vue === "total", true)}>
            Tout le parc
          </button>
          <button onClick={() => setVue("comptes")}
            style={segment(vue === "comptes", false, true)}>Par compte</button>
        </div>
        <div style={{ display: "flex" }}>
          <button onClick={() => setPas("cumul")} style={segment(pas === "cumul", true)}>
            Total
          </button>
          <button onClick={() => setPas("jour")} style={segment(pas === "jour", false, true)}>
            Par jour
          </button>
        </div>
        <button onClick={() => setTableau((v) => !v)}
          style={{ ...onglet(tableau), marginLeft: "auto" }}>
          {tableau ? "Graphique" : "Tableau"}
        </button>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4,
        flexWrap: "wrap" }}>
        {((donnees && donnees.courbes) || []).map((m) => (
          <button key={m} onClick={() => setMesure(m)} style={onglet(mesure === m)}>
            {libelles[m] || m}
          </button>
        ))}
      </div>

      {avis && (
        <div style={{ margin: "10px 0", padding: "8px 12px", borderRadius: 9, fontSize: 12,
          background: avis.ton === "ko" ? "rgba(255,92,108,.08)" : "rgba(0,224,138,.08)",
          border: "1px solid " + (avis.ton === "ko" ? "rgba(255,92,108,.25)"
            : "rgba(0,224,138,.25)"),
          color: avis.ton === "ko" ? "var(--danger, #ff5c6c)" : "var(--accent)" }}>
          {avis.texte}
        </div>
      )}

      {erreur ? (
        <div style={{ padding: "10px 14px", borderRadius: 9, fontSize: 12.5,
          background: "rgba(255,92,108,.08)", border: "1px solid rgba(255,92,108,.25)",
          color: "var(--danger, #ff5c6c)" }}>⚠ {erreur}</div>
      ) : !donnees ? (
        <div style={{ padding: 30, textAlign: "center", color: "var(--faint)" }}>chargement…</div>
      ) : !noms.length ? (
        <div style={{ padding: 26, textAlign: "center", color: "var(--faint)", fontSize: 12.5 }}>
          Aucun relevé. Branche un compte à l'API, puis « Relever maintenant » —
          bundle.social ne mesure que les comptes connectés.
        </div>
      ) : (
        <>
          {/* LA FRAÎCHEUR SE DIT EN GRAND OU ELLE NE SE DIT PAS. La date de relève vivait en
              gris minuscule à côté du titre : la courbe s'arrêtait au 2 septembre, on était le
              6, et rien ne le criait. Une courbe qui a l'air de finir aujourd'hui alors qu'elle
              a quatre jours de retard, c'est le pire mensonge qu'un tableau de bord puisse
              faire — parce qu'il est involontaire et qu'on ne le cherche pas. */}
          {retard >= 1.5 && (
            <div style={{ padding: "9px 13px", borderRadius: 10, fontSize: 12, marginBottom: 10,
              background: "rgba(245,185,66,.09)", border: "1px solid rgba(245,185,66,.28)",
              color: "var(--busy, #f5b942)" }}>
              ⚠ Les courbes s'arrêtent il y a <b>{retard.toFixed(1).replace(".", ",")} jours</b>.
              {" "}bundle.social ne relève qu'une fois par 24 h et la collecte automatique n'a
              pas tourné depuis — « ↻ Relever maintenant » va chercher les chiffres du jour.
              {muets.length ? " " + muets.length + " compte(s) ne répondent plus du tout : "
                + muets.slice(0, 3).join(", ") + "." : ""}
            </div>
          )}

          <FsBandeauJournee j={jrn} />

          {/* Les tuiles disent le TOTAL DU PARC — c'est écrit, parce que la courbe peut
              montrer le détail par compte et qu'on comparait alors deux choses différentes. */}
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", margin: "12px 0 2px" }}>
            {["views", "impressions", "likes", "comments", "followers", "postCount"].map((m) => {
              const v = der ? der[m] : 0;
              const ec = v - (prem ? prem[m] : 0);
              const actif = mesure === m;
              return (
                <button key={m} onClick={() => setMesure(m)} title={"Tout le parc · " + fsEntier(v)}
                  style={{ flex: "1 1 128px", minWidth: 118, textAlign: "left", cursor: "pointer",
                    font: "inherit", padding: "9px 12px", borderRadius: 10,
                    border: "1px solid " + (actif ? "var(--accent)" : "var(--border)"),
                    background: actif ? "var(--accent-soft, rgba(0,224,138,.09))"
                      : "var(--surface-2, rgba(255,255,255,.02))" }}>
                  <div style={{ fontSize: 10.5, color: "var(--faint)", letterSpacing: ".04em",
                    textTransform: "uppercase" }}>{libelles[m] || m}</div>
                  <div style={{ fontSize: 23, fontWeight: 700, color: "var(--text)",
                    lineHeight: 1.2 }}>{fsCompact(v)}</div>
                  <div style={{ fontSize: 11, color: ec > 0 ? "var(--accent)"
                    : ec < 0 ? "var(--danger, #ff5c6c)" : "var(--faint)" }}>
                    {ec === 0 ? "stable" : (ec > 0 ? "+" : "") + fsCompact(ec)}
                    <span style={{ color: "var(--faint)" }}> sur la période</span>
                  </div>
                  <FsTrace valeurs={cumul.map((c) => c[m])}
                    couleur={actif ? "var(--accent)" : "var(--faint)"} />
                </button>
              );
            })}
          </div>
          <div style={{ fontSize: 11, color: "var(--faint)", margin: "10px 0 10px" }}>
            Les six totaux ci-dessus portent sur <b>tout le parc</b> ({der ? der.comptes : 0}
            {" "}compte(s) branché(s)), et viennent de bundle.social. Le graphique montre :{" "}
            <b>{titreMesure}</b>. Le <b>temps d'engagement</b> n'est pas mesuré par
            bundle.social — il ne rend que ces six-là. Ce qui s'en approche :{" "}
            <b>{engagement}</b> de réactions par vue (j'aime + commentaires ÷ vues).
          </div>

          {tableau ? (
            <div style={{ overflowX: "auto" }}>
              <table style={{ borderCollapse: "collapse", fontSize: 12, width: "100%",
                fontVariantNumeric: "tabular-nums" }}>
                <thead>
                  <tr>
                    {["Jour", ...series.map((s) => s.nom), "Total"].map((h, i) => (
                      <th key={h + i} style={{ textAlign: i ? "right" : "left",
                        padding: "5px 8px", color: "var(--muted)", fontWeight: 600,
                        whiteSpace: "nowrap", borderBottom: "1px solid var(--border)" }}>
                        {i > 0 && i <= series.length && (
                          <i style={{ display: "inline-block", width: 8, height: 8,
                            borderRadius: 2, background: series[i - 1].couleur,
                            marginRight: 5 }} />
                        )}{h}
                      </th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {dates.map((j) => (
                    <tr key={j}>
                      <td style={{ padding: "4px 8px", color: "var(--muted)",
                        whiteSpace: "nowrap" }}>{j}</td>
                      {series.map((s) => {
                        const p = s.points.find((x) => x.jour === j);
                        return (
                          <td key={s.cle} style={{ padding: "4px 8px", textAlign: "right",
                            color: p && p.v !== null ? "var(--text)" : "var(--faint)" }}>
                            {p && p.v !== null ? fsEntier(p.v) : "—"}
                          </td>
                        );
                      })}
                      <td style={{ padding: "4px 8px", textAlign: "right",
                        color: "var(--muted)" }}>
                        {(() => {
                          const t = serieTotal.find((x) => x.jour === j);
                          return t && t.v !== null ? fsEntier(t.v) : "—";
                        })()}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          ) : dates.length < 2 ? (
            <div style={{ padding: 26, textAlign: "center", color: "var(--faint)", fontSize: 12.5 }}>
              Un seul relevé sur cette plage — il faut deux points pour tracer une courbe.
              Élargis la plage, ou attends la prochaine relève.
            </div>
          ) : (
            <div style={{ position: "relative" }}>
              <svg ref={svg} viewBox={"0 0 " + L + " " + H}
                style={{ width: "100%", height: "auto", display: "block" }}
                onMouseMove={surSouris} onMouseLeave={() => setSurvol(null)}>
                <defs>
                  {series.map((s) => (
                    <linearGradient key={s.cle} id={"fsg-" + s.cle} x1="0" y1="0" x2="0" y2="1">
                      <stop offset="0%" stopColor={s.couleur} stopOpacity=".22" />
                      <stop offset="100%" stopColor={s.couleur} stopOpacity="0" />
                    </linearGradient>
                  ))}
                </defs>

                {/* Grille : traits pleins d'un cheveu, une teinte au-dessus de la surface. */}
                {ech.ticks.map((v) => (
                  <g key={v}>
                    <line x1={mg.g} x2={L - mg.d} y1={py(v)} y2={py(v)}
                      stroke="var(--border)" strokeWidth="1" />
                    <text x={mg.g - 9} y={py(v) + 3.5} textAnchor="end" fontSize="10.5"
                      fill="var(--faint)" style={{ fontVariantNumeric: "tabular-nums" }}>
                      {fsCompact(v)}
                    </text>
                  </g>
                ))}
                {/* Le zéro se distingue quand la courbe peut passer dessous (mode « par jour »). */}
                {ech.bas < 0 && (
                  <line x1={mg.g} x2={L - mg.d} y1={py(0)} y2={py(0)}
                    stroke="var(--border-2, var(--border))" strokeWidth="1.5" />
                )}
                {dates.map((j, i) => (
                  (dates.length <= 9 || i === 0 || i === dates.length - 1
                    || i === Math.floor(dates.length / 2)) && (
                    <text key={j} x={px(i)} y={H - 12} textAnchor="middle" fontSize="10.5"
                      fill={survol === i ? "var(--text)" : "var(--faint)"}>
                      {fsJourCourt(j)}
                    </text>
                  )
                ))}

                {survol !== null && (
                  <line x1={px(survol)} x2={px(survol)} y1={mg.h} y2={H - mg.b}
                    stroke="var(--accent)" strokeWidth="1" opacity=".45" />
                )}

                {series.map((s) => {
                  const vus = s.points.filter((p) => p.v !== null);
                  if (!vus.length) return null;
                  const att = isole && isole !== s.cle ? .13 : 1;
                  const d = s.points.map((p, i) => p.v === null ? null
                    : (i === 0 || s.points[i - 1].v === null ? "M" : "L")
                      + px(i) + " " + py(p.v)).filter(Boolean).join(" ");
                  // L'aire sous la courbe n'est qu'un lavis : elle donne du poids à la série
                  // sans jamais devenir un bloc saturé. Une seule série l'affiche — empilées,
                  // les aires se cacheraient l'une l'autre.
                  const aire = series.length === 1 && pas === "cumul"
                    ? d + " L" + px(s.points.findLastIndex((p) => p.v !== null)) + " " + py(ech.bas)
                      + " L" + px(s.points.findIndex((p) => p.v !== null)) + " " + py(ech.bas) + " Z"
                    : null;
                  return (
                    <g key={s.cle} opacity={att}>
                      {aire && <path d={aire} fill={"url(#fsg-" + s.cle + ")"} stroke="none" />}
                      <path d={d} fill="none" stroke={s.couleur} strokeWidth="2"
                        strokeLinejoin="round" strokeLinecap="round"
                        strokeDasharray={s.muet ? "5 4" : undefined} />
                      {/* Un point par relevé : c'est la donnée, et sans eux une ligne droite
                          entre deux jours laisse croire à une mesure continue. */}
                      {s.points.map((p, i) => p.v === null ? null : (
                        <circle key={p.jour} cx={px(i)} cy={py(p.v)}
                          r={survol === i ? 5 : 3.5} fill={s.couleur}
                          stroke="var(--surface)" strokeWidth="2" />
                      ))}
                    </g>
                  );
                })}

                {/* Étiquette au bout, pour les séries qui vont jusqu'au bord droit. Une série
                    interrompue au milieu n'en reçoit pas : son nom flotterait au milieu du
                    tracé, et c'est la légende qui la porte. */}
                {series.map((s) => {
                  const i = s.points.findLastIndex((p) => p.v !== null);
                  if (i < dates.length - 1) return null;
                  const p = s.points[i];
                  return (
                    <text key={s.cle} x={px(i) + 12} y={py(p.v) + 4} fontSize="11.5"
                      fill="var(--text)" opacity={isole && isole !== s.cle ? .3 : 1}>
                      {s.nom.length > 13 ? s.nom.slice(0, 12) + "…" : s.nom}
                      <tspan fill="var(--faint)"> {fsCompact(p.v)}</tspan>
                    </text>
                  );
                })}
              </svg>

              {/* L'INFOBULLE : la date en toutes lettres, puis chaque compte avec sa valeur,
                  du plus grand au plus petit. C'est ce qui manquait — on voyait des courbes
                  sans jamais pouvoir lire un jour précis. */}
              {survol !== null && (
                <div style={{ position: "absolute", top: 6, pointerEvents: "none",
                  left: (px(survol) / L) * 100 > 55 ? "auto" : ((px(survol) + 14) / L) * 100 + "%",
                  right: (px(survol) / L) * 100 > 55
                    ? (100 - ((px(survol) - 14) / L) * 100) + "%" : "auto",
                  minWidth: 178, padding: "9px 11px", borderRadius: 10, fontSize: 12,
                  background: "var(--surface-3, var(--surface))",
                  border: "1px solid var(--border-2, var(--border))",
                  boxShadow: "0 10px 30px rgba(0,0,0,.45)" }}>
                  <div style={{ fontWeight: 700, color: "var(--text)", marginBottom: 6,
                    textTransform: "capitalize" }}>{fsJourLong(dates[survol])}</div>
                  {series.map((s) => {
                    const p = s.points[survol];
                    return { s, v: p ? p.v : null };
                  }).sort((a, b) => (b.v === null ? -1e18 : b.v) - (a.v === null ? -1e18 : a.v))
                    .map(({ s, v }) => (
                    <div key={s.cle} style={{ display: "flex", alignItems: "center", gap: 7,
                      marginTop: 3, color: v === null ? "var(--faint)" : "var(--text)" }}>
                      <i style={{ width: 9, height: 9, borderRadius: 3, background: s.couleur,
                        display: "inline-block", flex: "0 0 auto", opacity: v === null ? .35 : 1 }} />
                      <span style={{ flex: 1, whiteSpace: "nowrap" }}>{s.nom}</span>
                      <b style={{ fontVariantNumeric: "tabular-nums" }}>
                        {v === null ? "pas de relevé" : (pas === "jour" && v > 0 ? "+" : "")
                          + fsEntier(v)}
                      </b>
                    </div>
                  ))}
                  {vue === "comptes" && (
                    <div style={{ display: "flex", gap: 7, marginTop: 7, paddingTop: 6,
                      borderTop: "1px solid var(--border)", color: "var(--muted)" }}>
                      <span style={{ flex: 1 }}>Tout le parc</span>
                      <b style={{ fontVariantNumeric: "tabular-nums" }}>
                        {(() => {
                          const t = serieTotal[survol];
                          return t && t.v !== null ? fsEntier(t.v) : "—";
                        })()}
                      </b>
                    </div>
                  )}
                </div>
              )}
            </div>
          )}

          {/* La légende est toujours là dès deux séries : l'identité ne tient jamais à la
              seule couleur. Un clic isole une courbe — les autres s'effacent sans changer de
              teinte, sinon la couleur cesserait de désigner le compte. */}
          {vue === "comptes" && (
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
              {noms.map((u) => {
                const f = parCompte[u] || {};
                const actif = isole === u;
                return (
                  <button key={u} onClick={() => setIsole(actif ? null : u)}
                    title={f.muet ? "Plus relevé depuis " + f.retard_jours + " jours — son jeton "
                      + "Instagram est probablement mort" : "Cliquer pour isoler cette courbe"}
                    style={{ display: "inline-flex", alignItems: "center", gap: 7,
                      padding: "5px 10px", borderRadius: 999, cursor: "pointer", font: "inherit",
                      fontSize: 11.5, background: actif ? "var(--surface-2, rgba(255,255,255,.05))"
                        : "transparent",
                      border: "1px solid " + (actif ? "var(--border-2, var(--border))"
                        : "transparent"),
                      color: f.muet ? "var(--faint)" : "var(--muted)",
                      opacity: isole && !actif ? .5 : 1 }}>
                    <i style={{ width: 14, height: 2.5, borderRadius: 2, background: couleur(u),
                      display: "inline-block" }} />
                    @{u}
                    <b style={{ color: "var(--text)", fontVariantNumeric: "tabular-nums" }}>
                      {f.dernier ? fsCompact(f.dernier[mesure]) : "—"}
                    </b>
                    {f.muet && <span>· muet {f.retard_jours} j</span>}
                  </button>
                );
              })}
            </div>
          )}

          {/* SOUS LE GRAPHIQUE, LES COMPTES — c'est l'ordre demandé, et c'est le bon : les
              courbes disent ce qui s'est passé, le tableau dit à qui parler maintenant. */}
          <FsComptes j={jrn} />
        </>
      )}
    </div>
  );
}

Object.assign(window, { VueStats, FsBandeauJournee, FsComptes, FsJauge });
