/* Page Aujourd'hui — tableau de bord dense, action dans le 1er viewport.
   3 variantes : 'guide' (dashboard), 'tableau', 'intention'. */

// Délègue à la couche partagée polishLabel (ui.jsx). Repli local minimal si jamais
// ui.jsx n'était pas encore chargé (ne devrait pas arriver : il précède today.jsx).
function polishTodayText(text) {
  return window.polishLabel ? window.polishLabel(text) : text;
}

function DurationPicker({ value, onChange, options = [15, 30, 60, 90, 120, 180] }) {
  return (
    <div role="radiogroup" aria-label="Durée" style={{ display: "inline-flex", flexWrap: "wrap", gap: 3, background: "var(--card-2)", border: "1px solid var(--rule)", borderRadius: 18, padding: 3 }}>
      {options.map(o => (
        <button key={o} role="radio" aria-checked={value === o} onClick={() => onChange(o)} style={{
          border: "none", cursor: "pointer", borderRadius: 999, padding: "6px 13px",
          fontFamily: "var(--sans)", fontSize: 13.5, fontWeight: 600,
          color: value === o ? "#fdf3f0" : "var(--ink-soft)",
          background: value === o ? "var(--accent)" : "transparent", transition: "background .15s, color .15s",
        }}>{o}<span style={{ fontSize: 10.5, opacity: .7 }}>m</span></button>
      ))}
    </div>
  );
}

function IntentionChips({ value, onChange }) {
  const D = window.LANGUES_DATA.intentions;
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
      {D.map(it => {
        const on = value === it.id;
        return (
          <button key={it.id} onClick={() => onChange(it.id)} style={{
            display: "inline-flex", alignItems: "center", gap: 7, cursor: "pointer",
            padding: "6px 11px 6px 9px", borderRadius: 999,
            border: `1px solid ${on ? "var(--accent-line)" : "var(--rule)"}`,
            background: on ? "var(--accent-tint)" : "var(--card)",
            color: on ? "var(--accent-deep)" : "var(--ink-soft)",
            fontFamily: "var(--sans)", fontWeight: 600, fontSize: 13, transition: "all .15s",
          }} aria-label={`Intention : ${it.label}`} title={it.desc || it.label}>
            <Icon name={it.icon} size={15} />{it.label}
          </button>
        );
      })}
    </div>
  );
}

function ChargePicker({ value, onChange }) {
  const charges = window.LANGUES_DATA.charges;
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 8 }}>
      {charges.map(c => {
        const on = c.id === value;
        return (
          <button key={c.id} onClick={() => onChange(c.id)} style={{
            textAlign: "left", cursor: "pointer", padding: "10px 12px", borderRadius: "var(--radius-sm)",
            border: `1px solid ${on ? "var(--accent)" : "var(--rule)"}`,
            background: on ? "var(--accent-tint)" : "var(--card)", transition: "all .15s",
          }}>
            <div style={{ fontFamily: "var(--serif)", fontSize: 16, color: on ? "var(--accent-deep)" : "var(--ink)" }}>{c.label}</div>
            <div className="faint" style={{ fontSize: 11.5, marginTop: 2 }}>{c.desc}</div>
          </button>
        );
      })}
    </div>
  );
}

function DrillModePicker({ value, onChange }) {
  const modes = (window.LANGUES_DATA && window.LANGUES_DATA.drillModes) || [
    { id: "mixte", label: "Mixte", desc: "Équilibre entre rappel, dictée, input et production" },
    { id: "rappel", label: "Rappel", desc: "Cartes et vocabulaire en rappel actif" },
    { id: "reconnaissance", label: "Reconnaissance", desc: "QCM court comme échafaudage" },
    { id: "dictee", label: "Dictée", desc: "Audio vers hangeul et micro-écriture" },
    { id: "fluidite", label: "Fluidité", desc: "Réécoute, lecture et shadowing sans pression SRS" },
  ];
  const selected = modes.find((m) => m.id === value) || modes[0];
  return (
    <div style={{ display: "grid", gap: 6 }}>
      <Segmented
        value={value}
        onChange={onChange}
        size="sm"
        options={modes.map((m) => ({ value: m.id, label: m.label, title: m.desc }))}
      />
      {selected && <span className="faint" style={{ fontSize: 12.5 }}>{selected.desc}</span>}
    </div>
  );
}

function activeSessionProgress(draft) {
  const sessionBlocks = draft && draft.session ? (draft.session.blocks || []) : [];
  const planBlocks = draft && draft.plan ? (draft.plan.blocks || []) : [];
  const total = sessionBlocks.length || planBlocks.length || 0;
  const done = sessionBlocks.filter((b) => b.completion !== "pending").length;
  return { done, total, remaining: Math.max(0, total - done) };
}

function ResumeSessionCard({ draft, onResume }) {
  if (!draft) return null;
  const p = activeSessionProgress(draft);
  const label = p.total ? `${p.done}/${p.total} blocs faits` : "séance en cours";
  const remaining = p.remaining ? `${p.remaining} étape${p.remaining > 1 ? "s" : ""} restante${p.remaining > 1 ? "s" : ""}` : "bilan à finaliser";
  return (
    <div className="card fade-up" style={{ padding: "14px 16px", display: "flex", alignItems: "center", gap: 14, justifyContent: "space-between", borderLeft: "3px solid var(--accent)" }}>
      <div style={{ minWidth: 0 }}>
        <Eyebrow>Séance en cours</Eyebrow>
        <div style={{ fontFamily: "var(--serif)", fontSize: 18, color: "var(--accent-deep)", lineHeight: 1.2 }}>Reprendre le fil · {label}</div>
        <div className="faint" style={{ fontSize: 12.5, marginTop: 3 }}>{remaining}</div>
      </div>
      <Btn kind="secondary" size="sm" icon="play" aria-label="Reprendre la séance en cours" onClick={onResume}>Reprendre</Btn>
    </div>
  );
}

