// ============================================================
// DIRECTION 1 — Autres écrans
// ============================================================

// ──────────────────────────────────────────────────────────────
// Constantes Stripe — abonnement Nomerus Pro
// Le checkout passe par l'Edge Function create-checkout-session
// (plus de Payment Link en dur). Prix réel facturé : 14,99 CHF/mois.
// ──────────────────────────────────────────────────────────────
const NOMERUS_PRO_PRICE_LABEL = "14,99 CHF";

// ──────────────────────────────────────────────────────────────
// Primitives partagées (réutilisées par tous les écrans)
// ──────────────────────────────────────────────────────────────
const qtBtnPrimary = {
  padding: "8px 16px", background: QT.indigo, color: QT.bg, border: "none",
  borderRadius: 3, fontFamily: QT.mono, fontSize: 10, fontWeight: 700, letterSpacing: 1.5, cursor: "pointer",
};
const qtBtnGhost = {
  padding: "8px 16px", background: "transparent", color: QT.textDim,
  border: `1px solid ${QT.border}`, borderRadius: 3,
  fontFamily: QT.mono, fontSize: 10, fontWeight: 600, letterSpacing: 1.5, cursor: "pointer",
};
const qtInput = {
  width: "100%", padding: "8px 12px", background: QT.bg, border: `1px solid ${QT.border}`, borderRadius: 3,
  color: QT.text, fontFamily: QT.mono, fontSize: 12, outline: "none",
};
const QTField = ({ label, value, onChange, mono, icon, type = "text", placeholder = "", disabled = false, name, autoComplete }) => (
  <div>
    <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>{label}</div>
    <div style={{
      display: "flex", alignItems: "center", gap: 10, padding: "10px 12px",
      background: QT.bg, border: `1px solid ${QT.border}`, borderRadius: 3,
    }}>
      {icon && <Icon name={icon} size={14} style={{ color: QT.textMute }}/>}
      {onChange ? (
        <input
          type={type}
          name={name}
          autoComplete={autoComplete}
          value={value == null ? "" : value}
          onChange={(e) => onChange(e.target.value)}
          placeholder={placeholder}
          disabled={disabled}
          style={{
            flex: 1, background: "transparent", border: "none", outline: "none",
            color: QT.text, fontFamily: mono ? QT.mono : QT.sans, fontSize: 12,
          }}/>
      ) : (
        <span style={{ flex: 1, fontFamily: mono ? QT.mono : QT.sans, fontSize: 12, color: QT.text }}>{value}</span>
      )}
    </div>
  </div>
);

// ──────────────────────────────────────────────────────────────
// QTInstall — Connexion broker (form persisté Supabase)
// ──────────────────────────────────────────────────────────────

// Sonde de disponibilité du backend broker. Le gateway Supabase
// répond 404 (avec en-têtes CORS) quand une Edge Function n'est
// pas déployée → on détecte l'état côté navigateur et on affiche
// un écran honnête « beta privée » au lieu d'une erreur cryptique.
// Mémoïsée : une seule sonde par session d'onglet.
let _brokerBackendProbe = null;
const probeBrokerBackend = () => {
  if (!_brokerBackendProbe) {
    _brokerBackendProbe = (async () => {
      try {
        const res = await fetch(`${window.NOMERUS_SUPABASE_URL}/functions/v1/broker-connect`, { method: "POST" });
        return res.status !== 404; // 401/400 = fonction déployée (elle exige un JWT)
      } catch (_) {
        return false; // injoignable → on reste honnête (écran « bientôt »)
      }
    })();
  }
  return _brokerBackendProbe;
};

// Extrait le code d'erreur JSON d'une réponse non-2xx d'Edge Function
// (supabase-js expose la Response brute sur error.context).
const qtInvokeErrCode = async (error) => {
  try {
    if (error && error.context && typeof error.context.json === "function") {
      const body = await error.context.json();
      return (body && (body.error || body.code)) || null;
    }
  } catch (_) {}
  return null;
};

// Codes d'erreur backend → messages FR actionnables.
const qtBrokerErrMsg = (code) => ({
  token_invalide: "Clé API refusée par le broker. Vérifie le token (lecture seule) et l'environnement LIVE/DÉMO.",
  token_manquant: "Clé API manquante.",
  account_id_manquant: "ID de compte manquant.",
  broker_unsupported: "Broker non supporté.",
  use_oauth: "Ce broker se connecte via OAuth — utilise « SE CONNECTER ».",
  missing_fields: "Renseigne la clé API et l'ID de compte.",
  db_insert_failed: "Connexion testée avec succès mais enregistrement impossible. Réessaie dans un instant.",
  state_persist_failed: "Impossible de préparer l'autorisation. Réessaie dans un instant.",
  redirect_uri_not_configured: "La connexion broker n'est pas encore activée sur ce serveur.",
  broker_not_oauth: "Ce broker ne se connecte pas via OAuth.",
  reseau_oanda: "Impossible de joindre OANDA. Réessaie dans un instant.",
  invalid_state: "Session d'autorisation invalide. Relance la connexion.",
  state_expired: "Session d'autorisation expirée. Relance la connexion.",
  missing_code: "Le broker n'a pas renvoyé d'autorisation. Réessaie.",
  NOT_FOUND: "La synchronisation broker n'est pas encore activée (beta privée).",
}[code] || null);

const QTInstall = ({ go } = {}) => {
  // Brokers réellement supportés (un adapter broker-sync existe).
  // « Pas de bouton mort » : on n'affiche que ceux-là (plan §9).
  const SUPPORTED = [
    { id: "ctrader", name: "cTrader / IC Markets", type: "Forex / CFD", authType: "oauth" },
    { id: "oanda",   name: "OANDA",                type: "Forex / CFD", authType: "api_key" },
  ];
  const [selectedBroker, setSelectedBroker] = React.useState("ctrader");
  const broker = SUPPORTED.find((b) => b.id === selectedBroker) || SUPPORTED[0];
  const [apiKey, setApiKey] = React.useState("");
  const [accountId, setAccountId] = React.useState("");
  const [env, setEnv] = React.useState("LIVE");
  const [saving, setSaving] = React.useState(false);
  const [feedback, setFeedback] = React.useState(null); // { ok, message } — panneau formulaire
  const [listFeedback, setListFeedback] = React.useState(null); // { ok, message } — panneau connexions
  const [connected, setConnected] = React.useState([]);
  const [actingId, setActingId] = React.useState(null);
  const [confirmId, setConfirmId] = React.useState(null); // déconnexion en 2 temps
  const [backend, setBackend] = React.useState(null); // null = sonde en cours, true/false
  const envEnum = () => (env === "DÉMO" ? "DEMO" : "LIVE");

  React.useEffect(() => {
    let alive = true;
    probeBrokerBackend().then(ok => { if (alive) setBackend(ok); });
    return () => { alive = false; };
  }, []);

  // Le bouton CONFIRMER se désarme tout seul après 4 s sans clic.
  React.useEffect(() => {
    if (!confirmId) return;
    const t = setTimeout(() => setConfirmId(null), 4000);
    return () => clearTimeout(t);
  }, [confirmId]);

  // Charge les connexions existantes (avec état de sync). Si la
  // migration broker-sync n'est pas encore appliquée (colonnes
  // absentes → 42703), on retombe sur les colonnes de base pour
  // au moins lister l'existant au lieu d'un écran vide mensonger.
  const load = React.useCallback(async () => {
    try {
      const sb = window.nomerusSb();
      if (!sb) return;
      let { data, error } = await sb
        .from("broker_connections")
        .select("id, broker, label, asset_class, status, created_at, last_synced_at, trades_imported, auth_type, last_error")
        .order("created_at", { ascending: false });
      if (error) {
        ({ data } = await sb
          .from("broker_connections")
          .select("id, broker, label, asset_class, status, created_at")
          .order("created_at", { ascending: false }));
      }
      if (data) setConnected(data);
    } catch (e) { /* table peut ne pas exister */ }
  }, []);

  // Après un retour OAuth réussi, lance la première synchronisation
  // tout de suite (sans attendre le cron) : l'utilisateur voit ses
  // trades arriver pendant qu'il est encore sur l'écran.
  const kickFirstSync = React.useCallback(async () => {
    try {
      const sb = window.nomerusSb();
      if (!sb) return;
      const { data } = await sb
        .from("broker_connections")
        .select("id")
        .order("created_at", { ascending: false })
        .limit(1);
      if (data && data[0]) {
        await sb.functions.invoke("broker-sync", { body: { connection_id: data[0].id } });
        await load();
      }
    } catch (_) { /* le cron prendra le relais */ }
  }, [load]);

  React.useEffect(() => {
    load();
    // Retour du flux OAuth (broker-oauth-callback → ?connected=1 / ?broker_error=)
    try {
      const p = new URLSearchParams(window.location.search);
      const cameBack = p.get("connected") === "1";
      const oauthErr = p.get("broker_error");
      if (cameBack) {
        setFeedback({ ok: true, message: "Broker connecté. Import de ton historique lancé — tes trades arrivent." });
        kickFirstSync();
      } else if (oauthErr) {
        setFeedback({ ok: false, message: qtBrokerErrMsg(oauthErr) || ("Connexion échouée (" + oauthErr + "). Réessaie.") });
      }
      if (cameBack || oauthErr) {
        // Nettoie l'URL : un refresh ne doit pas rejouer le feedback.
        p.delete("connected"); p.delete("broker_error");
        const qs = p.toString();
        window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : ""));
      }
    } catch (_) {}
  }, [load, kickFirstSync]);

  // OAuth (cTrader) : « Se connecter » → autorisation sur le site du broker.
  const onConnectOAuth = async () => {
    setSaving(true); setFeedback(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) throw new Error("Connexion au serveur indisponible. Recharge la page.");
      const { data, error } = await sb.functions.invoke("broker-oauth-start", {
        body: { broker: broker.id, label: `${broker.name} · ${envEnum()}`, asset_class: broker.type, environment: envEnum() },
      });
      if (error) {
        const code = await qtInvokeErrCode(error);
        throw new Error(qtBrokerErrMsg(code) || "Impossible de démarrer la connexion. Réessaie dans un instant.");
      }
      if (!data?.auth_url) throw new Error("URL d'autorisation indisponible.");
      window.location.href = data.auth_url; // le navigateur part chez le broker
    } catch (e) {
      setFeedback({ ok: false, message: e.message || "Impossible de démarrer la connexion." });
      setSaving(false);
    }
  };

  // Token (OANDA) : TEST CONNECTION réel + enregistrement chiffré.
  const onConnectToken = async () => {
    if (!apiKey.trim() || !accountId.trim()) {
      setFeedback({ ok: false, message: "Renseigne la clé API (lecture seule) et l'ID de compte." });
      return;
    }
    setSaving(true); setFeedback(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) throw new Error("Connexion au serveur indisponible. Recharge la page.");
      const { data, error } = await sb.functions.invoke("broker-connect", {
        body: { broker: broker.id, environment: envEnum(), apiKey: apiKey.trim(), accountExternalId: accountId.trim() },
      });
      if (error) {
        const code = await qtInvokeErrCode(error);
        throw new Error(qtBrokerErrMsg(code) || "Test de connexion échoué. Réessaie dans un instant.");
      }
      if (!data?.ok) throw new Error(qtBrokerErrMsg(data?.error) || "Test de connexion échoué.");
      setFeedback({ ok: true, message: `Connecté à ${data.account_label || broker.name}. Import de ton historique lancé.` });
      setApiKey(""); setAccountId("");
      await load();
      // Première sync immédiate — le cron prendra le relais ensuite.
      kickFirstSync();
    } catch (e) {
      setFeedback({ ok: false, message: e.message || "Échec de la connexion." });
    } finally {
      setSaving(false);
    }
  };

  const onSyncNow = async (id) => {
    setActingId(id); setListFeedback(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) throw new Error("Connexion au serveur indisponible.");
      const { data, error } = await sb.functions.invoke("broker-sync", { body: { connection_id: id } });
      if (error) {
        const code = await qtInvokeErrCode(error);
        throw new Error(qtBrokerErrMsg(code) || "Synchronisation impossible. Réessaie dans un instant.");
      }
      const r = data && Array.isArray(data.results) ? data.results[0] : null;
      if (r && r.status === "ok") {
        const n = r.inserted || 0;
        setListFeedback({ ok: true, message: n > 0 ? `${n} nouveau${n > 1 ? "x" : ""} trade${n > 1 ? "s" : ""} importé${n > 1 ? "s" : ""}.` : "À jour — aucun nouveau trade." });
      } else if (r && r.status === "error") {
        setListFeedback({ ok: false, message: "Échec de la synchronisation : " + (r.error || "erreur broker") + "." });
      } else if (data && data.processed === 0) {
        setListFeedback({ ok: false, message: "Cette connexion n'est pas synchronisable (désactivée ou non supportée)." });
      } else {
        setListFeedback({ ok: true, message: "Synchronisation lancée." });
      }
    } catch (e) {
      setListFeedback({ ok: false, message: e.message || "Synchronisation impossible." });
    } finally {
      setActingId(null);
      await load();
    }
  };

  // Déconnexion en 2 temps : 1er clic arme (CONFIRMER ?), 2e clic supprime.
  const onDisconnect = async (id) => {
    if (confirmId !== id) { setConfirmId(id); return; }
    setConfirmId(null); setActingId(id); setListFeedback(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) throw new Error("Connexion au serveur indisponible.");
      const { error } = await sb.from("broker_connections").delete().eq("id", id);
      if (error) throw new Error("Déconnexion impossible. Réessaie dans un instant.");
      setListFeedback({ ok: true, message: "Broker déconnecté. Tes trades déjà importés restent dans le journal." });
    } catch (e) {
      setListFeedback({ ok: false, message: e.message || "Déconnexion impossible." });
    } finally {
      setActingId(null);
      await load();
    }
  };

  const fmtSync = (iso) => {
    if (!iso) return "jamais";
    try { return new Date(iso).toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }); }
    catch (_) { return "—"; }
  };

  // Étapes réelles, calculées sur l'état : pas de wizard décoratif.
  const hasActive = connected.some(c => c.status === "active");
  const totalImported = connected.reduce((s, c) => s + (c.trades_imported || 0), 0);
  const steps = [
    { n: 1, label: "CHOIX DU BROKER", done: true, current: false },
    { n: 2, label: "AUTORISATION", done: hasActive, current: !hasActive },
    { n: 3, label: "IMPORT AUTOMATIQUE", done: totalImported > 0, current: hasActive && totalImported === 0 },
  ];

  return (
    <div style={{ padding: 18, display: "flex", flexWrap: "wrap", gap: 14, alignItems: "flex-start" }}>
      {/* Étapes */}
      <div style={{ flex: "1 1 240px", maxWidth: 280, minWidth: 220 }}>
      <QTPanel title="ASSISTANT · CONNEXION BROKER">
        <div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
          {steps.map((s, i) => (
            <div key={s.n} style={{ display: "flex", gap: 12, padding: "12px 0", position: "relative" }}>
              {i < steps.length - 1 && (
                <div style={{ position: "absolute", left: 11, top: 32, bottom: -8, width: 1, background: QT.border }}/>
              )}
              <div style={{
                width: 22, height: 22, borderRadius: 4, flexShrink: 0,
                background: s.done ? QT.indigo : s.current ? QT.indigoSoft : "transparent",
                border: s.current ? `1px solid ${QT.indigo}` : `1px solid ${QT.border}`,
                display: "flex", alignItems: "center", justifyContent: "center",
                color: s.done ? QT.bg : s.current ? QT.indigo : QT.textMute,
                fontFamily: QT.mono, fontSize: 10, fontWeight: 700,
              }}>
                {s.done ? <Icon name="check" size={12}/> : s.n}
              </div>
              <div>
                <div style={{
                  fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 2,
                }}>ÉTAPE 0{s.n}</div>
                <div style={{
                  fontFamily: QT.sans, fontSize: 12, fontWeight: 600,
                  color: s.current ? QT.text : s.done ? QT.textDim : QT.textMute,
                }}>{s.label}</div>
              </div>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 16, padding: 10, background: QT.panel2, borderRadius: 3, fontFamily: QT.mono, fontSize: 10, color: QT.textDim, lineHeight: 1.6 }}>
          <div style={{ color: QT.indigo, marginBottom: 4 }}>// SÉCURITÉ</div>
          Connexion en lecture seule uniquement. Les tokens sont chiffrés (AES-256) et ne transitent jamais en clair. cTrader passe par OAuth — aucune clé à copier.
        </div>
      </QTPanel>
      </div>

      {/* Contenu principal */}
      <div style={{ flex: "3 1 420px", minWidth: 0 }}>
      <QTPanel title={`CONNEXION · ${broker.name.toUpperCase()}`}>
        {backend === false ? (
          /* Backend broker pas encore activé : état honnête, zéro bouton mort. */
          <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
            <div style={{ padding: 14, background: QT.panel2, border: `1px dashed ${QT.border}`, borderRadius: 4, display: "flex", gap: 10, alignItems: "flex-start" }}>
              <Icon name="clock" size={14} style={{ color: QT.amber, flexShrink: 0, marginTop: 2 }}/>
              <div style={{ fontFamily: QT.sans, fontSize: 12, color: QT.textDim, lineHeight: 1.6 }}>
                <div style={{ color: QT.text, fontWeight: 600, marginBottom: 2 }}>Sync broker automatique · beta privée</div>
                La synchronisation automatique (OANDA, cTrader) est en cours d'activation — elle apparaîtra ici dès qu'elle est ouverte,
                sans mise à jour de ton côté. En attendant, l'import CSV fait exactement le même travail : tes trades, tes stats,
                ton Coach IA, avec dédoublonnage automatique.
              </div>
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
              <button onClick={() => go && go("home")} style={qtBtnGhost}>← RETOUR</button>
              <button onClick={() => go && go("journal")} style={qtBtnPrimary}>IMPORTER UN CSV →</button>
            </div>
          </div>
        ) : (
        <React.Fragment>
        <div style={{ display: "flex", gap: 14, marginBottom: 18, flexWrap: "wrap" }}>
          {SUPPORTED.map(b => (
            <button key={b.id} onClick={() => { setSelectedBroker(b.id); setFeedback(null); }}
              className={selectedBroker === b.id ? "" : "qt-broker-card"}
              style={{
                flex: "1 1 140px", padding: 12, background: selectedBroker === b.id ? QT.indigoSoft : QT.panel2,
                border: `1px solid ${selectedBroker === b.id ? QT.indigo : QT.border}`,
                borderRadius: 4, cursor: "pointer", textAlign: "left", color: QT.text,
                transition: "border-color 120ms, background 120ms",
              }}>
              <div style={{ fontFamily: QT.sans, fontSize: 12, fontWeight: 600, marginBottom: 4 }}>{b.name}</div>
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1 }}>
                {b.authType === "oauth" ? "OAUTH · 1 CLIC" : "CLÉ API · LECTURE SEULE"}
              </div>
            </button>
          ))}
        </div>

        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          {/* Environnement — commun aux deux modes (compte réel vs démo) */}
          <div>
            <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>ENVIRONNEMENT</div>
            <div style={{ display: "flex", gap: 6 }}>
              {["LIVE", "DÉMO"].map(v => (
                <button key={v} onClick={() => setEnv(v)} style={{
                  padding: "10px 18px", fontFamily: QT.mono, fontSize: 11, letterSpacing: 1, cursor: "pointer",
                  color: env === v ? QT.indigo : QT.textDim,
                  background: env === v ? QT.indigoSoft : "transparent",
                  border: `1px solid ${env === v ? QT.borderStrong : QT.border}`, borderRadius: 3,
                }}>{v}</button>
              ))}
            </div>
            {broker.id === "oanda" && env === "DÉMO" && (
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, marginTop: 6 }}>
                DÉMO = compte practice OANDA (fxpractice) — le token doit venir de cet environnement.
              </div>
            )}
          </div>

          {broker.authType === "oauth" ? (
            /* OAuth : autorisation 1 clic sur le site du broker */
            <div style={{ padding: 14, background: QT.panel2, border: `1px solid ${QT.border}`, borderRadius: 4, display: "flex", gap: 10, alignItems: "flex-start" }}>
              <Icon name="shield" size={14} style={{ color: QT.indigo, flexShrink: 0, marginTop: 2 }}/>
              <div style={{ fontFamily: QT.sans, fontSize: 12, color: QT.textDim, lineHeight: 1.6 }}>
                <div style={{ color: QT.text, fontWeight: 600, marginBottom: 2 }}>Connexion sécurisée via {broker.name}</div>
                Tu seras redirigé vers {broker.name} pour autoriser Nomerus en <b style={{ color: QT.text }}>lecture seule</b> (scope «&nbsp;accounts&nbsp;»).
                Aucune clé à copier — l'accès reste révocable à tout moment chez ton broker.
              </div>
            </div>
          ) : (
            /* Token : clé read-only collée par l'utilisateur */
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <QTField label="CLÉ API (LECTURE SEULE)" value={apiKey} onChange={setApiKey} mono icon="key"
                type="password" name="broker-api-key" autoComplete="new-password" placeholder="ex. 1a2b3c4d••••-••••"/>
              <QTField label="ID DE COMPTE" value={accountId} onChange={setAccountId} mono icon="lock"
                name="broker-account-id" autoComplete="off" placeholder="ex. 001-011-1234567-001"/>
              <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute, lineHeight: 1.5 }}>
                Crée un token <b style={{ color: QT.textDim }}>read-only</b> dans ton espace OANDA (aucune permission de retrait).
              </div>
            </div>
          )}

          {feedback && (
            <div style={{
              padding: 10, borderRadius: 3, fontFamily: QT.mono, fontSize: 11,
              background: feedback.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
              border: `1px solid ${feedback.ok ? "rgba(91, 217, 154, 0.3)" : "rgba(255, 107, 122, 0.3)"}`,
              color: feedback.ok ? QT.green : QT.red,
            }}>{feedback.message}</div>
          )}
        </div>

        <div style={{ display: "flex", justifyContent: "space-between", marginTop: 22, paddingTop: 14, borderTop: `1px solid ${QT.border}`, flexWrap: "wrap", gap: 8 }}>
          <button onClick={() => go && go("home")} style={qtBtnGhost}>← RETOUR</button>
          <button onClick={broker.authType === "oauth" ? onConnectOAuth : onConnectToken} disabled={saving || backend === null} style={{
            ...qtBtnPrimary,
            opacity: (saving || backend === null) ? 0.6 : 1, cursor: saving ? "wait" : "pointer",
          }}>{backend === null ? "VÉRIFICATION…" : saving ? "CONNEXION…" : broker.authType === "oauth" ? "SE CONNECTER →" : "TESTER & CONNECTER →"}</button>
        </div>
        </React.Fragment>
        )}
      </QTPanel>
      </div>

      {/* Liste des brokers connectés */}
      <div style={{ flex: "1 1 280px", maxWidth: 360, minWidth: 240 }}>
      <QTPanel title="BROKERS CONNECTÉS">
        {listFeedback && (
          <div style={{
            padding: 8, marginBottom: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
            background: listFeedback.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
            border: `1px solid ${listFeedback.ok ? "rgba(91, 217, 154, 0.3)" : "rgba(255, 107, 122, 0.3)"}`,
            color: listFeedback.ok ? QT.green : QT.red, lineHeight: 1.5,
          }}>{listFeedback.message}</div>
        )}
        {connected.length === 0 ? (
          <div style={{ padding: 10, fontFamily: QT.mono, fontSize: 10, color: QT.textMute, lineHeight: 1.6 }}>
            {backend === false
              ? "La sync automatique arrive en beta privée. En attendant, l'import CSV (Journal) alimente les mêmes stats."
              : "Aucun broker connecté. Remplis le formulaire pour ajouter ta première connexion."}
          </div>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {connected.map(c => {
              const st = c.status === "active" ? { t: "ACTIF", col: QT.green }
                : c.status === "error" ? { t: "ERREUR", col: QT.red }
                : c.status === "unsupported" ? { t: "NON SUPPORTÉ", col: QT.textMute }
                : { t: "EN ATTENTE", col: QT.amber };
              const busyRow = actingId === c.id;
              return (
              <div key={c.id} className="qt-broker-card" style={{
                padding: 10, background: QT.panel2, border: `1px solid ${QT.border}`, borderRadius: 3,
                display: "flex", flexDirection: "column", gap: 8, transition: "border-color 120ms",
              }}>
                <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                  <div style={{
                    width: 30, height: 30, borderRadius: 3, background: QT.indigoSoft,
                    border: `1px solid ${QT.border}`,
                    display: "flex", alignItems: "center", justifyContent: "center",
                    color: QT.indigo, fontFamily: QT.mono, fontSize: 10, fontWeight: 700,
                  }}>{(c.broker || "??").slice(0, 2).toUpperCase()}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontFamily: QT.sans, fontSize: 11, color: QT.text, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{c.label || c.broker}</div>
                    <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1 }}>{c.auth_type === "oauth" ? "OAUTH" : "API KEY"}</div>
                  </div>
                  <span style={{ fontFamily: QT.mono, fontSize: 9, letterSpacing: 1, color: st.col }}>● {st.t}</span>
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", fontFamily: QT.mono, fontSize: 9, color: QT.textMute }}>
                  <span>SYNC : {fmtSync(c.last_synced_at)}</span>
                  <span>{c.trades_imported || 0} TRADES</span>
                </div>
                {c.status === "error" && c.last_error && (
                  <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.red, lineHeight: 1.5, wordBreak: "break-word" }}>
                    {String(c.last_error).slice(0, 140)}
                  </div>
                )}
                <div style={{ display: "flex", gap: 6 }}>
                  <button onClick={() => onSyncNow(c.id)} disabled={busyRow || backend === false} style={{
                    flex: 1, padding: "7px 8px", fontFamily: QT.mono, fontSize: 9, letterSpacing: 1,
                    cursor: busyRow ? "wait" : "pointer", color: QT.indigo,
                    background: QT.indigoSoft, border: `1px solid ${QT.border}`, borderRadius: 3,
                    opacity: backend === false ? 0.5 : 1,
                  }}>{busyRow ? "…" : "SYNCHRONISER"}</button>
                  <button onClick={() => onDisconnect(c.id)} disabled={busyRow} style={{
                    flex: 1, padding: "7px 8px", fontFamily: QT.mono, fontSize: 9, letterSpacing: 1,
                    cursor: busyRow ? "wait" : "pointer", color: confirmId === c.id ? QT.bg : QT.red,
                    background: confirmId === c.id ? QT.red : "transparent",
                    border: `1px solid ${confirmId === c.id ? QT.red : QT.border}`, borderRadius: 3,
                    fontWeight: confirmId === c.id ? 700 : 400,
                  }}>{confirmId === c.id ? "CONFIRMER ?" : "DÉCONNECTER"}</button>
                </div>
              </div>
              );
            })}
          </div>
        )}
      </QTPanel>
      </div>
    </div>
  );
};


