// Tempo — Statistiques : analyse Trading + Habitudes (refonte)
// Composants exposés : StatsPage({ state, initialView }) (desktop) et StatsMobile({ state, initialView }) (mobile).
// Aucune lib externe : tous les graphes sont du SVG inline fait main. Aucune couleur en dur — tout via CSS vars.
//
// Données :
//   - Trades : state.trades (déjà chargés) filtrés par période côté JS.
//   - Habitudes : requêtes Supabase dédiées (habit_completions + habits) sur la plage,
//     car useAppState ne fournit que l'instantané du jour.
//
// Philosophie : « moins mais mieux ». Quelques visualisations vraiment décisionnelles,
// beaucoup d'air, des chiffres-clés discrets plutôt qu'un mur de graphiques.

// ─── Helpers date / période (locaux au module) ───────────────────────
const STATS_MONTH_NAMES = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];
const STATS_MONTH_FULL  = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
const STATS_WD_LABELS   = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
const STATS_WD_FULL     = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];

// Construit une date locale à partir d'un YYYY-MM-DD (évite le décalage UTC).
function statsDateFromISO(iso) {
  if (!iso) return new Date();
  const d = new Date(String(iso).slice(0, 10) + 'T00:00:00');
  return isNaN(d.getTime()) ? new Date() : d;
}
function statsISO(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}
function statsAddDays(d, n) {
  const x = new Date(d);
  x.setDate(x.getDate() + n);
  return x;
}
// Indice jour de semaine Lun=0 … Dim=6
function statsWeekday(d) { return (d.getDay() + 6) % 7; }

// Renvoie { fromISO, toISO } (bornes inclusives) pour la période choisie.
// 'all' renvoie fromISO = null (pas de borne basse).
function statsPeriodRange(period) {
  const today = statsDateFromISO(typeof todayISO === 'function' ? todayISO() : statsISO(new Date()));
  const toISO = statsISO(today);
  if (period === 'all')   return { fromISO: null, toISO };
  if (period === 'week')  return { fromISO: statsISO(statsAddDays(today, -6)),   toISO };
  if (period === 'month') return { fromISO: statsISO(statsAddDays(today, -29)),  toISO };
  return { fromISO: statsISO(statsAddDays(today, -364)), toISO }; // year
}

const STATS_PERIODS = [
  { id: 'week',  label: 'Semaine' },
  { id: 'month', label: 'Mois' },
  { id: 'year',  label: 'Année' },
  { id: 'all',   label: 'Tout' },
];

// ─── Formatage (devise / pourcent / nombre / dates) ──────────────────
function statsCur() { return (typeof window !== 'undefined' && window.__appCurrency) || 'USD'; }
function statsTz()  { return (typeof window !== 'undefined' && window.__appTz) || 'Europe/Paris'; }

// Montant dans la devise du profil. signed=true → préfixe +/−.
function statsMoney(v, signed = false) {
  const n = Number(v) || 0;
  if (typeof fmtMoneyCur === 'function') return fmtMoneyCur(n, statsCur(), signed);
  return (signed && n > 0 ? '+' : '') + n.toLocaleString('fr-FR');
}
function statsPct(v, dec = 0) {
  const n = Number(v) || 0;
  return n.toLocaleString('fr-FR', { minimumFractionDigits: dec, maximumFractionDigits: dec }) + ' %';
}
function statsNum(v, dec = 2) {
  const n = Number(v) || 0;
  return n.toLocaleString('fr-FR', { minimumFractionDigits: dec, maximumFractionDigits: dec });
}
// R-multiple signé, compact (ex : +1,8 R / −2 R).
function statsR(v) {
  const n = Number(v) || 0;
  const s = Math.abs(n).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
  return (n > 0 ? '+' : n < 0 ? '−' : '') + s + ' R';
}
function statsFrShortDate(iso) {
  const d = statsDateFromISO(iso);
  return `${d.getDate()} ${STATS_MONTH_NAMES[d.getMonth()]}`;
}

// Clé jour local cohérente avec le calendrier (fuseau du profil).
function statsDayKey(iso) {
  if (window.localDayKey) return window.localDayKey(iso, statsTz());
  return String(iso || '').slice(0, 10);
}

// ─── Nombre animé (montant devise) ───────────────────────────────────
// Réutilise useCountUp si dispo, sinon valeur statique. Formate via fmtMoneyCur.
function StatsAnimatedMoney({ value, signed = true, style }) {
  const v = (typeof useCountUp === 'function') ? useCountUp(Number(value) || 0, { duration: 850 }) : (Number(value) || 0);
  return <span className="num" style={style}>{statsMoney(v, signed)}</span>;
}

// ─── Primitives UI partagées ─────────────────────────────────────────
function StatsSegmented({ options, value, onChange, size = 'md' }) {
  const pad = size === 'sm' ? '6px 12px' : '7px 15px';
  const fs  = size === 'sm' ? 12 : 12.5;
  return (
    <div style={{ display: 'inline-flex', gap: 4, background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 11, padding: 3 }}>
      {options.map(o => {
        const on = o.id === value;
        return (
          <button key={o.id} onClick={() => onChange(o.id)} className="tap" style={{
            padding: pad, borderRadius: 8, border: 'none', cursor: 'pointer',
            background: on ? 'var(--bg-card)' : 'transparent',
            color: on ? 'var(--fg)' : 'var(--fg-3)',
            fontSize: fs, fontWeight: on ? 600 : 500, letterSpacing: '-.005em',
            boxShadow: on ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
            display: 'inline-flex', alignItems: 'center', gap: 7, whiteSpace: 'nowrap',
          }}>
            {o.ico}{o.label}
          </button>
        );
      })}
    </div>
  );
}

// KPI « héros » : grande carte avec valeur animée optionnelle.
function StatsKpi({ label, value, animatedMoney, sub, tone, accent, big = false }) {
  const color = tone === 'pos' ? 'var(--green)' : tone === 'neg' ? 'var(--red)' : (accent || 'var(--fg)');
  return (
    <div className="card lift" style={{ padding: big ? '18px 20px' : '15px 17px' }}>
      <div style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--fg-3)', letterSpacing: '.01em', textTransform: 'uppercase', marginBottom: 11 }}>{label}</div>
      <div style={{ fontSize: big ? 30 : 23, fontWeight: 700, letterSpacing: '-.024em', color, lineHeight: 1 }}>
        {animatedMoney != null
          ? <StatsAnimatedMoney value={animatedMoney} signed style={{ fontSize: 'inherit', fontWeight: 'inherit', letterSpacing: 'inherit', color: 'inherit' }} />
          : <span className="num">{value}</span>}
      </div>
      {sub && <div style={{ fontSize: 11.5, color: 'var(--fg-3)', marginTop: 9 }}>{sub}</div>}
    </div>
  );
}