function AdaptivePlan({ compact, plan }) {
  const legacyPlan = window.LANGUES_DATA.sessionPlan;
  const steps = plan && plan.blocks && plan.blocks.length
    ? plan.blocks.map((block) => ({
        n: block.order,
        label: polishTodayText(block.label),
        meta: polishTodayText(block.exercise && block.exercise.type ? block.exercise.type.replace(/_/g, " ") : block.kind),
        min: block.minutes,
        srs: block.srsEffect && block.srsEffect !== "none",
      }))
    : legacyPlan;
  return (
    <ol style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: compact ? 5 : 8 }}>
      {steps.map(step => (
        <li key={step.n} style={{ display: "flex", alignItems: "center", gap: 11, padding: compact ? "7px 11px" : "10px 13px", background: "var(--card-2)", border: "1px solid var(--rule)", borderRadius: "var(--radius-sm)" }}>
          <span style={{ fontFamily: "var(--serif)", fontSize: 14, color: "var(--ochre)", width: 16, textAlign: "center" }}>{step.n}</span>
          <span style={{ flex: 1, fontWeight: 600, fontSize: 14 }}>{step.label}</span>
          <span className="faint" style={{ fontSize: 12 }}>{step.meta}</span>
          <span className="chip" style={{ fontSize: 10.5, padding: "2px 7px", background: step.srs ? "var(--accent-tint)" : "var(--card)", color: step.srs ? "var(--accent-deep)" : "var(--ink-faint)", borderColor: "transparent" }} title={step.srs ? "Modifie le SRS" : "Sans effet SRS"}>{step.srs ? "SRS" : "libre"}</span>
          <span className="chip" style={{ fontSize: 10.5, padding: "2px 7px" }}>{step.min}′</span>
        </li>
      ))}
    </ol>
  );
}

function planLaunchMinutes(plan, fallback) {
  const requested = plan && plan.requested;
  const n = requested && Number.isFinite(requested.minutes) ? requested.minutes : null;
  return n || fallback || 30;
}

function PlanNotice({ plan }) {
  const meta = (plan && plan.meta) || {};
  const duration = meta.durationPolicy || {};
  const durationAudit = meta.durationAudit || {};
  const drill = meta.drillModePolicy || {};
  const drillAudit = meta.drillModeAudit || {};
  const original = plan && plan.requested && plan.requested.originalMinutes;
  const showDuration = duration.kind && duration.kind !== "as_requested";
  const showDrill = !!drill.label;
  const showAudit = !!(durationAudit.plannedMinutes || drillAudit.primaryShare != null);
  if (!showDuration && !showDrill && !showAudit) return null;
  return (
    <div className="fade-up" style={{ display: "grid", gap: 6, padding: "10px 12px", borderRadius: "var(--radius-sm)", background: "var(--card-2)", border: "1px solid var(--rule)" }}>
      {showDuration && (
        <div style={{ display: "grid", gap: 3 }}>
          <div className="eyebrow">{duration.kind === "recovery_first" ? "Durée ajustée" : "Format long"}</div>
          <div style={{ fontSize: 13.5, color: "var(--ink)" }}>
            {duration.label}{original ? ` ${original} min demandées, ${planLaunchMinutes(plan)} min lancées.` : ""}
          </div>
          {/* Raisons concrètes (mêmes signaux que la bannière budget) : évite la
              contradiction « Sain » vs « Dette forte » en nommant la vraie cause. */}
          {duration.kind === "recovery_first" && Array.isArray(duration.reasons) && duration.reasons.length > 0 && (
            <ul style={{ margin: "2px 0 0", paddingLeft: 16, display: "grid", gap: 2 }}>
              {duration.reasons.map((r, i) => <li key={i} className="faint" style={{ fontSize: 12 }}>{r}</li>)}
            </ul>
          )}
        </div>
      )}
      {showDrill && <div className="faint" style={{ fontSize: 12.5 }}>{drill.label}</div>}
      {showAudit && (
        <div className="faint" style={{ fontSize: 12 }}>
          Plan {durationAudit.plannedMinutes || plan.minutes} min / {durationAudit.effectiveMinutes || planLaunchMinutes(plan)} min · {durationAudit.blockCount || (plan.blocks || []).length} blocs
          {durationAudit.avgBlockMinutes ? ` · ${durationAudit.avgBlockMinutes} min/bloc` : ""}
          {drillAudit.overriddenBy ? " · mode suspendu par récupération" : (drillAudit.requested && drillAudit.requested !== "mixte" ? ` · mode ${Math.round((drillAudit.primaryShare || 0) * 100)} % minutes / ${Math.round((drillAudit.primaryBlockShare || 0) * 100)} % blocs` : "")}
        </div>
      )}
    </div>
  );
}

// Lisibilité charge/intention (PROBLEMATIQUES_SEANCES §6.2) : on dit explicitement
// ce que la charge fait, on montre la composition réelle (minutes par brin), et sur
// les formats longs on assume honnêtement que la durée commande le volume global.
const CHARGE_EFFECT = {
  legere: "allège cartes et vocabulaire — séance plus courte, centrée sur la révision.",
  courte: "allège cartes et vocabulaire — séance plus courte, centrée sur la révision.",
  moderee: "équilibre révision, écoute et production.",
  standard: "équilibre révision, écoute et production.",
  conso: "renforce réparation, dictée et production ; freine le neuf.",
  consolidation: "renforce réparation, dictée et production ; freine le neuf.",
  intensive: "pousse le neuf et la production — séance dense.",
};
const COMPOSITION_STRANDS = ["study", "input", "output", "fluency", "meta"];
const COMPOSITION_LABELS = { study: "Étude", input: "Écoute", output: "Production", fluency: "Fluidité", meta: "Méta" };

