/* Runner - feedback partage : contraste morpheme->regle, gloses, libelles, QCM. Extrait de runner.jsx (decoupage 2026-06-12, sans changement de logique). */

const RUNNER_RATINGS = [
  { id: "rate", label: "Raté", color: "var(--warn)", tint: "var(--warn-tint)", correct: false },
  { id: "hard", label: "Difficile", color: "var(--ochre)", tint: "var(--warn-tint)", correct: true },
  { id: "good", label: "Bon", color: "var(--good)", tint: "var(--good-tint)", correct: true },
  { id: "easy", label: "Facile", color: "var(--accent)", tint: "var(--accent-tint)", correct: true },
];

const STRAND_LABEL = { study: "Étude", input: "Input", output: "Production", fluency: "Fluidité", meta: "Méta" };
const DRILL_LABEL = { mixte: "Mixte", rappel: "Rappel", reconnaissance: "Reconnaissance", dictee: "Dictée", fluidite: "Fluidité" };
const ACTIVITY_LABEL = {
  cards_srs: "Cartes",
  vocab_freq: "Vocabulaire",
  dialogue_listening: "Écoute",
  dialogue_comprehension: "Compréhension",
  source_listening: "Fluidité",
  guided_production: "Production",
  dialogue_reply: "Dialogue",
  dictation: "Dictée",
  dictogloss: "Dictogloss",
  error_repair: "Réparation",
};

function runnerDrillLabel(mode) {
  return DRILL_LABEL[mode] || (mode ? String(mode).replace(/[_-]/g, " ") : "Mixte");
}

function runnerActivityLabel(kind) {
  return ACTIVITY_LABEL[kind] || (kind ? polishRunnerText(String(kind).replace(/[_-]/g, " ")) : "Exercice");
}

const TAG_EXPLANATIONS = {
  sound: "Réécoutez le modèle et isolez le son qui change.",
  son: "Réécoutez le modèle et isolez le son qui change.",
  batchim: "Vérifiez la consonne finale : elle change souvent la syllabe entendue.",
  space: "L'espacement coréen se travaille par groupes de mots, pas mot à mot.",
  espace: "L'espacement coréen se travaille par groupes de mots, pas mot à mot.",
  particle: "Repérez la particule : elle marque le rôle du mot dans la phrase.",
  particule: "Repérez la particule : elle marque le rôle du mot dans la phrase.",
  ending: "La terminaison porte le temps, la politesse et l'attitude.",
  terminaison: "La terminaison porte le temps, la politesse et l'attitude.",
  form: "Comparez la forme attendue et la forme produite avant de continuer.",
  forme: "Comparez la forme attendue et la forme produite avant de continuer.",
  meaning: "Revenez au sens précis, pas seulement à une traduction proche.",
  sens: "Revenez au sens précis, pas seulement à une traduction proche.",
  order: "Gardez l'ordre coréen attendu avant de traduire mentalement.",
  ordre: "Gardez l'ordre coréen attendu avant de traduire mentalement.",
  grammar: "Nommez la règle en une phrase courte, puis réutilisez-la.",
  grammaire: "Nommez la règle en une phrase courte, puis réutilisez-la.",
  politeness: "Vérifiez le niveau de politesse avant la terminaison.",
  politesse: "Vérifiez le niveau de politesse avant la terminaison.",
};

function cleanTag(tag) {
  return String(tag || "").trim().replace(/[_-]/g, " ");
}

function explanationForTags(tags, fallback) {
  const list = (Array.isArray(tags) ? tags : []).map(cleanTag).filter(Boolean);
  const found = list.map(t => TAG_EXPLANATIONS[t.toLowerCase()]).filter(Boolean);
  const base = found[0] || fallback || "Reprenez le modèle, nommez l'écart, puis refaites une phrase courte.";
  return list.length ? `${base} (${list.join(" · ")})` : base;
}