// Bandeau compact de chiffres secondaires : une carte, plusieurs cellules.
function StatsStrip({ items, cols }) {
  const n = cols || items.length;
  return (
    <div className="card lift" style={{ padding: 0, display: 'grid', gridTemplateColumns: `repeat(${n}, 1fr)`, overflow: 'hidden' }}>
      {items.map((it, i) => {
        const color = it.tone === 'pos' ? 'var(--green)' : it.tone === 'neg' ? 'var(--red)' : (it.accent || 'var(--fg)');
        const startOfRow = i % n === 0;
        const secondRow = i >= n;
        return (
          <div key={i} style={{
            padding: '13px 16px',
            borderLeft: startOfRow ? 'none' : '1px solid var(--line)',
            borderTop: secondRow ? '1px solid var(--line)' : 'none',
          }}>
            <div style={{ fontSize: 10.5, fontWeight: 500, color: 'var(--fg-3)', letterSpacing: '.01em', textTransform: 'uppercase', marginBottom: 7, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{it.label}</div>
            <div className="num" style={{ fontSize: 17, fontWeight: 700, letterSpacing: '-.018em', color, lineHeight: 1 }}>{it.value}</div>
            {it.sub && <div style={{ fontSize: 10.5, color: 'var(--fg-4)', marginTop: 5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{it.sub}</div>}
          </div>
        );
      })}
    </div>
  );
}

function StatsPanel({ title, subtitle, right, children, pad = '18px 20px' }) {
  return (
    <div className="card lift" style={{ padding: pad, display: 'flex', flexDirection: 'column' }}>
      {(title || right) && (
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12 }}>
          <span style={{ fontSize: 13.5, fontWeight: 600, letterSpacing: '-.012em' }}>{title}</span>
          {right || (subtitle && <span style={{ fontSize: 11.5, color: 'var(--fg-3)', textAlign: 'right' }}>{subtitle}</span>)}
        </div>
      )}
      <div style={{ marginTop: 16, flex: 1 }}>{children}</div>
    </div>
  );
}

function StatsEmpty({ title, sub, icon }) {
  return (
    <div style={{ padding: '38px 16px', textAlign: 'center' }}>
      {icon && <div style={{ color: 'var(--fg-4)', marginBottom: 12, display: 'flex', justifyContent: 'center' }}>{icon}</div>}
      <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-2)', marginBottom: 6 }}>{title}</div>
      {sub && <div style={{ fontSize: 12.5, color: 'var(--fg-3)', maxWidth: 360, margin: '0 auto', lineHeight: 1.5 }}>{sub}</div>}
    </div>
  );
}

// Hook : largeur responsive d'un conteneur (pour SVG fluides).
function useStatsWidth(initial) {
  const ref = React.useRef(null);
  const [w, setW] = React.useState(initial);
  React.useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(entries => {
      const cw = entries[0]?.contentRect?.width;
      if (cw) setW(Math.max(220, Math.round(cw)));
    });
    ro.observe(ref.current);
    return () => ro.disconnect();
  }, []);
  return [ref, w];
}

// ═════════════════════════════════════════════════════════════════════
//  GRAPHES SVG
// ═════════════════════════════════════════════════════════════════════

// Courbe d'équité (P&L cumulé) — la pièce maîtresse. Aire remplie + survol.
// ddRange (optionnel) : { peak, trough } indices de la plus forte chute pic→creux.
function StatsEquityCurve({ points, dates, height = 240, ddRange = null }) {
  const [ref, w] = useStatsWidth(640);
  const [hover, setHover] = React.useState(null);

  if (!points || points.length < 2) {
    return <div ref={ref}><StatsEmpty title="Pas assez de trades" sub="Au moins 2 trades clôturés sont nécessaires pour tracer la courbe d'équité." /></div>;
  }

  const H = height;
  const padL = 8, padR = 62, padT = 18, padB = 22;
  const innerW = w - padL - padR, innerH = H - padT - padB;
  const max = Math.max(...points, 0), min = Math.min(...points, 0);
  const span = (max - min) || 1;
  const sx = innerW / (points.length - 1);
  const X = i => padL + i * sx;
  const Y = v => padT + innerH - ((v - min) / span) * innerH;
  const zeroY = Y(0);
  const last = points[points.length - 1];
  const tone = last >= 0 ? 'var(--green)' : 'var(--red)';
  const fillTone = last >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';

  // Path lissé (Catmull-Rom → Bézier).
  const pts = points.map((v, i) => [X(i), Y(v)]);
  let dLine = `M ${pts[0][0]} ${pts[0][1]}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
    const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
    const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
    dLine += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2[0]} ${p2[1]}`;
  }
  const dArea = `${dLine} L ${pts[pts.length - 1][0]} ${padT + innerH} L ${pts[0][0]} ${padT + innerH} Z`;
  const gid = 'eq-grad-' + (last >= 0 ? 'g' : 'r');

  function onMove(e) {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (w / rect.width);
    let i = Math.round((x - padL) / sx);
    i = Math.max(0, Math.min(points.length - 1, i));
    setHover(i);
  }

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <svg width="100%" viewBox={`0 0 ${w} ${H}`} style={{ display: 'block', overflow: 'visible' }}
        onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
        <defs>
          <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={fillTone} stopOpacity="0.95" />
            <stop offset="100%" stopColor={fillTone} stopOpacity="0" />
          </linearGradient>
        </defs>
        {/* bande de drawdown (pic → creux), discrète */}
        {ddRange && ddRange.trough > ddRange.peak && (
          <rect x={X(ddRange.peak)} y={padT} width={Math.max(0, X(ddRange.trough) - X(ddRange.peak))} height={innerH}
            fill="var(--red-soft)" opacity="0.55" style={{ animation: 'fade-in .6s var(--ease) .3s both' }} />
        )}
        {/* ligne zéro */}
        <line x1={padL} y1={zeroY} x2={padL + innerW} y2={zeroY} stroke="var(--line-2)" strokeWidth="1" strokeDasharray="3 4" />
        <path d={dArea} fill={`url(#${gid})`} />
        <path d={dLine} fill="none" stroke={tone} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round"
          style={{ strokeDasharray: 2400, strokeDashoffset: 2400, animation: 'draw 1.1s var(--ease) forwards' }} />
        {/* labels max / min à droite */}
        <text x={padL + innerW + 8} y={Y(max)} dominantBaseline="middle" style={{ fontSize: 10.5, fill: 'var(--fg-3)', fontWeight: 500 }}>{statsMoney(max, true)}</text>
        {min < 0 && <text x={padL + innerW + 8} y={Y(min)} dominantBaseline="middle" style={{ fontSize: 10.5, fill: 'var(--fg-3)', fontWeight: 500 }}>{statsMoney(min, true)}</text>}
        {hover != null && (
          <g>
            <line x1={X(hover)} y1={padT} x2={X(hover)} y2={padT + innerH} stroke="var(--line-2)" strokeWidth="1" />
            <circle cx={X(hover)} cy={Y(points[hover])} r="4" fill="var(--bg-card)" stroke={tone} strokeWidth="2" />
          </g>
        )}
      </svg>
      <div style={{ height: 18, marginTop: 6 }}>
        {hover != null && (
          <div style={{ fontSize: 11.5, color: 'var(--fg-2)', display: 'flex', gap: 10 }}>
            <span style={{ color: 'var(--fg-3)' }}>{dates && dates[hover] ? statsFrShortDate(dates[hover]) : `Trade ${hover + 1}`}</span>
            <span className="num" style={{ fontWeight: 600, color: points[hover] >= 0 ? 'var(--green)' : 'var(--red)' }}>{statsMoney(points[hover], true)}</span>
          </div>
        )}
      </div>
    </div>
  );
}