function ChargeComposition({ plan, charge }) {
  if (!plan || !plan.blocks || !plan.blocks.length) return null;
  const strands = (plan.meta && plan.meta.strands) || {};
  const total = COMPOSITION_STRANDS.reduce((sum, k) => sum + (strands[k] || 0), 0);
  if (!total) return null;
  const parts = COMPOSITION_STRANDS
    .filter((k) => strands[k])
    .map((k) => ({ k, pct: Math.round((strands[k] / total) * 100), min: Math.round(strands[k]) }));
  const key = String((plan.requested && plan.requested.charge) || charge || "").toLowerCase();
  const effect = CHARGE_EFFECT[key];
  const longFormat = planLaunchMinutes(plan) >= 90;
  return (
    <div className="fade-up" style={{ display: "grid", gap: 6, padding: "10px 12px", borderRadius: "var(--radius-sm)", background: "var(--card-2)", border: "1px solid var(--rule)" }}>
      <div className="eyebrow">Ce que fait cette charge</div>
      {effect && <div style={{ fontSize: 13.5, color: "var(--ink)" }}>Charge {todayChargeLabel(key)} : {effect}</div>}
      <div className="faint" style={{ fontSize: 12, display: "flex", gap: 10, flexWrap: "wrap" }}>
        {parts.map((p) => <span key={p.k}>{COMPOSITION_LABELS[p.k] || p.k} {p.pct}% ({p.min}′)</span>)}
      </div>
      {longFormat && (
        <div className="faint" style={{ fontSize: 12 }}>
          Format long : la durée fixe le volume global et ajoute une part d'écoute extensive et de reprises quelle que soit la charge — la charge déplace surtout l'équilibre ci-dessus, pas la taille totale.
        </div>
      )}
    </div>
  );
}

// Rappel/reconnaissance peu rempli faute de cartes dues (PROBLEMATIQUES_SEANCES §6.3) :
// c'est défendable (le SRS ne ramène une carte que lorsqu'elle est utile), mais on
// l'explique au lieu de le subir, et on propose une récupération libre (drill sans
// effet SRS) pour ceux qui veulent forcer du rappel maintenant.
const RECALL_MODE_LABELS = { rappel: "Rappel", reconnaissance: "Reconnaissance", fluidite: "Fluidité" };
function RecallNotice({ plan, drillMode, go }) {
  const audit = plan && plan.meta && plan.meta.drillModeAudit;
  if (!audit || audit.overriddenBy) return null;
  if (!RECALL_MODE_LABELS[drillMode]) return null;
  if (audit.respected) return null; // le mode se remplit correctement : rien à expliquer
  const pct = Math.round((audit.primaryShare || 0) * 100);
  return (
    <div className="fade-up" style={{ display: "grid", gap: 8, padding: "10px 12px", borderRadius: "var(--radius-sm)", background: "var(--accent-tint)", border: "1px solid var(--accent-line)" }}>
      <div className="eyebrow">Mode {RECALL_MODE_LABELS[drillMode]} peu rempli</div>
      <div style={{ fontSize: 13.5, color: "var(--ink)" }}>
        Peu de cartes sont dues aujourd'hui : ce mode ne représente que {pct}% de la séance. C'est sain — le SRS ne fait revenir une carte que lorsqu'elle est utile. Pour forcer du rappel maintenant, lance une récupération libre (sans effet sur le calendrier SRS).
      </div>
      {typeof go === "function" && (
        <div><Btn kind="secondary" size="sm" icon="repeat" onClick={() => go("cartes", { mode: "drill" })}>Récupération libre</Btn></div>
      )}
    </div>
  );
}

function AudioSparkline({ data }) {
  const values = Array.isArray(data) && data.length ? data : Array(7).fill(0);
  const max = Math.max(...values, 1);
  const today = new Date();
  const days = values.map((_, i) => {
    const d = new Date(today);
    d.setDate(today.getDate() - (values.length - 1 - i));
    return d.toLocaleDateString("fr-FR", { weekday: "short" }).slice(0, 1).toUpperCase();
  });
  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 5, height: 38 }}>
      {values.map((v, i) => (
        <div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, height: "100%", justifyContent: "flex-end" }}>
          <div style={{ width: "100%", height: `${(v / max) * 100}%`, minHeight: 2, background: v ? "var(--accent)" : "var(--rule)", borderRadius: 3, opacity: v ? .85 : 1 }} />
          <span className="faint" style={{ fontSize: 9.5 }}>{days[i]}</span>
        </div>
      ))}
    </div>
  );
}

const DAY_MS = 86400000;
const TODAY_ERROR_LABELS = {
  batchim: "Batchim final (받침)",
  son: "Prononciation",
  forme: "Forme",
  terminaison: "Terminaisons",
  particule: "Particules",
  contexte: "Contexte",
  sens: "Sens",
  orthographe: "Orthographe",
  politesse: "Politesse",
};
const TODAY_DIALOGUE_ID = "P1-04A";
const TODAY_DIALOGUE_FALLBACK = {
  id: TODAY_DIALOGUE_ID,
  title: "Café : commander et payer.",
  titleKr: "카페",
  lines: 3,
};

function fallbackTodayState() {
  return (window.LANGUES_DATA && window.LANGUES_DATA.todayState) || {};
}

function todayChargeLabel(chargeId, fallback) {
  const charges = window.LANGUES_DATA && window.LANGUES_DATA.charges ? window.LANGUES_DATA.charges : [];
  const charge = charges.find((c) => c.id === chargeId);
  return charge ? charge.label : (fallback || chargeId || "modérée");
}

function todayErrorLabel(tag) {
  const key = String(tag || "").trim();
  if (!key) return "Erreur nommée";
  return TODAY_ERROR_LABELS[key] || key.replace(/[_-]/g, " ");
}