function repairSpecificHint(drill, fallback) {
  if (!drill || !Array.isArray(drill.choices)) return fallback;
  const choices = drill.choices.map(String);
  if (!(choices.includes("이에요") && choices.includes("예요"))) return fallback;
  const stem = String(drill.cloze || "").split("____")[0].trim();
  const targetEnding = String(drill.answer || "").trim();
  const yoursEnding = choices.find((choice) => choice !== targetEnding) || "";
  if (window.LangValidate && window.LangValidate.copulaEndingHint) {
    const hint = window.LangValidate.copulaEndingHint({ stem, targetEnding, yoursEnding });
    if (hint) return hint;
  }
  return fallback;
}

// Contraste item-spécifique : « ta forme / forme cible / pourquoi » sur l'item exact
// (AUDIT_PEDAGOGIQUE §6). S'appuie sur LangValidate.contrast (morphème + rôle réel).
function contrastFor(model, answer, dictation) {
  if (!model || !window.LangValidate || !window.LangValidate.contrast) return null;
  const c = window.LangValidate.contrast(model, answer, { ignoreSpaces: false, ignorePunct: !!dictation });
  return c && !c.correct ? c : null;
}

function ContrastFeedback({ contrast }) {
  // Lien feedback -> règle (P12/P19) : si le morphème erroné est enseigné par la
  // banque de grammaire, on propose de REVOIR LA RÈGLE sur place, sans quitter la
  // séance (la réparation reste locale ; Structures sert à la rencontre guidée).
  const [rulePoint, setRulePoint] = useState(null);
  const [ruleOpen, setRuleOpen] = useState(false);
  useEffect(() => {
    let alive = true;
    setRulePoint(null);
    setRuleOpen(false);
    if (!contrast || contrast.correct || !window.LangGrammarLink || !window.LangData || !window.LangData.loadGrammar) return () => { alive = false; };
    window.LangData.loadGrammar()
      .then(g => { if (alive) setRulePoint(window.LangGrammarLink.findPoint(g.points, contrast)); })
      .catch(() => {});
    return () => { alive = false; };
    // contrast est recalculé à chaque render (objet neuf) : on dépend de ses champs.
  }, [contrast && contrast.dominant, contrast && contrast.target, contrast && contrast.yours]);
  if (!contrast) return null;
  const scopeLabel = contrast.dominant === "ordre" ? "ordre des mots"
    : contrast.scope === "morpheme" && contrast.dominant === "particule" ? "particule"
    : contrast.scope === "morpheme" && contrast.dominant === "terminaison" ? "terminaison"
    : contrast.scope === "morpheme" ? "morphème"
    : contrast.scope === "phrase" ? "découpage des mots" : "mot";
  const ruleExample = rulePoint && rulePoint.examples && rulePoint.examples[0];
  return (
    <div className="fade-up" style={{ display: "grid", gap: 6 }}>
      <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "3px 12px", alignItems: "baseline" }}>
        <span className="faint" style={{ fontSize: 12 }}>Ta forme</span>
        <span className="kr" style={{ fontSize: 16, color: "var(--warn)", textDecoration: "line-through", textDecorationColor: "var(--warn)" }}>{contrast.yours || "—"}</span>
        <span className="faint" style={{ fontSize: 12 }}>Forme cible</span>
        <span className="kr" style={{ fontSize: 16, color: "var(--good)", fontWeight: 700 }}>{contrast.target || "—"}</span>
      </div>
      <div style={{ fontSize: 13.5, color: "var(--ink)" }}><span className="faint">Pourquoi ({scopeLabel}) : </span>{contrast.reason}</div>
      {rulePoint && !ruleOpen && (
        <button onClick={() => setRuleOpen(true)} className="chip" style={{ cursor: "pointer", fontSize: 12.5, justifySelf: "start" }}>
          Revoir la règle : <span className="kr" style={{ fontWeight: 700 }}>{rulePoint.form}</span>
        </button>
      )}
      {rulePoint && ruleOpen && (
        <div className="fade-up" style={{ display: "grid", gap: 6, padding: "10px 12px", borderRadius: "var(--radius-sm)", background: "var(--card)", border: "1px solid var(--rule)" }}>
          <div style={{ display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }}>
            <span className="kr" style={{ fontSize: 17, fontWeight: 700, color: "var(--accent-deep)" }}>{rulePoint.form}</span>
            <span className="faint" style={{ fontSize: 12.5 }}>{rulePoint.label}</span>
          </div>
          <div style={{ fontSize: 13, color: "var(--ink)" }}>{rulePoint.rule}</div>
          {ruleExample && (
            <div style={{ display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }}>
              <Speakable text={ruleExample.kr} as="span" gloss={ruleExample.fr} className="kr" style={{ fontSize: 15 }}>{ruleExample.kr}</Speakable>
              <span className="faint" style={{ fontSize: 12 }}>{ruleExample.fr}</span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function inputProofText(prompt, expected, feedback, line) {
  const values = [
    expected && expected.fr,
    expected && expected.en,
    feedback && feedback.meaning,
    prompt && prompt.meaning,
    line && line.fr,
    line && line.en,
    prompt && prompt.fr,
    prompt && prompt.en,
  ];
  return String(values.find(v => typeof v === "string" && v.trim()) || "").trim();
}

function polishRunnerPrompt(text) {
  return polishRunnerText(String(text || "").replace(/^Quelle replique correspond a ce sens\s*:/i, "Quelle réplique correspond à ce sens :"));
}

// Délègue à la couche partagée polishLabel (ui.jsx, chargé avant runner.jsx).
function polishRunnerText(text) {
  return window.polishLabel ? window.polishLabel(text) : text;
}

/* ----- Blocs focalisés (un par grande famille) ----- */

// Palier de qualité de contexte (#2) d'une phrase porteuse, avec repli depuis la source.
const CONTEXT_TIER_FROM_SOURCE = { dialogue: "reel", ecoute: "reel", carte: "reel", ecrit: "ecrit", lexique: "collocation", patron: "patron" };
const CONTEXT_TIER_LABEL = { reel: "contexte réel", ecrit: "phrase validée", collocation: "collocation", patron: "patron généré" };
function carrierTier(s) {
  if (s && s.contextQuality) return s.contextQuality;
  if (s && s.quality === "generated_scaffold") return "patron";
  return (s && CONTEXT_TIER_FROM_SOURCE[s.source]) || "autre";
}

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

function contextualGlossFor(glossFor, focusKr, focusMeaning) {
  const target = hangulToken(focusKr);
  const meaning = String(focusMeaning || "").trim();
  if (!target || !meaning) return glossFor;
  const stem = target.endsWith("다") ? target.slice(0, -1) : "";
  return (token) => {
    const clean = hangulToken(token);
    if (clean === target) return meaning;
    if (target === "하다" && /^해(요)?$/.test(clean)) return meaning;
    if (stem.length >= 2 && clean.startsWith(stem)) return meaning;
    return glossFor ? glossFor(token) : undefined;
  };
}

function CarrierSentences({ carriers, glossFor, contextStatus, focusKr, focusMeaning }) {
  if (!carriers || !carriers.length) return null;
  const topTier = carrierTier(carriers[0]);
  const tierLabel = CONTEXT_TIER_LABEL[topTier] || "contexte relié";
  const label = contextStatus === "linked"
    ? (topTier === "patron" ? "Phrase porteuse · patron généré" : `Dans une phrase · ${tierLabel}`)
    : "Dans une phrase";
  const localGlossFor = contextualGlossFor(glossFor, focusKr, focusMeaning);
  return (
    <div className="fade-up" style={{ display: "grid", gap: 8 }}>
      <div className="eyebrow">{label}</div>
      {carriers.map((s, i) => (
        <div key={s.id || i} style={{ padding: "10px 13px", borderRadius: "var(--radius-sm)", background: "var(--card-2)", border: "1px solid var(--rule)", display: "grid", gap: 4 }}>
          <PhraseWords text={s.kr} glossFor={localGlossFor} audioUrl={s.audioUrl} audioRef={s.kr} size={22} justify="flex-start" />
          {s.fr && <span className="faint" style={{ fontSize: 12.5 }}>{s.fr}</span>}
        </div>
      ))}
    </div>
  );
}

function VocabContext({ exercise, carriers, glossFor }) {
  const sourceKind = exercise && exercise.source && exercise.source.kind;
  if (sourceKind !== "vocab_freq") return null;
  const meta = exercise.meta || {};
  if (carriers && carriers.length) {
    return <CarrierSentences carriers={carriers} glossFor={glossFor} contextStatus={meta.contextStatus} focusKr={exercise.expected && exercise.expected.kr} focusMeaning={exercise.expected && exercise.expected.fr} />;
  }
  if (meta.contextStatus !== "missing") return null;
  return (
    <div className="fade-up" style={{ padding: "10px 13px", borderRadius: "var(--radius-sm)", background: "var(--card-2)", border: "1px dashed var(--rule)", display: "grid", gap: 3 }}>
      <div className="eyebrow">Contexte</div>
      <span className="faint" style={{ fontSize: 12.5 }}>Contexte à enrichir dans la banque de phrases.</span>
    </div>
  );
}

function hasHangul(text) {
  return /[\uac00-\ud7a3]/.test(String(text || ""));
}

function glossPairs(text, glossFor) {
  if (!glossFor) return [];
  return String(text || "").trim().split(/\s+/).filter(Boolean).map((token) => {
    const clean = token.replace(/^[^\uac00-\ud7a3]+|[^\uac00-\ud7a3]+$/g, "");
    const gloss = clean ? glossFor(clean) : "";
    return gloss ? { token: clean, gloss } : null;
  }).filter(Boolean).slice(0, 10);
}

function MixedGlossText({ text, glossFor, style, glossRevealed = true }) {
  const parts = String(text || "").split(/(\s+)/);
  return (
    <div style={style}>
      {parts.map((part, i) => {
        if (!hasHangul(part)) return <React.Fragment key={i}>{part}</React.Fragment>;
        const clean = part.replace(/^[^\uac00-\ud7a3]+|[^\uac00-\ud7a3]+$/g, "");
        const gloss = clean && glossFor ? glossFor(clean) : undefined;
        return <Speakable key={i} text={clean || part} audioRef={clean || part} gloss={gloss} glossRevealed={glossRevealed} frame={false} className="kr" style={{ padding: "0 2px", color: "inherit" }}>{part}</Speakable>;
      })}
    </div>
  );
}

function GlossAid({ text, glossFor }) {
  const [open, setOpen] = useState(false);
  const pairs = glossPairs(text, glossFor);
  if (!pairs.length) return null;
  return (
    <div style={{ display: "grid", gap: 6, marginTop: 2 }}>
      <button type="button" aria-expanded={open} onClick={() => setOpen(o => !o)} className="chip" style={{ justifySelf: "start", cursor: "pointer", background: open ? "var(--accent-tint)" : "var(--card)", color: open ? "var(--accent-deep)" : "var(--ink-soft)", borderColor: open ? "transparent" : "var(--rule)" }}>
        <Icon name={open ? "eyeoff" : "eye"} size={13} /> Gloses
      </button>
      {open && (
        <div className="fade-up" style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
          {pairs.map((p, i) => (
            <span key={`${p.token}_${i}`} className="chip" style={{ fontSize: 12, background: "var(--card-2)" }}>
              <span className="kr">{p.token}</span><span className="faint">{p.gloss}</span>
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

// QCM de reconnaissance explicite : correct + distracteurs.
function buildRunnerQcm(choices, correct, n) {
  const others = Array.from(new Set(choices)).filter(c => c && c !== correct);
  const sel = [correct].concat(others).slice(0, Math.max(2, n));
  return sel.sort(() => Math.random() - 0.5);
}