// Histogramme (barres verticales, +/-). data: [{ label, value, color? }].
function StatsBars({ data, height = 180, valueFmt }) {
  const [ref, w] = useStatsWidth(520);
  const [hover, setHover] = React.useState(null);
  if (!data || !data.length) return <div ref={ref}><StatsEmpty title="Aucune donnée" /></div>;

  const H = height;
  const padT = 16, padB = 28, padX = 6;
  const innerW = w - padX * 2, innerH = H - padT - padB;
  const max = Math.max(0, ...data.map(d => d.value));
  const min = Math.min(0, ...data.map(d => d.value));
  const span = (max - min) || 1;
  const zeroY = padT + innerH - ((0 - min) / span) * innerH;
  const slot = innerW / data.length;
  const bw = Math.min(48, slot * 0.6);
  const fmt = valueFmt || (v => statsMoney(v, true));

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <svg width="100%" viewBox={`0 0 ${w} ${H}`} style={{ display: 'block', overflow: 'visible' }}>
        <line x1={padX} y1={zeroY} x2={padX + innerW} y2={zeroY} stroke="var(--line-2)" strokeWidth="1" />
        {data.map((d, i) => {
          const cx = padX + slot * i + slot / 2;
          const v = d.value;
          const pos = v >= 0;
          const top = pos ? padT + innerH - ((v - min) / span) * innerH : zeroY;
          const bh = Math.max(v === 0 ? 0 : 2, Math.abs((v / span) * innerH));
          const on = hover === i;
          const fill = d.color || (pos ? 'var(--green)' : 'var(--red)');
          return (
            <g key={i} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)} style={{ cursor: 'default' }}>
              <rect x={cx - slot / 2} y={padT} width={slot} height={innerH} fill="transparent" />
              {bh > 0 && (
                <rect x={cx - bw / 2} y={top} width={bw} height={bh} rx="4" fill={fill}
                  opacity={hover == null || on ? 1 : 0.4}
                  style={{ transformBox: 'fill-box', transformOrigin: 'center bottom', animation: `bar-grow .5s var(--ease) ${0.03 * i}s both` }} />
              )}
              {on && (
                <text x={cx} y={pos ? top - 7 : top + bh + 14} textAnchor="middle" className="num"
                  style={{ fontSize: 11, fontWeight: 700, fill: 'var(--fg)' }}>{fmt(v)}</text>
              )}
              <text x={cx} y={H - 9} textAnchor="middle" style={{ fontSize: 10.5, fill: 'var(--fg-3)', fontWeight: 500 }}>{d.label}</text>
            </g>
          );
        })}
      </svg>
    </div>
  );
}

// Barres horizontales (classement). rows: [{ label, value, sub, tone? }].
function StatsHBars({ rows, valueFmt, colorFor }) {
  if (!rows || !rows.length) return <StatsEmpty title="Aucune donnée" />;
  const maxAbs = Math.max(1, ...rows.map(r => Math.abs(r.value)));
  const fmt = valueFmt || (v => statsMoney(v, true));
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {rows.map((r, i) => {
        const pct = (Math.abs(r.value) / maxAbs) * 100;
        const color = colorFor ? colorFor(r.value, r)
          : r.tone === 'neutral' ? 'var(--blue)'
          : (r.value >= 0 ? 'var(--green)' : 'var(--red)');
        return (
          <div key={i}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6, gap: 10 }}>
              <span style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--fg)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.label}</span>
              <span className="num" style={{ fontSize: 12.5, fontWeight: 600, color, flexShrink: 0 }}>
                {fmt(r.value)}
                {r.sub != null && <span style={{ color: 'var(--fg-3)', fontWeight: 500, marginLeft: 7 }}>{r.sub}</span>}
              </span>
            </div>
            <div style={{ height: 8, borderRadius: 5, background: 'var(--bg-elev)', overflow: 'hidden' }}>
              <div style={{ width: `${Math.max(2, pct)}%`, height: '100%', borderRadius: 5, background: color, transition: 'width .7s var(--ease)' }} />
            </div>
          </div>
        );
      })}
    </div>
  );
}

// Sparkline (tendance). values: ratios 0..1.
function StatsSparkline({ values, height = 60, color = 'var(--blue)' }) {
  const [ref, w] = useStatsWidth(480);
  const uid = typeof React.useId === 'function' ? React.useId() : 'spark';
  if (!values || values.length < 2) return <div ref={ref} style={{ height }} />;

  const H = height, padT = 5, padB = 5;
  const innerH = H - padT - padB;
  const sx = w / (values.length - 1);
  const X = i => i * sx;
  const Y = v => padT + innerH - Math.max(0, Math.min(1, v)) * innerH;
  const pts = values.map((v, i) => [X(i), Y(v)]);
  let dLine = `M ${pts[0][0]} ${pts[0][1]}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
    const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
    const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
    dLine += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2[0]} ${p2[1]}`;
  }
  const dArea = `${dLine} L ${pts[pts.length - 1][0]} ${H} L ${pts[0][0]} ${H} Z`;
  const gid = 'spark-' + String(uid).replace(/[:]/g, '');

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <svg width="100%" viewBox={`0 0 ${w} ${H}`} style={{ display: 'block' }}>
        <defs>
          <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={color} stopOpacity="0.2" />
            <stop offset="100%" stopColor={color} stopOpacity="0" />
          </linearGradient>
        </defs>
        <path d={dArea} fill={`url(#${gid})`} />
        <path d={dLine} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round"
          style={{ strokeDasharray: 2000, strokeDashoffset: 2000, animation: 'draw 1s var(--ease) forwards' }} />
      </svg>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════