function normalizeTodayDialogue(dialogue) {
  const d = dialogue || TODAY_DIALOGUE_FALLBACK;
  const polish = window.polishLabel || ((text) => text);
  const rawTitle = d.title || TODAY_DIALOGUE_FALLBACK.title;
  return {
    id: d.id || TODAY_DIALOGUE_ID,
    title: polish(rawTitle === "Cafe : commander et payer." ? TODAY_DIALOGUE_FALLBACK.title : rawTitle),
    titleKr: d.titleKr || d.title_kr || (d.id === TODAY_DIALOGUE_ID ? TODAY_DIALOGUE_FALLBACK.titleKr : (d.kind === "micro_p1_04" ? "상황 대화" : TODAY_DIALOGUE_FALLBACK.titleKr)),
    lines: d.line_count || d.lines || TODAY_DIALOGUE_FALLBACK.lines,
  };
}

function selectTodayDialogue(dialogues) {
  const items = Array.isArray(dialogues) ? dialogues : [];
  return normalizeTodayDialogue(
    items.find((d) => d && d.id === TODAY_DIALOGUE_ID) ||
    items.find((d) => d && d.kind === "micro_p1_04") ||
    null
  );
}

function dayStart(ms) {
  const d = new Date(ms);
  d.setHours(0, 0, 0, 0);
  return d.getTime();
}

function sessionList(Log, store) {
  if (!Log) return [];
  if (typeof Log.all === "function") return Log.all(store) || [];
  if (typeof Log.recent === "function") return Log.recent(store, 60) || [];
  return [];
}

function activityLast7Days(sessions, now) {
  const today = dayStart(now || Date.now());
  const buckets = Array(7).fill(0);
  sessions.forEach((s) => {
    const ts = s && (s.finishedAt || s.startedAt);
    if (!ts) return;
    const diff = Math.round((today - dayStart(ts)) / DAY_MS);
    if (diff < 0 || diff > 6) return;
    const minutes = Math.round(((s.summary || {}).minutesActive) || 0);
    buckets[6 - diff] += minutes || 1;
  });
  return buckets;
}

function sessionStreak(sessions, now) {
  if (!sessions.length) return 0;
  const days = new Set(sessions.map((s) => s && (s.finishedAt || s.startedAt)).filter(Boolean).map(dayStart));
  let cursor = dayStart(now || Date.now());
  if (!days.has(cursor)) cursor -= DAY_MS;
  let streak = 0;
  while (days.has(cursor)) {
    streak += 1;
    cursor -= DAY_MS;
  }
  return streak;
}

function todayCountLabel(n, one, many) {
  return `${n} ${n === 1 ? one : many}`;
}

function lastSessionLabel(sessions, fallback) {
  const s = sessions.length ? sessions[sessions.length - 1] : null;
  if (!s) return "Aucune séance terminée";
  const summary = s.summary || {};
  const counts = summary.counts || {};
  const date = s.finishedAt
    ? new Date(s.finishedAt).toLocaleDateString("fr-FR", { weekday: "short", day: "2-digit", month: "2-digit" })
    : "Dernière";
  const bits = [];
  const minutes = Math.round(summary.minutesActive || 0);
  if (minutes) bits.push(`${minutes} min`);
  // Ratio honnête acquis / passés (audit §passages) plutôt que des compteurs bruts.
  const total = summary.blocksTotal || 0;
  if (total) bits.push(`${summary.blocksMastered || 0}/${total} acquis`);
  if (counts.passed) bits.push(todayCountLabel(counts.passed, "passé", "passés"));
  if (counts.reviewed) bits.push(todayCountLabel(counts.reviewed, "carte", "cartes"));
  if (counts.produced) bits.push(`${counts.produced} prod.`);
  return bits.length ? `${date} · ${bits.join(" · ")}` : (fallback || "Séance terminée");
}

function weakPointFromAggregate(agg) {
  const top = agg && agg.topErrors && agg.topErrors[0];
  if (!top) return { label: "Aucun point faible nommé", n: 0, kind: "stable" };
  return { label: todayErrorLabel(top.tag), n: top.count || 0, kind: top.tag || "erreur" };
}

function liveTodayState(chargeId) {
  const fallback = fallbackTodayState();
  const state = { ...fallback, charge: todayChargeLabel(chargeId, fallback.charge) };
  const store = window.LangStore;
  const cards = store && store.cards;
  if (cards) {
    const now = Date.now();
    const all = typeof cards.all === "function" ? cards.all() : {};
    state.srsDue = typeof cards.due === "function"
      ? cards.due(now).length
      : Object.keys(all || {}).filter((id) => (((all || {})[id] || {}).due || 0) <= now).length;
  }

  const Log = window.LangSessionLog;
  if (!store || !Log || typeof Log.aggregate !== "function") return state;

  const agg = Log.aggregate(store);
  const sessions = sessionList(Log, store);
  state.ratés = Number.isFinite(agg.errors) ? agg.errors : 0;
  state.productionDone = Number.isFinite(agg.produced) ? agg.produced : 0;
  state.productionTarget = fallback.productionTarget || 3;
  state.streak = sessionStreak(sessions);
  state.audio7j = activityLast7Days(sessions);
  state.lastSession = lastSessionLabel(sessions, fallback.lastSession);
  state.weakTop = weakPointFromAggregate(agg);
  return state;
}

