/* Atelier - Cardio vocab : lots de pratique du vocabulaire frequent.
   Surface de revision conforme METHODE §4 : note explicite, echec objectif
   force, effet FSRS borne par practice-lots.boundedSrsReview, journalise.
   Extrait d'atelier.jsx (decoupage 2026-06-12, sans changement de logique). */

const PRACTICE_SELECTORS = [
  { id: "active", label: "Lot actif", icon: "play" },
  { id: "last", label: "Dernier", icon: "grid" },
  { id: "last2", label: "2 derniers", icon: "layers" },
  { id: "today", label: "Aujourd'hui", icon: "today" },
  { id: "recent", label: "Recents", icon: "refresh" },
  { id: "fragile", label: "Fragiles", icon: "wrench" },
];

const PRACTICE_MODES = [
  { value: "recall", label: "Rappel" },
  { value: "recognition", label: "QCM" },
  { value: "context", label: "Contexte" },
];

const PRACTICE_RATINGS = [
  { id: "rate", label: "Rate", color: "var(--warn)", tint: "var(--warn-tint)" },
  { id: "hard", label: "Difficile", color: "var(--ochre)", tint: "var(--warn-tint)" },
  { id: "good", label: "Bon", color: "var(--good)", tint: "var(--good-tint)" },
  { id: "easy", label: "Facile", color: "var(--accent)", tint: "var(--accent-tint)" },
];

const PRACTICE_TIER_LABELS = {
  reel: "contexte reel",
  ecrit: "phrase validee",
  collocation: "collocation",
  patron: "patron",
  autre: "contexte",
};

function practiceMeaning(item) {
  return String((item && (item.fr || item.meaning_fr || item.answer_primary)) || "").trim();
}

function practiceKr(item) {
  return String((item && (item.kr || item.script)) || "").trim();
}

function practiceReading(item) {
  return String((item && (item.reading || item.ro || item.romanisation_temporaire)) || "").trim();
}

function practiceAudioRef(item) {
  return (item && (item.audioRef || item.audio_ref || (item.audio && (item.audio.ref || item.audio.id)))) || practiceKr(item);
}

function practiceAudioUrl(item) {
  return (item && (item.audioUrl || (item.audio && item.audio.url))) || "";
}

function practiceHash(text) {
  const s = String(text || "");
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
  return h >>> 0;
}

function practiceChoices(item, allItems, count = 4) {
  const correct = practiceMeaning(item);
  const out = [];
  function add(v) {
    const s = String(v || "").trim();
    if (s && out.indexOf(s) < 0) out.push(s);
  }
  add(correct);
  (Array.isArray(item && item.choices) ? item.choices : []).forEach(add);
  const pool = (allItems || [])
    .map(practiceMeaning)
    .filter(Boolean)
    .filter((x) => out.indexOf(x) < 0)
    .sort((a, b) => (practiceHash(`${item.id}:${a}`) - practiceHash(`${item.id}:${b}`)));
  pool.slice(0, Math.max(0, count - out.length)).forEach(add);
  return out
    .slice(0, count)
    .sort((a, b) => (practiceHash(`${item.id}:choice:${a}`) - practiceHash(`${item.id}:choice:${b}`)));
}

function practiceQueueKey(id) {
  return String(id || "").trim();
}

function practiceLotTitle(lot) {
  if (!lot) return "Lot";
  return lot.title || `Lot ${String(lot.id || "").split("_").slice(-1)[0] || ""}`;
}

function practicePercent(v) {
  return `${Math.round((Number.isFinite(v) ? v : 0) * 100)} %`;
}

function practiceTime(ts) {
  if (!ts) return "jamais";
  const d = new Date(ts);
  return d.toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}

function practiceTierLabel(tier) {
  return PRACTICE_TIER_LABELS[tier] || PRACTICE_TIER_LABELS.autre;
}

function practiceTierStyle(tier) {
  if (tier === "reel" || tier === "ecrit") return { background: "var(--good-tint)", color: "var(--good)", borderColor: "transparent" };
  if (tier === "collocation") return { background: "var(--accent-tint)", color: "var(--accent-deep)", borderColor: "transparent" };
  if (tier === "patron") return { background: "var(--warn-tint)", color: "var(--warn)", borderColor: "transparent" };
  return { background: "var(--card-2)" };
}