//  CALENDRIER P&L (un mois) — style TradeZella, mais sobre
// ═════════════════════════════════════════════════════════════════════
// dayMap : { 'YYYY-MM-DD': { pnl, count } }. monthDate : Date pointant le mois affiché.
function StatsPnlCalendar({ dayMap, monthDate, onPrev, onNext, canPrev, canNext }) {
  const year = monthDate.getFullYear();
  const month = monthDate.getMonth();
  const first = new Date(year, month, 1);
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const lead = statsWeekday(first); // cases vides avant le 1er

  // Amplitude pour l'intensité de fond (échelle relative au mois).
  let maxAbs = 1;
  for (let d = 1; d <= daysInMonth; d++) {
    const key = statsISO(new Date(year, month, d));
    const e = dayMap[key];
    if (e) maxAbs = Math.max(maxAbs, Math.abs(e.pnl));
  }

  const cells = [];
  for (let i = 0; i < lead; i++) cells.push(null);
  for (let d = 1; d <= daysInMonth; d++) {
    const key = statsISO(new Date(year, month, d));
    cells.push({ d, key, e: dayMap[key] || null });
  }
  while (cells.length % 7 !== 0) cells.push(null);

  // Total du mois.
  let monthPnl = 0, tradingDays = 0;
  for (let d = 1; d <= daysInMonth; d++) {
    const e = dayMap[statsISO(new Date(year, month, d))];
    if (e) { monthPnl += e.pnl; tradingDays++; }
  }

  function bg(e) {
    if (!e) return 'transparent';
    const intensity = Math.min(1, Math.abs(e.pnl) / maxAbs);
    const a = 0.12 + intensity * 0.5;
    return e.pnl >= 0
      ? `color-mix(in srgb, var(--green) ${Math.round(a * 100)}%, transparent)`
      : `color-mix(in srgb, var(--red) ${Math.round(a * 100)}%, transparent)`;
  }

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
        <span style={{ fontSize: 13, fontWeight: 600 }}>{STATS_MONTH_FULL[month]} {year}</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <span className="num" style={{ fontSize: 13, fontWeight: 700, color: monthPnl > 0 ? 'var(--green)' : monthPnl < 0 ? 'var(--red)' : 'var(--fg-3)' }}>
            {statsMoney(monthPnl, true)}
          </span>
          <div style={{ display: 'flex', gap: 4 }}>
            <button onClick={onPrev} disabled={!canPrev} className="tap" style={navBtn(canPrev)}>{Ico.arrL}</button>
            <button onClick={onNext} disabled={!canNext} className="tap" style={navBtn(canNext)}>{Ico.arrR}</button>
          </div>
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 5, marginBottom: 6 }}>
        {STATS_WD_LABELS.map((d, i) => (
          <div key={i} style={{ textAlign: 'center', fontSize: 10, fontWeight: 600, color: 'var(--fg-4)', letterSpacing: '.02em' }}>{d}</div>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 5 }}>
        {cells.map((c, i) => {
          if (!c) return <div key={i} style={{ aspectRatio: '1 / 1' }} />;
          const e = c.e;
          const has = !!e;
          return (
            <div key={i} title={has ? `${c.d} ${STATS_MONTH_NAMES[month]} · ${statsMoney(e.pnl, true)} · ${e.count} trade${e.count > 1 ? 's' : ''}` : ''}
              style={{
                aspectRatio: '1 / 1', borderRadius: 9,
                border: '1px solid var(--line)',
                background: bg(e),
                position: 'relative', overflow: 'hidden',
                display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
                padding: '6px 7px',
              }}>
              <span style={{ fontSize: 12, fontWeight: 600, color: has ? 'var(--fg-2)' : 'var(--fg-4)' }}>{c.d}</span>
              {has && (
                <div>
                  <div className="num" style={{ fontSize: 12.5, fontWeight: 700, lineHeight: 1.1,
                    color: e.pnl >= 0 ? 'var(--green-text)' : 'var(--red-text)',
                    overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {statsMoney(e.pnl, true)}
                  </div>
                  {(e.wins || e.losses) ? (
                    <div style={{ display: 'flex', gap: 6, marginTop: 4, fontSize: 10.5, fontWeight: 700, lineHeight: 1 }}>
                      {e.wins > 0 && <span style={{ color: 'var(--green-text)' }}>{e.wins}W</span>}
                      {e.losses > 0 && <span style={{ color: 'var(--red-text)' }}>{e.losses}L</span>}
                    </div>
                  ) : null}
                </div>
              )}
            </div>
          );
        })}
      </div>
      <div style={{ marginTop: 12, fontSize: 11, color: 'var(--fg-3)' }}>
        {tradingDays} jour{tradingDays > 1 ? 's' : ''} de trading ce mois-ci
      </div>
    </div>
  );
}

function navBtn(enabled) {
  return {
    width: 28, height: 28, borderRadius: 8, border: '1px solid var(--line)',
    background: 'var(--bg-card)', color: enabled ? 'var(--fg-2)' : 'var(--fg-5)',
    cursor: enabled ? 'pointer' : 'default', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
    opacity: enabled ? 1 : 0.5,
  };
}

// ─── Heatmap d'habitudes (colonnes = semaines, lignes = Lun→Dim) ─────
function StatsHeatmap({ heat }) {
  if (!heat || !heat.length) return <StatsEmpty title="Aucune donnée" />;
  const first = statsDateFromISO(heat[0].date);
  const offset = statsWeekday(first);
  const cells = [];
  for (let i = 0; i < offset; i++) cells.push(null);
  for (const h of heat) cells.push(h);
  const weeks = [];
  for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));

  const colorFor = ratio => {
    if (ratio <= 0)     return 'var(--bg-elev)';
    if (ratio < 0.34)   return 'var(--green-soft)';
    if (ratio < 0.67)   return 'var(--green)';
    return 'var(--green-text)';
  };
  const dayLabels = ['L', '', 'M', '', 'V', '', 'D'];
  const cell = 13, gap = 3;

  return (
    <div style={{ width: '100%', overflowX: 'auto' }} className="scroll">
      <div style={{ display: 'inline-flex', gap: 8, paddingBottom: 4 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap }}>
          {dayLabels.map((d, i) => (
            <div key={i} style={{ height: cell, width: 11, fontSize: 9, color: 'var(--fg-4)', display: 'flex', alignItems: 'center' }}>{d}</div>
          ))}
        </div>
        {weeks.map((wk, wi) => (
          <div key={wi} style={{ display: 'flex', flexDirection: 'column', gap }}>
            {wk.map((c, di) => (
              <div key={di} title={c ? `${statsFrShortDate(c.date)} · ${c.count} complétée${c.count > 1 ? 's' : ''}` : ''}
                style={{
                  width: cell, height: cell, borderRadius: 3,
                  background: c ? colorFor(c.ratio) : 'transparent',
                  border: c ? '1px solid var(--line)' : '1px solid transparent',
                }} />
            ))}
          </div>
        ))}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 12, fontSize: 10.5, color: 'var(--fg-3)' }}>
        <span>Moins</span>
        {['var(--bg-elev)', 'var(--green-soft)', 'var(--green)', 'var(--green-text)'].map((c, i) => (
          <span key={i} style={{ width: 12, height: 12, borderRadius: 3, background: c, border: '1px solid var(--line)' }} />
        ))}
        <span>Plus</span>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════