// ──────────────────────────────────────────────────────────────
// QTAnalyze — Analyse profonde + filtres + CSV + graphiques additionnels
// ──────────────────────────────────────────────────────────────
const QTAnalyze = ({ go } = {}) => {
  const [trades, setTrades] = React.useState([]);
  const [loaded, setLoaded] = React.useState(false);
  const [account, setAccount] = React.useState("TOUS");
  const today = new Date();
  const ninetyDaysAgo = new Date(Date.now() - 90 * 86400e3);
  const [dateFrom, setDateFrom] = React.useState(ninetyDaysAgo.toISOString().slice(0, 10));
  const [dateTo, setDateTo] = React.useState(today.toISOString().slice(0, 10));

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const sb = window.nomerusSb();
        if (!sb) { setLoaded(true); return; }
        const { data, error } = await sb
          .from("trades")
          .select("id, date, asset, direction, entry_price, exit_price, pnl, r_multiple, result, strategy, session, platform, account, volume, is_demo")
          .order("date", { ascending: true });
        if (!cancelled && !error) setTrades(data || []);
      } catch (e) {} finally {
        if (!cancelled) setLoaded(true);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // Liste des comptes uniques pour les filtres
  const accountsList = React.useMemo(() => {
    const set = new Set();
    trades.forEach(t => { if (t.account) set.add(t.account); else if (t.platform) set.add(t.platform); });
    return Array.from(set);
  }, [trades]);

  // Trades filtrés selon compte + plage de dates
  const filtered = React.useMemo(() => {
    const from = dateFrom ? new Date(dateFrom + "T00:00:00").getTime() : -Infinity;
    const to = dateTo ? new Date(dateTo + "T23:59:59").getTime() : Infinity;
    return trades.filter(t => {
      const ts = t.date ? new Date(t.date).getTime() : 0;
      if (ts < from || ts > to) return false;
      if (account !== "TOUS") {
        const ta = t.account || t.platform;
        if (ta !== account) return false;
      }
      return true;
    });
  }, [trades, account, dateFrom, dateTo]);

  // KPIs calculés
  const stats = React.useMemo(() => {
    let net = 0, gross = 0, fees = 0, wins = 0, losses = 0;
    let bestPnl = -Infinity, worstPnl = Infinity;
    for (const t of filtered) {
      const p = Number(t.pnl) || 0;
      net += p;
      if (p >= 0) { gross += p; wins += 1; bestPnl = Math.max(bestPnl, p); }
      else { fees += p; losses += 1; worstPnl = Math.min(worstPnl, p); }
    }
    const winRate = filtered.length > 0 ? (wins / filtered.length) * 100 : 0;
    const avgWin = wins > 0 ? gross / wins : 0;
    const avgLoss = losses > 0 ? Math.abs(fees / losses) : 0;
    const pf = avgLoss > 0 && wins > 0 ? gross / Math.abs(fees) : (wins > 0 ? Infinity : 0);
    const expectancy = filtered.length > 0 ? net / filtered.length : 0;
    return {
      net, gross, fees: Math.abs(fees),
      winRate, profitFactor: pf,
      expectancy,
      wins, losses, avgWin, avgLoss,
      best: isFinite(bestPnl) ? bestPnl : 0,
      worst: isFinite(worstPnl) ? worstPnl : 0,
    };
  }, [filtered]);

  // Courbe d'équité — ancrée au début de la plage et prolongée jusqu'à
  // aujourd'hui (ou la fin de plage) : le temps continue même sans trades.
  const equityPoints = React.useMemo(() => {
    const dated = filtered.filter(t => t.date);
    if (dated.length === 0) return [];
    const start = dateFrom ? new Date(dateFrom + "T00:00:00").getTime() : new Date(dated[0].date).getTime();
    const end = Math.min(Date.now(), dateTo ? new Date(dateTo + "T23:59:59").getTime() : Date.now());
    let eq = 0;
    const pts = [{ value: 0, date: new Date(start).toISOString() }];
    for (const t of dated) {
      eq += Number(t.pnl) || 0;
      pts.push({ value: eq, date: t.date });
    }
    if (end > new Date(dated[dated.length - 1].date).getTime()) {
      pts.push({ value: eq, date: new Date(end).toISOString() });
    }
    return pts;
  }, [filtered, dateFrom, dateTo]);

  // Drawdown underwater — en CHF : le % d'un P&L cumulé sans capital de
  // référence n'a pas de sens (un pic à 100 CHF suivi d'un creux à 10
  // afficherait -90% de « drawdown »). Suit la même échelle de temps.
  const drawdownPoints = React.useMemo(() => {
    let peak = 0;
    return equityPoints.map(p => {
      if (p.value > peak) peak = p.value;
      return { value: Math.min(0, p.value - peak), date: p.date };
    });
  }, [equityPoints]);

  // Stats DD (en CHF)
  const ddStats = React.useMemo(() => {
    if (drawdownPoints.length === 0) return { max: 0, avg: 0, recoveryDays: "—" };
    const vals = drawdownPoints.map(p => p.value);
    const max = Math.min(...vals);
    const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
    // Récupération approchée : nombre de jours entre point min et retour à 0
    let minIdx = vals.indexOf(max), recoveryIdx = -1;
    for (let i = minIdx; i < vals.length; i++) { if (vals[i] >= 0) { recoveryIdx = i; break; } }
    let recoveryDays = "—";
    if (recoveryIdx >= 0 && minIdx >= 0) {
      const d1 = new Date(drawdownPoints[minIdx].date);
      const d2 = new Date(drawdownPoints[recoveryIdx].date);
      recoveryDays = `${Math.max(1, Math.round((d2 - d1) / 86400e3))}j`;
    }
    return { max, avg, recoveryDays };
  }, [drawdownPoints]);

  // ── Métriques avancées : ratios de gestion de risque calculés en local ──
  const advanced = React.useMemo(() => {
    // P&L quotidien : base des ratios Sharpe / Sortino (annualisés ×√252)
    const byDay = new Map();
    for (const t of filtered) {
      if (!t.date) continue;
      const k = String(t.date).slice(0, 10);
      byDay.set(k, (byDay.get(k) || 0) + (Number(t.pnl) || 0));
    }
    const daily = Array.from(byDay.values());
    const nD = daily.length;
    const meanD = nD > 0 ? daily.reduce((a, b) => a + b, 0) / nD : 0;
    const variance = nD > 1 ? daily.reduce((a, b) => a + (b - meanD) * (b - meanD), 0) / (nD - 1) : 0;
    const stdD = Math.sqrt(variance);
    const downs = daily.filter(v => v < 0);
    const downDev = nD > 0 ? Math.sqrt(downs.reduce((a, b) => a + b * b, 0) / nD) : 0;
    const sharpe = nD >= 5 && stdD > 0 ? (meanD / stdD) * Math.sqrt(252) : null;
    const sortino = nD >= 5 && downDev > 0 ? (meanD / downDev) * Math.sqrt(252) : null;

    // Max drawdown (CHF) + séries de gains / pertes consécutifs
    let eq = 0, peak = 0, maxDd = 0;
    let curW = 0, curL = 0, maxW = 0, maxL = 0;
    for (const t of filtered) {
      const p = Number(t.pnl) || 0;
      eq += p;
      if (eq > peak) peak = eq;
      maxDd = Math.min(maxDd, eq - peak);
      if (p >= 0) { curW += 1; curL = 0; maxW = Math.max(maxW, curW); }
      else { curL += 1; curW = 0; maxL = Math.max(maxL, curL); }
    }

    // Payoff (gain moyen / perte moyenne) et critère de Kelly
    const pWin = filtered.length > 0 ? stats.winRate / 100 : 0;
    const payoff = stats.avgLoss > 0 && stats.wins > 0 ? stats.avgWin / stats.avgLoss : null;
    const kelly = payoff != null && payoff > 0 ? pWin - (1 - pWin) / payoff : null;
    const recovery = maxDd < 0 ? stats.net / Math.abs(maxDd) : null;

    // Espérance en R (uniquement si r_multiple est renseigné)
    const rs = filtered.map(t => Number(t.r_multiple)).filter(r => isFinite(r));
    const expectancyR = rs.length > 0 ? rs.reduce((a, b) => a + b, 0) / rs.length : null;

    return { sharpe, sortino, maxDd, recovery, payoff, kelly, maxW, maxL, expectancyR, nDays: nD };
  }, [filtered, stats]);

  // Distribution Win/Loss
  const winLossData = React.useMemo(() => {
    const wins = filtered.filter(t => Number(t.pnl) >= 0).length;
    const losses = filtered.length - wins;
    return [{ label: "Gagnants", value: wins, color: QT.green }, { label: "Perdants", value: losses, color: QT.red }];
  }, [filtered]);

  // P&L par paire
  const pnlByPair = React.useMemo(() => {
    const map = new Map();
    for (const t of filtered) {
      const k = t.asset || "?";
      map.set(k, (map.get(k) || 0) + (Number(t.pnl) || 0));
    }
    return Array.from(map.entries()).map(([label, value]) => ({ label, value }))
      .sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
      .slice(0, 8);
  }, [filtered]);

  // P&L par jour de la semaine
  const pnlByDow = React.useMemo(() => {
    const labels = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
    const arr = [0, 0, 0, 0, 0, 0, 0];
    for (const t of filtered) {
      if (!t.date) continue;
      const d = new Date(t.date);
      const dow = (d.getDay() + 6) % 7; // 0=lundi
      arr[dow] += Number(t.pnl) || 0;
    }
    return arr.map((value, i) => ({ label: labels[i], value }));
  }, [filtered]);

  // P&L par session (Londres / New York / Asie…) — champ optionnel
  const pnlBySession = React.useMemo(() => {
    const map = new Map();
    for (const t of filtered) {
      if (!t.session) continue;
      const s = map.get(t.session) || { pnl: 0, n: 0 };
      s.pnl += Number(t.pnl) || 0; s.n += 1;
      map.set(t.session, s);
    }
    return Array.from(map.entries())
      .map(([label, s]) => ({ label, value: s.pnl, n: s.n }))
      .sort((a, b) => b.value - a.value);
  }, [filtered]);

  // P&L par stratégie — champ optionnel
  const pnlByStrategy = React.useMemo(() => {
    const map = new Map();
    for (const t of filtered) {
      if (!t.strategy) continue;
      const s = map.get(t.strategy) || { pnl: 0, n: 0 };
      s.pnl += Number(t.pnl) || 0; s.n += 1;
      map.set(t.strategy, s);
    }
    return Array.from(map.entries())
      .map(([label, s]) => ({ label, value: s.pnl, n: s.n }))
      .sort((a, b) => b.value - a.value)
      .slice(0, 8);
  }, [filtered]);

  // P&L par heure d'entrée — ignoré si les dates n'ont pas d'horodatage
  // (import CSV à minuit : toutes les « heures » seraient identiques)
  const pnlByHour = React.useMemo(() => {
    const map = new Map();
    for (const t of filtered) {
      if (!t.date) continue;
      const h = new Date(t.date).getHours();
      const s = map.get(h) || { pnl: 0, n: 0 };
      s.pnl += Number(t.pnl) || 0; s.n += 1;
      map.set(h, s);
    }
    if (map.size <= 1) return [];
    return Array.from(map.entries())
      .sort((a, b) => a[0] - b[0])
      .map(([h, s]) => ({ label: `${String(h).padStart(2, "0")}h`, value: s.pnl, n: s.n }));
  }, [filtered]);

  // P&L mensuel — table calendrier (année × mois)
  const monthly = React.useMemo(() => {
    const map = new Map(); // "2026-04" → pnl
    for (const t of filtered) {
      if (!t.date) continue;
      const k = String(t.date).slice(0, 7);
      map.set(k, (map.get(k) || 0) + (Number(t.pnl) || 0));
    }
    const years = new Map();
    map.forEach((v, k) => {
      const [y, m] = k.split("-");
      if (!years.has(y)) years.set(y, Array(12).fill(null));
      years.get(y)[Number(m) - 1] = v;
    });
    return Array.from(years.entries())
      .sort((a, b) => a[0].localeCompare(b[0]))
      .map(([year, months]) => ({ year, months, total: months.reduce((a, b) => a + (b || 0), 0) }));
  }, [filtered]);

  // ── Diagnostic de l'analyste — verdicts chiffrés, calculés en local ──
  // Chaque règle n'est émise que si l'échantillon la justifie ; le tri met
  // les risques en premier, les forces à la fin.
  const diagnostic = React.useMemo(() => {
    const n = filtered.length;
    if (n === 0) return [];
    const out = [];
    const push = (kind, title, body) => out.push({ kind, title, body });
    const fmtChf = (v) => `${qtSign(v)}${qtFmt(Math.abs(v))} CHF`;

    // 1. Espérance & profit factor — le cœur du système
    const pf = stats.profitFactor;
    if (stats.expectancy > 0) {
      const tier = !isFinite(pf) ? "aucune perte enregistrée sur la période" :
        pf >= 2 ? `profit factor ${qtFmt(pf)} : excellent, le système encaisse largement ses pertes` :
        pf >= 1.5 ? `profit factor ${qtFmt(pf)} : solide` :
        pf >= 1.2 ? `profit factor ${qtFmt(pf)} : correct mais fragile — quelques mauvais trades suffisent à repasser sous 1` :
        `profit factor ${qtFmt(pf)} : à peine rentable, une série de pertes normale peut tout effacer`;
      push(pf >= 1.5 || !isFinite(pf) ? "force" : "faiblesse", "Espérance positive",
        `${fmtChf(stats.expectancy)} par trade en moyenne — ${tier}.`);
    } else {
      push("risque", "Espérance négative",
        `${fmtChf(stats.expectancy)} par trade en moyenne : en l'état, chaque trade supplémentaire coûte de l'argent. Réduis la taille au minimum le temps d'identifier la fuite (voir les points ci-dessous).`);
    }

    // 2. Cohérence taux de réussite / payoff
    if (advanced.payoff != null && n >= 10) {
      if (stats.winRate >= 50 && advanced.payoff < 1) {
        push("faiblesse", "Tes pertes moyennes dépassent tes gains moyens",
          `${qtFmt(stats.winRate, 0)}% de réussite mais un payoff de ${qtFmt(advanced.payoff)} (gain moyen ${qtFmt(stats.avgWin, 0)} CHF vs perte moyenne ${qtFmt(stats.avgLoss, 0)} CHF). Schéma classique du « je coupe mes gains et laisse courir mes pertes » : audite tes sorties, pas tes entrées.`);
      } else if (stats.winRate < 40 && advanced.payoff >= 2) {
        push("force", "Profil coureur de tendance assumé",
          `${qtFmt(stats.winRate, 0)}% de réussite compensé par un payoff de ${qtFmt(advanced.payoff)} : le système vit sur quelques gros gains. Protège-les — couper un runner par impatience détruit ce type d'edge.`);
      }
    }

    // 3. Kelly → dimensionnement concret
    if (advanced.kelly != null && n >= 20) {
      if (advanced.kelly <= 0) {
        push("risque", "Aucune taille de position n'est justifiée",
          `Le critère de Kelly ressort négatif (${qtFmt(advanced.kelly * 100, 1)}%) : statistiquement, ce système n'a pas d'edge sur la période. Le problème n'est pas le sizing, c'est la stratégie.`);
      } else {
        push("action", "Dimensionnement suggéré",
          `Kelly ressort à ${qtFmt(advanced.kelly * 100, 1)}% du capital par trade. En pratique, vise le demi-Kelly (~${qtFmt((advanced.kelly / 2) * 100, 1)}%) et plafonne à 2% : Kelly plein suppose des statistiques stables, ce qu'aucun trader n'a.`);
      }
    }

    // 4. Séries de pertes → préparation du risque
    if (advanced.maxL >= 4) {
      push("risque", `Jusqu'à ${advanced.maxL} pertes d'affilée sur la période`,
        `À 1% de risque par trade, cette série coûte ~${advanced.maxL}% du compte ; à 3%, ~${advanced.maxL * 3}%. Dimensionne-toi pour pouvoir encaisser cette série deux fois de suite sans être hors-jeu ni tilté.`);
    }

    // 5. Drawdown vs gain : le rendement paye-t-il le risque ?
    if (advanced.maxDd < 0 && n >= 10) {
      if (stats.net > 0 && advanced.recovery != null) {
        push(advanced.recovery >= 2 ? "force" : "faiblesse",
          advanced.recovery >= 2 ? "Le rendement paye le risque" : "Le rendement paye mal le risque",
          `Max drawdown ${qtFmt(Math.abs(advanced.maxDd), 0)} CHF pour ${fmtChf(stats.net)} de résultat net — recovery factor ${qtFmt(advanced.recovery)}. ${advanced.recovery >= 2 ? "Au-dessus de 2, la courbe gagne nettement plus qu'elle ne rend." : "Sous 2, chaque franc gagné a exigé un stress disproportionné : réduis la taille ou filtre les setups médiocres."}`);
      } else if (stats.net <= 0) {
        push("risque", "Compte sous l'eau",
          `Drawdown en cours de ${qtFmt(Math.abs(advanced.maxDd), 0)} CHF sans nouveau sommet d'équité. Priorité : stopper l'hémorragie (taille minimale), pas la rattraper (grossir les positions après une perte est le chemin le plus court vers zéro).`);
      }
    }

    // 6. Concentration des gains par actif
    {
      const posByPair = new Map();
      let grossPos = 0;
      for (const t of filtered) {
        const p = Number(t.pnl) || 0;
        if (p > 0) {
          const k = t.asset || "?";
          grossPos += p;
          posByPair.set(k, (posByPair.get(k) || 0) + p);
        }
      }
      if (grossPos > 0 && posByPair.size > 1) {
        const top = Array.from(posByPair.entries()).reduce((a, b) => (b[1] > a[1] ? b : a));
        const share = (top[1] / grossPos) * 100;
        if (share >= 60) {
          push("faiblesse", `${top[0]} concentre ${qtFmt(share, 0)}% de tes gains`,
            `Ton résultat dépend d'un seul actif. Ce n'est pas un défaut en soi (les spécialistes existent), mais sache que ton P&L vit et meurt avec les conditions de ${top[0]} — et demande-toi si tes trades sur les autres actifs méritent leur place.`);
        }
      }
    }

    // 7. Pire session / meilleure session (échantillon ≥ 5 trades)
    {
      const sessions = pnlBySession.filter(s => s.n >= 5);
      if (sessions.length >= 2) {
        const worst = sessions[sessions.length - 1];
        const best = sessions[0];
        if (worst.value < 0) {
          push("action", `La session ${worst.label} te coûte de l'argent`,
            `${fmtChf(worst.value)} sur ${worst.n} trades, quand ${best.label} rapporte ${fmtChf(best.value)} sur ${best.n}. Le même trader n'a pas le même edge à toute heure : coupe ou réduis fortement la taille sur ${worst.label} pendant un mois et compare.`);
        }
      }
    }

    // 8. Sur-activité : les grosses journées rapportent-elles vraiment ?
    {
      const dayMap = new Map();
      for (const t of filtered) {
        if (!t.date) continue;
        const k = String(t.date).slice(0, 10);
        const d = dayMap.get(k) || { n: 0, pnl: 0 };
        d.n += 1; d.pnl += Number(t.pnl) || 0;
        dayMap.set(k, d);
      }
      const days = Array.from(dayMap.values());
      if (days.length >= 10) {
        const counts = days.map(d => d.n).sort((a, b) => a - b);
        const median = counts[Math.floor(counts.length / 2)];
        const heavy = days.filter(d => d.n > median * 2);
        const normal = days.filter(d => d.n <= median * 2);
        if (heavy.length >= 3 && normal.length >= 3) {
          const avgHeavy = heavy.reduce((a, b) => a + b.pnl, 0) / heavy.length;
          const avgNormal = normal.reduce((a, b) => a + b.pnl, 0) / normal.length;
          if (avgHeavy < avgNormal) {
            push("faiblesse", "La sur-activité te coûte",
              `Tes ${heavy.length} journées à plus de ${median * 2} trades rapportent ${fmtChf(avgHeavy)} en moyenne, contre ${fmtChf(avgNormal)} les journées normales. Plus de trades ≠ plus de gains : au-delà de ton volume habituel, tu rends l'argent au marché.`);
          }
        }
      }
    }

    // Tri par sévérité, puis avertissement d'échantillon en tête
    const order = { risque: 0, faiblesse: 1, action: 2, force: 3 };
    out.sort((a, b) => (order[a.kind] ?? 9) - (order[b.kind] ?? 9));
    if (n < 30) {
      out.unshift({
        kind: "info", title: "Échantillon limité",
        body: `${n} trades sur la période. En dessous de ~30 trades, aucune statistique n'est fiable : lis ces verdicts comme des tendances à surveiller, pas des certitudes.`,
      });
    }
    return out;
  }, [filtered, stats, advanced, pnlBySession]);

  // Distribution des durées (en mn) — on n'a pas duration en DB → approximé par 0 si pas dispo
  // À défaut, on regroupe par r_multiple en buckets
  const rMultipleHisto = React.useMemo(() => {
    const buckets = [
      { label: "≤-2R", min: -Infinity, max: -2, count: 0 },
      { label: "-2 à -1R", min: -2, max: -1, count: 0 },
      { label: "-1 à 0R", min: -1, max: 0, count: 0 },
      { label: "0 à 1R", min: 0, max: 1, count: 0 },
      { label: "1 à 2R", min: 1, max: 2, count: 0 },
      { label: "≥2R", min: 2, max: Infinity, count: 0 },
    ];
    for (const t of filtered) {
      const r = Number(t.r_multiple);
      if (!isFinite(r)) continue;
      for (const b of buckets) { if (r > b.min && r <= b.max) { b.count += 1; break; } }
    }
    return buckets;
  }, [filtered]);

  // Performance par compte (mini-courbes)
  const accountStats = React.useMemo(() => {
    const map = new Map();
    for (const t of filtered) {
      const k = t.account || t.platform || "—";
      if (!map.has(k)) map.set(k, { trades: [], pnl: 0, wins: 0 });
      const s = map.get(k);
      const p = Number(t.pnl) || 0;
      s.trades.push({ value: 0, date: t.date });
      s.pnl += p;
      if (p >= 0) s.wins += 1;
    }
    // Recalcule la courbe cumulée par compte
    const out = [];
    map.forEach((v, key) => {
      let eq = 0;
      const tradesForK = filtered.filter(t => (t.account || t.platform || "—") === key);
      const curve = tradesForK.map(t => { eq += Number(t.pnl) || 0; return eq; });
      out.push({
        account: key,
        pnl: v.pnl,
        trades: tradesForK.length,
        winRate: tradesForK.length > 0 ? (v.wins / tradesForK.length) * 100 : 0,
        curve,
      });
    });
    return out;
  }, [filtered]);

  // Export CSV
  const exportCsv = () => {
    const headers = ["date", "asset", "direction", "entry_price", "exit_price", "pnl", "r_multiple", "result", "strategy", "session", "account", "platform", "volume"];
    const escape = (v) => {
      if (v == null) return "";
      const s = String(v);
      if (s.includes(",") || s.includes("\"") || s.includes("\n")) return `"${s.replace(/"/g, '""')}"`;
      return s;
    };
    const lines = [headers.join(",")];
    for (const t of filtered) lines.push(headers.map(h => escape(t[h])).join(","));
    const csv = lines.join("\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `nomerus-trades-${dateFrom}-${dateTo}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ padding: 18, display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 14 }}>
      {/* Filtres */}
      <div style={{ gridColumn: "1 / -1", display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center", padding: "10px 14px", background: QT.panel, border: `1px solid ${QT.border}`, borderRadius: 4 }}>
        <span style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute, letterSpacing: 1.5 }}>FILTRE ─</span>
        <button onClick={() => setAccount("TOUS")} style={qtPillStyle(account === "TOUS")}>TOUS LES COMPTES</button>
        {accountsList.map(a => (
          <button key={a} onClick={() => setAccount(a)} style={qtPillStyle(account === a)}>{a.toUpperCase()}</button>
        ))}
        <div style={{ flex: 1 }}/>
        <label style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textDim, display: "inline-flex", alignItems: "center", gap: 6 }}>
          DE
          <input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)} style={{ ...qtInput, width: 130, padding: "4px 8px" }}/>
        </label>
        <label style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textDim, display: "inline-flex", alignItems: "center", gap: 6 }}>
          À
          <input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)} style={{ ...qtInput, width: 130, padding: "4px 8px" }}/>
        </label>
        <button onClick={exportCsv} style={qtBtnGhost}>
          <Icon name="download" size={11} style={{ marginRight: 6 }}/>EXPORT CSV
        </button>
      </div>

      {/* Gros KPIs */}
      <div style={{ gridColumn: "1 / -1", display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 0, background: QT.panel, border: `1px solid ${QT.border}`, borderRadius: 4 }}>
        {[
          ["P&L NET", `${qtSign(stats.net)}${qtFmt(Math.abs(stats.net))} CHF`, stats.net >= 0 ? QT.green : QT.red],
          ["P&L BRUT", `${qtSign(stats.gross)}${qtFmt(Math.abs(stats.gross))} CHF`, QT.text],
          ["PERTES BRUTES", `-${qtFmt(stats.fees)} CHF`, QT.red],
          ["TX RÉUSSITE", `${qtFmt(stats.winRate, 1)}%`, QT.text],
          ["PROFIT FACTOR", isFinite(stats.profitFactor) ? qtFmt(stats.profitFactor) : "—", QT.text],
          ["ESPÉRANCE / TRADE", `${qtSign(stats.expectancy)}${qtFmt(Math.abs(stats.expectancy))} CHF`, QT.text],
          ["MEILLEUR", `${qtSign(stats.best)}${qtFmt(Math.abs(stats.best))} CHF`, QT.green],
          ["PIRE", `${qtSign(stats.worst)}${qtFmt(Math.abs(stats.worst))} CHF`, QT.red],
        ].map(([l, v, c], i, arr) => (
          <div key={l} style={{ padding: "14px 16px", borderRight: i < arr.length - 1 ? `1px solid ${QT.border}` : "none", minWidth: 0, overflow: "hidden" }}>
            <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6, whiteSpace: "nowrap" }}>{l}</div>
            <div style={{ fontFamily: QT.mono, fontSize: 16, color: c, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</div>
          </div>
        ))}
      </div>

      {/* KPIs avancés — ratios de gestion de risque */}
      <div style={{ gridColumn: "1 / -1", display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 0, background: QT.panel, border: `1px solid ${QT.border}`, borderRadius: 4 }}>
        {[
          ["SHARPE · ANN.", advanced.sharpe != null ? qtFmt(advanced.sharpe) : "—", advanced.sharpe != null ? (advanced.sharpe >= 1 ? QT.green : advanced.sharpe >= 0 ? QT.text : QT.red) : QT.textMute],
          ["SORTINO · ANN.", advanced.sortino != null ? qtFmt(advanced.sortino) : "—", advanced.sortino != null ? (advanced.sortino >= 1.5 ? QT.green : advanced.sortino >= 0 ? QT.text : QT.red) : QT.textMute],
          ["MAX DRAWDOWN", advanced.maxDd < 0 ? `-${qtFmt(Math.abs(advanced.maxDd), 0)} CHF` : "0 CHF", QT.red],
          ["RECOVERY FACTOR", advanced.recovery != null ? qtFmt(advanced.recovery) : "—", advanced.recovery != null ? (advanced.recovery >= 2 ? QT.green : QT.text) : QT.textMute],
          ["PAYOFF · GAIN/PERTE", advanced.payoff != null ? qtFmt(advanced.payoff) : "—", advanced.payoff != null ? (advanced.payoff >= 1 ? QT.green : QT.red) : QT.textMute],
          ["KELLY", advanced.kelly != null ? `${qtFmt(advanced.kelly * 100, 1)}%` : "—", advanced.kelly != null ? (advanced.kelly > 0 ? QT.text : QT.red) : QT.textMute],
          ["ESPÉRANCE · R", advanced.expectancyR != null ? `${qtSign(advanced.expectancyR)}${qtFmt(Math.abs(advanced.expectancyR))}R` : "—", advanced.expectancyR != null ? (advanced.expectancyR >= 0 ? QT.green : QT.red) : QT.textMute],
          ["SÉRIE MAX · G/P", `${advanced.maxW}G · ${advanced.maxL}P`, QT.text],
        ].map(([l, v, c], i, arr) => (
          <div key={l} style={{ padding: "14px 16px", borderRight: i < arr.length - 1 ? `1px solid ${QT.border}` : "none", minWidth: 0, overflow: "hidden" }}>
            <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6, whiteSpace: "nowrap" }}>{l}</div>
            <div style={{ fontFamily: QT.mono, fontSize: 16, color: c, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</div>
          </div>
        ))}
      </div>

      {/* Courbe d'équité */}
      <div style={{ gridColumn: "1 / 3" }}>
        <QTPanel title="ÉQUITÉ · STRATÉGIE" pad={false}>
          <div style={{ position: "relative", height: 220, padding: 14 }}>
            <QTInteractiveCurve data={equityPoints} height={192}
              formatValue={(v) => `${qtSign(v)}${qtFmt(Math.abs(v))} CHF`}/>
            <div style={{ position: "absolute", top: 14, left: 14, display: "flex", gap: 14, fontFamily: QT.mono, fontSize: 10 }}>
              <span style={{ color: QT.indigo }}>● NOMERUS</span>
            </div>
          </div>
        </QTPanel>
      </div>

      {/* Drawdown — labels lisibles + tooltip (item 9) */}
      <div>
        <QTPanel title="DRAWDOWN · SOUS L'EAU · CHF" pad={false}>
          <div style={{ position: "relative", height: 220, padding: 14 }}>
            <div style={{ position: "absolute", top: 8, right: 14, display: "flex", flexDirection: "column", gap: 4,
              fontFamily: QT.mono, fontSize: 10, color: QT.textMute, textAlign: "right",
              background: QT.panel, padding: "6px 8px", borderRadius: 3, border: `1px solid ${QT.border}`,
              zIndex: 2,
            }}>
              <div>MAX <span style={{ color: QT.red, fontWeight: 600 }}>{qtFmt(ddStats.max, 0)} CHF</span></div>
              <div>MOY <span style={{ color: QT.text, fontWeight: 600 }}>{qtFmt(ddStats.avg, 0)} CHF</span></div>
              <div>RÉCUP <span style={{ color: QT.text, fontWeight: 600 }}>{ddStats.recoveryDays}</span></div>
            </div>
            <QTInteractiveCurve data={drawdownPoints} color={QT.red} height={192}
              formatValue={(v) => `${qtFmt(v, 0)} CHF`}/>
          </div>
        </QTPanel>
      </div>

      {/* Diagnostic de l'analyste */}
      <div style={{ gridColumn: "1 / -1" }}>
        <QTPanel title="◆ DIAGNOSTIC DE L'ANALYSTE · CALCULÉ SUR TES TRADES">
          {diagnostic.length === 0 ? (
            <div style={{ padding: 10, fontFamily: QT.mono, fontSize: 10, color: QT.textMute, letterSpacing: 1 }}>
              Aucun trade dans la plage sélectionnée.
            </div>
          ) : (
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", gap: 12 }}>
              {diagnostic.map((d, i) => {
                const meta = {
                  risque:    { tag: "RISQUE",    color: QT.red },
                  faiblesse: { tag: "FAIBLESSE", color: QT.amber },
                  action:    { tag: "ACTION",    color: QT.indigo },
                  force:     { tag: "FORCE",     color: QT.green },
                  info:      { tag: "NOTE",      color: QT.textMute },
                }[d.kind] || { tag: "NOTE", color: QT.textMute };
                return (
                  <div key={i} style={{
                    padding: "10px 12px", background: QT.panel2,
                    borderLeft: `2px solid ${meta.color}`, borderRadius: "0 3px 3px 0",
                  }}>
                    <div style={{ fontFamily: QT.mono, fontSize: 9, color: meta.color, letterSpacing: 1.5, marginBottom: 4, fontWeight: 700 }}>
                      [{meta.tag}]
                    </div>
                    <div style={{ fontFamily: QT.sans, fontSize: 12, color: QT.text, fontWeight: 600, marginBottom: 4 }}>{d.title}</div>
                    <div style={{ fontFamily: QT.sans, fontSize: 11, color: QT.textDim, lineHeight: 1.55 }}>{d.body}</div>
                  </div>
                );
              })}
            </div>
          )}
        </QTPanel>
      </div>

      {/* Win / Loss distribution */}
      <div>
        <QTPanel title="DISTRIBUTION · GAGNANTS / PERDANTS">
          <QTBars data={winLossData} valueFormatter={(v) => String(v)} height={160}/>
        </QTPanel>
      </div>

      {/* P&L par paire */}
      <div>
        <QTPanel title="P&L PAR PAIRE">
          <QTBars data={pnlByPair} valueFormatter={(v) => `${qtSign(v)}${qtFmt(Math.abs(v), 0)} CHF`} height={200} colorByValue/>
        </QTPanel>
      </div>

      {/* P&L par jour de la semaine */}
      <div>
        <QTPanel title="P&L PAR JOUR DE SEMAINE">
          <QTBars data={pnlByDow} valueFormatter={(v) => `${qtSign(v)}${qtFmt(Math.abs(v), 0)} CHF`} height={160} colorByValue/>
        </QTPanel>
      </div>

      {/* P&L par session (si le champ est renseigné) */}
      {pnlBySession.length > 0 && (
        <div>
          <QTPanel title="P&L PAR SESSION">
            <QTBars data={pnlBySession} valueFormatter={(v) => `${qtSign(v)}${qtFmt(Math.abs(v), 0)} CHF`} height={160} colorByValue/>
          </QTPanel>
        </div>
      )}

      {/* P&L par stratégie (si le champ est renseigné) */}
      {pnlByStrategy.length > 0 && (
        <div>
          <QTPanel title="P&L PAR STRATÉGIE">
            <QTBars data={pnlByStrategy} valueFormatter={(v) => `${qtSign(v)}${qtFmt(Math.abs(v), 0)} CHF`} height={160} colorByValue/>
          </QTPanel>
        </div>
      )}

      {/* P&L par heure d'entrée (si les dates ont un horodatage) */}
      {pnlByHour.length > 0 && (
        <div>
          <QTPanel title="P&L PAR HEURE D'ENTRÉE">
            <QTBars data={pnlByHour} valueFormatter={(v) => `${qtSign(v)}${qtFmt(Math.abs(v), 0)} CHF`} height={200} colorByValue/>
          </QTPanel>
        </div>
      )}

      {/* Distribution des R-multiples (proxy distribution durée) */}
      <div style={{ gridColumn: "1 / 3" }}>
        <QTPanel title="DISTRIBUTION · R-MULTIPLES">
          <QTBars data={rMultipleHisto.map(b => ({ label: b.label, value: b.count }))}
            valueFormatter={(v) => String(v)} height={170}/>
        </QTPanel>
      </div>

      {/* P&L mensuel — table calendrier */}
      <div style={{ gridColumn: "1 / -1" }}>
        <QTPanel title="P&L MENSUEL · CHF" pad={false}>
          {monthly.length === 0 ? (
            <div style={{ padding: 14, fontFamily: QT.mono, fontSize: 10, color: QT.textMute }}>
              Aucun trade dans la plage sélectionnée.
            </div>
          ) : (
            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", minWidth: 900, borderCollapse: "collapse", fontFamily: QT.mono, fontSize: 11 }}>
                <thead>
                  <tr style={{ color: QT.textMute, fontSize: 9, letterSpacing: 1.5 }}>
                    {["", "JAN", "FÉV", "MAR", "AVR", "MAI", "JUIN", "JUIL", "AOÛT", "SEP", "OCT", "NOV", "DÉC", "ANNÉE"].map((h, idx) => (
                      <th key={idx} style={{
                        textAlign: idx === 0 ? "left" : "right", padding: "8px 12px",
                        fontWeight: 500, borderBottom: `1px solid ${QT.border}`, whiteSpace: "nowrap",
                      }}>{h}</th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {monthly.map(row => (
                    <tr key={row.year} style={{ borderBottom: `1px solid ${QT.border}` }}>
                      <td style={{ padding: "10px 12px", color: QT.text, fontWeight: 600 }}>{row.year}</td>
                      {row.months.map((v, mi) => (
                        <td key={mi} style={{
                          padding: "10px 12px", textAlign: "right", whiteSpace: "nowrap",
                          color: v == null ? QT.textMute : v >= 0 ? QT.green : QT.red,
                          background: v == null ? "transparent" : v >= 0 ? "rgba(91, 217, 154, 0.05)" : "rgba(255, 107, 122, 0.05)",
                        }}>{v == null ? "·" : `${qtSign(v)}${qtFmt(Math.abs(v), 0)}`}</td>
                      ))}
                      <td style={{
                        padding: "10px 12px", textAlign: "right", fontWeight: 700, whiteSpace: "nowrap",
                        color: row.total >= 0 ? QT.green : QT.red,
                      }}>{qtSign(row.total)}{qtFmt(Math.abs(row.total), 0)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </QTPanel>
      </div>

      {/* Performance par compte (cliquable) */}
      <div style={{ gridColumn: "1 / -1" }}>
        <QTPanel title="PERFORMANCE PAR COMPTE · CLIQUE POUR FILTRER" pad={false}>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))" }}>
            {accountStats.length === 0 && (
              <div style={{ padding: 14, fontFamily: QT.mono, fontSize: 10, color: QT.textMute }}>
                Aucun trade dans la plage sélectionnée.
              </div>
            )}
            {accountStats.map((s, i, arr) => (
              <button key={s.account} onClick={() => setAccount(s.account)} style={{
                padding: 14, borderRight: i < arr.length - 1 ? `1px solid ${QT.border}` : "none",
                background: account === s.account ? QT.indigoSoft : "transparent",
                border: "none", textAlign: "left", cursor: "pointer", color: QT.text,
                borderTop: account === s.account ? `2px solid ${QT.indigo}` : "2px solid transparent",
              }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
                  <div style={{ width: 6, height: 6, borderRadius: "50%", background: QT.indigo }}/>
                  <div style={{ fontFamily: QT.sans, fontSize: 12, color: QT.text, fontWeight: 600 }}>{s.account}</div>
                </div>
                <div style={{ fontFamily: QT.mono, fontSize: 20, color: s.pnl >= 0 ? QT.green : QT.red, fontWeight: 600, marginBottom: 4 }}>
                  {qtSign(s.pnl)}{qtFmt(Math.abs(s.pnl))} CHF
                </div>
                <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute, marginBottom: 10 }}>
                  {s.trades} trades · {qtFmt(s.winRate, 0)}% gagnants
                </div>
                <QTSpark data={s.curve.length > 1 ? s.curve : [0, 0]} color={s.pnl >= 0 ? QT.green : QT.red} height={40}/>
              </button>
            ))}
          </div>
        </QTPanel>
      </div>
    </div>
  );
};

const qtPillStyle = (active) => ({
  padding: "4px 10px", fontFamily: QT.mono, fontSize: 10, letterSpacing: 1,
  color: active ? QT.indigo : QT.textDim,
  background: active ? QT.indigoSoft : "transparent",
  border: `1px solid ${active ? QT.borderStrong : QT.border}`,
  borderRadius: 3, cursor: "pointer",
});

// ──────────────────────────────────────────────────────────────
// QTBars — graphique en barres horizontales/verticales simple
// data = [{label, value, color?}], colorByValue : vert si >=0 sinon rouge
// ──────────────────────────────────────────────────────────────
const QTBars = ({ data, height = 200, valueFormatter, colorByValue = false }) => {
  if (!data || data.length === 0) {
    return <div style={{ height, fontFamily: QT.mono, fontSize: 10, color: QT.textMute, display: "flex", alignItems: "center", justifyContent: "center" }}>Aucune donnée.</div>;
  }
  const maxAbs = Math.max(...data.map(d => Math.abs(d.value)), 1);
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: QT.mono, fontSize: 10 }}>
      {data.map((d, i) => {
        const widthPct = (Math.abs(d.value) / maxAbs) * 100;
        const color = d.color || (colorByValue ? (d.value >= 0 ? QT.green : QT.red) : QT.indigo);
        return (
          <div key={i} style={{ display: "grid", gridTemplateColumns: "80px 1fr 80px", gap: 10, alignItems: "center" }}>
            <span style={{ color: QT.textDim, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{d.label}</span>
            <div style={{ height: 14, background: "rgba(124,158,255,0.06)", borderRadius: 2, overflow: "hidden", position: "relative" }}>
              <div style={{
                width: `${widthPct}%`, height: "100%", background: color, opacity: 0.7,
                transition: "width 200ms",
              }}/>
            </div>
            <span style={{ color: QT.text, textAlign: "right", whiteSpace: "nowrap" }}>
              {valueFormatter ? valueFormatter(d.value) : d.value}
            </span>
          </div>
        );
      })}
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// QT2FAManager — enroll/verify/disable TOTP via Supabase MFA SDK
// Le bouton « Gérer le 2FA » qui était retiré du site (DEFERRED #4)
// est remplacé par ce panneau complet, intégralement côté client.
// ──────────────────────────────────────────────────────────────
const QT2FAManager = ({ factors, reloadFactors }) => {
  const verified = (factors || []).find(f => f.status === "verified");
  const [mode, setMode] = React.useState("overview"); // overview | enrolling
  const [enroll, setEnroll] = React.useState(null);    // { factorId, qr, secret }
  const [code, setCode] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState("");
  const [copied, setCopied] = React.useState(false);

  const startEnroll = async () => {
    setBusy(true); setError("");
    try {
      const sb = window.nomerusSb();
      // Supabase autorise jusqu'à 10 facteurs unverified ; on nettoie les vieux
      // pour ne pas saturer si l'utilisateur a abandonné un enroll précédent.
      const stale = (factors || []).filter(f => f.status !== "verified");
      for (const s of stale) {
        try { await sb.auth.mfa.unenroll({ factorId: s.id }); } catch (_) {}
      }
      const { data, error: err } = await sb.auth.mfa.enroll({
        factorType: "totp",
        issuer: "Nomerus",
        friendlyName: `Nomerus · ${new Date().toLocaleDateString("fr-FR")}`,
      });
      if (err) throw err;
      setEnroll({
        factorId: data.id,
        qr: data.totp && data.totp.qr_code,
        secret: data.totp && data.totp.secret,
      });
      setMode("enrolling");
    } catch (e) {
      setError(window.nomerusAuth.errorToFr(e));
    } finally { setBusy(false); }
  };

  const cancel = async () => {
    if (enroll && enroll.factorId) {
      try { await window.nomerusSb().auth.mfa.unenroll({ factorId: enroll.factorId }); } catch (_) {}
    }
    setEnroll(null); setCode(""); setError(""); setMode("overview");
    if (reloadFactors) reloadFactors();
  };

  const verify = async () => {
    if (!enroll || code.length !== 6) return;
    setBusy(true); setError("");
    try {
      const sb = window.nomerusSb();
      const { error: err } = await sb.auth.mfa.challengeAndVerify({
        factorId: enroll.factorId,
        code,
      });
      if (err) throw err;
      setEnroll(null); setCode(""); setMode("overview");
      if (reloadFactors) reloadFactors();
    } catch (e) {
      setError(window.nomerusAuth.errorToFr(e));
    } finally { setBusy(false); }
  };

  const disable = async () => {
    if (!verified) return;
    if (!confirm("Désactiver le 2FA ? Ton compte sera moins protégé.")) return;
    setBusy(true); setError("");
    try {
      const sb = window.nomerusSb();
      const { error: err } = await sb.auth.mfa.unenroll({ factorId: verified.id });
      if (err) throw err;
      if (reloadFactors) reloadFactors();
    } catch (e) {
      setError(window.nomerusAuth.errorToFr(e));
    } finally { setBusy(false); }
  };

  const copySecret = () => {
    if (!enroll || !enroll.secret) return;
    try {
      navigator.clipboard.writeText(enroll.secret);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch (_) {}
  };

  // ── Render: enrollment flow ─────────────────────────────────
  if (mode === "enrolling" && enroll) {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
        <div style={{ fontFamily: QT.sans, fontSize: 12, color: QT.textDim, lineHeight: 1.55 }}>
          Scanne ce QR-code dans <strong style={{ color: QT.text }}>Google Authenticator</strong>,
          {" "}<strong style={{ color: QT.text }}>Authy</strong> ou
          {" "}<strong style={{ color: QT.text }}>1Password</strong>, puis saisis le code à 6 chiffres.
        </div>
        {enroll.qr && (
          <div style={{
            display: "flex", justifyContent: "center", padding: 14, background: "#FFFFFF",
            border: `1px solid ${QT.borderStrong}`, borderRadius: 4,
          }}>
            <img src={enroll.qr} alt="QR Code 2FA" style={{ width: 180, height: 180, display: "block" }}/>
          </div>
        )}
        {enroll.secret && (
          <div>
            <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>
              CLÉ SECRÈTE · SAISIE MANUELLE
            </div>
            <div style={{ display: "flex", gap: 6, alignItems: "stretch" }}>
              <code style={{
                flex: 1, padding: "10px 12px", background: QT.bg, border: `1px solid ${QT.border}`,
                borderRadius: 3, color: QT.text, fontFamily: QT.mono, fontSize: 12,
                letterSpacing: 1.2, wordBreak: "break-all",
              }}>{enroll.secret}</code>
              <button onClick={copySecret} style={{ ...qtBtnGhost, minWidth: 78 }}>
                {copied ? "✓ COPIÉ" : "COPIER"}
              </button>
            </div>
          </div>
        )}
        <QTField label="CODE À 6 CHIFFRES" value={code}
          onChange={(v) => setCode(String(v).replace(/\D/g, "").slice(0, 6))}
          mono placeholder="123456"/>
        {error && (
          <div style={{
            padding: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
            background: "rgba(255, 107, 122, 0.08)", color: QT.red,
          }}>{error}</div>
        )}
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <button onClick={verify} disabled={busy || code.length !== 6} style={{
            ...qtBtnPrimary,
            opacity: (busy || code.length !== 6) ? 0.5 : 1,
            cursor: (busy || code.length !== 6) ? "not-allowed" : "pointer",
          }}>{busy ? "VÉRIFICATION…" : "ACTIVER LE 2FA"}</button>
          <button onClick={cancel} disabled={busy} style={qtBtnGhost}>ANNULER</button>
        </div>
        <div style={{
          padding: 10, background: "rgba(255, 180, 84, 0.06)",
          border: "1px dashed rgba(255, 180, 84, 0.25)", borderRadius: 3,
          fontFamily: QT.mono, fontSize: 10, color: QT.amber, lineHeight: 1.6,
        }}>
          ⚠ Sauvegarde la clé secrète dans un gestionnaire de mots de passe. Sans elle, perdre ton appareil = perdre l'accès au compte.
        </div>
      </div>
    );
  }

  // ── Render: 2FA already verified ─────────────────────────────
  if (verified) {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
        <div style={{
          display: "flex", alignItems: "center", gap: 12, padding: 12,
          background: "rgba(91, 217, 154, 0.06)",
          border: "1px solid rgba(91, 217, 154, 0.25)", borderRadius: 3,
        }}>
          <div style={{
            width: 32, height: 32, borderRadius: 4, background: "rgba(91, 217, 154, 0.15)",
            display: "flex", alignItems: "center", justifyContent: "center",
            color: QT.green, fontFamily: QT.mono, fontSize: 18, fontWeight: 700,
          }}>✓</div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontFamily: QT.sans, fontSize: 13, color: QT.green, fontWeight: 600 }}>2FA activé</div>
            <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textDim, marginTop: 2 }}>
              TOTP · {verified.friendly_name || "Nomerus"}
            </div>
          </div>
        </div>
        {error && (
          <div style={{
            padding: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
            background: "rgba(255, 107, 122, 0.08)", color: QT.red,
          }}>{error}</div>
        )}
        <button onClick={disable} disabled={busy} style={{
          ...qtBtnGhost, color: QT.red, borderColor: "rgba(255, 107, 122, 0.3)",
        }}>{busy ? "…" : "DÉSACTIVER LE 2FA"}</button>
      </div>
    );
  }

  // ── Render: 2FA disabled (default state) ─────────────────────
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 12, padding: 12,
        background: QT.panel2, border: `1px solid ${QT.border}`, borderRadius: 3,
      }}>
        <div style={{
          width: 32, height: 32, borderRadius: 4, background: QT.indigoSoft,
          display: "flex", alignItems: "center", justifyContent: "center", color: QT.indigo,
        }}>
          <Icon name="shield" size={16}/>
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: QT.sans, fontSize: 13, color: QT.text, fontWeight: 600 }}>2FA désactivé</div>
          <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textDim, marginTop: 2 }}>
            Ajoute une deuxième couche via TOTP (app authenticator).
          </div>
        </div>
      </div>
      {error && (
        <div style={{
          padding: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
          background: "rgba(255, 107, 122, 0.08)", color: QT.red,
        }}>{error}</div>
      )}
      <button onClick={startEnroll} disabled={busy} style={qtBtnPrimary}>
        {busy ? "PRÉPARATION…" : "ACTIVER LE 2FA →"}
      </button>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// QTSessionsManager — liste + révocation des sessions actives