function normalizePracticeTextToken(token) {
  return String(token || "").replace(/^[^\uac00-\ud7a3]+|[^\uac00-\ud7a3]+$/g, "");
}

function PracticePhrase({ sentence, focusKr }) {
  if (!sentence || !sentence.kr) return null;
  const parts = String(sentence.kr).split(/(\s+)/);
  const focus = normalizePracticeTextToken(focusKr);
  const stem = focus && focus.endsWith("다") ? focus.slice(0, -1) : "";
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
        {sentence.audioUrl
          ? <SpeakBtn text={sentence.kr} audioRef={sentence.audioRef || sentence.kr} audioUrl={sentence.audioUrl} size={28} title="Ecouter la phrase" />
          : <span className="chip" style={{ fontSize: 11 }}>texte seul</span>}
        <span className="chip" style={{ fontSize: 11, ...practiceTierStyle(sentence.contextQuality) }}>{practiceTierLabel(sentence.contextQuality)}</span>
      </div>
      <div className="kr" style={{ fontSize: 22, lineHeight: 1.65, color: "var(--ink)" }}>
        {parts.map((part, index) => {
          if (!part.trim()) return <span key={index}>{part}</span>;
          const clean = normalizePracticeTextToken(part);
          const hit = clean && (clean === focus || (stem.length >= 2 && clean.startsWith(stem)));
          if (!clean) return <span key={index}>{part}</span>;
          return (
            <Speakable key={index} text={clean} audioRef={clean} frame={false} className="kr"
              style={{ padding: "1px 3px", background: hit ? "var(--accent-tint)" : "transparent", color: hit ? "var(--accent-deep)" : "inherit", fontWeight: hit ? 700 : 500 }}>
              {part}
            </Speakable>
          );
        })}
      </div>
      {(sentence.fr || sentence.en) && <div className="muted" style={{ fontSize: 14 }}>{sentence.fr || sentence.en}</div>}
    </div>
  );
}

function PracticeContext({ item, chunkIndex }) {
  const carriers = window.LangPracticeLots ? window.LangPracticeLots.contextForItem(item, chunkIndex, 2) : [];
  const kr = practiceKr(item);
  if (!carriers.length) {
    return <div className="muted" style={{ fontSize: 14 }}>Aucune phrase porteuse reliee pour ce mot.</div>;
  }
  return (
    <div style={{ display: "grid", gap: 10 }}>
      {carriers.map((sentence) => (
        <div key={sentence.id} style={{ border: "1px solid var(--rule)", background: "var(--card-2)", borderRadius: "var(--radius-sm)", padding: 14 }}>
          <PracticePhrase sentence={sentence} focusKr={kr} />
        </div>
      ))}
    </div>
  );
}

function PracticeStatsStrip({ lots, passes, activeWorkset }) {
  const activeTotal = activeWorkset && activeWorkset.stats ? activeWorkset.stats.total : 0;
  const mastery = activeWorkset && activeWorkset.stats ? practicePercent(activeWorkset.stats.mastery) : "0 %";
  return (
    <StatStrip items={[
      { value: lots.length, label: "Lots" },
      { value: activeTotal, label: "Items selectionnes", accent: true },
      { value: mastery, label: "Solides dans la selection" },
      { value: passes.length, label: "Passes" },
    ]} />
  );
}

function LotRow({ lot, active, summary, onSelect, onStart }) {
  const mastered = !!lot.masteredAt;
  return (
    <div style={{ padding: "12px 0", borderTop: "1px solid var(--rule)", display: "grid", gap: 8 }}>
      <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
        <button onClick={onSelect} className="chip" style={{
          cursor: "pointer",
          background: active ? "var(--accent-tint)" : "var(--card)",
          color: active ? "var(--accent-deep)" : "var(--ink-soft)",
          borderColor: active ? "var(--accent-line)" : "var(--rule)",
          fontWeight: 800,
        }}>
          {practiceLotTitle(lot)}
        </button>
        <span className="chip" style={{ fontSize: 11 }}>{lot.wordIds.length} mots</span>
        <span className="chip" style={{ fontSize: 11, ...(mastered ? { background: "var(--good-tint)", color: "var(--good)", borderColor: "transparent" } : {}) }}>
          {mastered ? "maitrise" : practicePercent(summary.stats.mastery)}
        </span>
      </div>
      <div className="faint" style={{ fontSize: 12 }}>
        Cree {practiceTime(lot.createdAt)} · derniere passe {practiceTime(lot.lastPracticedAt)} · fragile {summary.stats.fragile}
      </div>
      <div>
        <Btn kind="ghost" size="sm" icon="play" onClick={onStart}>Travailler ce lot</Btn>
      </div>
    </div>
  );
}