//  TRADING
// ═════════════════════════════════════════════════════════════════════
function statsTradesInRange(trades, range) {
  if (!Array.isArray(trades)) return [];
  if (!range.fromISO) return trades.slice();
  const fromMs = statsDateFromISO(range.fromISO).getTime();
  const toMs = statsDateFromISO(range.toISO).getTime() + 24 * 3600 * 1000 - 1;
  return trades.filter(t => {
    const ts = new Date(t.executed_at).getTime();
    return ts >= fromMs && ts <= toMs;
  });
}

function StatsTrading({ trades, range, period, compact = false }) {
  const list = React.useMemo(() => statsTradesInRange(trades, range), [trades, range]);
  const stats = React.useMemo(() => computeStats(list), [list]);

  const sortedAsc = React.useMemo(
    () => [...list].sort((a, b) => new Date(a.executed_at) - new Date(b.executed_at)),
    [list]
  );

  // P&L cumulé + dates + métriques dérivées (drawdown, séries, extrêmes).
  const { cumulative, cumDates, derived } = React.useMemo(() => {
    let run = 0;
    const c = [], d = [];
    let peak = 0, maxDD = 0, ddPeakIdx = 0, ddTroughIdx = 0, curPeakIdx = 0;
    let curWin = 0, curLoss = 0, bestWin = 0, bestLoss = 0;
    let largestWin = 0, largestLoss = 0;
    for (let i = 0; i < sortedAsc.length; i++) {
      const p = Number(sortedAsc[i].pnl) || 0;
      run += p;
      c.push(run); d.push(sortedAsc[i].executed_at);
      if (run >= peak) { peak = run; curPeakIdx = i; }
      const dd = peak - run;
      if (dd > maxDD) { maxDD = dd; ddPeakIdx = curPeakIdx; ddTroughIdx = i; }
      if (p > 0) { curWin++; curLoss = 0; if (curWin > bestWin) bestWin = curWin; }
      else if (p < 0) { curLoss++; curWin = 0; if (curLoss > bestLoss) bestLoss = curLoss; }
      if (p > largestWin) largestWin = p;
      if (p < largestLoss) largestLoss = p;
    }
    return { cumulative: c, cumDates: d, derived: { maxDD, ddPeakIdx, ddTroughIdx, bestWin, bestLoss, largestWin, largestLoss } };
  }, [sortedAsc]);

  // Distribution des R-multiples — seulement si des R sont renseignés.
  const rDist = React.useMemo(() => {
    const RED = 'var(--red)', GREEN = 'var(--green)';
    const buckets = [
      { label: '≤ −2R', min: -Infinity, max: -2, value: 0, color: RED },
      { label: '−2…−1', min: -2, max: -1, value: 0, color: RED },
      { label: '−1…0',  min: -1, max: 0,  value: 0, color: RED },
      { label: '0…1',   min: 0,  max: 1,  value: 0, color: GREEN },
      { label: '1…2',   min: 1,  max: 2,  value: 0, color: GREEN },
      { label: '≥ 2R',  min: 2,  max: Infinity, value: 0, color: GREEN },
    ];
    let rTrades = 0;
    for (const t of list) {
      const rv = Number(t.r_multiple);
      if (t.r_multiple == null || t.r_multiple === '' || !Number.isFinite(rv) || rv === 0) continue;
      rTrades++;
      for (const b of buckets) { if (rv >= b.min && (rv < b.max || b.max === Infinity)) { b.value++; break; } }
    }
    return rTrades > 0 ? { buckets, rTrades } : null;
  }, [list]);

  // Performance par setup (P&L net), top 6 par |valeur|, + win rate.
  const bySetup = React.useMemo(() => {
    const m = {};
    for (const t of list) {
      const key = (t.setup && String(t.setup).trim()) || 'Sans setup';
      if (!m[key]) m[key] = { label: key, value: 0, count: 0, wins: 0 };
      const p = Number(t.pnl) || 0;
      m[key].value += p; m[key].count++; if (p > 0) m[key].wins++;
    }
    const arr = Object.values(m);
    return {
      hasSetups: arr.some(s => s.label !== 'Sans setup'),
      rows: arr.sort((a, b) => Math.abs(b.value) - Math.abs(a.value)).slice(0, 6).map(s => ({
        label: s.label, value: s.value,
        sub: `${s.count}T · ${statsPct(s.count ? (s.wins / s.count) * 100 : 0, 0)}`,
      })),
    };
  }, [list]);

  // Performance par jour de la semaine (P&L net).
  const byWeekday = React.useMemo(() => {
    const sums = [0, 0, 0, 0, 0, 0, 0];
    const counts = [0, 0, 0, 0, 0, 0, 0];
    for (const t of list) {
      const d = statsDateFromISO(statsDayKey(t.executed_at));
      const wd = statsWeekday(d);
      sums[wd] += Number(t.pnl) || 0; counts[wd]++;
    }
    const any = counts.some(c => c > 0);
    return any ? STATS_WD_LABELS.map((lab, i) => ({ label: lab, value: sums[i], count: counts[i] })) : null;
  }, [list]);

  // P&L par jour (pour calendrier + meilleur/pire jour).
  const dayMap = React.useMemo(() => {
    const m = {};
    for (const t of list) {
      const day = statsDayKey(t.executed_at);
      if (!day) continue;
      if (!m[day]) m[day] = { pnl: 0, count: 0, wins: 0, losses: 0 };
      const v = Number(t.pnl) || 0;
      m[day].pnl += v; m[day].count++;
      if (v > 0) m[day].wins++; else if (v < 0) m[day].losses++;
    }
    return m;
  }, [list]);

  const bestWorst = React.useMemo(() => {
    const entries = Object.entries(dayMap);
    if (!entries.length) return null;
    let best = null, worst = null;
    for (const [day, v] of entries) {
      if (!best || v.pnl > best.pnl) best = { day, pnl: v.pnl };
      if (!worst || v.pnl < worst.pnl) worst = { day, pnl: v.pnl };
    }
    return { best, worst };
  }, [dayMap]);

  // Mois affiché dans le calendrier P&L (navigable).
  const [calMonth, setCalMonth] = React.useState(() => {
    const t = statsDateFromISO(typeof todayISO === 'function' ? todayISO() : statsISO(new Date()));
    return new Date(t.getFullYear(), t.getMonth(), 1);
  });
  const todayMonth = React.useMemo(() => {
    const t = statsDateFromISO(typeof todayISO === 'function' ? todayISO() : statsISO(new Date()));
    return new Date(t.getFullYear(), t.getMonth(), 1);
  }, []);
  const minMonth = React.useMemo(() => {
    const keys = Object.keys(dayMap).sort();
    if (!keys.length) return todayMonth;
    const d = statsDateFromISO(keys[0]);
    return new Date(d.getFullYear(), d.getMonth(), 1);
  }, [dayMap, todayMonth]);
  const canPrev = calMonth > minMonth;
  const canNext = calMonth < todayMonth;

  if (list.length === 0) {
    return (
      <div className="card">
        <StatsEmpty icon={<span style={{ display: 'inline-flex', transform: 'scale(2)' }}>{Ico.trades}</span>}
          title="Aucun trade sur cette période"
          sub="Change de période ou importe tes trades pour voir ton analyse de performance." />
      </div>
    );
  }

  const pf = stats.profitFactor;
  const pfLabel = stats.count === 0 ? '—' : !Number.isFinite(pf) ? '∞' : statsNum(pf, 2);
  const expectancy = stats.count ? stats.netPnl / stats.count : 0;
  const ddRange = derived.maxDD > 0 ? { peak: derived.ddPeakIdx, trough: derived.ddTroughIdx } : null;
  const kpiCols = compact ? 2 : 4;
  const gap = compact ? 10 : 14;

  return (
    <div className="stagger" style={{ display: 'flex', flexDirection: 'column', gap: compact ? 12 : 16 }}>
      {/* KPI principaux — les 4 chiffres qui pilotent une décision */}
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${kpiCols}, 1fr)`, gap }}>
        <StatsKpi big label="P&L net" animatedMoney={stats.netPnl} tone={stats.netPnl >= 0 ? 'pos' : 'neg'}
          sub={`${stats.count} trade${stats.count > 1 ? 's' : ''}`} />
        <StatsKpi big label="Win rate" value={statsPct(stats.winRate, 1)} accent="var(--blue)"
          sub={`${stats.wins} G · ${stats.losses} P`} />
        <StatsKpi big label="Profit factor" value={pfLabel} accent="var(--blue)"
          sub={stats.avgLoss > 0 ? `gain/perte ${statsNum(stats.avgWin / stats.avgLoss, 2)}×` : 'aucune perte'} />
        <StatsKpi big label="Espérance / trade" animatedMoney={expectancy} tone={expectancy >= 0 ? 'pos' : 'neg'}
          sub="P&L moyen par position" />
      </div>

      {/* Bandeau secondaire : R moyen, gain/perte moyens, nb trades */}
      <StatsStrip cols={compact ? 2 : 4} items={[
        { label: 'R moyen', value: statsR(stats.avgR), tone: stats.avgR > 0 ? 'pos' : stats.avgR < 0 ? 'neg' : undefined },
        { label: 'Gain moyen', value: statsMoney(stats.avgWin, true), tone: 'pos' },
        { label: 'Perte moyenne', value: stats.avgLoss > 0 ? statsMoney(-stats.avgLoss, true) : statsMoney(0), tone: stats.avgLoss > 0 ? 'neg' : undefined },
        { label: 'Drawdown max', value: derived.maxDD > 0 ? statsMoney(-derived.maxDD, true) : statsMoney(0), tone: derived.maxDD > 0 ? 'neg' : undefined },
      ]} />

      {/* Courbe d'équité — pièce maîtresse */}
      <StatsPanel title="Courbe d'équité"
        subtitle={ddRange ? `Drawdown max ${statsMoney(-derived.maxDD, true)}` : `P&L cumulé · ${list.length} trade${list.length > 1 ? 's' : ''}`}>
        <StatsEquityCurve points={cumulative} dates={cumDates} ddRange={ddRange} height={compact ? 190 : 250} />
      </StatsPanel>

      {/* Chiffres-clés discrets : meilleur/pire jour + extrêmes + séries */}
      {bestWorst && (
        <StatsStrip cols={compact ? 2 : 4} items={[
          { label: 'Meilleur jour', value: statsMoney(bestWorst.best.pnl, true), tone: 'pos', sub: statsFrShortDate(bestWorst.best.day) },
          { label: 'Pire jour', value: statsMoney(bestWorst.worst.pnl, true), tone: 'neg', sub: statsFrShortDate(bestWorst.worst.day) },
          { label: 'Série gains', value: derived.bestWin + (derived.bestWin > 1 ? ' victoires' : ' victoire'), accent: 'var(--green)', sub: 'd\'affilée' },
          { label: 'Série pertes', value: derived.bestLoss + (derived.bestLoss > 1 ? ' pertes' : ' perte'), accent: 'var(--red)', sub: 'd\'affilée' },
        ]} />
      )}

      {/* Calendrier P&L mensuel — affiché si pas en mode ultra-compact */}
      <StatsPanel title="Calendrier P&L" subtitle="P&L net par jour">
        <StatsPnlCalendar
          dayMap={dayMap} monthDate={calMonth}
          onPrev={() => canPrev && setCalMonth(m => new Date(m.getFullYear(), m.getMonth() - 1, 1))}
          onNext={() => canNext && setCalMonth(m => new Date(m.getFullYear(), m.getMonth() + 1, 1))}
          canPrev={canPrev} canNext={canNext}
        />
      </StatsPanel>

      {/* Deux colonnes : distribution R + perf par jour de semaine (ou setup) */}
      <div style={{ display: 'grid', gridTemplateColumns: compact ? '1fr' : '1fr 1fr', gap }}>
        <StatsPanel title="Distribution des R"
          subtitle={rDist ? `${rDist.rTrades} trade${rDist.rTrades > 1 ? 's' : ''} avec R` : null}>
          {rDist
            ? <StatsBars data={rDist.buckets.map(b => ({ label: b.label, value: b.value, color: b.value > 0 ? b.color : 'var(--line-2)' }))}
                height={compact ? 160 : 186} valueFmt={v => String(v)} />
            : <StatsEmpty title="Aucun R renseigné" sub="Ajoute un R-multiple à tes trades pour voir leur distribution." />}
        </StatsPanel>

        <StatsPanel title="P&L par jour de la semaine" subtitle="quel jour est ton edge ?">
          {byWeekday
            ? <StatsBars data={byWeekday.map(d => ({ label: d.label, value: d.value }))} height={compact ? 160 : 186} />
            : <StatsEmpty title="Aucune donnée" />}
        </StatsPanel>
      </div>

      {/* Performance par setup */}
      <StatsPanel title="Performance par setup" subtitle="P&L net · nb trades · win rate">
        {bySetup.hasSetups
          ? <StatsHBars rows={bySetup.rows} />
          : <StatsEmpty title="Aucun setup renseigné" sub="Renseigne le setup de tes trades pour identifier ceux qui constituent ton edge." />}
      </StatsPanel>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════
//  HABITUDES
// ═════════════════════════════════════════════════════════════════════
function useHabitStats(range) {
  const [data, setData] = React.useState({ loading: true, error: null, habits: [], completions: [] });
  React.useEffect(() => {
    let live = true;
    (async () => {
      if (!window.sb) { setData({ loading: false, error: 'offline', habits: [], completions: [] }); return; }
      setData(d => ({ ...d, loading: true, error: null }));
      try {
        let cq = window.sb.from('habit_completions').select('habit_id, date');
        if (range.fromISO) cq = cq.gte('date', range.fromISO);
        cq = cq.lte('date', range.toISO);
        const [habRes, compRes] = await Promise.all([
          window.sb.from('habits').select('id, name, section, position, archived').eq('archived', false),
          cq,
        ]);
        if (!live) return;
        if (habRes.error || compRes.error) { setData({ loading: false, error: 'load', habits: [], completions: [] }); return; }
        setData({ loading: false, error: null, habits: habRes.data || [], completions: compRes.data || [] });
      } catch (e) {
        if (live) setData({ loading: false, error: 'load', habits: [], completions: [] });
      }
    })();
    return () => { live = false; };
  }, [range.fromISO, range.toISO]);
  return data;
}

// Liste des dates ISO d'une plage (bornée, max 120 jours pour 'all').
function statsDateList(range) {
  const to = statsDateFromISO(range.toISO);
  const from = range.fromISO ? statsDateFromISO(range.fromISO) : statsAddDays(to, -119);
  const out = [];
  let d = new Date(from), guard = 0;
  while (d <= to && guard < 1000) { out.push(statsISO(d)); d = statsAddDays(d, 1); guard++; }
  return out;
}

function StatsHabits({ range, compact = false }) {
  const { loading, error, habits, completions } = useHabitStats(range);

  const agg = React.useMemo(() => {
    const dates = statsDateList(range);
    const dateSet = new Set(dates);
    const perDate = {};   // date -> count
    const perHabit = {};  // habit_id -> Set(date)
    for (const c of completions) {
      if (!dateSet.has(c.date)) continue;
      perDate[c.date] = (perDate[c.date] || 0) + 1;
      if (!perHabit[c.habit_id]) perHabit[c.habit_id] = new Set();
      perHabit[c.habit_id].add(c.date);
    }
    const nbDays = dates.length || 1;
    const nbHabits = habits.length || 0;

    const habitRows = habits.map(h => {
      const set = perHabit[h.id] || new Set();
      const done = set.size;
      let streak = 0;
      for (let i = dates.length - 1; i >= 0; i--) { if (set.has(dates[i])) streak++; else break; }
      let best = 0, cur = 0;
      for (const d of dates) { if (set.has(d)) { cur++; best = Math.max(best, cur); } else cur = 0; }
      return { id: h.id, name: h.name, done, rate: (done / nbDays) * 100, streak, best };
    }).sort((a, b) => b.rate - a.rate);

    const heat = dates.map(d => {
      const count = perDate[d] || 0;
      return { date: d, count, ratio: nbHabits > 0 ? count / nbHabits : 0 };
    });

    const totalDone = completions.filter(c => dateSet.has(c.date)).length;
    const possible = nbHabits * nbDays;
    const globalRate = possible > 0 ? (totalDone / possible) * 100 : 0;
    let globalStreak = 0;
    for (let i = dates.length - 1; i >= 0; i--) { if ((perDate[dates[i]] || 0) > 0) globalStreak++; else break; }

    const trend = heat.map(h => h.ratio);
    const half = Math.floor(nbDays / 2);
    const avg = a => (a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0);
    const firstHalf = avg(trend.slice(0, half)) * 100;
    const lastHalf = avg(trend.slice(half)) * 100;
    const trendDelta = half > 0 ? lastHalf - firstHalf : 0;

    return { habitRows, heat, totalDone, globalRate, nbHabits, nbDays, globalStreak, trend, trendDelta };
  }, [habits, completions, range.fromISO, range.toISO]);

  if (loading) {
    return <div className="card"><StatsEmpty title="Chargement des habitudes…" /></div>;
  }
  if (error === 'offline' || error === 'load') {
    return <div className="card"><StatsEmpty title="Impossible de charger les habitudes" sub="Vérifie ta connexion et réessaie." /></div>;
  }
  if (!habits.length) {
    return (
      <div className="card">
        <StatsEmpty icon={<span style={{ display: 'inline-flex', transform: 'scale(2)' }}>{Ico.book}</span>}
          title="Aucune habitude active"
          sub="Crée des habitudes depuis Routines pour suivre ta régularité ici." />
      </div>
    );
  }

  const kpiCols = compact ? 2 : 4;
  const gap = compact ? 10 : 14;
  const dlt = agg.trendDelta;
  const trendTone = Math.abs(dlt) < 0.5 ? undefined : dlt > 0 ? 'pos' : 'neg';
  const trendVal = (dlt > 0 ? '+' : dlt < 0 ? '−' : '') + statsPct(Math.abs(dlt), 0);

  return (
    <div className="stagger" style={{ display: 'flex', flexDirection: 'column', gap: compact ? 12 : 16 }}>
      {/* KPI */}
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${kpiCols}, 1fr)`, gap }}>
        <StatsKpi big label="Taux global" value={statsPct(agg.globalRate, 0)} accent="var(--blue)"
          sub={`${agg.nbHabits} habitude${agg.nbHabits > 1 ? 's' : ''}`} />
        <StatsKpi big label="Tendance" value={Math.abs(dlt) < 0.5 ? 'Stable' : trendVal} tone={trendTone}
          sub="2e moitié vs 1re" />
        <StatsKpi big label="Total complétées" value={String(agg.totalDone)} accent="var(--green)"
          sub={`sur ${agg.nbDays} jour${agg.nbDays > 1 ? 's' : ''}`} />
        <StatsKpi big label="Série en cours" value={agg.globalStreak + ' j'} accent="var(--blue)"
          sub="jours consécutifs actifs" />
      </div>

      {/* Tendance de régularité */}
      <StatsPanel title="Tendance de régularité" subtitle="% d'habitudes complétées / jour">
        <StatsSparkline values={agg.trend} height={compact ? 52 : 66}
          color={trendTone === 'neg' ? 'var(--red)' : 'var(--blue)'} />
      </StatsPanel>

      {/* Heatmap calendrier */}
      <StatsPanel title="Régularité jour par jour"
        subtitle={range.fromISO ? `${statsFrShortDate(range.fromISO)} → ${statsFrShortDate(range.toISO)}` : '120 derniers jours'}>
        <StatsHeatmap heat={agg.heat} />
      </StatsPanel>

      {/* Taux par habitude */}
      <StatsPanel title="Taux par habitude" subtitle="% de jours complétés">
        <StatsHBars rows={agg.habitRows.map(h => ({ label: h.name, value: Math.round(h.rate), sub: `${h.done} j` }))}
          valueFmt={v => statsPct(v, 0)} colorFor={() => 'var(--blue)'} />
      </StatsPanel>

      {/* Séries (streaks) */}
      <StatsPanel title="Séries" subtitle="en cours · record">
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          {agg.habitRows.map((h, i) => (
            <div key={h.id} style={{
              display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
              padding: '10px 0', borderTop: i ? '1px solid var(--line)' : 'none',
            }}>
              <span style={{ fontSize: 12.5, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{h.name}</span>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
                <span className="pill" style={{ background: h.streak > 0 ? 'var(--green-soft)' : 'var(--bg-elev)', color: h.streak > 0 ? 'var(--green-text)' : 'var(--fg-3)', border: '1px solid var(--line)' }}>
                  <span style={{ fontWeight: 700 }} className="num">{h.streak}</span> en cours
                </span>
                <span className="pill pill-gray"><span className="num" style={{ fontWeight: 700 }}>{h.best}</span> record</span>
              </div>
            </div>
          ))}
        </div>
      </StatsPanel>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════
//  BARRE DE CONTRÔLE (sous-onglets + période) — partagée desktop/mobile
// ═════════════════════════════════════════════════════════════════════
function StatsControls({ view, setView, period, setPeriod, mobile = false }) {
  const viewOptions = [
    { id: 'trading', label: 'Trading', ico: <span style={{ display: 'inline-flex' }}>{Ico.trades}</span> },
    { id: 'habits',  label: 'Habitudes', ico: <span style={{ display: 'inline-flex' }}>{Ico.book}</span> },
  ];
  if (mobile) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 16 }}>
        <StatsSegmented value={view} onChange={setView} options={viewOptions} />
        <div style={{ overflowX: 'auto' }} className="scroll">
          <StatsSegmented value={period} onChange={setPeriod} options={STATS_PERIODS} size="sm" />
        </div>
      </div>
    );
  }
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 20 }}>
      <StatsSegmented value={view} onChange={setView} options={viewOptions} />
      <StatsSegmented value={period} onChange={setPeriod} options={STATS_PERIODS} size="sm" />
    </div>
  );
}