function SeanceDuJour({ duration, setDuration, intention, setIntention, charge, setCharge, drillMode, setDrillMode, onLaunch, plan, planError, go }) {
  const [planOpen, setPlanOpen] = useState(false);
  const launchMinutes = planLaunchMinutes(plan, duration);
  const originalMinutes = plan && plan.requested && plan.requested.originalMinutes;
  return (
    <div className="card" style={{ padding: "calc(var(--pad-card) + 2px)", display: "grid", gap: 16 }}>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: "10px 12px", flexWrap: "wrap" }}>
        <div style={{ flex: "1 1 auto" }}>
          <Eyebrow>Séance du jour</Eyebrow>
          <div style={{ fontFamily: "var(--serif)", fontSize: 23, color: "var(--accent-deep)", marginTop: 3, lineHeight: 1.15 }}>Votre plan du jour</div>
        </div>
        <DurationPicker value={duration} onChange={setDuration} />
      </div>

      <div style={{ display: "grid", gap: 12 }}>
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Charge pédagogique</div>
          <ChargePicker value={charge} onChange={setCharge} />
        </div>
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Intention</div>
          <IntentionChips value={intention} onChange={setIntention} />
        </div>
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Mode de drill</div>
          <DrillModePicker value={drillMode} onChange={setDrillMode} />
        </div>
      </div>

      <PlanNotice plan={plan} />
      <ChargeComposition plan={plan} charge={charge} />
      <RecallNotice plan={plan} drillMode={drillMode} go={go} />

      {/* plan preview */}
      <div>
        <button onClick={() => setPlanOpen(o => !o)} style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", border: "none", background: "transparent", cursor: "pointer", padding: 0, color: "var(--ink-soft)" }}>
          <span className="eyebrow">Plan adaptatif · {plan && plan.blocks ? plan.blocks.length : 5} étapes</span>
          <span style={{ flex: 1 }} />
          <span style={{ fontSize: 13 }}>{planOpen ? "Masquer" : "Voir"}</span>
          <Icon name="chevron" size={16} style={{ transform: planOpen ? "rotate(180deg)" : "none", transition: "transform .2s" }} />
        </button>
        {planOpen && <div className="fade-up" style={{ marginTop: 10 }}><AdaptivePlan compact plan={plan} />{planError && <p className="faint" style={{ fontSize: 12, margin: "8px 0 0" }}>{planError}</p>}</div>}
      </div>

      <div style={{ display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center" }}>
        <Btn kind="primary" size="lg" icon="play" onClick={onLaunch}>Lancer la séance · {launchMinutes} min{originalMinutes ? ` (${originalMinutes} demandées)` : ""}</Btn>
      </div>

    </div>
  );
}

function PointFaibleCard({ go, todayState }) {
  const w = ((todayState || fallbackTodayState()).weakTop) || { label: "Aucun point faible nommé", n: 0, kind: "stable" };
  const n = Number(w.n) || 0;
  const hasWeak = n > 0;
  return (
    <div className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 10 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
        <Eyebrow>Point faible prioritaire</Eyebrow>
        <span className="chip" style={{ fontSize: 11, background: hasWeak ? "var(--warn-tint)" : "var(--good-tint)", color: hasWeak ? "var(--warn)" : "var(--good)", borderColor: "transparent" }}>{w.kind}</span>
      </div>
      <div style={{ fontFamily: "var(--serif)", fontSize: 19, color: "var(--accent-deep)" }}>{w.label}</div>
      <p className="muted" style={{ fontSize: 13.5, margin: 0 }}>{hasWeak ? `${n} occurrence${n > 1 ? "s" : ""} récente${n > 1 ? "s" : ""}. Cibler maintenant évite qu'il s'installe.` : "Les erreurs nommées apparaîtront ici après une dictée, une production ou une réparation."}</p>
      <Btn kind="secondary" size="sm" icon="wrench" disabled={!hasWeak} title={hasWeak ? undefined : "Aucun point faible enregistré"} onClick={() => go("runner", { duration: 15, charge: "consolidation", intention: "reparer", drillMode: "dictee", source: "weak-point" })}>Réparer ce point</Btn>
    </div>
  );
}

function EtatDuJour({ todayState }) {
  const s = todayState || fallbackTodayState();
  return (
    <div className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <Eyebrow>État du jour</Eyebrow>
        <span className="chip" style={{ fontSize: 11 }}>Charge {s.charge}</span>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 18px" }}>
        {[
          { v: s.srsDue, l: "Cartes dues (SRS)", a: true },
          { v: s.ratés, l: "Ratés d'exercices" },
          { v: `${s.productionDone}/${s.productionTarget}`, l: "Production du jour" },
          { v: `${s.streak} j`, l: "Série" },
        ].map((x, i) => (
          <div key={i}>
            <div style={{ fontFamily: "var(--serif)", fontSize: 24, color: x.a ? "var(--accent)" : "var(--ink)", lineHeight: 1 }}>{x.v}</div>
            <div className="faint" style={{ fontSize: 11.5, marginTop: 4 }}>{x.l}</div>
          </div>
        ))}
      </div>
      <div>
        <div className="faint" style={{ fontSize: 11.5, marginBottom: 6 }}>Activité réelle · 7 derniers jours</div>
        <AudioSparkline data={s.audio7j} />
      </div>
      <p className="faint" style={{ fontSize: 12, margin: 0, paddingTop: 4, borderTop: "1px solid var(--rule)" }}>Dernière séance : {s.lastSession}</p>
    </div>
  );
}

function DialogueDuJour({ onOpen, dialogue }) {
  const d = normalizeTodayDialogue(dialogue);
  return (
    <div className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 11 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
        <Eyebrow>Dialogue du jour</Eyebrow>
        <span className="chip" style={{ fontSize: 11 }}><Icon name="speaker" size={12} /> audio</span>
      </div>
      <div>
        <span className="kr" style={{ fontSize: 21, color: "var(--accent-deep)" }}>{d.titleKr}</span>
        <span style={{ fontFamily: "var(--serif)", fontSize: 17, marginLeft: 8 }}>{d.title}</span>
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <Btn kind="secondary" size="sm" icon="play" onClick={() => onOpen("lecture")}>Lire</Btn>
        <Btn kind="ghost" size="sm" onClick={() => onOpen("entrainement")}>S'entraîner</Btn>
      </div>
    </div>
  );
}

