/* Runner - bloc cartes/vocab : rappel actif, reconnaissance objective, notes FSRS. Extrait de runner.jsx (decoupage 2026-06-12, sans changement de logique). */

/* Famille sino du mot (élaboration P13) : transforme le mot isolé en nœud de
   réseau au moment où ça rapporte le plus — la première rencontre. */
function SinoFamilyPanel({ families }) {
  if (!families || !families.length) return null;
  return (
    <div style={{ display: "grid", gap: 8, padding: "10px 12px", borderRadius: "var(--radius-sm)", background: "var(--card-2)", border: "1px solid var(--rule)" }}>
      <span className="eyebrow">Même racine sino — un mot en ouvre d'autres</span>
      {families.map(f => (
        <div key={f.root} style={{ display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>
          <span className="chip kr" style={{ fontSize: 14, fontWeight: 700 }}>{f.root}</span>
          {f.siblings.map(s => (
            <span key={s.id} style={{ display: "inline-flex", alignItems: "baseline", gap: 5 }}>
              <Speakable text={s.kr} as="span" gloss={s.fr} className="kr" style={{ fontSize: 16 }}>{s.kr}</Speakable>
              {s.fr && <span className="faint" style={{ fontSize: 12 }}>{s.fr}</span>}
            </span>
          ))}
        </div>
      ))}
    </div>
  );
}

function RunnerCardBlock({ exercise, glossFor, carriers, onGrade }) {
  const [revealed, setRevealed] = useState(false);
  const [qcm, setQcm] = useState(null);
  const [picked, setPicked] = useState(null);
  const [answer, setAnswer] = useState("");
  const [checked, setChecked] = useState(null);
  const [introDone, setIntroDone] = useState(false);
  const [choicesShown, setChoicesShown] = useState(false);
  // Première rencontre élaborée : PRETEST (deviner avant de voir — la génération,
  // même erronée, potentialise l'encodage ; Kornell & al.) + famille sino.
  const [pretestPick, setPretestPick] = useState(null);
  const [sino, setSino] = useState([]);
  const p = exercise.prompt || {};
  const e = exercise.expected || {};
  const f = exercise.feedback || {};
  const kr = p.kr || p.front || e.kr || "";
  const fr = e.fr || e.back || p.fr || "";
  const settings = window.LangStore ? window.LangStore.settings.get() : {};
  const nChoices = Math.max(2, Math.min(6, Number(settings.qcmChoices) || 3));
  const choices = (f.choices && f.choices.length ? f.choices : (e.choices || []));
  const mode = p.mode || "active_recall";
  const qcmMode = mode === "qcm";
  const dictationMode = mode === "audio_to_hangeul";
  const listeningMode = mode === "audio_to_meaning";
  const productionMode = mode === "fr_to_kr";
  const presentationMode = mode === "new_presentation";
  const canQcm = choices.length >= 2 && !!fr;
  useEffect(() => {
    setRevealed(false);
    setPicked(null);
    setAnswer("");
    setChecked(null);
    setIntroDone(false);
    setChoicesShown(false);
    setPretestPick(null);
    setQcm(canQcm ? { options: buildRunnerQcm(choices, fr, nChoices), immediate: qcmMode } : null);
  }, [exercise.id, mode, qcmMode, canQcm, fr, nChoices]);

  // Famille sino du mot présenté (chargée des banques cachées par LangData).
  useEffect(() => {
    setSino([]);
    if (!presentationMode || !window.LangSinoFamilies || !window.LangData || !exercise.source || exercise.source.kind !== "vocab_freq") return undefined;
    let alive = true;
    Promise.all([window.LangData.loadChunkIndex(), window.LangData.loadVocabFreq()])
      .then(([idx, voc]) => {
        if (!alive) return;
        const byId = {};
        (voc.items || []).forEach(v => { byId[v.id] = v; });
        const item = byId[exercise.source.id];
        if (item) setSino(window.LangSinoFamilies.familiesFor({ item, chunkIndex: idx, vocabById: byId }));
      })
      .catch(() => {});
    return () => { alive = false; };
  }, [exercise.id, presentationMode]);

  const onRate = (r) => {
    onGrade({ rating: r.id, correct: r.correct, errorTags: r.correct ? [] : (exercise.errorTags || []) });
  };

  if (dictationMode) {
    const done = checked != null;
    const tags = done && checked.tags && checked.tags.length ? checked.tags : (exercise.errorTags || []);
    return (
      <div style={{ display: "grid", gap: 16 }}>
        <div className="card" style={{ padding: 24, display: "grid", gap: 14 }}>
          <Eyebrow>Dictée</Eyebrow>
          <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
            <SpeakBtn text={kr} audioRef={p.audioRef || kr} audioUrl={p.audioUrl} size={44} title="Écouter" />
          </div>
          <textarea value={answer} onChange={ev => setAnswer(ev.target.value)} rows={2} placeholder="Ce que vous entendez..." className="kr" lang="ko" autoCapitalize="off" autoCorrect="off" spellCheck={false}
            style={{ width: "100%", padding: "11px 13px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: "var(--card-2)", color: "var(--ink)", fontSize: 20, resize: "vertical" }} />
          <KoreanKeyboard value={answer} onChange={(next) => { setAnswer(next); setChecked(null); }} compact />
          {done && (
            <div className="fade-up" style={{ display: "grid", gap: 8, padding: "12px 14px", borderRadius: "var(--radius-sm)", background: checked.correct ? "var(--good-tint)" : "var(--warn-tint)" }}>
              <div style={{ fontWeight: 700, color: checked.correct ? "var(--good)" : "var(--warn)" }}>{dictationFeedbackLabel(checked)}</div>
              <div>Modèle : <span className="kr" style={{ fontSize: 18 }}>{kr}</span></div>
              {p.reading && <div className="faint" style={{ fontFamily: "var(--mono)", fontSize: 12.5 }}>Prononciation : {p.reading}</div>}
              {fr && <div className="faint" style={{ fontSize: 13 }}>{fr}</div>}
              {!checked.correct && <ContrastFeedback contrast={contrastFor(kr, answer, true)} />}
              {!checked.correct && tags.length > 0 && <div className="faint" style={{ fontSize: 12 }}>Tags : {tags.join(" · ")}</div>}
            </div>
          )}
          {done && <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
          {!done
            ? <Btn kind="secondary" icon="eye" disabled={!answer.trim()} onClick={() => setChecked(compareDictationAnswer(kr, answer))}>Vérifier</Btn>
            : <Btn kind="primary" icon="check" onClick={() => onGrade({ rating: checked.correct ? "good" : "rate", correct: checked.correct, answer, errorTags: checked.correct ? [] : tags, feedbackShown: true })}>Continuer</Btn>}
        </div>
      </div>
    );
  }

  if (presentationMode && !introDone) {
    // Pretest : deviner AVANT de voir le sens (génération, même erronée, qui
    // potentialise l'encodage). La famille sino sert d'indice — génération guidée.
    const pretestActive = canQcm && qcm;
    const guessed = pretestPick != null;
    const showMeaning = !pretestActive || guessed;
    const guessCorrect = guessed && qcm.options[pretestPick] === fr;
    return (
      <div style={{ display: "grid", gap: 16 }}>
        <div className="card" style={{ padding: 24, display: "grid", gap: 14 }}>
          <Eyebrow>{showMeaning ? "Présentation" : "Première rencontre — devinez d'abord"}</Eyebrow>
          <PhraseWords text={kr} glossFor={showMeaning ? glossFor : undefined} audioUrl={p.audioUrl} audioRef={p.audioRef} size={48} justify="flex-start" glossRevealed={showMeaning} />
          {p.reading && <div className="faint" style={{ fontFamily: "var(--mono)", fontSize: 13 }}>{p.reading}</div>}
          {!showMeaning && (
            <>
              {sino.length > 0 && (
                <div className="faint" style={{ fontSize: 13 }}>
                  Indice — même racine que {sino[0].siblings.map(s => `${s.kr} (${s.fr})`).join(", ")}.
                </div>
              )}
              <div style={{ display: "grid", gap: 8 }}>
                {qcm.options.map((c, idx) => (
                  <button key={idx} onClick={() => setPretestPick(idx)} style={{ padding: "12px 15px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: "var(--card)", color: "var(--ink)", fontFamily: "var(--serif)", fontSize: 17, textAlign: "left", cursor: "pointer" }}>{c}</button>
                ))}
              </div>
              <span className="faint" style={{ fontSize: 12 }}>Se tromper ici n'a aucun coût — l'effort de deviner prépare la mémoire.</span>
            </>
          )}
          {showMeaning && (
            <>
              {guessed && (
                <span className="chip fade-up" style={{ fontSize: 12, justifySelf: "start", background: guessCorrect ? "var(--good-tint)" : "var(--card-2)", color: guessCorrect ? "var(--good)" : "var(--ink-soft)", borderColor: "transparent" }}>
                  {guessCorrect ? "Bien deviné" : `Vous aviez dit « ${qcm.options[pretestPick]} »`}
                </span>
              )}
              <div style={{ fontFamily: "var(--serif)", fontSize: 24, color: "var(--ink)" }}>{fr}</div>
              <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />
              <SinoFamilyPanel families={sino} />
            </>
          )}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end" }}>
          {showMeaning && <Btn kind="primary" icon="eyeoff" onClick={() => setIntroDone(true)}>Masquer et rappeler</Btn>}
        </div>
      </div>
    );
  }

  if (listeningMode) {
    const selected = picked == null || !qcm ? null : qcm.options[picked];
    const correct = selected === fr;
    return (
      <div style={{ display: "grid", gap: 16 }}>
        <div className="card" style={{ padding: 24, display: "grid", gap: 14 }}>
          <Eyebrow>Écoute active</Eyebrow>
          <div style={{ display: "grid", gap: 8 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
              <SpeakBtn text={kr} audioRef={p.audioRef || kr} audioUrl={p.audioUrl} size={46} title="Écouter sans lire" />
              <span className="faint" style={{ fontSize: 13.5 }}>Écoutez d'abord, puis choisissez le sens.</span>
            </div>
          </div>
          {choicesShown && qcm && (
            <div className="fade-up" style={{ display: "grid", gap: 8 }}>
              {qcm.options.map((c, idx) => {
                const reveal = picked != null;
                let bg = "var(--card)", col = "var(--ink)";
                if (reveal && c === fr) { bg = "var(--good-tint)"; col = "var(--good)"; }
                else if (reveal && picked === idx && c !== fr) { bg = "var(--warn-tint)"; col = "var(--warn)"; }
                return <button key={idx} onClick={() => { if (picked == null) { setPicked(idx); setRevealed(true); } }} disabled={reveal} style={{ padding: "12px 15px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: bg, color: col, fontFamily: "var(--serif)", fontSize: 17, textAlign: "left", cursor: reveal ? "default" : "pointer" }}>{c}</button>;
              })}
            </div>
          )}
          {revealed && (
            <div className="fade-up" style={{ display: "grid", gap: 8, padding: "12px 14px", borderRadius: "var(--radius-sm)", background: picked == null || correct ? "var(--good-tint)" : "var(--warn-tint)" }}>
              <div style={{ fontWeight: 700, color: picked == null || correct ? "var(--good)" : "var(--warn)" }}>{picked == null ? "Modèle" : (correct ? "Juste" : "À revoir")}</div>
              <PhraseWords text={kr} glossFor={glossFor} audioUrl={p.audioUrl} audioRef={p.audioRef} size={30} justify="flex-start" glossRevealed />
              <div style={{ fontFamily: "var(--serif)", fontSize: 20, color: "var(--ink)" }}>{fr}</div>
              <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />
            </div>
          )}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, flexWrap: "wrap" }}>
          {!choicesShown && qcm && <Btn kind="secondary" icon="ear" onClick={() => setChoicesShown(true)}>Choisir le sens</Btn>}
          {!qcm && !revealed && <Btn kind="secondary" icon="eye" onClick={() => setRevealed(true)}>Afficher le modèle</Btn>}
          {picked != null && <Btn kind="primary" icon="check" onClick={() => onGrade({ rating: correct ? "good" : "rate", correct, answer: selected || "", errorTags: correct ? [] : (exercise.errorTags || []), feedbackShown: true })}>Continuer</Btn>}
          {!qcm && revealed && (
            <>
              <Btn kind="secondary" onClick={() => onGrade({ rating: "rate", correct: false, errorTags: exercise.errorTags || [], feedbackShown: true })}>À revoir</Btn>
              <Btn kind="primary" icon="check" onClick={() => onGrade({ rating: "good", correct: true, feedbackShown: true })}>Compris</Btn>
            </>
          )}
        </div>
      </div>
    );
  }

  if (productionMode) {
    const done = checked != null;
    const tags = done && checked.tags && checked.tags.length ? checked.tags : (exercise.errorTags || []);
    return (
      <div style={{ display: "grid", gap: 16 }}>
        <div className="card" style={{ padding: 24, display: "grid", gap: 14 }}>
          <Eyebrow>Production</Eyebrow>
          <div style={{ fontFamily: "var(--serif)", fontSize: 24, color: "var(--accent-deep)" }}>{fr}</div>
          <textarea value={answer} onChange={ev => { setAnswer(ev.target.value); setChecked(null); }} rows={2} placeholder="Produisez en coréen..." className="kr" lang="ko" autoCapitalize="off" autoCorrect="off" spellCheck={false}
            style={{ width: "100%", padding: "11px 13px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: "var(--card-2)", color: "var(--ink)", fontSize: 20, resize: "vertical" }} />
          <KoreanKeyboard value={answer} onChange={(next) => { setAnswer(next); setChecked(null); }} compact />
          {done && (
            <div className="fade-up" style={{ display: "grid", gap: 8, padding: "12px 14px", borderRadius: "var(--radius-sm)", background: checked.correct ? "var(--good-tint)" : "var(--warn-tint)" }}>
              <div style={{ fontWeight: 700, color: checked.correct ? "var(--good)" : "var(--warn)" }}>{checked.correct ? "Correct" : "À revoir"}</div>
              <div>Modèle : <span className="kr" style={{ fontSize: 18 }}>{kr}</span></div>
              {p.reading && <div className="faint" style={{ fontFamily: "var(--mono)", fontSize: 12.5 }}>Prononciation : {p.reading}</div>}
              {!checked.correct && <ContrastFeedback contrast={contrastFor(kr, answer, false)} />}
              {!checked.correct && tags.length > 0 && <div className="faint" style={{ fontSize: 12 }}>Tags : {tags.join(" · ")}</div>}
              <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />
            </div>
          )}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
          {!done
            ? <Btn kind="secondary" icon="eye" disabled={!answer.trim()} onClick={() => setChecked(compareDictationAnswer(kr, answer))}>Vérifier</Btn>
            : <Btn kind="primary" icon="check" onClick={() => onGrade({ rating: checked.correct ? "good" : "rate", correct: checked.correct, answer, errorTags: checked.correct ? [] : tags, feedbackShown: true })}>Continuer</Btn>}
        </div>
      </div>
    );
  }

  if (qcmMode && qcm) {
    const selected = picked == null ? null : qcm.options[picked];
    const correct = selected === fr;
    return (
      <div style={{ display: "grid", gap: 16 }}>
        <div className="card" style={{ padding: 24, display: "grid", gap: 14 }}>
          <Eyebrow>QCM</Eyebrow>
          <PhraseWords text={kr} glossFor={glossFor} audioUrl={p.audioUrl} audioRef={p.audioRef} size={46} justify="flex-start" />
          {p.reading && <div className="faint" style={{ fontFamily: "var(--mono)", fontSize: 13 }}>{p.reading}</div>}
          <div style={{ display: "grid", gap: 8 }}>
            {qcm.options.map((c, idx) => {
              const reveal = picked != null;
              let bg = "var(--card)", col = "var(--ink)";
              if (reveal && c === fr) { bg = "var(--good-tint)"; col = "var(--good)"; }
              else if (reveal && picked === idx && c !== fr) { bg = "var(--warn-tint)"; col = "var(--warn)"; }
              return <button key={idx} onClick={() => picked == null && setPicked(idx)} disabled={reveal} style={{ padding: "12px 15px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: bg, color: col, fontFamily: "var(--serif)", fontSize: 17, textAlign: "left", cursor: reveal ? "default" : "pointer" }}>{c}</button>;
            })}
          </div>
          {picked != null && <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />}
        </div>
        <div style={{ display: "flex", justifyContent: "flex-end" }}>
          <Btn kind="primary" icon="check" disabled={picked == null} onClick={() => onGrade({ rating: correct ? "good" : "rate", correct, answer: selected || "", errorTags: correct ? [] : (exercise.errorTags || []), feedbackShown: true })}>Continuer</Btn>
        </div>
      </div>
    );
  }

  // Rappel d'abord (réponse cachée), PUIS vérification objective par reconnaissance
  // du sens (réutilise expected.choices) : juste/faux pilote la boucle + FSRS, plus
  // d'auto-déclaration. Repli sur l'auto-note seulement si aucun distracteur.
  const recognize = !!qcm;
  const selectedChoice = recognize && picked != null ? qcm.options[picked] : null;
  const recogCorrect = selectedChoice != null && selectedChoice === fr;
  const eyebrowText = revealed
    ? (recognize ? (recogCorrect ? "Juste" : "À revoir") : "Vérifiez")
    : (choicesShown ? "Choisissez le sens" : "Rappel actif");
  return (
    <div style={{ display: "grid", gap: 18 }}>
      <div className="card" style={{ padding: 30, display: "grid", placeItems: "center", minHeight: 200 }}>
        <div style={{ textAlign: "center" }}>
          <Eyebrow>{eyebrowText}</Eyebrow>
          <div style={{ margin: "14px 0" }}>
            <PhraseWords text={kr} glossFor={glossFor} audioUrl={p.audioUrl} audioRef={p.audioRef} size={56} glossRevealed={revealed} />
          </div>
          {revealed
            ? <div className="fade-up" style={{ fontFamily: "var(--serif)", fontSize: 30, color: "var(--ink)" }}>{fr}</div>
            : <p className="faint" style={{ fontSize: 14 }}>{choicesShown ? "Rappel fait ? Choisissez le bon sens — sans deviner." : "Cliquez un mot pour l'écouter, rappelez le sens de tête, puis vérifiez."}</p>}
        </div>
      </div>
      {revealed && <VocabContext exercise={exercise} carriers={carriers} glossFor={glossFor} />}
      {recognize ? (
        !choicesShown
          ? <div style={{ display: "flex", justifyContent: "center" }}><Btn kind="secondary" icon="eye" aria-label="Vérifier" title="Vérifier mon rappel" onClick={() => setChoicesShown(true)}>Vérifier <span aria-hidden="true" className="faint" style={{ fontSize: 12, fontFamily: "var(--mono)" }}>espace</span></Btn></div>
          : (
            <div className="fade-up" style={{ display: "grid", gap: 10 }}>
              <div className="eyebrow">Choisissez le bon sens</div>
              {qcm.options.map((c, idx) => {
                const correct = c === fr, reveal = picked != null;
                let bg = "var(--card)", col = "var(--ink)";
                if (reveal && correct) { bg = "var(--good-tint)"; col = "var(--good)"; }
                else if (reveal && picked === idx && !correct) { bg = "var(--warn-tint)"; col = "var(--warn)"; }
                return <button key={idx} onClick={() => { if (picked == null) { setPicked(idx); setRevealed(true); } }} disabled={picked != null} style={{ padding: "12px 15px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: bg, color: col, fontFamily: "var(--serif)", fontSize: 17, textAlign: "left", cursor: picked == null ? "pointer" : "default" }}>{c}</button>;
              })}
              {picked != null && (
                <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
                  {recogCorrect && <Btn kind="ghost" size="sm" onClick={() => onGrade({ rating: "easy", correct: true, feedbackShown: true })}>C'était facile</Btn>}
                  <Btn kind="primary" icon="check" onClick={() => onGrade({ rating: recogCorrect ? "good" : "rate", correct: recogCorrect, answer: selectedChoice || "", errorTags: recogCorrect ? [] : (exercise.errorTags || []), feedbackShown: true })}>Continuer</Btn>
                </div>
              )}
            </div>
          )
      ) : !revealed
        ? <div style={{ display: "flex", justifyContent: "center" }}><Btn kind="secondary" icon="eye" aria-label="Vérifier" title="Vérifier" onClick={() => setRevealed(true)}>Vérifier <span aria-hidden="true" className="faint" style={{ fontSize: 12, fontFamily: "var(--mono)" }}>espace</span></Btn></div>
        : (
          <div className="fade-up" style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 10 }}>
            {RUNNER_RATINGS.map(r => (
              <button key={r.id} onClick={() => onRate(r)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, padding: "14px 8px", borderRadius: "var(--radius-sm)", border: "1px solid var(--rule)", background: "var(--card)", cursor: "pointer", transition: "all .15s" }}
                onMouseEnter={ev => { ev.currentTarget.style.background = r.tint; ev.currentTarget.style.borderColor = "transparent"; }}
                onMouseLeave={ev => { ev.currentTarget.style.background = "var(--card)"; ev.currentTarget.style.borderColor = "var(--rule)"; }}>
                <span style={{ fontFamily: "var(--serif)", fontSize: 18, color: r.color }}>{r.label}</span>
              </button>
            ))}
          </div>
        )}
    </div>
  );
}