// Appelle les Edge Functions list-sessions / revoke-session ;
// fallback gracieux si les fonctions ne sont pas encore déployées.
// ──────────────────────────────────────────────────────────────
const QTSessionsManager = () => {
  const [sessions, setSessions] = React.useState(null); // null = loading, [] | [..] = data, "fallback"
  const [currentSid, setCurrentSid] = React.useState(null);
  const [busy, setBusy] = React.useState(null); // session id en cours de révocation
  const [error, setError] = React.useState("");

  const load = React.useCallback(async () => {
    setError("");
    try {
      const sb = window.nomerusSb();
      if (!sb) return;
      try {
        const { data: { session } } = await sb.auth.getSession();
        if (session && session.access_token) {
          const payload = JSON.parse(atob(session.access_token.split(".")[1]));
          setCurrentSid(payload.session_id || null);
        }
      } catch (_) {}
      const { data, error: err } = await sb.functions.invoke("list-sessions");
      if (err) { setSessions("fallback"); return; }
      setSessions(Array.isArray(data && data.sessions) ? data.sessions : []);
    } catch (_) {
      setSessions("fallback");
    }
  }, []);

  React.useEffect(() => { load(); }, [load]);

  const revoke = async (sid) => {
    if (!confirm("Terminer cette session ? L'appareil sera déconnecté immédiatement.")) return;
    setBusy(sid); setError("");
    try {
      const sb = window.nomerusSb();
      const { error: err } = await sb.functions.invoke("revoke-session", { body: { session_id: sid } });
      if (err) throw err;
      if (sid === currentSid) { await sb.auth.signOut(); window.location.reload(); return; }
      await load();
    } catch (e) {
      setError(window.nomerusAuth.errorToFr(e));
    } finally { setBusy(null); }
  };

  const onLogoutAll = async () => {
    if (!confirm("Te déconnecter de toutes les sessions actives ?")) return;
    await window.nomerusSb().auth.signOut({ scope: "global" });
    window.location.reload();
  };

  const parseUA = (ua) => {
    if (!ua) return "Appareil inconnu";
    const m = String(ua);
    let os = "—", browser = "—";
    if (/Windows/i.test(m)) os = "Windows";
    else if (/Mac OS X|Macintosh/i.test(m)) os = "macOS";
    else if (/Android/i.test(m)) os = "Android";
    else if (/iPhone|iPad|iOS/i.test(m)) os = "iOS";
    else if (/Linux/i.test(m)) os = "Linux";
    if (/Edg\//i.test(m)) browser = "Edge";
    else if (/Chrome\//i.test(m)) browser = "Chrome";
    else if (/Firefox\//i.test(m)) browser = "Firefox";
    else if (/Safari\//i.test(m)) browser = "Safari";
    return `${browser} · ${os}`;
  };

  if (sessions === null) {
    return <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute }}>Chargement…</div>;
  }

  if (sessions === "fallback") {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
        <div style={{
          padding: 10, background: "rgba(124, 158, 255, 0.04)",
          border: `1px dashed ${QT.border}`, borderRadius: 3,
          fontFamily: QT.mono, fontSize: 10, color: QT.textDim, lineHeight: 1.6,
        }}>
          <span style={{ color: QT.amber }}>// INFO</span> · Le détail par appareil arrive bientôt.
          Tu peux déjà fermer toutes tes sessions actives avec le bouton ci-dessous.
        </div>
        <button onClick={onLogoutAll} style={qtBtnGhost}>SE DÉCONNECTER PARTOUT</button>
      </div>
    );
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
      {error && (
        <div style={{
          padding: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
          background: "rgba(255, 107, 122, 0.08)", color: QT.red,
        }}>{error}</div>
      )}
      {sessions.length === 0 && (
        <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute }}>Aucune session active.</div>
      )}
      {sessions.map((s) => {
        const isCurrent = s.id === currentSid;
        const updated = s.updated_at ? new Date(s.updated_at) : null;
        return (
          <div key={s.id} style={{
            display: "flex", alignItems: "center", gap: 12, padding: "10px 12px",
            background: isCurrent ? QT.indigoSoft : QT.panel2,
            border: `1px solid ${isCurrent ? "rgba(124, 158, 255, 0.3)" : QT.border}`,
            borderRadius: 3,
          }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{
                fontFamily: QT.sans, fontSize: 12, color: QT.text, fontWeight: 600,
                whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
              }}>
                {parseUA(s.user_agent)}
                {isCurrent && (
                  <span style={{ marginLeft: 8, fontFamily: QT.mono, fontSize: 9, color: QT.indigo, letterSpacing: 1 }}>
                    · CETTE SESSION
                  </span>
                )}
              </div>
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1, marginTop: 2 }}>
                IP {s.ip || "—"} · vu {updated ? updated.toLocaleString("fr-FR", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : "—"}
              </div>
            </div>
            <button onClick={() => revoke(s.id)} disabled={busy === s.id} style={{
              ...qtBtnGhost, padding: "6px 10px", fontSize: 9,
              color: isCurrent ? QT.amber : QT.red,
              borderColor: isCurrent ? "rgba(255, 180, 84, 0.3)" : "rgba(255, 107, 122, 0.3)",
            }}>{busy === s.id ? "…" : "TERMINER"}</button>
          </div>
        );
      })}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 6, flexWrap: "wrap", gap: 8 }}>
        <span style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1 }}>
          {sessions.length} SESSION{sessions.length > 1 ? "S" : ""} · TEMPS RÉEL
        </span>
        <button onClick={onLogoutAll} style={{ ...qtBtnGhost, padding: "6px 12px", fontSize: 9 }}>
          SE DÉCONNECTER PARTOUT
        </button>
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// QTAccount — Profil utilisateur réel + édition
// ──────────────────────────────────────────────────────────────
const QTAccount = ({ go } = {}) => {
  const [user, setUser] = React.useState(null);
  const [profile, setProfile] = React.useState(null);
  const [sub, setSub] = React.useState(null);
  const [factors, setFactors] = React.useState([]);
  const [editing, setEditing] = React.useState(false);
  const [displayName, setDisplayName] = React.useState("");
  const [emailEdit, setEmailEdit] = React.useState("");
  const [passwordEdit, setPasswordEdit] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [feedback, setFeedback] = React.useState(null);

  const reloadFactors = React.useCallback(async () => {
    try {
      const sb = window.nomerusSb();
      if (!sb) return;
      const { data: f } = await sb.auth.mfa.listFactors();
      setFactors((f && f.all) || []);
    } catch (e) {}
  }, []);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const sb = window.nomerusSb();
        if (!sb) return;
        const { data: { user: u } } = await sb.auth.getUser();
        if (cancelled) return;
        setUser(u);
        if (u) {
          setEmailEdit(u.email || "");
          setDisplayName((u.user_metadata && u.user_metadata.display_name) || "");
        }
        const { data: prof } = await sb.from("profiles").select("*").maybeSingle();
        if (!cancelled) setProfile(prof);
        const { data: s } = await sb.from("subscriptions").select("plan, status, current_period_end, stripe_customer_id").maybeSingle();
        if (!cancelled) setSub(s);
        if (!cancelled) await reloadFactors();
      } catch (e) {}
    })();
    return () => { cancelled = true; };
  }, [reloadFactors]);

  const onSave = async () => {
    setSaving(true);
    setFeedback(null);
    try {
      const sb = window.nomerusSb();
      const updates = { data: { display_name: displayName } };
      if (emailEdit && emailEdit !== (user && user.email)) updates.email = emailEdit;
      if (passwordEdit) updates.password = passwordEdit;
      const { error: authErr } = await sb.auth.updateUser(updates);
      if (authErr) throw authErr;
      // Reflète le nom dans la table profiles si présente
      if (profile) {
        await sb.from("profiles").update({ display_name: displayName }).eq("id", user.id);
      }
      setFeedback({ ok: true, message: "Profil mis à jour." });
      setEditing(false);
      setPasswordEdit("");
    } catch (e) {
      setFeedback({ ok: false, message: e.message || "Erreur lors de la sauvegarde." });
    } finally {
      setSaving(false);
    }
  };

  const goCheckout = async () => {
    // Crée une session Stripe Checkout (plan mensuel) via l'Edge Function
    // et redirige vers l'URL de paiement. Le client_reference_id (user.id)
    // est posé côté serveur pour que le webhook retrouve l'utilisateur.
    try {
      const sb = window.nomerusSb();
      if (window.NomerusAnalytics) window.NomerusAnalytics.track("checkout_started", { cadence: "monthly", from: "account" });
      const { data, error } = await sb.functions.invoke("create-checkout-session", {
        body: { price_lookup_key: "pro_monthly" },
      });
      if (error || !data || !data.checkout_url) {
        alert("Impossible de créer la session de paiement. Réessaie dans un instant.");
        return;
      }
      window.location.href = data.checkout_url;
    } catch (_) {
      alert("Erreur réseau. Réessaie dans un instant.");
    }
  };

  const goPortal = async () => {
    // Ouvre le portail client Stripe (gérer/annuler l'abonnement, factures).
    try {
      const sb = window.nomerusSb();
      const { data, error } = await sb.functions.invoke("create-portal-session");
      if (error || !data || !data.portal_url) {
        // 404 no_stripe_customer = compte Pro accordé manuellement, sans client
        // Stripe : il n'y a pas d'abonnement à gérer, on l'explique clairement.
        let code = "";
        try {
          const body = error && error.context ? await error.context.json() : null;
          code = (body && body.error) || "";
        } catch (_) {}
        if (code === "no_stripe_customer") {
          alert("Ton accès Pro a été accordé manuellement : il n'y a pas d'abonnement Stripe à gérer.");
        } else if (code === "stale_stripe_customer") {
          alert("La référence de paiement de ton compte n'existe plus côté Stripe. Écris-nous à contact@nomerus.com pour la corriger.");
        } else {
          alert("Impossible d'ouvrir la gestion de l'abonnement. Réessaie dans un instant.");
        }
        return;
      }
      window.location.href = data.portal_url;
    } catch (_) {
      alert("Erreur réseau. Réessaie dans un instant.");
    }
  };

  const userId = user ? user.id.slice(0, 8) : "—";
  const memberSince = user && user.created_at ? new Date(user.created_at).toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric" }) : "—";
  const plan = sub ? sub.plan : "FREE";
  const has2FA = (factors || []).some(f => f.status === "verified");
  const verified = user && user.email_confirmed_at ? "VÉRIFIÉ ✓" : "EN ATTENTE";
  // Les comptes founder/grant manuel ont un current_period_end placeholder très
  // lointain (ou pas de client Stripe du tout) : on affiche « Accès fondateur »
  // plutôt qu'une date, et on ne propose pas le portail Stripe (rien à y gérer).
  const periodEnd = sub && sub.current_period_end ? new Date(sub.current_period_end) : null;
  const isPro = (plan || "").toLowerCase() === "pro";
  const isFounderGrant =
    (periodEnd && periodEnd.getTime() > Date.now() + 5 * 365 * 86400e3) ||
    (isPro && sub && !sub.stripe_customer_id);
  const nextInvoice = isFounderGrant ? "ACCÈS FONDATEUR"
    : !periodEnd ? "—"
    : periodEnd.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric" });

  return (
    <div style={{ padding: 18, display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", gap: 14 }}>
      <div>
        <QTPanel title="UTILISATEUR · IDENTITÉ">
          <div style={{ display: "flex", gap: 14, alignItems: "center", marginBottom: 16 }}>
            <div style={{
              width: 56, height: 56, borderRadius: 4, background: QT.indigoSoft,
              display: "flex", alignItems: "center", justifyContent: "center",
              color: QT.indigo, fontFamily: QT.mono, fontSize: 20, fontWeight: 700,
              border: `1px solid ${QT.borderStrong}`,
            }}>{(user && user.email ? user.email.slice(0, 2) : "VA").toUpperCase()}</div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontFamily: QT.sans, fontSize: 16, color: QT.text, fontWeight: 600 }}>
                {displayName || (user && user.email) || "Utilisateur Nomerus"}
              </div>
              <div style={{ fontFamily: QT.mono, fontSize: 11, color: QT.textDim, wordBreak: "break-all" }}>{user ? user.email : "—"}</div>
            </div>
          </div>

          {!editing ? (
            <>
              <div style={{ display: "flex", flexDirection: "column", gap: 10, fontFamily: QT.mono, fontSize: 11 }}>
                {[
                  ["ID UTILISATEUR", userId, QT.text],
                  ["PLAN", (plan || "FREE").toUpperCase(), QT.indigo],
                  ["MEMBRE DEPUIS", memberSince, QT.text],
                  ["EMAIL", verified, user && user.email_confirmed_at ? QT.green : QT.amber],
                  ["2FA", has2FA ? "ACTIVÉ" : "DÉSACTIVÉ", has2FA ? QT.green : QT.textDim],
                ].map(([k, v, c]) => (
                  <div key={k} style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${QT.border}` }}>
                    <span style={{ color: QT.textMute, letterSpacing: 1 }}>{k}</span>
                    <span style={{ color: c, fontWeight: 500 }}>{v}</span>
                  </div>
                ))}
              </div>
              <div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
                <button onClick={() => setEditing(true)} style={qtBtnPrimary}>MODIFIER LE PROFIL</button>
              </div>
            </>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              <QTField label="NOM AFFICHÉ" value={displayName} onChange={setDisplayName} placeholder="Ton prénom"/>
              <QTField label="EMAIL" value={emailEdit} onChange={setEmailEdit} mono type="email"/>
              <QTField label="NOUVEAU MOT DE PASSE (FACULTATIF)" value={passwordEdit} onChange={setPasswordEdit} mono type="password" placeholder="Laisse vide pour ne pas changer"/>
              {feedback && (
                <div style={{
                  padding: 8, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
                  background: feedback.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
                  color: feedback.ok ? QT.green : QT.red,
                }}>{feedback.message}</div>
              )}
              <div style={{ display: "flex", gap: 8 }}>
                <button onClick={onSave} disabled={saving} style={{ ...qtBtnPrimary, opacity: saving ? 0.6 : 1 }}>
                  {saving ? "ENREGISTREMENT…" : "SAUVEGARDER"}
                </button>
                <button onClick={() => { setEditing(false); setFeedback(null); }} style={qtBtnGhost}>ANNULER</button>
              </div>
            </div>
          )}
        </QTPanel>
      </div>

      <div>
        <QTPanel title="SÉCURITÉ · 2FA · TOTP">
          <QT2FAManager factors={factors} reloadFactors={reloadFactors}/>
        </QTPanel>
      </div>

      <div>
        <QTPanel title="SÉCURITÉ · SESSIONS ACTIVES">
          <QTSessionsManager/>
        </QTPanel>
      </div>

      <div>
        <QTPanel title="FACTURATION · ABONNEMENT">
          <div style={{ padding: 12, background: QT.panel2, border: `1px solid ${QT.borderStrong}`, borderRadius: 3, marginBottom: 12 }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8 }}>
              <div style={{ fontFamily: QT.sans, fontSize: 16, color: QT.text, fontWeight: 700, letterSpacing: -0.3 }}>
                NOMERUS {(plan || "FREE").toUpperCase()}
              </div>
              <div style={{ fontFamily: QT.mono, fontSize: 14, color: QT.indigo, fontWeight: 600 }}>
                {isFounderGrant ? "OFFERT" : isPro ? `${NOMERUS_PRO_PRICE_LABEL} / mois` : "0 CHF"}
              </div>
            </div>
            <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textDim, lineHeight: 1.6 }}>
              ✓ JOURNAL DE TRADING ILLIMITÉ<br/>
              ✓ ANALYSES PROFONDES + EXPORTS CSV<br/>
              ✓ COACH IA<br/>
              ✓ SYNC BROKER (BETA PRIVÉE À VENIR)
            </div>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 10, fontFamily: QT.mono, fontSize: 11 }}>
            <div style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${QT.border}` }}>
              <span style={{ color: QT.textMute }}>PROCHAINE ÉCHÉANCE</span>
              <span style={{ color: QT.text }}>{nextInvoice}</span>
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${QT.border}` }}>
              <span style={{ color: QT.textMute }}>STATUT</span>
              <span style={{ color: sub && sub.status === "active" ? QT.green : QT.amber }}>
                {sub ? (sub.status || "—").toUpperCase() : "—"}
              </span>
            </div>
          </div>
          <div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
            {!isPro ? (
              <button onClick={goCheckout} style={qtBtnPrimary}>PASSER EN PRO · {NOMERUS_PRO_PRICE_LABEL} →</button>
            ) : !isFounderGrant && (
              <button onClick={goPortal} style={qtBtnGhost}>GÉRER MON ABONNEMENT →</button>
            )}
          </div>
        </QTPanel>
      </div>

      <div>
        <QTPanel title="ACTIVITÉ · 7 DERNIERS ÉVÉNEMENTS">
          <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.textMute, lineHeight: 1.6 }}>
            Le journal d'activité détaillé sera disponible après l'activation du logger Supabase.
            En attendant, tu peux consulter tes connexions et trades depuis l'onglet ANALYSE.
          </div>
        </QTPanel>
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// QTSettings — persistance localStorage (+ Supabase si dispo) + notifs in-app
// ──────────────────────────────────────────────────────────────
const SETTINGS_KEY = "nomerus_user_settings_v1";
const DEFAULT_SETTINGS = {
  risk_per_trade: 2,
  max_drawdown: 15,
  max_open_positions: 12,
  daily_loss_limit: 2500,
  slippage_protection: true,
  weekend_trading: true,
  news_blackout: true,
  shadow_mode: false,
  notif_in_app: {
    trade_executed: true,
    bot_paused_error: true,
    daily_pnl_summary: true,
    drawdown_alert: true,
    ai_insight_generated: false,
    broker_disconnected: true,
  },
  notif_email: {
    trade_executed: false,
    bot_paused_error: true,
    daily_pnl_summary: true,
    drawdown_alert: true,
    ai_insight_generated: false,
    broker_disconnected: true,
  },
};

// Clés « métier » persistées cross-appareil (les préférences d'affichage
// — thème, densité… — restent locales à chaque appareil, par design).
const QT_RISK_KEYS = ["risk_per_trade", "max_drawdown", "max_open_positions", "daily_loss_limit"];

const QTSettings = ({ reloadNotifs } = {}) => {
  const [settings, setSettings] = React.useState(() => {
    try {
      const raw = localStorage.getItem(SETTINGS_KEY);
      if (raw) return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
    } catch (e) {}
    return DEFAULT_SETTINGS;
  });
  const [savedAt, setSavedAt] = React.useState(null);
  const [testMsg, setTestMsg] = React.useState(null); // { ok, message } — remplace les alert()
  // Thème/densité/format numérique/animations/mode trader vivent dans le
  // store global (dashboard.jsx) pour que tout l'écran de l'app y réagisse
  // en direct, pas seulement ce panneau.
  const display = window.useNomerusDisplaySettings();
  const skipPersist = React.useRef(true);   // pas d'écriture au montage
  const skipEmailSync = React.useRef(true);
  const persistTimer = React.useRef(null);
  const emailTimer = React.useRef(null);

  // Hydratation depuis le cloud (profiles.settings) : les réglages
  // risque/notifications suivent l'utilisateur d'un appareil à l'autre.
  // Tolérant si la colonne n'existe pas encore (migration 20260702) —
  // localStorage reste alors la seule source.
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const sb = window.nomerusSb();
        if (!sb) return;
        const { data, error } = await sb.from("profiles").select("settings").maybeSingle();
        if (cancelled || error || !data || !data.settings || typeof data.settings !== "object") return;
        const s = data.settings;
        setSettings(prev => {
          const next = { ...prev };
          QT_RISK_KEYS.forEach(k => { if (typeof s[k] === "number" && Number.isFinite(s[k])) next[k] = s[k]; });
          if (s.notif_in_app && typeof s.notif_in_app === "object") next.notif_in_app = { ...prev.notif_in_app, ...s.notif_in_app };
          if (s.notif_email && typeof s.notif_email === "object") next.notif_email = { ...prev.notif_email, ...s.notif_email };
          return next;
        });
      } catch (_) {}
    })();
    return () => { cancelled = true; };
  }, []);

  // Persistance auto à chaque modif — localStorage tout de suite, cloud
  // debouncé (un slider génère des dizaines de ticks, pas des dizaines
  // d'UPDATE). Fusionne avec le store d'affichage global pour ne pas
  // écraser theme/density/etc. (les deux stores partagent la même clé).
  React.useEffect(() => {
    if (skipPersist.current) { skipPersist.current = false; return; }
    const merged = { ...settings, ...window.nomerusDisplaySettings };
    try {
      localStorage.setItem(SETTINGS_KEY, JSON.stringify(merged));
      setSavedAt(new Date());
    } catch (e) {}
    if (persistTimer.current) clearTimeout(persistTimer.current);
    persistTimer.current = setTimeout(async () => {
      try {
        const sb = window.nomerusSb();
        if (!sb) return;
        const { data: { user } } = await sb.auth.getUser();
        if (!user) return;
        await sb.from("profiles").update({ settings: merged }).eq("id", user.id);
      } catch (e) { /* colonne optionnelle tant que la migration n'est pas appliquée */ }
    }, 800);
  }, [settings]);

  const set = (k, v) => setSettings(s => ({ ...s, [k]: v }));
  const setNotif = (k, v) => setSettings(s => ({ ...s, notif_in_app: { ...s.notif_in_app, [k]: v } }));
  const setEmailNotif = (k, v) => setSettings(s => ({ ...s, notif_email: { ...s.notif_email, [k]: v } }));

  // Saisie numérique tolérante : champ vidable pendant l'édition,
  // valeur bornée dès qu'elle est interprétable (pas de négatif/NaN).
  const setIntClamped = (k, raw, min, max) => {
    if (raw === "") { set(k, ""); return; }
    const n = parseInt(raw, 10);
    if (!Number.isFinite(n)) return;
    set(k, Math.max(min, Math.min(max, n)));
  };
  const restoreIfEmpty = (k) => {
    setSettings(s => (s[k] === "" ? { ...s, [k]: DEFAULT_SETTINGS[k] } : s));
  };

  // Sync dédié vers profiles.email_notifications (utilisé par l'Edge Function send-email).
  // On garde l'écriture full-settings dans profiles.settings via l'effet ci-dessus,
  // mais le worker email lit la colonne dédiée pour des perfs/RLS plus fines.
  React.useEffect(() => {
    if (skipEmailSync.current) { skipEmailSync.current = false; return; }
    if (emailTimer.current) clearTimeout(emailTimer.current);
    const payload = settings.notif_email;
    emailTimer.current = setTimeout(async () => {
      try {
        const sb = window.nomerusSb();
        if (!sb) return;
        const { data: { user } } = await sb.auth.getUser();
        if (!user) return;
        await sb.from("profiles")
          .update({ email_notifications: payload })
          .eq("id", user.id);
      } catch (e) { /* colonne optionnelle tant que la migration n'est pas appliquée */ }
    }, 800);
  }, [settings.notif_email]);

  // Le message de test s'efface tout seul.
  React.useEffect(() => {
    if (!testMsg) return;
    const t = setTimeout(() => setTestMsg(null), 8000);
    return () => clearTimeout(t);
  }, [testMsg]);

  // Déclencheur de test : crée une notification in-app et tente d'envoyer un
  // email. Feedback inline (pas d'alert() bloquant).
  const triggerTestNotif = async () => {
    setTestMsg(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) { setTestMsg({ ok: false, message: "Connexion au serveur indisponible. Recharge la page." }); return; }
      const { data: { user } } = await sb.auth.getUser();
      if (!user) { setTestMsg({ ok: false, message: "Session expirée — reconnecte-toi." }); return; }
      const { error } = await sb.from("notifications").insert({
        user_id: user.id,
        kind: "drawdown_alert",
        title: "Test · alerte drawdown",
        body: `Test généré depuis Paramètres. Limite : ${settings.max_drawdown}%.`,
        read: false,
      });
      if (error) {
        setTestMsg({ ok: false, message: "Les notifications ne sont pas encore disponibles sur ton compte. Réessaie plus tard." });
        return;
      }
      if (reloadNotifs) reloadNotifs();
      // Tente d'envoyer un email via l'Edge Function. Si la fonction n'est pas
      // déployée, on échoue proprement — l'in-app reste fonctionnelle.
      let emailNote = "";
      if (settings.notif_email.drawdown_alert) {
        try {
          const { error: emailErr } = await sb.functions.invoke("send-email", {
            body: {
              kind: "drawdown_alert",
              title: "Test · alerte drawdown",
              body: `Test généré depuis Paramètres. Limite : ${settings.max_drawdown}%.`,
            },
          });
          emailNote = emailErr ? " (Email non envoyé — les notifications email arrivent bientôt.)" : " Email envoyé.";
        } catch (_) {
          emailNote = " (Email non envoyé — les notifications email arrivent bientôt.)";
        }
      }
      setTestMsg({ ok: true, message: "Notification envoyée — ouvre la cloche en haut à droite." + emailNote });
    } catch (e) {
      setTestMsg({ ok: false, message: e.message || "Erreur lors du test." });
    }
  };

  return (
    <div style={{ padding: 18, display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))", gap: 14 }}>
      <QTPanel title="GESTION DU RISQUE · GLOBAL" right={
        savedAt && <span style={{ color: QT.green, fontFamily: QT.mono, fontSize: 9 }}>● ENREGISTRÉ</span>
      }>
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          <QTSetting label="RISQUE PAR TRADE" value={`${settings.risk_per_trade}%`} desc="% max d'équité risqué sur une seule position">
            <QTSlider value={settings.risk_per_trade} setValue={(v) => set("risk_per_trade", v)} min={0.1} max={5} step={0.1}/>
          </QTSetting>
          <QTSetting label="DRAWDOWN MAX · COUPE-CIRCUIT" value={`${settings.max_drawdown}%`} desc="Mise en pause si l'équité chute de ce %">
            <QTSlider value={settings.max_drawdown} setValue={(v) => set("max_drawdown", v)} min={5} max={30} step={1}/>
          </QTSetting>
          <QTSetting label="POSITIONS OUVERTES MAX" value={String(settings.max_open_positions)}>
            <input type="number" min={1} max={200} value={settings.max_open_positions}
              onChange={(e) => setIntClamped("max_open_positions", e.target.value, 1, 200)}
              onBlur={() => restoreIfEmpty("max_open_positions")} style={qtInput}/>
          </QTSetting>
          <QTSetting label="LIMITE DE PERTE JOURNALIÈRE" value={`${settings.daily_loss_limit} CHF`} desc="Pause auto pour la journée si dépassée">
            <input type="number" min={0} max={1000000} step={50} value={settings.daily_loss_limit}
              onChange={(e) => setIntClamped("daily_loss_limit", e.target.value, 0, 1000000)}
              onBlur={() => restoreIfEmpty("daily_loss_limit")} style={qtInput}/>
          </QTSetting>
        </div>
      </QTPanel>

      <QTPanel title="NOTIFICATIONS · CANAUX" right={
        <button onClick={triggerTestNotif} style={{ ...qtBtnGhost, padding: "4px 10px", fontSize: 9 }}>TEST</button>
      }>
        {testMsg && (
          <div style={{
            padding: 8, marginBottom: 10, borderRadius: 3, fontFamily: QT.mono, fontSize: 10,
            background: testMsg.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
            border: `1px solid ${testMsg.ok ? "rgba(91, 217, 154, 0.3)" : "rgba(255, 107, 122, 0.3)"}`,
            color: testMsg.ok ? QT.green : QT.red, lineHeight: 1.5,
          }}>{testMsg.message}</div>
        )}
        <div style={{ display: "grid", gridTemplateColumns: "1fr auto auto", columnGap: 16, rowGap: 4, alignItems: "center" }}>
          {/* En-tête des colonnes */}
          <span/>
          <span style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, textAlign: "center", padding: "4px 0" }}>IN-APP</span>
          <span style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, textAlign: "center", padding: "4px 0" }}>EMAIL</span>
          {[
            ["trade_executed", "TRADE EXÉCUTÉ"],
            ["bot_paused_error", "BROKER EN PAUSE · ERREUR"],
            ["daily_pnl_summary", "RÉSUMÉ P&L JOURNALIER"],
            ["drawdown_alert", "ALERTE DRAWDOWN"],
            ["ai_insight_generated", "INSIGHT IA GÉNÉRÉ"],
            ["broker_disconnected", "BROKER DÉCONNECTÉ"],
          ].map(([k, label]) => (
            <React.Fragment key={k}>
              <span style={{ fontFamily: QT.mono, fontSize: 11, color: QT.text, letterSpacing: 1, padding: "10px 0", borderBottom: `1px solid ${QT.border}` }}>{label}</span>
              <div style={{ display: "flex", justifyContent: "center", padding: "10px 0", borderBottom: `1px solid ${QT.border}` }}>
                <QTCheck on={settings.notif_in_app[k]} onClick={() => setNotif(k, !settings.notif_in_app[k])}/>
              </div>
              <div style={{ display: "flex", justifyContent: "center", padding: "10px 0", borderBottom: `1px solid ${QT.border}` }}>
                <QTCheck on={settings.notif_email[k]} onClick={() => setEmailNotif(k, !settings.notif_email[k])}/>
              </div>
            </React.Fragment>
          ))}
        </div>
        {!display.trader_mode && (
          <div style={{ marginTop: 14, padding: 10, background: "rgba(124, 158, 255, 0.04)", border: `1px dashed ${QT.border}`, borderRadius: 3,
            fontFamily: QT.mono, fontSize: 10, color: QT.textDim, lineHeight: 1.6 }}>
            <span style={{ color: QT.amber }}>// INFO</span> · Les notifications email sont en cours d'activation.
            En attendant, les notifications in-app (cloche en haut à droite) sont pleinement actives.
          </div>
        )}
      </QTPanel>

      <QTPanel title="APPARENCE · AFFICHAGE">
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          <QTSetting label="THÈME" value="">
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {["DARK", "TAMISÉ", "TERMINAL"].map(v => (
                <button key={v} onClick={() => window.nomerusSetSetting("theme", v)} style={qtPillStyle(display.theme === v)}>{v}</button>
              ))}
            </div>
          </QTSetting>
          <QTSetting label="DENSITÉ" value="">
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {["COMPACT", "NORMAL", "AÉRÉ"].map(v => (
                <button key={v} onClick={() => window.nomerusSetSetting("density", v)} style={qtPillStyle(display.density === v)}>{v}</button>
              ))}
            </div>
          </QTSetting>
          <QTSetting label="FORMAT NUMÉRIQUE" value="">
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {[["fr","1 234,56"],["us","1,234.56"],["de","1.234,56"]].map(([k, label]) => (
                <button key={k} onClick={() => window.nomerusSetSetting("number_format", k)} style={qtPillStyle(display.number_format === k)}>{label}</button>
              ))}
            </div>
          </QTSetting>
          <QTToggle label="RÉDUIRE LES ANIMATIONS" desc="Désactive transitions et animations" value={display.reduce_motion} setValue={(v) => window.nomerusSetSetting("reduce_motion", v)}/>
          <QTToggle label="MODE TRADER" desc="Cache les explications, tables plus denses" value={display.trader_mode} setValue={(v) => window.nomerusSetSetting("trader_mode", v)}/>
        </div>
      </QTPanel>
    </div>
  );
};

const QTSetting = ({ label, value, desc, children }) => {
  const { trader_mode } = window.useNomerusDisplaySettings();
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
        <span style={{ fontFamily: QT.mono, fontSize: 10, color: QT.text, letterSpacing: 1.5, fontWeight: 600 }}>{label}</span>
        {value && <span style={{ fontFamily: QT.mono, fontSize: 12, color: QT.indigo, fontWeight: 600 }}>{value}</span>}
      </div>
      {desc && !trader_mode && <div style={{ fontFamily: QT.sans, fontSize: 11, color: QT.textMute, marginBottom: 10 }}>{desc}</div>}
      <div>{children}</div>
    </div>
  );
};
const QTSlider = ({ value, setValue, min, max, step }) => (
  <div style={{ position: "relative", padding: "10px 0" }}>
    <input type="range" min={min} max={max} step={step} value={value} onChange={e => setValue(parseFloat(e.target.value))}
      style={{ width: "100%", accentColor: QT.indigo }}/>
  </div>
);
const QTToggle = ({ label, desc, value, setValue }) => {
  const { trader_mode } = window.useNomerusDisplaySettings();
  return (
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
      <div>
        <div style={{ fontFamily: QT.mono, fontSize: 10, color: QT.text, letterSpacing: 1.5, fontWeight: 600 }}>{label}</div>
        {desc && !trader_mode && <div style={{ fontFamily: QT.sans, fontSize: 11, color: QT.textMute, marginTop: 2 }}>{desc}</div>}
      </div>
      <button onClick={() => setValue(!value)} style={{
        width: 36, height: 20, borderRadius: 10, position: "relative", flexShrink: 0,
        background: value ? QT.indigo : QT.panel2, border: `1px solid ${value ? QT.indigo : QT.border}`,
        cursor: "pointer", padding: 0,
      }}>
        <div style={{
          position: "absolute", top: 1, left: value ? 17 : 1, width: 16, height: 16, borderRadius: "50%",
          background: value ? QT.bg : QT.textDim, transition: "left 150ms",
        }}/>
      </button>
    </div>
  );
};
const QTCheck = ({ on, onClick }) => (
  <button onClick={onClick} style={{
    width: 18, height: 18, borderRadius: 3, padding: 0,
    background: on ? QT.indigo : "transparent",
    border: `1px solid ${on ? QT.indigo : QT.border}`,
    display: "flex", alignItems: "center", justifyContent: "center",
    color: QT.bg, cursor: onClick ? "pointer" : "default",
  }}>
    {on && <Icon name="check" size={11} stroke={3}/>}
  </button>
);

// ──────────────────────────────────────────────────────────────
// QTJournal — liste éditable (chaque trade cliquable)
// ──────────────────────────────────────────────────────────────
// ── Import CSV : parsing + mapping colonnes broker ───────────────
const qtParseCsv = (text) => {
  const nl = text.indexOf("\n");
  const firstLine = nl < 0 ? text : text.slice(0, nl);
  let delim = ",", best = 0;
  for (const d of [",", ";", "\t"]) {
    const n = firstLine.split(d).length;
    if (n > best) { best = n; delim = d; }
  }
  const rows = [];
  let row = [], field = "", inQ = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (inQ) {
      if (c === '"') {
        if (text[i + 1] === '"') { field += '"'; i++; }
        else inQ = false;
      } else field += c;
    } else if (c === '"') inQ = true;
    else if (c === delim) { row.push(field); field = ""; }
    else if (c === "\n" || c === "\r") {
      if (c === "\r" && text[i + 1] === "\n") i++;
      row.push(field); field = "";
      if (row.some(f => f.trim() !== "")) rows.push(row);
      row = [];
    } else field += c;
  }
  row.push(field);
  if (row.some(f => f.trim() !== "")) rows.push(row);
  return rows;
};

// Synonymes d'en-têtes acceptés (exports MT4/MT5, cTrader, Binance, génériques)
const QT_CSV_SYNONYMS = {
  date: ["date", "time", "close time", "open time", "close_time", "open_time", "heure", "datetime", "date/time", "time(utc)", "date(utc)"],
  asset: ["asset", "symbol", "pair", "instrument", "symbole", "market", "actif", "paire"],
  direction: ["direction", "side", "type", "sens", "position"],
  entry_price: ["entry_price", "entry", "open price", "open_price", "price open", "prix d'entrée", "prix d'entree", "avg entry price", "entry price"],
  exit_price: ["exit_price", "exit", "close price", "close_price", "price close", "prix de sortie", "avg exit price", "exit price"],
  pnl: ["pnl", "profit", "p&l", "p/l", "realized pnl", "realized profit", "realized p&l", "net profit", "gain", "closed p/l", "profit/loss"],
  volume: ["volume", "size", "lots", "qty", "quantity", "taille", "amount", "contracts"],
  r_multiple: ["r_multiple", "r", "rr", "r-multiple"],
  result: ["result", "outcome", "résultat", "win/loss"],
  strategy: ["strategy", "stratégie", "strategie", "setup"],
  session: ["session"],
  account: ["account", "compte", "account name"],
  platform: ["platform", "plateforme", "broker", "exchange"],
};

// "1 234,56" / "1,234.56" / "-92.96€" → Number
const qtCsvNum = (raw) => {
  if (raw == null) return null;
  let s = String(raw).trim().replace(/[€$\s ']/g, "");
  if (!s) return null;
  const hasC = s.includes(","), hasD = s.includes(".");
  if (hasC && hasD) {
    if (s.lastIndexOf(",") > s.lastIndexOf(".")) s = s.replace(/\./g, "").replace(",", ".");
    else s = s.replace(/,/g, "");
  } else if (hasC) s = s.replace(",", ".");
  const n = Number(s);
  return isFinite(n) ? n : null;
};

// "2026.06.10 14:32" (MT4/5), "10/06/2026", ISO, epoch → Date
const qtCsvDate = (raw) => {
  if (!raw) return null;
  let s = String(raw).trim();
  const mt = s.match(/^(\d{4})\.(\d{2})\.(\d{2})(.*)$/);
  if (mt) s = `${mt[1]}-${mt[2]}-${mt[3]}${mt[4]}`;
  const eu = s.match(/^(\d{2})[\/.](\d{2})[\/.](\d{4})(.*)$/);
  if (eu) s = `${eu[3]}-${eu[2]}-${eu[1]}${eu[4]}`;
  if (/^\d{13}$/.test(s)) return new Date(Number(s));
  if (/^\d{10}$/.test(s)) return new Date(Number(s) * 1000);
  const d = new Date(s);
  return isNaN(d.getTime()) ? null : d;
};

const qtHashStr = (s) => {
  let h = 5381;
  for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
  return "csv-" + Math.abs(h).toString(36);
};

const qtSessionFromHour = (h) => {
  if (h >= 0 && h < 7) return "ASIA";
  if (h >= 7 && h < 13) return "LONDON";
  if (h >= 13 && h < 21) return "NY";
  return "OFF";
};

// Transforme les lignes CSV en payloads `trades`. Retourne { trades, skipped, error }.
const qtCsvToTrades = (text, userId) => {
  const rows = qtParseCsv(text);
  if (rows.length < 2) return { error: "Fichier vide ou sans lignes de données." };
  const headers = rows[0].map(h => h.trim().toLowerCase());
  const colIdx = {};
  for (const [field, syns] of Object.entries(QT_CSV_SYNONYMS)) {
    const i = headers.findIndex(h => syns.includes(h));
    if (i >= 0) colIdx[field] = i;
  }
  if (colIdx.date == null || colIdx.asset == null || colIdx.pnl == null) {
    return {
      error: "Colonnes minimales introuvables (il faut au moins : date, actif/symbol, pnl/profit). " +
        `En-têtes détectés : ${headers.join(", ")}`,
    };
  }
  const trades = [];
  let skipped = 0;
  for (let r = 1; r < rows.length; r++) {
    const row = rows[r];
    const get = (f) => (colIdx[f] != null ? (row[colIdx[f]] || "").trim() : "");
    const d = qtCsvDate(get("date"));
    const asset = get("asset").toUpperCase().replace(/[^A-Z0-9\/._-]/g, "");
    const pnl = qtCsvNum(get("pnl"));
    if (!d || !asset || pnl == null) { skipped += 1; continue; }
    const dirRaw = get("direction").toLowerCase();
    const direction = /sell|short|vente/.test(dirRaw) ? "SHORT" : "LONG";
    const resRaw = get("result").toUpperCase();
    const result = resRaw === "WIN" || resRaw === "LOSS" ? resRaw : (pnl >= 0 ? "WIN" : "LOSS");
    const entry = qtCsvNum(get("entry_price"));
    const exit = qtCsvNum(get("exit_price"));
    const volume = qtCsvNum(get("volume"));
    const rMultiple = qtCsvNum(get("r_multiple"));
    const dateIso = d.toISOString();
    trades.push({
      user_id: userId,
      date: dateIso,
      asset,
      direction,
      entry_price: entry != null ? entry : 0,
      exit_price: exit != null ? exit : 0,
      pnl,
      r_multiple: rMultiple != null ? rMultiple : 0,
      result,
      strategy: get("strategy"),
      session: get("session") || qtSessionFromHour(d.getUTCHours()),
      platform: get("platform") || "CSV",
      account: get("account") || "Import CSV",
      volume: volume != null ? volume : 0,
      is_demo: false,
      stable_hash: qtHashStr(`${dateIso}|${asset}|${direction}|${entry}|${exit}|${pnl}`),
    });
  }
  return { trades, skipped };
};

const QTJournal = ({ go } = {}) => {
  const [trades, setTrades] = React.useState([]);
  const [loaded, setLoaded] = React.useState(false);
  const [showDemo, setShowDemo] = React.useState(true);
  const [deleting, setDeleting] = React.useState(false);

  const load = React.useCallback(async () => {
    try {
      const sb = window.nomerusSb();
      if (!sb) { setLoaded(true); return; }
      const { data } = await sb
        .from("trades")
        .select("id, date, asset, direction, entry_price, exit_price, pnl, r_multiple, result, strategy, session, platform, account, volume, is_demo")
        .order("date", { ascending: false })
        .limit(200);
      setTrades(data || []);
    } catch (e) {} finally {
      setLoaded(true);
    }
  }, []);

  React.useEffect(() => { load(); }, [load]);

  const filtered = showDemo ? trades : trades.filter(t => !t.is_demo);
  const demoCount = trades.filter(t => t.is_demo).length;
  const totalPnl = filtered.reduce((acc, t) => acc + (Number(t.pnl) || 0), 0);

  const onDeleteDemo = async () => {
    if (!confirm(`Supprimer définitivement ${demoCount} trades de démo ?`)) return;
    setDeleting(true);
    const res = await window.nomerusDemoSeeder.clear();
    setDeleting(false);
    if (res.ok) {
      load();
    } else {
      alert(res.error || "Erreur lors de la suppression.");
    }
  };

  // ── Import CSV ────────────────────────────────────────────────
  const fileRef = React.useRef(null);
  const [importing, setImporting] = React.useState(false);
  const [importReport, setImportReport] = React.useState(null); // { ok, message }

  const onImportFile = async (file) => {
    if (!file || importing) return;
    setImporting(true);
    setImportReport(null);
    try {
      const sb = window.nomerusSb();
      if (!sb) throw new Error("Connexion Supabase indisponible.");
      const { data: { user } } = await sb.auth.getUser();
      if (!user) throw new Error("Utilisateur non authentifié.");

      const text = await file.text();
      const { trades: parsed, skipped, error } = qtCsvToTrades(text, user.id);
      if (error) throw new Error(error);
      if (!parsed || parsed.length === 0) {
        throw new Error(`Aucune ligne exploitable dans ce fichier${skipped ? ` (${skipped} lignes ignorées)` : ""}.`);
      }

      // Dédup : on écarte les trades dont le stable_hash existe déjà
      const { data: existing } = await sb
        .from("trades")
        .select("stable_hash")
        .eq("user_id", user.id)
        .not("stable_hash", "is", null);
      const known = new Set((existing || []).map(e => e.stable_hash));
      const fresh = parsed.filter(t => !known.has(t.stable_hash));
      const dupes = parsed.length - fresh.length;

      if (fresh.length === 0) {
        setImportReport({ ok: true, message: `Rien à importer : les ${parsed.length} trades du fichier sont déjà dans ton journal.` });
        return;
      }

      // Insert par lots de 100
      for (let i = 0; i < fresh.length; i += 100) {
        const { error: insErr } = await sb.from("trades").insert(fresh.slice(i, i + 100));
        if (insErr) throw new Error(insErr.message || "Erreur lors de l'insertion.");
      }

      // L'import initial lève le plafond Free sur l'historique importé
      try { await sb.rpc("mark_initial_import_done"); } catch (e) { /* non bloquant */ }

      setImportReport({
        ok: true,
        message: `${fresh.length} trades importés` +
          (dupes > 0 ? ` · ${dupes} doublons ignorés` : "") +
          (skipped > 0 ? ` · ${skipped} lignes illisibles` : "") + ".",
      });
      load();
    } catch (e) {
      setImportReport({ ok: false, message: e.message || "Erreur lors de l'import." });
    } finally {
      setImporting(false);
      if (fileRef.current) fileRef.current.value = "";
    }
  };

  return (
    <div style={{ padding: 18, display: "flex", flexDirection: "column", gap: 14 }}>
      <input ref={fileRef} type="file" accept=".csv,text/csv" style={{ display: "none" }}
        onChange={(e) => onImportFile(e.target.files && e.target.files[0])}/>
      <QTPanel title="JOURNAL" right={
        <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
          {demoCount > 0 && (
            <>
              <button onClick={() => setShowDemo(!showDemo)} style={qtPillStyle(showDemo)}>
                {showDemo ? "DÉMO AFFICHÉ" : "DÉMO MASQUÉ"}
              </button>
              <button onClick={onDeleteDemo} disabled={deleting} style={{
                ...qtBtnGhost, padding: "4px 10px", fontSize: 9, color: QT.red, borderColor: "rgba(255,107,122,0.3)",
              }}>{deleting ? "SUPPR…" : `SUPPRIMER DÉMO (${demoCount})`}</button>
            </>
          )}
          <button onClick={() => fileRef.current && fileRef.current.click()} disabled={importing} style={{
            ...qtBtnGhost, padding: "4px 10px", fontSize: 9, opacity: importing ? 0.6 : 1,
          }}>{importing ? "IMPORT…" : "IMPORTER CSV"}</button>
          <button onClick={() => go && go("trade", { id: "new" })} style={{
            ...qtBtnPrimary, padding: "4px 10px", fontSize: 9,
          }}>+ NOUVEAU TRADE</button>
        </div>
      }>
        {importReport && (
          <div style={{
            margin: "12px 14px 0", padding: 10, borderRadius: 3, fontFamily: QT.mono, fontSize: 11,
            background: importReport.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
            border: `1px solid ${importReport.ok ? "rgba(91, 217, 154, 0.3)" : "rgba(255, 107, 122, 0.3)"}`,
            color: importReport.ok ? QT.green : QT.red,
          }}>{importReport.message}</div>
        )}
        <div style={{
          display: "grid", gridTemplateColumns: "repeat(4, 1fr)",
          borderBottom: `1px solid ${QT.border}`,
        }}>
          {[
            ["TRADES (AFFICHÉS)", String(filtered.length), QT.text],
            ["P&L NET", `${qtSign(totalPnl)}${qtFmt(Math.abs(totalPnl))} CHF`, totalPnl >= 0 ? QT.green : QT.red],
            ["DONT DÉMO", String(demoCount), demoCount > 0 ? QT.amber : QT.textMute],
            ["DERNIER TRADE", filtered[0] ? new Date(filtered[0].date).toLocaleString("fr-FR", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : "—", QT.text],
          ].map(([l, v, c], i) => (
            <div key={l} style={{
              padding: "14px 18px",
              borderRight: i < 3 ? `1px solid ${QT.border}` : "none",
            }}>
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5 }}>{l}</div>
              <div style={{ fontFamily: QT.mono, fontSize: 16, color: c, fontWeight: 600, marginTop: 6 }}>{v}</div>
            </div>
          ))}
        </div>
        <div style={{ overflowX: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: QT.mono, fontSize: 12 }}>
            <thead>
              <tr style={{ background: QT.panel2 }}>
                {["HEURE", "COMPTE", "PAIRE", "SENS", "ENTRÉE", "SORTIE", "TAILLE", "RÉSULTAT", "P&L"].map(h => (
                  <th key={h} style={{
                    textAlign: h === "P&L" ? "right" : "left",
                    padding: "10px 14px",
                    color: QT.textMute, fontFamily: QT.mono, fontSize: 9,
                    letterSpacing: 1.5, fontWeight: 500,
                    borderBottom: `1px solid ${QT.border}`, whiteSpace: "nowrap",
                  }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {!loaded && (
                <tr><td colSpan={9} style={{ padding: 18, fontFamily: QT.mono, color: QT.textMute }}>Chargement…</td></tr>
              )}
              {loaded && filtered.length === 0 && (
                <tr><td colSpan={9} style={{ padding: 18, fontFamily: QT.mono, color: QT.textMute }}>Aucun trade.</td></tr>
              )}
              {filtered.map(t => (
                <tr key={t.id}
                  className="qt-row"
                  onClick={() => go && go("trade", { id: t.id })}
                  style={{ borderBottom: `1px solid ${QT.border}`, cursor: "pointer" }}>
                  <td style={{ padding: "8px 14px", color: QT.textDim }}>
                    {t.date ? new Date(t.date).toLocaleString("fr-FR", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : "—"}
                  </td>
                  <td style={{ padding: "8px 14px", color: QT.text }}>{t.account || t.platform || "—"}</td>
                  <td style={{ padding: "8px 14px", color: QT.text, fontWeight: 600 }}>{t.asset}</td>
                  <td style={{ padding: "8px 14px", color: (t.direction || "").toUpperCase() === "LONG" ? QT.green : QT.red, textTransform: "uppercase" }}>{t.direction}</td>
                  <td style={{ padding: "8px 14px", color: QT.textDim }}>{t.entry_price}</td>
                  <td style={{ padding: "8px 14px", color: QT.textDim }}>{t.exit_price}</td>
                  <td style={{ padding: "8px 14px", color: QT.textDim, textAlign: "right" }}>{t.volume}</td>
                  <td style={{ padding: "8px 14px", color: t.result === "WIN" ? QT.green : QT.red }}>{t.result}</td>
                  <td style={{ padding: "8px 14px", color: Number(t.pnl) >= 0 ? QT.green : QT.red, textAlign: "right", fontWeight: 600 }}>
                    {qtSign(t.pnl)}{qtFmt(Math.abs(Number(t.pnl) || 0))} CHF
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </QTPanel>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
// QTTradeDetail — édition d'un trade par id
// ──────────────────────────────────────────────────────────────
const QTTradeDetail = ({ tradeId, go } = {}) => {
  const isNew = tradeId === "new";
  const [trade, setTrade] = React.useState(null);
  const [loaded, setLoaded] = React.useState(false);
  const [form, setForm] = React.useState(() => isNew ? {
    asset: "", direction: "LONG", entry_price: "", exit_price: "", volume: "",
    pnl: "", r_multiple: "", result: "WIN", strategy: "", session: "",
    notes: "", platform: "MANUEL", account: "Saisie manuelle",
    // datetime-local attend "YYYY-MM-DDTHH:MM" en heure locale
    date: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16),
  } : {});
  const [saving, setSaving] = React.useState(false);
  const [feedback, setFeedback] = React.useState(null);
  const [deleting, setDeleting] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!tradeId || isNew) { setLoaded(true); return; }
      try {
        const sb = window.nomerusSb();
        if (!sb) { setLoaded(true); return; }
        const { data, error } = await sb.from("trades").select("*").eq("id", tradeId).maybeSingle();
        if (!cancelled && !error) {
          setTrade(data);
          setForm({
            asset: data ? data.asset : "",
            direction: data ? data.direction : "LONG",
            entry_price: data ? data.entry_price : "",
            exit_price: data ? data.exit_price : "",
            volume: data ? data.volume : "",
            pnl: data ? data.pnl : "",
            r_multiple: data ? data.r_multiple : "",
            result: data ? data.result : "WIN",
            strategy: data ? data.strategy : "",
            session: data ? data.session : "",
            notes: data ? (data.notes || "") : "",
            platform: data ? data.platform : "",
            account: data ? data.account : "",
          });
        }
      } catch (e) {} finally {
        if (!cancelled) setLoaded(true);
      }
    })();
    return () => { cancelled = true; };
  }, [tradeId]);

  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const onSave = async () => {
    setSaving(true); setFeedback(null);
    try {
      const sb = window.nomerusSb();
      const payload = { ...form };
      // Coerce types numériques
      ["entry_price","exit_price","volume","pnl","r_multiple"].forEach(k => {
        if (payload[k] === "" || payload[k] == null) payload[k] = null;
        else payload[k] = Number(payload[k]);
      });
      if (isNew) {
        if (!payload.asset || !payload.date || payload.pnl == null) {
          throw new Error("Renseigne au minimum l'actif, la date et le P&L.");
        }
        const { data: { user } } = await sb.auth.getUser();
        if (!user) throw new Error("Utilisateur non authentifié.");
        payload.user_id = user.id;
        payload.date = new Date(payload.date).toISOString();
        payload.is_demo = false;
        if (!payload.result) payload.result = payload.pnl >= 0 ? "WIN" : "LOSS";
        const { data: inserted, error } = await sb.from("trades").insert(payload).select("id").single();
        if (error) throw error;
        setFeedback({ ok: true, message: "Trade ajouté au journal." });
        if (go && inserted) go("trade", { id: inserted.id });
      } else {
        delete payload.date;
        const { error } = await sb.from("trades").update(payload).eq("id", tradeId);
        if (error) throw error;
        setFeedback({ ok: true, message: "Trade enregistré." });
      }
    } catch (e) {
      setFeedback({ ok: false, message: e.message || "Erreur d'enregistrement." });
    } finally {
      setSaving(false);
    }
  };

  const onDelete = async () => {
    if (!confirm("Supprimer définitivement ce trade ?")) return;
    setDeleting(true);
    try {
      const sb = window.nomerusSb();
      await sb.from("trades").delete().eq("id", tradeId);
      if (go) go("journal");
    } catch (e) {
      alert(e.message || "Erreur lors de la suppression.");
    } finally {
      setDeleting(false);
    }
  };

  if (!tradeId) {
    return (
      <div style={{ padding: 30, fontFamily: QT.mono, color: QT.textMute }}>
        Aucun trade sélectionné. <button onClick={() => go && go("journal")} style={{ ...qtBtnGhost, marginLeft: 12 }}>Retour au Journal</button>
      </div>
    );
  }
  if (!loaded) {
    return <div style={{ padding: 30, fontFamily: QT.mono, color: QT.textMute }}>Chargement…</div>;
  }
  if (!trade && !isNew) {
    return (
      <div style={{ padding: 30, fontFamily: QT.mono, color: QT.textMute }}>
        Trade introuvable. <button onClick={() => go && go("journal")} style={{ ...qtBtnGhost, marginLeft: 12 }}>Retour au Journal</button>
      </div>
    );
  }

  return (
    <div style={{ padding: 18, display: "grid", gridTemplateColumns: "2fr 1fr", gap: 14 }}>
      <div>
        <QTPanel title={isNew ? "NOUVEAU TRADE · SAISIE MANUELLE" : `TRADE · ${form.asset || trade.asset} · ${(form.direction || "").toUpperCase()}`}>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
            {isNew && (
              <QTField label="DATE / HEURE" value={form.date} onChange={(v) => set("date", v)} mono type="datetime-local"/>
            )}
            <QTField label="PAIRE / ACTIF" value={form.asset} onChange={(v) => set("asset", v)} mono placeholder="ex. EURUSD"/>
            <div>
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>SENS</div>
              <div style={{ display: "flex", gap: 6 }}>
                {["LONG", "SHORT"].map(v => (
                  <button key={v} onClick={() => set("direction", v)} style={qtPillStyle(form.direction === v)}>{v}</button>
                ))}
              </div>
            </div>
            <QTField label="PRIX D'ENTRÉE" value={form.entry_price} onChange={(v) => set("entry_price", v)} mono type="number"/>
            <QTField label="PRIX DE SORTIE" value={form.exit_price} onChange={(v) => set("exit_price", v)} mono type="number"/>
            <QTField label="TAILLE / VOLUME" value={form.volume} onChange={(v) => set("volume", v)} mono type="number"/>
            <QTField label="P&L (CHF)" value={form.pnl} onChange={(v) => set("pnl", v)} mono type="number"/>
            <QTField label="R-MULTIPLE" value={form.r_multiple} onChange={(v) => set("r_multiple", v)} mono type="number"/>
            <div>
              <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>RÉSULTAT</div>
              <div style={{ display: "flex", gap: 6 }}>
                {["WIN", "LOSS"].map(v => (
                  <button key={v} onClick={() => set("result", v)} style={qtPillStyle(form.result === v)}>{v === "WIN" ? "GAGNÉ" : "PERDU"}</button>
                ))}
              </div>
            </div>
            <QTField label="STRATÉGIE" value={form.strategy} onChange={(v) => set("strategy", v)}/>
            <QTField label="SESSION" value={form.session} onChange={(v) => set("session", v)} mono/>
            <QTField label="COMPTE" value={form.account} onChange={(v) => set("account", v)}/>
            <QTField label="PLATEFORME" value={form.platform} onChange={(v) => set("platform", v)}/>
          </div>

          <div style={{ marginTop: 16 }}>
            <div style={{ fontFamily: QT.mono, fontSize: 9, color: QT.textMute, letterSpacing: 1.5, marginBottom: 6 }}>NOTES</div>
            <textarea value={form.notes} onChange={(e) => set("notes", e.target.value)} rows={5}
              style={{ ...qtInput, fontFamily: QT.sans, resize: "vertical" }}
              placeholder="Contexte, raison d'entrée, ce que tu as appris…"/>
          </div>

          {feedback && (
            <div style={{
              marginTop: 12,
              padding: 10, borderRadius: 3, fontFamily: QT.mono, fontSize: 11,
              background: feedback.ok ? "rgba(91, 217, 154, 0.08)" : "rgba(255, 107, 122, 0.08)",
              color: feedback.ok ? QT.green : QT.red,
            }}>{feedback.message}</div>
          )}

          <div style={{ display: "flex", justifyContent: "space-between", marginTop: 18, paddingTop: 14, borderTop: `1px solid ${QT.border}`, gap: 8, flexWrap: "wrap" }}>
            <button onClick={() => go && go("journal")} style={qtBtnGhost}>← RETOUR AU JOURNAL</button>
            <div style={{ display: "flex", gap: 8 }}>
              {!isNew && (
                <button onClick={onDelete} disabled={deleting} style={{
                  ...qtBtnGhost, color: QT.red, borderColor: "rgba(255,107,122,0.3)",
                  opacity: deleting ? 0.6 : 1,
                }}>{deleting ? "SUPPR…" : "SUPPRIMER"}</button>
              )}
              <button onClick={onSave} disabled={saving} style={{
                ...qtBtnPrimary,
                opacity: saving ? 0.6 : 1, cursor: saving ? "wait" : "pointer",
              }}>{saving ? "ENREGISTREMENT…" : isNew ? "AJOUTER AU JOURNAL" : "SAUVEGARDER"}</button>
            </div>
          </div>
        </QTPanel>
      </div>

      <div>
        <QTPanel title="MÉTADONNÉES">
          <div style={{ display: "flex", flexDirection: "column", gap: 10, fontFamily: QT.mono, fontSize: 11 }}>
            {(isNew ? [
              ["ID", "— (généré à l'ajout)"],
              ["SOURCE", "SAISIE MANUELLE"],
            ] : [
              ["ID", trade.id ? String(trade.id).slice(0, 12) : "—"],
              ["DATE", trade.date ? new Date(trade.date).toLocaleString("fr-FR") : "—"],
              ["SOURCE", trade.is_demo ? "DÉMO" : "RÉEL"],
            ]).map(([k, v]) => (
              <div key={k} style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: `1px solid ${QT.border}` }}>
                <span style={{ color: QT.textMute }}>{k}</span>
                <span style={{ color: QT.text }}>{v}</span>
              </div>
            ))}
          </div>
        </QTPanel>
      </div>
    </div>
  );
};

window.QTInstall = QTInstall;       // alias historique
window.QTSetup = QTInstall;         // nom canonique
window.QTAnalyze = QTAnalyze;       // alias historique
window.QTAnalytics = QTAnalyze;     // nom canonique
window.QTJournal = QTJournal;
window.QTAccount = QTAccount;
window.QTSettings = QTSettings;
window.QTTradeDetail = QTTradeDetail;
window.qtBtnPrimary = qtBtnPrimary;
window.qtBtnGhost = qtBtnGhost;
window.qtInput = qtInput;
window.NOMERUS_PRO_PRICE_LABEL = NOMERUS_PRO_PRICE_LABEL;