function ShortAction({ go, todayState }) {
  const s = todayState || fallbackTodayState();
  const misses = Number(s.ratés) || 0;
  return (
    <div className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 10 }}>
      <Eyebrow>Action courte · 5 min</Eyebrow>
      <div style={{ fontFamily: "var(--serif)", fontSize: 18, color: "var(--accent-deep)" }}>Reprendre mes ratés</div>
      <p className="muted" style={{ fontSize: 13.5, margin: 0 }}>{misses ? `${todayCountLabel(misses, "item signalé", "items signalés")} à revoir avant qu'ils ne se perdent.` : "Aucun raté enregistré pour le moment."}</p>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        <Btn kind="secondary" size="sm" icon="refresh" disabled={!misses} title={misses ? undefined : "Aucun raté enregistré"} onClick={() => go("runner", { duration: 5, charge: "consolidation", intention: "reparer", drillMode: "dictee", source: "rates" })}>Reprendre ({misses})</Btn>
        <Btn kind="ghost" size="sm" icon="ear" onClick={() => go("apprendre", { tab: "audio", focus: "dictee" })}>Dictée</Btn>
      </div>
    </div>
  );
}

function AccesDirects({ go }) {
  const links = [
    { k: "cartes", opts: { mode: "jour" }, label: "Cartes P0", icon: "grid", desc: "Leitner — jour, drill, faibles" },
    { k: "apprendre", opts: { tab: "hangeul" }, label: "Hangeul", icon: "spark", desc: "Bases de lecture" },
    { k: "apprendre", opts: { tab: "exercices" }, label: "Exercices", icon: "learn", desc: "Entraînement libre" },
    { k: "dialogues", opts: {}, label: "Dialogues", icon: "chat", desc: "Hub des dialogues" },
    { k: "produire", opts: {}, label: "Production", icon: "pen", desc: "Cahier d'écriture" },
    { k: "apprendre", opts: { tab: "audio" }, label: "Audio", icon: "ear", desc: "Lecture lente, dictée" },
    { k: "pilotage", opts: { view: "liste" }, label: "Liste", icon: "list", desc: "Bibliothèque paginée" },
    { k: "pilotage", opts: {}, label: "Pilotage", icon: "chart", desc: "Diagnostic complet" },
  ];
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(190px,1fr))", gap: 10 }}>
      {links.map((l, i) => <Tile key={i} icon={l.icon} label={l.label} meta={l.desc} onClick={() => go(l.k, l.opts || {})} />)}
    </div>
  );
}

/* ====== Alternative layouts ====== */
function HeroIntention({ duration, setDuration, intention, setIntention, drillMode, setDrillMode, onLaunch }) {
  const D = window.LANGUES_DATA.intentions;
  return (
    <div>
      <Eyebrow>Angle du jour</Eyebrow>
      <h2 style={{ fontSize: 26, margin: "6px 0 4px" }}>Par quoi commencer ?</h2>
      <p className="muted" style={{ fontSize: 15, margin: "0 0 16px" }}>Choisissez une intention — le plan se prépare tout seul.</p>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(210px,1fr))", gap: 12, marginBottom: 16 }}>
        {D.map(it => {
          const on = it.id === intention;
          return (
            <button key={it.id} onClick={() => setIntention(it.id)} style={{
              textAlign: "left", cursor: "pointer", padding: "16px", borderRadius: "var(--radius)",
              border: `1.5px solid ${on ? "var(--accent)" : "var(--rule)"}`, background: on ? "var(--accent-tint)" : "var(--card)", transition: "all .18s",
            }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                <span style={{ color: on ? "var(--accent)" : "var(--ochre)" }}><Icon name={it.icon} size={21} /></span>
                <span className="kr faint" style={{ fontSize: 14 }}>{it.kr}</span>
              </div>
              <div style={{ fontFamily: "var(--serif)", fontSize: 18, marginTop: 10, color: "var(--accent-deep)" }}>{it.label}</div>
              <div className="muted" style={{ fontSize: 13, marginTop: 3 }}>{it.desc}</div>
            </button>
          );
        })}
      </div>
      <div className="card" style={{ padding: 16, display: "flex", flexWrap: "wrap", gap: 16, alignItems: "center", justifyContent: "space-between" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 14 }}><span className="eyebrow">Durée</span><DurationPicker value={duration} onChange={setDuration} /></div>
        <div style={{ display: "grid", gap: 5, minWidth: 260 }}><span className="eyebrow">Drill</span><DrillModePicker value={drillMode} onChange={setDrillMode} /></div>
        <Btn kind="primary" size="lg" icon="play" onClick={onLaunch}>Lancer · {duration} min</Btn>
      </div>
    </div>
  );
}

function HeroTableau({ duration, setDuration, drillMode, setDrillMode, onLaunch, go, todayState, todayDialogue }) {
  const d = normalizeTodayDialogue(todayDialogue);
  const s = todayState || fallbackTodayState();
  const misses = Number(s.ratés) || 0;
  const Col = ({ eyebrow, title, kr, body, btn, onClick, primary, disabled, titleAttr }) => (
    <div className="card" style={{ padding: "var(--pad-card)", display: "flex", flexDirection: "column", gap: 12, borderLeft: primary ? "3px solid var(--accent)" : "1px solid var(--rule)" }}>
      <Eyebrow>{eyebrow}</Eyebrow>
      <div>{kr && <div className="kr" style={{ fontSize: 20, color: "var(--accent-deep)" }}>{kr}</div>}<div style={{ fontFamily: "var(--serif)", fontSize: 20, color: "var(--accent-deep)" }}>{title}</div></div>
      <p className="muted" style={{ fontSize: 13.5, margin: 0, flex: 1 }}>{body}</p>
      <Btn kind={primary ? "primary" : "secondary"} size="sm" icon={primary ? "play" : null} disabled={disabled} title={titleAttr} onClick={onClick}>{btn}</Btn>
    </div>
  );
  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14, marginBottom: 14, flexWrap: "wrap" }}>
        <Eyebrow>Que faire maintenant</Eyebrow>
        <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}><span className="faint" style={{ fontSize: 13 }}>Durée</span><DurationPicker value={duration} onChange={setDuration} /></div>
          <div style={{ display: "grid", gap: 4 }}><span className="faint" style={{ fontSize: 13 }}>Drill</span><DrillModePicker value={drillMode} onChange={setDrillMode} /></div>
        </div>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(220px,1fr))", gap: "var(--gap)" }}>
        <Col primary eyebrow="Parcours" title="Séance du jour" body={`${duration} min — révisions, nouveaux mots, dialogue, une phrase.`} btn={`Lancer · ${duration} min`} onClick={onLaunch} />
        <Col eyebrow="Dialogue" title={d.title} kr={d.titleKr} body={`${d.lines} lignes. Écoute et compréhension, traduction cachée.`} btn="Lire le dialogue" onClick={() => go("dialogues", { dialogue: d.id, mode: "lecture" })} />
        <Col eyebrow="Court" title="Reprendre mes ratés" body={misses ? `${todayCountLabel(misses, "item signalé", "items signalés")} à revoir. ~5 minutes.` : "Aucun raté enregistré pour le moment."} btn={`Reprendre (${misses})`} disabled={!misses} titleAttr={misses ? undefined : "Aucun raté enregistré"} onClick={() => go("runner", { duration: 5, charge: "consolidation", intention: "reparer", drillMode: "dictee", source: "rates" })} />
      </div>
    </div>
  );
}