function WorksetButtons({ selected, worksets, onSelect, onStart }) {
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(135px, 1fr))", gap: 8 }}>
        {PRACTICE_SELECTORS.map((s) => {
          const ws = worksets[s.id];
          const disabled = !ws || !ws.wordIds.length;
          const on = selected === s.id;
          return (
            <button key={s.id} disabled={disabled} onClick={() => onSelect(s.id)} title={disabled ? "Aucun item dans cette selection" : undefined} style={{
              border: `1px solid ${on ? "var(--accent-line)" : "var(--rule)"}`,
              background: on ? "var(--accent-tint)" : "var(--card)",
              color: on ? "var(--accent-deep)" : "var(--ink-soft)",
              opacity: disabled ? .48 : 1,
              borderRadius: "var(--radius-sm)",
              padding: 12,
              cursor: disabled ? "not-allowed" : "pointer",
              display: "grid",
              gap: 5,
              textAlign: "left",
            }}>
              <span style={{ display: "flex", alignItems: "center", gap: 7, fontWeight: 800 }}><Icon name={s.icon} size={16} />{s.label}</span>
              <span className="faint" style={{ fontSize: 12 }}>{ws ? ws.wordIds.length : 0} items</span>
            </button>
          );
        })}
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
        <Btn kind="primary" icon="play" disabled={!worksets[selected] || !worksets[selected].wordIds.length} onClick={() => onStart(selected)}>Lancer la selection</Btn>
      </div>
    </div>
  );
}