// État de vue/période partagé (gère initialView + prop period optionnelle).
function useStatsState(initialView, initialPeriod) {
  const [view, setView] = React.useState(initialView === 'habits' ? 'habits' : 'trading');
  const [period, setPeriod] = React.useState(
    ['week', 'month', 'year', 'all'].includes(initialPeriod) ? initialPeriod : 'month'
  );
  React.useEffect(() => {
    if (initialView === 'habits' || initialView === 'trading') setView(initialView);
  }, [initialView]);
  const range = React.useMemo(() => statsPeriodRange(period), [period]);
  return { view, setView, period, setPeriod, range };
}

// ═════════════════════════════════════════════════════════════════════
//  PAGE DESKTOP
// ═════════════════════════════════════════════════════════════════════
function StatsPage({ state, initialView, period: periodProp }) {
  const { view, setView, period, setPeriod, range } = useStatsState(initialView, periodProp);
  const trades = (state && state.trades) || [];

  return (
    <div style={{ width: '100%', height: '100%', overflow: 'auto', display: 'flex', flexDirection: 'column' }} className="scroll">
      <PageHeader title="Statistiques" />
      <div style={{ padding: '20px 24px 40px', flex: 1, maxWidth: 1080, width: '100%', margin: '0 auto' }}>
        <StatsControls view={view} setView={setView} period={period} setPeriod={setPeriod} />
        {view === 'trading'
          ? <StatsTrading trades={trades} range={range} period={period} />
          : <StatsHabits range={range} />}
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════
//  PAGE MOBILE
// ═════════════════════════════════════════════════════════════════════
function StatsMobile({ state, initialView, period: periodProp }) {
  const { view, setView, period, setPeriod, range } = useStatsState(initialView, periodProp);
  const trades = (state && state.trades) || [];

  return (
    <div style={{ padding: '14px 16px 28px' }}>
      <h1 style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-.025em', margin: '0 0 16px' }}>Statistiques</h1>
      <StatsControls view={view} setView={setView} period={period} setPeriod={setPeriod} mobile />
      {view === 'trading'
        ? <StatsTrading trades={trades} range={range} period={period} compact />
        : <StatsHabits range={range} compact />}
    </div>
  );
}

Object.assign(window, { StatsPage, StatsMobile });