// État SRS passé au planner : oriente la file de révision par priorité (poids).
function planState() {
  if (!window.LangStore || !window.LangStore.cards) return undefined;
  return {
    cards: window.LangStore.cards.all(),
    now: Date.now(),
    priorityFn: window.FSRS && window.FSRS.priority,
    settings: window.LangStore.settings && window.LangStore.settings.get ? window.LangStore.settings.get() : {},
  };
}

// Budget asservi : dette de révision + neuf capé/throttlé (cf. daily-budget.js).
function planBudget() {
  if (!window.LangDailyBudget || !window.LangStore || !window.LangStore.cards) return null;
  return window.LangDailyBudget.compute({
    cards: window.LangStore.cards.all(),
    sessions: window.LangStore.sessions ? window.LangStore.sessions.all() : [],
    now: Date.now(),
    settings: window.LangStore.settings && window.LangStore.settings.get ? window.LangStore.settings.get() : {},
  });
}

// État apprenant : reviews-first, réparation ciblée, brin en retard (cf. learner-state.js).
// Réutilise le budget déjà calculé comme source unique de la dette.
function planLearnerState(budget) {
  if (!window.LangLearnerState || !window.LangStore || !window.LangStore.cards) return null;
  return window.LangLearnerState.compute({
    cards: window.LangStore.cards.all(),
    sessions: window.LangStore.sessions ? window.LangStore.sessions.all() : [],
    budget: budget || null,
    now: Date.now(),
    settings: window.LangStore.settings && window.LangStore.settings.get ? window.LangStore.settings.get() : {},
  });
}