function PracticeRun({ run, setRun, data, practice, persistPractice, refreshPractice }) {
  const [mode, setMode] = useState(run.mode || "recall");
  const [revealed, setRevealed] = useState(mode === "context");
  const [picked, setPicked] = useState(null);
  const [tick, setTick] = useState(0);
  const [audioMsg, setAudioMsg] = useState(null);
  const sessionRef = useRef(null);
  const shownAtRef = useRef(Date.now());

  const vocabMap = data.vocabMap;
  const allItems = data.items;
  const currentId = run.queue[run.pos];
  const currentItem = vocabMap[currentId];
  const currentAttempt = (run.attempts[currentId] || 0) + 1;
  const choices = currentItem ? practiceChoices(currentItem, allItems, 4) : [];
  const selectedMeaning = picked == null ? "" : choices[picked];
  const selectedCorrect = picked == null ? null : selectedMeaning === practiceMeaning(currentItem);

  useEffect(() => {
    setMode(run.mode || "recall");
    setRevealed((run.mode || "recall") === "context");
    setPicked(null);
    shownAtRef.current = Date.now();
  }, [run.id, run.pos]);

  function ensureSession() {
    if (!window.LangSessionLog || !window.LangStore) return null;
    if (!sessionRef.current) {
      sessionRef.current = window.LangSessionLog.startSession({
        profile: window.LangStore.profile,
        planId: `practice_${run.selector}`,
        templateId: "practice_lots",
        intention: "maitrise_vocab",
        charge: "acceleree",
        drillMode: mode,
        plannedMinutes: 0,
        blocks: run.workset.wordIds.map((id) => ({ id: `practice_${id}`, kind: "practice_vocab", strand: mode === "context" ? "input" : "study", required: true })),
      });
    }
    return sessionRef.current;
  }

  function finishPass(nextRun) {
    const now = Date.now();
    const recorded = window.LangPracticeLots.recordPass(practice, {
      startedAt: run.startedAt,
      finishedAt: now,
      mode,
      selector: run.selector,
      lotIds: run.workset.lotIds,
      itemIds: run.workset.wordIds,
      results: nextRun.results,
    });
    persistPractice(recorded.state);
    if (sessionRef.current && window.LangSessionLog && window.LangStore) {
      window.LangSessionLog.finishSession(sessionRef.current, window.LangStore, now);
      sessionRef.current = null;
    }
    setRun(Object.assign({}, nextRun, { finished: true, pass: recorded.pass }));
  }

  function submitRating(ratingId) {
    if (!currentItem) return;
    const now = Date.now();
    const rating = selectedCorrect === false ? "rate" : ratingId;
    const grade = window.LangPracticeLots.GRADE_BY_RATING[rating];
    const correct = grade >= 2;
    const prev = window.LangStore && window.LangStore.cards ? window.LangStore.cards.state(currentId) : null;
    const settings = window.LangStore && window.LangStore.settings ? window.LangStore.settings.get() : {};
    const bounded = window.LangPracticeLots.boundedSrsReview({
      itemId: currentId,
      rating,
      prev,
      now,
      scheduler: window.FSRS,
      retention: settings.retention || (window.FSRS && window.FSRS.DEFAULT_RETENTION) || 0.9,
      errorTags: correct ? [] : ["sens"],
    });
    if (bounded.card && window.LangStore && window.LangStore.cards) {
      window.LangStore.cards.setState(currentId, bounded.card);
    }
    const result = {
      itemId: currentId,
      rating,
      grade,
      correct,
      answer: selectedMeaning,
      attempt: currentAttempt,
      startedAt: shownAtRef.current,
      finishedAt: now,
      srsPolicy: bounded.policy,
      srsUpdated: !!bounded.update,
    };
    const session = ensureSession();
    if (session && window.LangSessionLog) {
      window.LangSessionLog.logResult(session, {
        exerciseId: `practice_${currentId}`,
        itemId: currentId,
        kind: "practice_vocab",
        strand: mode === "context" ? "input" : "study",
        startedAt: shownAtRef.current,
        finishedAt: now,
        grade,
        correct,
        answer: selectedMeaning,
        errorTags: correct ? [] : ["sens"],
        feedbackShown: true,
        srsUpdates: bounded.update ? [bounded.update] : [],
        notes: bounded.policy,
      });
    }

    const attempts = Object.assign({}, run.attempts, { [currentId]: currentAttempt });
    let queue = run.queue.slice();
    const shouldRequeue = rating !== "easy" && currentAttempt < 3;
    const nextPos = run.pos + 1;
    if (shouldRequeue) {
      if (window.LangReviewQueue) {
        queue = window.LangReviewQueue.requeueAfterGap(queue, currentId, nextPos, { keyFor: practiceQueueKey, gap: 2 });
      } else {
        const insertAt = Math.min(queue.length, nextPos + 2);
        queue.splice(insertAt, 0, currentId);
      }
    }
    if (window.LangReviewQueue) {
      queue = window.LangReviewQueue.avoidImmediateRepeatAtCursor(queue, nextPos, currentId, { keyFor: practiceQueueKey });
    }
    const nextRun = Object.assign({}, run, {
      mode,
      attempts,
      queue,
      pos: nextPos,
      results: run.results.concat(result),
    });
    setPicked(null);
    setRevealed(mode === "context");
    shownAtRef.current = Date.now();
    if (nextPos >= queue.length) finishPass(nextRun);
    else setRun(nextRun);
    setTick(tick + 1);
    refreshPractice();
  }

  if (!currentItem && !run.finished) {
    return <EmptyState icon="grid" title="Selection vide" body="Cree un lot ou choisis une autre selection." />;
  }

  if (run.finished) {
    const sum = run.pass && run.pass.summary ? run.pass.summary : window.LangPracticeLots.summarizeResults(run.results);
    return (
      <section className="card fade-up" style={{ padding: "var(--pad-card)", display: "grid", gap: 18 }}>
        <div>
          <Eyebrow>Passage termine</Eyebrow>
          <h2 style={{ fontSize: 34, marginTop: 4 }}>{sum.perfect ? "Lot maitrise" : "A consolider"}</h2>
          <p className="muted" style={{ margin: "8px 0 0", maxWidth: 620 }}>
            {sum.easy} faciles sur {sum.total}. {sum.errors ? `${sum.errors} erreur(s) ou reprise(s) : relance une passe ciblee.` : "Aucune erreur dans cette passe."}
          </p>
        </div>
        <StatStrip items={[
          { value: sum.total, label: "Items" },
          { value: sum.easy, label: "Faciles", accent: true },
          { value: sum.attempts, label: "Tentatives" },
          { value: practicePercent(sum.easyRatio || 0), label: "Maitrise" },
        ]} />
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <Btn kind="primary" icon="refresh" onClick={() => setRun(startPracticeRun(run.workset, run.selector, mode))}>Refaire la meme selection</Btn>
          <Btn kind="secondary" icon="wrench" onClick={() => setRun(startPracticeRun(window.LangPracticeLots.selectWorkset(practice, data.payload, "fragile", { cardStates: window.LangStore.cards.all(), now: Date.now() }), "fragile", mode))}>Reprendre les fragiles</Btn>
          <Btn kind="ghost" icon="grid" onClick={() => setRun(null)}>Retour atelier</Btn>
        </div>
      </section>
    );
  }

  const progress = run.queue.length ? run.pos / run.queue.length : 0;
  const meaning = practiceMeaning(currentItem);
  const kr = practiceKr(currentItem);
  const reading = practiceReading(currentItem);
  const canRate = mode === "recognition" ? picked != null : revealed;
  const itemImage = currentItem && window.primaryImageForItem
    ? window.primaryImageForItem(data.imageBank, [currentItem.id], mode === "context" ? "vocab_context" : "card_back")
    : null;
  const showItemImage = itemImage && (revealed || picked != null || mode === "context");
  const reportAudio = () => {
    const api = window.LangAudio;
    if (!api || typeof api.reportLastAudio !== "function") { setAudioMsg("Audio indisponible"); setTimeout(() => setAudioMsg(null), 2200); return; }
    Promise.resolve(api.reportLastAudio({ card: currentItem && currentItem.id, fr: meaning || "" })).then((res) => {
      const msg = !res || !res.ok ? "Écoute d'abord le mot, puis re-signale"
        : res.mode === "queue" ? `« ${res.entry.text} » → file de régénération`
        : `« ${res.entry.text} » copié — à coller dans la file`;
      setAudioMsg(msg); setTimeout(() => setAudioMsg(null), 2800);
    });
  };

  return (
    <section className="card fade-up" style={{ padding: "var(--pad-card)", display: "grid", gap: 18 }}>
      <div style={{ display: "flex", gap: 12, justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap" }}>
        <div>
          <Eyebrow>{run.workset.label}</Eyebrow>
          <h2 style={{ fontSize: 32, marginTop: 4 }}>Atelier vocabulaire</h2>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
          <Segmented value={mode} options={PRACTICE_MODES} onChange={(v) => { setMode(v); setRevealed(v === "context"); setPicked(null); }} size="sm" />
          <span className="chip" style={{ fontSize: 12 }}>{run.pos + 1} / {run.queue.length}</span>
          {currentAttempt > 1 && <span className="chip" style={{ fontSize: 12, background: "var(--warn-tint)", color: "var(--warn)", borderColor: "transparent" }}>essai {currentAttempt}</span>}
          <Btn kind="ghost" size="sm" icon="speaker" onClick={reportAudio} title="Signaler le dernier audio écouté comme mauvais (file de régénération)">Audio faux</Btn>
          {audioMsg && <span className="chip" style={{ fontSize: 12, background: "var(--accent-tint)", color: "var(--accent-deep)", borderColor: "transparent" }}>{audioMsg}</span>}
        </div>
      </div>
      <Progress value={progress} />

      <div style={{ display: "grid", gap: 16, justifyItems: "center", textAlign: "center", padding: "8px 0 4px" }}>
        <Speakable text={kr} audioRef={practiceAudioRef(currentItem)} audioUrl={practiceAudioUrl(currentItem)} className="kr" gloss={revealed ? meaning : undefined} glossRevealed={revealed}
          style={{ fontSize: "clamp(42px, 7vw, 76px)", lineHeight: 1.05, padding: "6px 14px", color: "var(--accent-deep)" }}>
          {kr}
        </Speakable>
        <div className="faint" style={{ fontFamily: "var(--mono)", fontSize: 14 }}>{reading}</div>
        {showItemImage && <LearningImage image={itemImage} compact style={{ justifySelf: "center" }} />}
      </div>

      {mode === "recognition" && (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))", gap: 8 }}>
          {choices.map((choice, index) => {
            const on = picked === index;
            const correct = choice === meaning;
            const shown = picked != null;
            return (
              <button key={choice} onClick={() => setPicked(index)} style={{
                border: `1px solid ${on ? "var(--accent-line)" : "var(--rule)"}`,
                background: shown && correct ? "var(--good-tint)" : on ? "var(--accent-tint)" : "var(--card-2)",
                color: shown && correct ? "var(--good)" : on ? "var(--accent-deep)" : "var(--ink-soft)",
                borderRadius: "var(--radius-sm)",
                padding: "12px 14px",
                textAlign: "left",
                cursor: "pointer",
                fontWeight: 800,
              }}>{choice}</button>
            );
          })}
        </div>
      )}

      {mode !== "recognition" && !revealed && (
        <div style={{ display: "flex", justifyContent: "center" }}>
          <Btn kind="secondary" icon="eye" onClick={() => setRevealed(true)}>Verifier</Btn>
        </div>
      )}

      {(revealed || picked != null || mode === "context") && (
        <div className="fade-up" style={{ display: "grid", gap: 14 }}>
          <div style={{ border: "1px solid var(--rule)", background: "var(--card-2)", borderRadius: "var(--radius-sm)", padding: 16 }}>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Sens</div>
            <div style={{ fontSize: 22, fontFamily: "var(--serif)", color: "var(--ink)" }}>{meaning}</div>
            {(currentItem.classe || currentItem.pos || currentItem.level) && (
              <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 10 }}>
                {(currentItem.level || currentItem.niveau_cible) && <span className="chip" style={{ fontSize: 11 }}>{currentItem.level || currentItem.niveau_cible}</span>}
                {(currentItem.classe || currentItem.pos) && <span className="chip" style={{ fontSize: 11 }}>{currentItem.classe || currentItem.pos}</span>}
              </div>
            )}
          </div>
          <PracticeContext item={currentItem} chunkIndex={data.chunkIndex} />
        </div>
      )}

      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 8 }}>
        {PRACTICE_RATINGS.map((rating) => {
          const disabled = !canRate || (selectedCorrect === false && rating.id !== "rate");
          return (
            <button key={rating.id} disabled={disabled} onClick={() => submitRating(rating.id)} style={{
              border: "1px solid var(--rule)",
              borderRadius: "var(--radius-sm)",
              padding: "12px 8px",
              background: rating.tint,
              color: rating.color,
              opacity: disabled ? .4 : 1,
              cursor: disabled ? "not-allowed" : "pointer",
              fontWeight: 900,
            }}>{rating.label}</button>
          );
        })}
      </div>
      <div className="faint" style={{ fontSize: 12 }}>
        SRS : une premiere rencontre planifie le mot ; les repetitions faciles le meme jour sont journalisees sans agrandir encore l'intervalle.
      </div>
    </section>
  );
}