// Bannière du budget asservi : dette de révision + robinet de nouveauté + santé.
function BudgetBanner({ budget }) {
  if (!budget) return null;
  const HEALTH = {
    "sain": { color: "var(--good)", tint: "var(--good-tint)" },
    "chargé": { color: "var(--ochre, var(--warn))", tint: "var(--warn-tint)" },
    "saturé": { color: "var(--warn)", tint: "var(--warn-tint)" },
  };
  const h = HEALTH[budget.health.level] || HEALTH["sain"];
  const acc = budget.recentAccuracy == null ? "—" : `${Math.round(budget.recentAccuracy * 100)} %`;
  const newLabel = budget.throttle.level === "paused"
    ? "en pause"
    : `${budget.newAllowed}${budget.throttle.level === "reduced" ? " (réduit)" : ""}`;
  const Cell = ({ label, value, strong }) => (
    <div style={{ display: "grid", gap: 2, minWidth: 92 }}>
      <span className="faint" style={{ fontSize: 11.5, textTransform: "uppercase", letterSpacing: ".04em" }}>{label}</span>
      <span style={{ fontSize: 20, fontWeight: 700, fontFamily: "var(--serif)", color: strong || "var(--ink)" }}>{value}</span>
    </div>
  );
  return (
    <div className="card fade-up" style={{ display: "flex", alignItems: "center", gap: 22, flexWrap: "wrap", padding: "14px 18px", borderLeft: `4px solid ${h.color}` }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <span className="chip" style={{ background: h.tint, color: h.color, borderColor: "transparent", fontWeight: 700 }}>{budget.health.label}</span>
      </div>
      <Cell label="Révisions dues" value={budget.due} strong={budget.due > 0 ? h.color : "var(--ink)"} />
      <Cell label="Faites aujourd'hui" value={budget.reviewsToday} />
      <Cell label="Nouveaux" value={newLabel} />
      <Cell label="Réussite récente" value={acc} />
      <p className="faint" style={{ fontSize: 12.5, margin: 0, flex: 1, minWidth: 180 }}>
        {budget.throttle.level === "open"
          ? "Robinet de nouveauté ouvert : tu peux découvrir."
          : `Nouveauté ${budget.throttle.level === "paused" ? "suspendue" : "réduite"} — ${budget.throttle.reason}. Les révisions passent d'abord.`}
      </p>
    </div>
  );
}

function TodayPage({ t, go }) {
  const settings = window.LangStore ? window.LangStore.settings.get() : {};
  const [duration, setDurationState] = useState(() => Number(settings.parcoursDuration) || 30);
  const [intention, setIntentionState] = useState(() => settings.todayIntention || "decouvrir");
  const [charge, setChargeState] = useState(() => settings.todayCharge || "moderee");
  const [drillMode, setDrillModeState] = useState(() => settings.todayDrillMode || "mixte");
  const [activeDraft, setActiveDraft] = useState(() => window.LangSessionRunner && window.LangSessionRunner.readDraft ? window.LangSessionRunner.readDraft(window.LangStore) : null);
  const [plan, setPlan] = useState(null);
  const [planError, setPlanError] = useState("");
  const [todayDialogue, setTodayDialogue] = useState(() => normalizeTodayDialogue(null));
  const launch = () => {
    const selectedMinutes = Number(duration) || 30;
    const launchMinutes = planLaunchMinutes(plan, selectedMinutes);
    go("runner", { plan, duration: launchMinutes, requestedDuration: selectedMinutes, intention, charge, drillMode, source: "today" });
  };
  const resume = () => go("runner", { resume: true, source: "today" });
  const saveTodaySettings = (patch) => {
    if (window.LangStore && window.LangStore.settings && window.LangStore.settings.set) window.LangStore.settings.set(patch);
  };
  const setDuration = (value) => {
    const next = Number(value) || 30;
    setDurationState(next);
    saveTodaySettings({ parcoursDuration: next });
  };
  const setIntention = (value) => {
    setIntentionState(value);
    saveTodaySettings({ todayIntention: value });
  };
  const setCharge = (value) => {
    setChargeState(value);
    saveTodaySettings({ todayCharge: value });
  };
  const setDrillMode = (value) => {
    setDrillModeState(value);
    saveTodaySettings({ todayDrillMode: value });
  };
  const openDialogue = (mode) => go("dialogues", { dialogue: (todayDialogue && todayDialogue.id) || TODAY_DIALOGUE_ID, mode });
  const layout = t.todayLayout;
  const todayState = liveTodayState(charge);

  useEffect(() => {
    if (window.LangSessionRunner && window.LangSessionRunner.readDraft) setActiveDraft(window.LangSessionRunner.readDraft(window.LangStore));
  }, []);

  useEffect(() => {
    if (!window.LangData || !window.LangPlanner) return undefined;
    let alive = true;
    Promise.all([
      window.LangData.loadCardsP0(),
      window.LangData.loadVocabFreq(),
      window.LangData.loadPrompts(),
      window.LangData.loadDialogues(),
      window.LangData.loadExercises(),
      window.LangData.loadErrorBank(),
      window.LangData.loadSessionTemplates(),
      window.LangData.loadListening(),
      window.LangData.loadChunkIndex ? window.LangData.loadChunkIndex() : Promise.resolve(null),
    ]).then(([cardsP0, vocabFreq, prompts, dialogues, exercises, errorBank, sessionTemplates, listening, chunkIndex]) => {
      if (!alive) return;
      const dialogueItems = dialogues && Array.isArray(dialogues.dialogues) ? dialogues.dialogues : [];
      setTodayDialogue(selectTodayDialogue(dialogueItems));
      const budget = planBudget();
      setPlan(window.LangPlanner.buildPlan({
        banks: { cardsP0, vocabFreq, prompts, dialogues, exercises, errorBank, sessionTemplates, listening, chunkIndex },
        minutes: duration,
        charge,
        intention,
        drillMode,
        state: planState(),
        budget,
        learnerState: planLearnerState(budget),
      }));
      setPlanError("");
    }).catch(() => {
      if (!alive) return;
      setPlan(null);
      setPlanError("Aperçu moteur indisponible, repli prototype affiché.");
    });
    return () => { alive = false; };
  }, [duration, charge, intention, drillMode]);

  return (
    <div style={{ maxWidth: 1180, margin: "0 auto", display: "grid", gap: "var(--gap-lg)" }}>
      <header className="fade-up" style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
        <div>
          <Eyebrow>{new Date().toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" })}</Eyebrow>
          <h1 style={{ fontSize: 40, margin: "4px 0 0" }}>Aujourd'hui</h1>
        </div>
        <p className="muted" style={{ fontSize: 15, margin: 0, maxWidth: 360, textAlign: "right" }}>Qu'est-ce que je fais maintenant ? Réglez, puis lancez.</p>
      </header>

      <ResumeSessionCard draft={activeDraft} onResume={resume} />
      <BudgetBanner budget={planBudget()} />

      {layout === "guide" && (
        <div className="today-grid fade-up" style={{ display: "grid", gridTemplateColumns: "minmax(0,1.5fr) minmax(0,1fr)", gap: "var(--gap)", alignItems: "start" }}>
          <SeanceDuJour duration={duration} setDuration={setDuration} intention={intention} setIntention={setIntention} charge={charge} setCharge={setCharge} drillMode={drillMode} setDrillMode={setDrillMode} onLaunch={launch} plan={plan} planError={planError} go={go} />
          <div style={{ display: "grid", gap: "var(--gap)" }}>
            <EtatDuJour todayState={todayState} />
            <PointFaibleCard go={go} todayState={todayState} />
            <DialogueDuJour onOpen={openDialogue} dialogue={todayDialogue} />
            <ShortAction go={go} todayState={todayState} />
          </div>
        </div>
      )}
      {layout === "intention" && (
        <div className="fade-up"><HeroIntention duration={duration} setDuration={setDuration} intention={intention} setIntention={setIntention} drillMode={drillMode} setDrillMode={setDrillMode} onLaunch={launch} />
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(260px,1fr))", gap: "var(--gap)", marginTop: "var(--gap-lg)" }}>
            <EtatDuJour todayState={todayState} /><PointFaibleCard go={go} todayState={todayState} />
          </div>
        </div>
      )}
      {layout === "tableau" && (
        <div className="fade-up"><HeroTableau duration={duration} setDuration={setDuration} drillMode={drillMode} setDrillMode={setDrillMode} onLaunch={launch} go={go} todayState={todayState} todayDialogue={todayDialogue} />
          <div style={{ marginTop: "var(--gap-lg)", display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(260px,1fr))", gap: "var(--gap)" }}>
            <EtatDuJour todayState={todayState} /><PointFaibleCard go={go} todayState={todayState} />
          </div>
        </div>
      )}

      <Collapsible eyebrow="Avancé" title="Accès directs" hint="Cartes · Hangeul · Exercices · Audio · Liste…">
        <AccesDirects go={go} />
      </Collapsible>
    </div>
  );
}

Object.assign(window, { TodayPage });