function startPracticeRun(workset, selector, mode) {
  const ids = (workset && workset.wordIds ? workset.wordIds : []).slice();
  return {
    id: `run_${Date.now()}_${selector || "active"}`,
    selector: selector || "active",
    mode: mode || "recall",
    workset,
    queue: ids,
    pos: 0,
    attempts: {},
    results: [],
    startedAt: Date.now(),
    finished: false,
  };
}


function VocabWorkshop({ go }) {
  const [load, setLoad] = useState(() => ({ status: window.LangData && window.LangData.loadVocabFreq ? "loading" : "error", payload: null, items: [], vocabMap: {}, chunkIndex: null, imageBank: null, error: null }));
  const [practice, setPractice] = useState(() => window.LangPracticeLots && window.LangStore && window.LangStore.practice ? window.LangPracticeLots.normalizePractice(window.LangStore.practice.all()) : { lots: [], passes: [], activeLotId: null });
  const [selector, setSelector] = useState("active");
  const [mode, setMode] = useState("recall");
  const [run, setRun] = useState(null);
  const [notice, setNotice] = useState("");

  function refreshPractice() {
    if (!window.LangPracticeLots || !window.LangStore || !window.LangStore.practice) return;
    setPractice(window.LangPracticeLots.normalizePractice(window.LangStore.practice.all()));
  }

  function persistPractice(next) {
    const normalized = window.LangPracticeLots.normalizePractice(next);
    if (window.LangStore && window.LangStore.practice) window.LangStore.practice.set(normalized);
    setPractice(normalized);
    return normalized;
  }

  useEffect(() => {
    let alive = true;
    if (!window.LangData || !window.LangData.loadVocabFreq) {
      setLoad((s) => ({ ...s, status: "error", error: new Error("loadVocabFreq missing") }));
      return () => { alive = false; };
    }
    Promise.all([
      window.LangData.loadVocabFreq(),
      window.LangData.loadChunkIndex ? window.LangData.loadChunkIndex() : Promise.resolve(null),
      window.LangData.loadImageBank ? window.LangData.loadImageBank().catch(() => null) : Promise.resolve(null),
    ]).then(([payload, chunkIndex, imageBank]) => {
      if (!alive) return;
      const items = window.LangPracticeLots ? window.LangPracticeLots.orderedVocab(payload) : (payload.items || []);
      const vocabMap = {};
      items.forEach((item) => { vocabMap[item.id] = item; });
      setLoad({ status: "ready", payload, items, vocabMap, chunkIndex, imageBank, error: null });
    }).catch((error) => {
      if (alive) setLoad((s) => ({ ...s, status: "error", error }));
    });
    return () => { alive = false; };
  }, []);

  if (load.status === "loading") return <div style={{ display: "grid", gap: 16 }}><LoadingCard /><LoadingCard lines={4} /></div>;
  if (load.status === "error") return <ErrorState title="Atelier indisponible" body={load.error && load.error.message} onRetry={() => location.reload()} />;

  const cardStates = window.LangStore && window.LangStore.cards ? window.LangStore.cards.all() : {};
  const worksets = {};
  PRACTICE_SELECTORS.forEach((s) => {
    worksets[s.id] = window.LangPracticeLots.selectWorkset(practice, load.payload, s.id, { cardStates, now: Date.now() });
  });
  const activeWorkset = worksets[selector] || worksets.active;
  const lotSummaries = window.LangPracticeLots.summarizeLots(practice, cardStates, Date.now()).slice().sort((a, b) => b.createdAt - a.createdAt);
  const coverage = activeWorkset ? window.LangPracticeLots.contextCoverage(activeWorkset.wordIds, load.chunkIndex) : null;

  function createLot(count) {
    const created = window.LangPracticeLots.createLot(practice, load.payload, { count, now: Date.now(), cardStates });
    if (!created.lot) {
      setNotice("Aucun nouveau mot disponible pour ce lot.");
      return;
    }
    const saved = persistPractice(created.state);
    setSelector("active");
    setNotice(`${practiceLotTitle(created.lot)} cree avec ${created.lot.wordIds.length} mots.`);
    const ws = window.LangPracticeLots.selectWorkset(saved, load.payload, "active", { cardStates: window.LangStore.cards.all(), now: Date.now() });
    setRun(startPracticeRun(ws, "active", mode));
  }

  function setActiveLot(lotId) {
    const next = Object.assign({}, practice, { activeLotId: lotId });
    persistPractice(next);
    setSelector("active");
  }

  function startSelector(sel, nextMode) {
    const ws = window.LangPracticeLots.selectWorkset(practice, load.payload, sel, { cardStates: window.LangStore.cards.all(), now: Date.now() });
    if (!ws.wordIds.length) {
      setNotice("Cette selection est vide pour l'instant.");
      return;
    }
    setRun(startPracticeRun(ws, sel, nextMode || mode));
  }

  return (
    <div className="fade-up" style={{ display: "grid", gap: 24 }}>
      <section style={{ display: "flex", justifyContent: "space-between", gap: 20, alignItems: "flex-end", flexWrap: "wrap" }}>
        <div>
          <Eyebrow>Atelier</Eyebrow>
          <h1 style={{ fontSize: "clamp(36px, 5vw, 58px)", marginTop: 4 }}>Lots de vocabulaire</h1>
          <p className="muted" style={{ maxWidth: 720, margin: "10px 0 0", fontSize: 16 }}>
            Avancer plus vite, reprendre les lots vus plus tot, puis passer aux phrases porteuses quand le rappel tient.
          </p>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
          <Btn kind="secondary" icon="grid" onClick={() => go("cartes", { source: "vocabulaire", mode: "jour" })}>Cartes vocab</Btn>
          <Btn kind="ghost" icon="today" onClick={() => go("today")}>Aujourd'hui</Btn>
        </div>
      </section>

      <PracticeStatsStrip lots={practice.lots} passes={practice.passes} activeWorkset={activeWorkset} />

      {notice && <div className="chip" style={{ justifySelf: "start", background: "var(--accent-tint)", color: "var(--accent-deep)", borderColor: "transparent" }}>{notice}</div>}

      <div className="atelier-grid" style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) 330px", gap: 22, alignItems: "start" }}>
        <div style={{ display: "grid", gap: 18 }}>
          {run ? (
            <PracticeRun run={run} setRun={setRun} data={load} practice={practice} persistPractice={persistPractice} refreshPractice={refreshPractice} />
          ) : (
            <>
              <section className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 18 }}>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 14, flexWrap: "wrap", alignItems: "center" }}>
                  <div>
                    <Eyebrow>Creation</Eyebrow>
                    <h2 style={{ fontSize: 30, marginTop: 4 }}>Nouveau lot frequentiel</h2>
                  </div>
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                    {[5, 10, 20].map((n) => <Btn key={n} kind={n === 20 ? "primary" : "secondary"} icon="plus" onClick={() => createLot(n)}>{n} mots</Btn>)}
                  </div>
                </div>
                <div className="muted" style={{ fontSize: 14 }}>
                  Les mots deja places dans un lot ou deja introduits en SRS sont evites lors de la creation automatique.
                </div>
              </section>

              <section className="card" style={{ padding: "var(--pad-card)", display: "grid", gap: 18 }}>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 14, alignItems: "center", flexWrap: "wrap" }}>
                  <div>
                    <Eyebrow>Workset</Eyebrow>
                    <h2 style={{ fontSize: 30, marginTop: 4 }}>Recombiner les lots</h2>
                  </div>
                  <Segmented value={mode} options={PRACTICE_MODES} onChange={setMode} size="sm" />
                </div>
                <WorksetButtons selected={selector} worksets={worksets} onSelect={setSelector} onStart={(sel) => startSelector(sel)} />
                {coverage && activeWorkset && activeWorkset.wordIds.length > 0 && (
                  <div style={{ borderTop: "1px solid var(--rule)", paddingTop: 14, display: "flex", gap: 7, flexWrap: "wrap" }}>
                    <span className="chip" style={{ fontSize: 11 }}>Contexte {coverage.total - coverage.missing}/{coverage.total}</span>
                    <span className="chip" style={{ fontSize: 11 }}>Audio phrase {coverage.audio}</span>
                    <span className="chip" style={{ fontSize: 11, ...practiceTierStyle("reel") }}>reel {coverage.reel + coverage.ecrit}</span>
                    <span className="chip" style={{ fontSize: 11, ...practiceTierStyle("collocation") }}>collocation {coverage.collocation}</span>
                    <span className="chip" style={{ fontSize: 11, ...practiceTierStyle("patron") }}>patron {coverage.patron}</span>
                  </div>
                )}
              </section>
            </>
          )}
        </div>

        <aside className="atelier-side" style={{ position: "sticky", top: 22, display: "grid", gap: 16 }}>
          <section className="card" style={{ padding: 18, display: "grid", gap: 12 }}>
            <div>
              <Eyebrow>Lots</Eyebrow>
              <h2 style={{ fontSize: 24, marginTop: 4 }}>Historique</h2>
            </div>
            {lotSummaries.length ? (
              <div>
                {lotSummaries.slice(0, 8).map((lot) => (
                  <LotRow key={lot.id} lot={lot} active={practice.activeLotId === lot.id} summary={lot} onSelect={() => setActiveLot(lot.id)} onStart={() => {
                    setActiveLot(lot.id);
                    const next = Object.assign({}, practice, { activeLotId: lot.id });
                    const ws = window.LangPracticeLots.selectWorkset(next, load.payload, "active", { cardStates, now: Date.now() });
                    setRun(startPracticeRun(ws, "active", mode));
                  }} />
                ))}
              </div>
            ) : (
              <EmptyState icon="grid" title="Aucun lot" body="Cree un premier lot pour commencer l'entrainement accelere." />
            )}
          </section>

          <section className="card" style={{ padding: 18, display: "grid", gap: 10 }}>
            <Eyebrow>Regle SRS</Eyebrow>
            <div className="muted" style={{ fontSize: 13.5 }}>
              Les tentatives sont toutes journalisees. FSRS est mis a jour a l'introduction, aux vraies echeances, et sur une erreur significative ; les repetitions faciles le meme jour restent en entrainement.
            </div>
          </section>
        </aside>
      </div>
    </div>
  );
}


Object.assign(window, { VocabWorkshop, startPracticeRun, practiceHash });
