/* ==================================================================
   Manual Paper Authoring — section/question composer
   Routed via: Test Library → "Create test" → Test Details modal → here
   ================================================================== */

const { Icon: MIcon, StatusPill: MStatusPill } = window.KXUI;

const TYPE_OPTIONS = [
  { value: "MCQ",   label: "MCQ",       defaultMarks: 1 },
  { value: "Short", label: "Short",     defaultMarks: 2 },
  { value: "Long",  label: "Long",      defaultMarks: 5 },
  { value: "TF",    label: "True/False", defaultMarks: 1 },
  { value: "Fill",  label: "Fill blank", defaultMarks: 1 },
  { value: "Match", label: "Match",     defaultMarks: 2 },
];

const SECTION_NAMES = ["A","B","C","D","E","F","G","H"];

let _uidCounter = 0;
const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${++_uidCounter}`;

const newSubQuestion = () => ({ id: uid("sq"), text: "" });

const newQuestion = (type = "MCQ") => ({
  id: uid("q"),
  text: "",
  type,
  options: type === "MCQ" || type === "TF"
    ? (type === "TF"
        ? [{ id:"a", text:"True", correct:false }, { id:"b", text:"False", correct:false }]
        : [{ id:"a", text:"", correct:false }, { id:"b", text:"", correct:false }, { id:"c", text:"", correct:false }, { id:"d", text:"", correct:false }])
    : [],
  subQuestions: type === "Long" ? [newSubQuestion(), newSubQuestion()] : [],
  expectedAnswer: "",
  concepts: [],
  marks: null, // when null, inherits from section.marks
});

const newSection = (idx) => ({
  id: uid("s"),
  label: `Section ${SECTION_NAMES[idx] || (idx + 1)}`,
  type: "MCQ",
  marks: 1,
  target: 5,
  instructions: "",
  questions: [newQuestion("MCQ")],
});

/* ---------- main component ---------- */
const ManualAuthoring = ({ meta, initialSections, onSave, onCancel }) => {
  const [title, setTitle] = useState(meta?.title || "Untitled test");
  const [sections, setSections] = useState(() => (initialSections && initialSections.length) ? initialSections : [newSection(0)]);
  const [editing, setEditing] = useState(null); // { sectionId, questionId }
  const [savedAt, setSavedAt] = useState(null);
  const [showSaveConfirm, setShowSaveConfirm] = useState(false);

  /* ------- derived stats ------- */
  const totalQ = sections.reduce((a, s) => a + s.questions.length, 0);
  const totalMarks = sections.reduce(
    (a, s) => a + s.questions.reduce((b, q) => b + (q.marks ?? s.marks), 0),
    0
  );
  const targetQ = sections.reduce((a, s) => a + s.target, 0);
  const emptyQuestions = sections.reduce(
    (a, s) => a + s.questions.filter(q => !q.text.trim()).length,
    0
  );
  const validation = {
    emptyQ: emptyQuestions,
    underTarget: sections.filter(s => s.questions.length < s.target).length,
    mcqMissingCorrect: sections.reduce((a, s) =>
      a + s.questions.filter(q =>
        (q.type === "MCQ" || q.type === "TF") && !q.options.some(o => o.correct)
      ).length, 0),
  };
  const blocking = validation.emptyQ + validation.mcqMissingCorrect;

  /* ------- autosave indicator (cosmetic) ------- */
  useEffect(() => {
    const t = setTimeout(() => setSavedAt(new Date()), 600);
    return () => clearTimeout(t);
  }, [sections, title]);

  /* ------- section mutations ------- */
  const updateSection = (id, patch) =>
    setSections(s => s.map(x => x.id === id ? { ...x, ...patch } : x));
  const insertSection = (afterIdx) => {
    setSections(s => {
      const copy = [...s];
      copy.splice(afterIdx + 1, 0, newSection(copy.length));
      return copy;
    });
  };
  const deleteSection = (id) =>
    setSections(s => s.length > 1 ? s.filter(x => x.id !== id) : s);

  /* ------- question mutations ------- */
  const updateQuestion = (sid, qid, patch) =>
    setSections(s => s.map(x => x.id !== sid ? x : {
      ...x, questions: x.questions.map(q => q.id === qid ? { ...q, ...patch } : q)
    }));
  const addQuestion = (sid) =>
    setSections(s => s.map(x => x.id !== sid ? x : {
      ...x, questions: [...x.questions, newQuestion(x.type)]
    }));
  const deleteQuestion = (sid, qid) =>
    setSections(s => s.map(x => x.id !== sid ? x : {
      ...x, questions: x.questions.filter(q => q.id !== qid)
    }));
  const duplicateQuestion = (sid, qid) =>
    setSections(s => s.map(x => x.id !== sid ? x : {
      ...x, questions: x.questions.flatMap(q => q.id === qid ? [q, { ...q, id: uid("q") }] : [q])
    }));

  /* ------- on section type change, normalise question types & options ------- */
  const changeSectionType = (sid, newType) => {
    setSections(s => s.map(x => {
      if (x.id !== sid) return x;
      const defMarks = TYPE_OPTIONS.find(t => t.value === newType)?.defaultMarks ?? x.marks;
      return {
        ...x, type: newType, marks: defMarks,
        questions: x.questions.map(q => {
          if (q.type === newType) return q;
          const fresh = newQuestion(newType);
          return { ...fresh, id: q.id, text: q.text, concepts: q.concepts };
        }),
      };
    }));
  };

  /* ------- save ------- */
  const doSave = async (asDraft) => {
    if (!asDraft && blocking > 0) {
      setShowSaveConfirm(true);
      return;
    }
    const newTest = {
      title: title || "Untitled test",
      class_section_ids: meta.class_section_ids,
      className: meta.className,
      subject: meta.subject,
      topic: meta.topic || "—",
      chapter: meta.chapter || "—",
      date: meta.date,
      status: asDraft ? "draft" : "live",
      questions: totalQ,
      students: 0, reviewed: 0,
      language: meta.language || "bn",
      _payload: { title, meta, sections, asDraft },
    };
    try { await onSave(newTest); }
    catch { /* parent already surfaced the error; stay on the authoring screen */ }
  };

  const editingSection = editing ? sections.find(s => s.id === editing.sectionId) : null;
  const editingQ = editingSection?.questions.find(q => q.id === editing?.questionId);

  return (
    <div style={{ display:"flex", flexDirection:"column", height:"100%", background:"var(--bg-0)" }}>
      {/* ----- sticky top bar ----- */}
      <div style={{ padding:"10px 20px", borderBottom:"1px solid var(--line)", background:"var(--bg-1)", display:"flex", alignItems:"center", gap:10, flexWrap:"wrap" }}>
        <span className="muted mono" style={{ fontSize:11, cursor:"pointer" }} onClick={onCancel}>Test Library</span>
        <MIcon name="chevronRight" size={9}/>
        <span className="muted mono" style={{ fontSize:11 }}>New test · manual</span>
        <MIcon name="chevronRight" size={9}/>
        <input
          value={title}
          onChange={e => setTitle(e.target.value)}
          style={{ background:"transparent", border:"none", outline:"none", color:"var(--ink-0)", fontSize:14, fontWeight:600, padding:"2px 4px", borderRadius:3, minWidth:260, width:`${Math.max(260, title.length * 9)}px` }}
          onFocus={e => e.target.style.background = "var(--bg-2)"}
          onBlur={e => e.target.style.background = "transparent"}
        />
        <span className="pill" style={{ fontSize:10 }}>Class {meta.className}</span>
        <span className="pill" style={{ fontSize:10 }}>{meta.subject}</span>
        {meta.chapter && <span className="pill" style={{ fontSize:10 }}>{meta.chapter}</span>}
        <span className="pill mono" style={{ fontSize:10 }}>{meta.date}</span>
        <MStatusPill status="draft"/>
        <span className="muted" style={{ fontSize:11, marginLeft:8 }}>
          {savedAt ? `Autosaved ${savedAt.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}` : "Editing…"}
        </span>
        <span style={{ marginLeft:"auto", display:"flex", gap:8 }}>
          <button className="btn sm" onClick={onCancel}>Cancel</button>
          <button className="btn sm" onClick={() => doSave(true)}>Save draft</button>
          <button className="btn sm primary" onClick={() => doSave(false)} disabled={totalQ === 0}>
            <MIcon name="check" size={11}/> Save test
          </button>
        </span>
      </div>

      {/* ----- main split ----- */}
      <div style={{ flex:1, display:"grid", gridTemplateColumns:"300px 1fr", gap:20, padding:"20px 24px", overflow:"hidden" }}>
        {/* LEFT: outline summary */}
        <ManualOutline
          title={title}
          meta={meta}
          sections={sections}
          totalQ={totalQ}
          totalMarks={totalMarks}
          targetQ={targetQ}
          validation={validation}
          onJump={(sid) => {
            const el = document.querySelector(`[data-sec-id="${sid}"]`);
            if (el) el.scrollIntoView({ block:"start" });
          }}
        />

        {/* RIGHT: workspace */}
        <div style={{ overflow:"auto", paddingRight:8 }}>
          {sections.map((sec, idx) => (
            <React.Fragment key={sec.id}>
              <SectionCard
                section={sec}
                index={idx}
                onUpdate={(patch) => updateSection(sec.id, patch)}
                onTypeChange={(t) => changeSectionType(sec.id, t)}
                onDelete={() => deleteSection(sec.id)}
                canDelete={sections.length > 1}
                onUpdateQuestion={(qid, patch) => updateQuestion(sec.id, qid, patch)}
                onAddQuestion={() => addQuestion(sec.id)}
                onDeleteQuestion={(qid) => deleteQuestion(sec.id, qid)}
                onDuplicateQuestion={(qid) => duplicateQuestion(sec.id, qid)}
                onEditQuestion={(qid) => setEditing({ sectionId: sec.id, questionId: qid })}
                questionStartIndex={sections.slice(0, idx).reduce((a, s) => a + s.questions.length, 0)}
              />
              <AddSectionDivider onClick={() => insertSection(idx)} />
            </React.Fragment>
          ))}

          {/* footer save */}
          <div style={{ display:"flex", justifyContent:"flex-end", marginTop:24, gap:8 }}>
            <button className="btn" onClick={() => doSave(true)}>Save as draft</button>
            <button className="btn primary" onClick={() => doSave(false)} disabled={totalQ === 0}>
              <MIcon name="check" size={12}/> Save test
            </button>
          </div>
          <div style={{ height:40 }}/>
        </div>
      </div>

      {/* Edit question drawer */}
      {editing && editingQ && (
        <QuestionEditDrawer
          q={editingQ}
          sectionMarks={editingSection.marks}
          sectionType={editingSection.type}
          onUpdate={(patch) => updateQuestion(editing.sectionId, editing.questionId, patch)}
          onClose={() => setEditing(null)}
          onDelete={() => { deleteQuestion(editing.sectionId, editing.questionId); setEditing(null); }}
        />
      )}

      {/* Save-with-issues confirmation */}
      {showSaveConfirm && (
        <SaveConfirmModal
          validation={validation}
          onCancel={() => setShowSaveConfirm(false)}
          onSaveDraft={() => { setShowSaveConfirm(false); doSave(true); }}
        />
      )}
    </div>
  );
};

/* ---------- Left outline panel ---------- */
const ManualOutline = ({ title, meta, sections, totalQ, totalMarks, targetQ, validation, onJump }) => (
  <div style={{ display:"flex", flexDirection:"column", gap:14, overflow:"auto" }}>
    <div className="card">
      <div className="card-head">
        <MIcon name="report" size={13}/>
        <span className="card-title">Paper outline</span>
      </div>
      <div className="card-body" style={{ display:"flex", flexDirection:"column", gap:10 }}>
        <div>
          <div className="muted" style={{ fontSize:10, letterSpacing:".08em", textTransform:"uppercase", marginBottom:4 }}>Test</div>
          <div style={{ color:"var(--ink-0)", fontSize:13.5, fontWeight:600, lineHeight:1.35 }}>{title || "Untitled test"}</div>
          <div className="muted" style={{ fontSize:11, marginTop:3 }}>
            Class {meta.className} · {meta.subject}
            {meta.chapter ? ` · ${meta.chapter}` : ""}
          </div>
        </div>

        <div style={{ display:"grid", gridTemplateColumns:"1fr 1fr", gap:8, marginTop:4 }}>
          <Stat3 label="Questions" value={totalQ} sub={`target ${targetQ}`}/>
          <Stat3 label="Total marks" value={totalMarks}/>
          <Stat3 label="Sections" value={sections.length}/>
          <Stat3 label="Issues" value={validation.emptyQ + validation.mcqMissingCorrect}
            accent={(validation.emptyQ + validation.mcqMissingCorrect) > 0 ? "var(--red)" : "var(--green)"}/>
        </div>
      </div>
    </div>

    <div className="card">
      <div className="card-head">
        <MIcon name="list" size={13}/>
        <span className="card-title">Sections</span>
      </div>
      <div className="card-body" style={{ padding:0 }}>
        {sections.map((s, i) => {
          const sMarks = s.questions.reduce((a, q) => a + (q.marks ?? s.marks), 0);
          const onTrack = s.questions.length >= s.target;
          return (
            <div key={s.id} onClick={() => onJump(s.id)}
              style={{ padding:"10px 14px", borderBottom: i < sections.length - 1 ? "1px solid var(--line-soft)" : "none", cursor:"pointer", display:"flex", alignItems:"center", gap:10 }}
              onMouseEnter={e => e.currentTarget.style.background = "var(--bg-2)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <div style={{ width:24, height:24, borderRadius:4, background:"var(--bg-2)", border:"1px solid var(--line-strong)", display:"grid", placeItems:"center", color:"var(--accent)", fontSize:11, fontWeight:600 }}>
                {SECTION_NAMES[i] || i + 1}
              </div>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ color:"var(--ink-0)", fontSize:12, fontWeight:500, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>{s.label}</div>
                <div className="muted" style={{ fontSize:10.5, marginTop:1 }}>
                  {s.type} · {s.questions.length}/{s.target} Q · {sMarks} marks
                </div>
              </div>
              <span className="swatch" style={{ background: onTrack ? "var(--green)" : "var(--amber)" }}/>
            </div>
          );
        })}
      </div>
    </div>

    {(validation.emptyQ + validation.mcqMissingCorrect + validation.underTarget) > 0 && (
      <div className="card">
        <div className="card-head">
          <MIcon name="warn" size={13}/>
          <span className="card-title">Pre-save checks</span>
        </div>
        <div className="card-body" style={{ display:"flex", flexDirection:"column", gap:6, fontSize:11.5 }}>
          {validation.emptyQ > 0 && (
            <div style={{ color:"var(--red)" }}>
              <span className="swatch" style={{ background:"var(--red)", marginRight:6 }}/>
              {validation.emptyQ} question{validation.emptyQ === 1 ? "" : "s"} missing text
            </div>
          )}
          {validation.mcqMissingCorrect > 0 && (
            <div style={{ color:"var(--red)" }}>
              <span className="swatch" style={{ background:"var(--red)", marginRight:6 }}/>
              {validation.mcqMissingCorrect} MCQ{validation.mcqMissingCorrect === 1 ? "" : "s"} without correct answer
            </div>
          )}
          {validation.underTarget > 0 && (
            <div style={{ color:"var(--amber)" }}>
              <span className="swatch" style={{ background:"var(--amber)", marginRight:6 }}/>
              {validation.underTarget} section{validation.underTarget === 1 ? "" : "s"} under target count
            </div>
          )}
        </div>
      </div>
    )}

    <div className="card" style={{ fontSize:11, padding:"10px 12px", color:"var(--ink-3)", lineHeight:1.55 }}>
      <b style={{ color:"var(--ink-2)" }}>Tips:</b> Click any question to edit its stem, options, and concept tags. Sections inherit marks per Q; override per question in the editor.
    </div>
  </div>
);

const Stat3 = ({ label, value, sub, accent }) => (
  <div style={{ padding:"8px 10px", background:"var(--bg-2)", border:"1px solid var(--line)", borderRadius:6 }}>
    <div className="muted" style={{ fontSize:9.5, letterSpacing:".08em", textTransform:"uppercase", marginBottom:2 }}>{label}</div>
    <div className="mono" style={{ fontSize:16, fontWeight:600, color: accent || "var(--ink-0)" }}>{value}</div>
    {sub && <div className="muted" style={{ fontSize:10, marginTop:1 }}>{sub}</div>}
  </div>
);

/* ---------- Section card ---------- */
const SectionCard = ({
  section, index,
  onUpdate, onTypeChange, onDelete, canDelete,
  onUpdateQuestion, onAddQuestion, onDeleteQuestion, onDuplicateQuestion, onEditQuestion,
  questionStartIndex,
}) => (
  <div data-sec-id={section.id} style={{ marginBottom:20 }}>
    {/* section header row — orange-tinted, matches wireframe */}
    <div style={{
      display:"grid",
      gridTemplateColumns:"minmax(180px, 1.4fr) 140px 140px 140px auto",
      gap:10,
      padding:"10px 12px",
      background:"rgba(255,186,90,0.08)",
      border:"1px solid rgba(255,186,90,0.35)",
      borderRadius:8,
      alignItems:"center",
      marginBottom:12,
    }}>
      <SectionField label="Section" >
        <input
          value={section.label}
          onChange={e => onUpdate({ label: e.target.value })}
          style={{ ...inputStyle, color:"var(--accent)", fontWeight:600 }}
        />
      </SectionField>
      <SectionField label="Type">
        <select value={section.type} onChange={e => onTypeChange(e.target.value)} style={inputStyle}>
          {TYPE_OPTIONS.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
        </select>
      </SectionField>
      <SectionField label="Marks each Q">
        <input type="number" min={1} max={50} value={section.marks}
          onChange={e => onUpdate({ marks: parseInt(e.target.value) || 1 })}
          style={inputStyle}/>
      </SectionField>
      <SectionField label="Number of Q">
        <input type="number" min={1} max={100} value={section.target}
          onChange={e => onUpdate({ target: parseInt(e.target.value) || 1 })}
          style={inputStyle}/>
      </SectionField>
      <button
        className="btn sm ghost"
        onClick={onDelete}
        disabled={!canDelete}
        title="Delete section"
        style={{ opacity: canDelete ? 1 : 0.3, alignSelf:"end", marginBottom:2 }}>
        <MIcon name="trash" size={12}/>
      </button>
    </div>

    {/* instructions row (optional) */}
    <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:10, paddingLeft:4 }}>
      <span className="muted" style={{ fontSize:11 }}>Instructions:</span>
      <input
        value={section.instructions}
        onChange={e => onUpdate({ instructions: e.target.value })}
        placeholder={`e.g. ${section.type === "MCQ" ? "Tick the correct option." : "Answer in 2–3 sentences."}`}
        style={{ flex:1, background:"transparent", border:"none", borderBottom:"1px dashed var(--line-strong)", color:"var(--ink-1)", fontSize:12, padding:"3px 0", outline:"none", fontStyle: section.instructions ? "normal" : "italic" }}/>
      <span className="muted mono" style={{ fontSize:10 }}>
        {section.questions.length}/{section.target} Q · {section.questions.reduce((a,q) => a + (q.marks ?? section.marks), 0)} marks
      </span>
    </div>

    {/* questions */}
    <div style={{ display:"flex", flexDirection:"column", gap:8 }}>
      {section.questions.map((q, i) => (
        <QuestionRow
          key={q.id}
          q={q}
          serial={questionStartIndex + i + 1}
          sectionMarks={section.marks}
          onUpdate={(patch) => onUpdateQuestion(q.id, patch)}
          onEdit={() => onEditQuestion(q.id)}
          onDelete={() => onDeleteQuestion(q.id)}
          onDuplicate={() => onDuplicateQuestion(q.id)}
          canDelete={section.questions.length > 1}
        />
      ))}

      {/* add question button — green pill (matches wireframe) */}
      <div style={{ display:"flex", justifyContent:"flex-end", paddingRight:48, marginTop:4 }}>
        <button
          onClick={onAddQuestion}
          style={{
            display:"inline-flex", alignItems:"center", gap:6,
            padding:"6px 14px", borderRadius:999,
            background:"rgba(75,201,123,0.12)", border:"1px solid rgba(75,201,123,0.45)",
            color:"var(--green)", fontSize:11.5, fontWeight:600, cursor:"pointer",
          }}
          onMouseEnter={e => e.currentTarget.style.background = "rgba(75,201,123,0.22)"}
          onMouseLeave={e => e.currentTarget.style.background = "rgba(75,201,123,0.12)"}>
          <MIcon name="plus" size={11}/> Add question
        </button>
      </div>
    </div>
  </div>
);

const SectionField = ({ label, children }) => (
  <div style={{ display:"flex", flexDirection:"column", gap:3 }}>
    <span className="muted" style={{ fontSize:9.5, letterSpacing:".08em", textTransform:"uppercase" }}>{label}</span>
    {children}
  </div>
);

const inputStyle = {
  width:"100%",
  background:"var(--bg-1)",
  border:"1px solid var(--line-strong)",
  borderRadius:5,
  padding:"6px 9px",
  color:"var(--ink-0)",
  fontSize:12.5,
  outline:"none",
  fontFamily:"inherit",
};

/* ---------- Question row ---------- */
const QuestionRow = ({ q, serial, sectionMarks, onUpdate, onEdit, onDelete, onDuplicate, canDelete }) => {
  const isMcq = q.type === "MCQ" || q.type === "TF";
  const isLong = q.type === "Long";
  const hasCorrect = isMcq && q.options.some(o => o.correct);
  const empty = !q.text.trim() && (!isLong || (q.subQuestions || []).every(sq => !sq.text.trim()));
  const marks = q.marks ?? sectionMarks;

  /* ---- LONG ANSWER: stacked sub-question rows ---- */
  if (isLong) {
    const subs = q.subQuestions || [];
    const updateSub = (sid, patch) =>
      onUpdate({ subQuestions: subs.map(s => s.id === sid ? { ...s, ...patch } : s) });
    const addSub = () =>
      onUpdate({ subQuestions: [...subs, newSubQuestion()] });
    const removeSub = (sid) =>
      onUpdate({ subQuestions: subs.length > 1 ? subs.filter(s => s.id !== sid) : subs });

    return (
      <div style={{ display:"flex", alignItems:"stretch", gap:8 }}>
        {/* tall serial badge spans the whole group */}
        <div style={{
          width:56, flexShrink:0,
          display:"flex", flexDirection:"column", justifyContent:"center", alignItems:"center",
          background:"rgba(109,211,255,0.08)",
          border:"1px solid rgba(109,211,255,0.4)",
          borderRadius:6,
          color:"var(--accent-2, #6dd3ff)",
          fontSize:11, fontWeight:600,
          padding:"10px 0",
        }}>
          <span className="mono">Q{serial}</span>
          <span className="muted mono" style={{ fontSize:9, marginTop:2 }}>{marks}m</span>
          <span className="muted mono" style={{ fontSize:9, marginTop:6 }}>Long</span>
        </div>

        {/* sub-questions stack */}
        <div style={{ flex:1, display:"flex", flexDirection:"column", gap:8 }}>
          {/* optional parent stem (small, dashed) */}
          <input
            value={q.text}
            onChange={e => onUpdate({ text: e.target.value })}
            placeholder="Optional context / parent prompt (e.g. “Answer the following parts based on the passage above.”)"
            style={{
              background:"transparent", border:"1px dashed var(--line-strong)", borderRadius:5,
              color:"var(--ink-1)", padding:"6px 10px", fontSize:11.5, outline:"none",
              fontStyle: q.text ? "normal" : "italic",
            }}/>

          {subs.map((sq, i) => (
            <div key={sq.id} style={{
              display:"flex", alignItems:"stretch", gap:8,
              border:`1px solid ${!sq.text.trim() ? "rgba(255,107,107,0.4)" : "rgba(109,211,255,0.4)"}`,
              borderRadius:6,
              background:"var(--bg-1)",
            }}>
              <div style={{
                width:46, flexShrink:0, display:"grid", placeItems:"center",
                color:"var(--accent)", fontSize:12, fontWeight:600,
                borderRight:"1px dashed var(--line-soft)",
              }}>
                <span className="mono">{String.fromCharCode(97 + i)}.</span>
              </div>
              <textarea
                value={sq.text}
                onChange={e => updateSub(sq.id, { text: e.target.value })}
                placeholder={`Sub-question ${String.fromCharCode(97 + i)} text`}
                rows={1}
                style={{
                  flex:1,
                  background:"transparent", border:"none", outline:"none", resize:"none",
                  color:"var(--accent)", padding:"10px 12px",
                  fontSize:13, lineHeight:1.5, fontFamily:"inherit", fontWeight:500,
                  minHeight:38,
                }}
                onInput={e => { e.target.style.height = "auto"; e.target.style.height = e.target.scrollHeight + "px"; }}/>
              <button className="btn sm ghost" onClick={() => removeSub(sq.id)}
                disabled={subs.length <= 1}
                title="Remove sub-question"
                style={{ padding:"0 10px", opacity: subs.length <= 1 ? 0.3 : 1, alignSelf:"stretch" }}>
                <MIcon name="trash" size={11}/>
              </button>
            </div>
          ))}

          {/* add sub-question + meta strip */}
          <div style={{ display:"flex", alignItems:"center", gap:10, padding:"4px 2px 0", fontSize:10.5, color:"var(--ink-3)" }}>
            <span className="mono">Long · {subs.length} sub-Q</span>
            {q.concepts.length > 0 && (
              <span style={{ display:"flex", gap:4 }}>
                {q.concepts.slice(0, 2).map((c, i) => <span key={i} className="pill mono" style={{ fontSize:9 }}>{c}</span>)}
                {q.concepts.length > 2 && <span className="muted" style={{ fontSize:10 }}>+{q.concepts.length - 2}</span>}
              </span>
            )}
            <span style={{ marginLeft:"auto", display:"flex", gap:6, alignItems:"center" }}>
              <button onClick={addSub}
                style={{
                  display:"inline-flex", alignItems:"center", gap:6,
                  padding:"5px 12px", borderRadius:6,
                  background:"transparent",
                  border:"1px solid var(--line-strong)",
                  color:"var(--ink-1)", fontSize:11, fontWeight:500, cursor:"pointer",
                }}
                onMouseEnter={e => e.currentTarget.style.background = "var(--bg-2)"}
                onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
                <MIcon name="plus" size={10}/> Add sub-question
              </button>
              <button className="btn sm ghost" style={{ padding:"4px 7px" }} onClick={onDuplicate} title="Duplicate question">
                <MIcon name="copy" size={10}/>
              </button>
              <button className="btn sm ghost" style={{ padding:"4px 7px" }} onClick={onDelete} disabled={!canDelete} title="Delete question">
                <MIcon name="trash" size={10}/>
              </button>
            </span>
          </div>
        </div>

        {/* Edit pill */}
        <button
          onClick={onEdit}
          style={{
            alignSelf:"stretch",
            padding:"0 18px",
            borderRadius:6,
            background:"rgba(255,107,107,0.08)",
            border:"1px solid rgba(255,107,107,0.45)",
            color:"var(--red)",
            fontSize:11.5, fontWeight:600,
            cursor:"pointer",
            display:"flex", alignItems:"center", gap:6,
          }}
          onMouseEnter={e => e.currentTarget.style.background = "rgba(255,107,107,0.18)"}
          onMouseLeave={e => e.currentTarget.style.background = "rgba(255,107,107,0.08)"}>
          <MIcon name="edit" size={11}/> Edit
        </button>
      </div>
    );
  }

  return (
    <div style={{ display:"flex", alignItems:"stretch", gap:8 }}>
      {/* serial badge — blue (matches wireframe) */}
      <div style={{
        width:56, flexShrink:0,
        display:"flex", flexDirection:"column", justifyContent:"center", alignItems:"center",
        background:"rgba(109,211,255,0.08)",
        border:"1px solid rgba(109,211,255,0.4)",
        borderRadius:6,
        color:"var(--accent-2, #6dd3ff)",
        fontSize:11, fontWeight:600,
      }}>
        <span className="mono">Q{serial}</span>
        <span className="muted mono" style={{ fontSize:9, marginTop:2 }}>{marks}m</span>
      </div>

      {/* main question text area */}
      <div style={{
        flex:1,
        border:`1px solid ${empty ? "rgba(255,107,107,0.4)" : "rgba(109,211,255,0.4)"}`,
        borderRadius:6,
        background:"var(--bg-1)",
        display:"flex",
        flexDirection:"column",
      }}>
        <textarea
          value={q.text}
          onChange={e => onUpdate({ text: e.target.value })}
          placeholder={`Question text · ${q.type}${isMcq ? " · select correct option in editor" : ""}`}
          rows={1}
          style={{
            background:"transparent", border:"none", outline:"none", resize:"none",
            color:"var(--ink-0)", padding:"10px 12px",
            fontSize:13, lineHeight:1.5, fontFamily:"inherit",
            minHeight:38,
          }}
          onInput={e => { e.target.style.height = "auto"; e.target.style.height = e.target.scrollHeight + "px"; }}
        />

        {/* compact preview row when MCQ has option text filled */}
        {isMcq && q.options.some(o => o.text) && (
          <div style={{ padding:"0 12px 8px", display:"flex", gap:14, fontSize:11, color:"var(--ink-2)", flexWrap:"wrap" }}>
            {q.options.map(o => (
              <span key={o.id} style={{ display:"inline-flex", alignItems:"center", gap:4, color: o.correct ? "var(--green)" : "var(--ink-2)" }}>
                {o.correct && <MIcon name="check" size={10}/>}
                <b style={{ marginRight:2 }}>({o.id})</b> {o.text || <span className="muted" style={{ fontStyle:"italic" }}>empty</span>}
              </span>
            ))}
          </div>
        )}

        {/* meta strip */}
        <div style={{ display:"flex", alignItems:"center", gap:10, padding:"6px 12px", borderTop:"1px dashed var(--line-soft)", fontSize:10.5, color:"var(--ink-3)" }}>
          <span className="mono">{q.type}</span>
          {q.concepts.length > 0 && (
            <span style={{ display:"flex", gap:4 }}>
              {q.concepts.slice(0, 2).map((c, i) => <span key={i} className="pill mono" style={{ fontSize:9 }}>{c}</span>)}
              {q.concepts.length > 2 && <span className="muted" style={{ fontSize:10 }}>+{q.concepts.length - 2}</span>}
            </span>
          )}
          {isMcq && !hasCorrect && !empty && <span className="pill red" style={{ fontSize:9 }}>no correct option</span>}
          {empty && <span className="pill red" style={{ fontSize:9 }}>empty</span>}
          <span style={{ marginLeft:"auto", display:"flex", gap:6 }}>
            <button className="btn sm ghost" style={{ padding:"2px 7px", fontSize:10 }} onClick={onDuplicate} title="Duplicate">
              <MIcon name="copy" size={10}/>
            </button>
            <button className="btn sm ghost" style={{ padding:"2px 7px", fontSize:10 }} onClick={onDelete} disabled={!canDelete} title="Delete">
              <MIcon name="trash" size={10}/>
            </button>
          </span>
        </div>
      </div>

      {/* Edit button — pill (matches wireframe) */}
      <button
        onClick={onEdit}
        style={{
          alignSelf:"stretch",
          padding:"0 18px",
          borderRadius:6,
          background:"rgba(255,107,107,0.08)",
          border:"1px solid rgba(255,107,107,0.45)",
          color:"var(--red)",
          fontSize:11.5, fontWeight:600,
          cursor:"pointer",
          display:"flex", alignItems:"center", gap:6,
        }}
        onMouseEnter={e => e.currentTarget.style.background = "rgba(255,107,107,0.18)"}
        onMouseLeave={e => e.currentTarget.style.background = "rgba(255,107,107,0.08)"}>
        <MIcon name="edit" size={11}/> Edit
      </button>
    </div>
  );
};

/* ---------- Add-section divider ---------- */
const AddSectionDivider = ({ onClick }) => (
  <div style={{ display:"flex", alignItems:"center", gap:0, margin:"6px 0 18px" }}>
    <div style={{ flex:1, height:1, background:"rgba(255,186,90,0.3)" }}/>
    <button onClick={onClick}
      style={{
        padding:"6px 18px",
        background:"rgba(255,186,90,0.10)",
        border:"1px solid rgba(255,186,90,0.4)",
        borderRadius:999,
        color:"var(--accent)",
        fontSize:11.5, fontWeight:600,
        cursor:"pointer",
        display:"inline-flex", alignItems:"center", gap:6,
      }}
      onMouseEnter={e => e.currentTarget.style.background = "rgba(255,186,90,0.20)"}
      onMouseLeave={e => e.currentTarget.style.background = "rgba(255,186,90,0.10)"}>
      <MIcon name="plus" size={11}/> Add section
    </button>
    <div style={{ flex:1, height:1, background:"rgba(255,186,90,0.3)" }}/>
  </div>
);

/* ---------- Question Edit Drawer ---------- */
const QuestionEditDrawer = ({ q, sectionMarks, sectionType, onUpdate, onClose, onDelete }) => {
  const [conceptDraft, setConceptDraft] = useState("");
  const [suggesting, setSuggesting] = useState(false);
  const [suggestErr, setSuggestErr] = useState(null);
  const isMcq = q.type === "MCQ" || q.type === "TF";
  const effectiveMarks = q.marks ?? sectionMarks;
  const overrideMarks = q.marks != null;

  const setOption = (idx, patch) => {
    onUpdate({ options: q.options.map((o, i) => i === idx ? { ...o, ...patch } : o) });
  };
  const toggleCorrect = (idx) => {
    // single-correct for MCQ/TF
    onUpdate({ options: q.options.map((o, i) => ({ ...o, correct: i === idx ? !o.correct : false })) });
  };
  const addOption = () => {
    if (q.options.length >= 6) return;
    const nextId = String.fromCharCode(97 + q.options.length);
    onUpdate({ options: [...q.options, { id: nextId, text:"", correct:false }] });
  };
  const removeOption = (idx) => {
    if (q.options.length <= 2) return;
    onUpdate({ options: q.options.filter((_, i) => i !== idx).map((o, i) => ({ ...o, id: String.fromCharCode(97 + i) })) });
  };
  const addConcept = () => {
    const t = conceptDraft.trim();
    if (!t) return;
    if (q.concepts.includes(t)) { setConceptDraft(""); return; }
    onUpdate({ concepts: [...q.concepts, t] });
    setConceptDraft("");
  };
  const removeConcept = (c) => onUpdate({ concepts: q.concepts.filter(x => x !== c) });

  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.55)", zIndex:200, display:"flex", justifyContent:"flex-end" }}>
      <div onClick={e => e.stopPropagation()} style={{
        width:560, maxWidth:"100%", height:"100%",
        background:"var(--bg-1)", borderLeft:"1px solid var(--line-strong)",
        display:"flex", flexDirection:"column", overflow:"hidden",
        animation:"slideInRight .2s ease-out",
      }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:10 }}>
          <MIcon name="edit" size={14}/>
          <span style={{ color:"var(--ink-0)", fontSize:14, fontWeight:600 }}>Edit question</span>
          <span className="pill mono" style={{ marginLeft:6, fontSize:10 }}>{q.type}</span>
          <span className="pill mono" style={{ fontSize:10 }}>{effectiveMarks}m</span>
          <button className="btn sm ghost" style={{ marginLeft:"auto" }} onClick={onClose}>✕</button>
        </div>

        <div style={{ flex:1, overflow:"auto", padding:"16px 20px", display:"flex", flexDirection:"column", gap:16 }}>
          {/* Stem */}
          <div className="field">
            <span className="label">Question stem</span>
            <textarea
              className="textarea"
              value={q.text}
              onChange={e => onUpdate({ text: e.target.value })}
              placeholder="Type the question. Supports LaTeX (e.g. $\frac{a}{b}$) and unicode."
              style={{ minHeight:90, fontSize:13.5, lineHeight:1.55 }}/>
          </div>

          {/* Type override */}
          <div style={{ display:"grid", gridTemplateColumns:"1fr 1fr", gap:12 }}>
            <div className="field">
              <span className="label">Type override</span>
              <select className="select" value={q.type} onChange={e => {
                const fresh = newQuestion(e.target.value);
                onUpdate({ type: e.target.value, options: fresh.options });
              }}>
                {TYPE_OPTIONS.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
              </select>
            </div>
            <div className="field">
              <span className="label">
                Marks
                <label style={{ marginLeft:8, fontSize:10, color:"var(--ink-3)", cursor:"pointer" }}>
                  <input type="checkbox" checked={overrideMarks}
                    onChange={e => onUpdate({ marks: e.target.checked ? sectionMarks : null })}
                    style={{ verticalAlign:"middle", marginRight:4 }}/>
                  override
                </label>
              </span>
              <input className="input" type="number" min={1} max={50}
                value={effectiveMarks}
                disabled={!overrideMarks}
                onChange={e => onUpdate({ marks: parseInt(e.target.value) || 1 })}
                style={{ opacity: overrideMarks ? 1 : 0.6 }}/>
            </div>
          </div>

          {/* Options for MCQ / TF */}
          {isMcq && (
            <div className="field">
              <span className="label">Options · select correct answer</span>
              <div style={{ display:"flex", flexDirection:"column", gap:6 }}>
                {q.options.map((o, i) => (
                  <div key={o.id} style={{
                    display:"flex", alignItems:"center", gap:8,
                    padding:"6px 8px",
                    border:`1px solid ${o.correct ? "var(--green)" : "var(--line-strong)"}`,
                    background: o.correct ? "rgba(75,201,123,0.06)" : "var(--bg-2)",
                    borderRadius:5,
                  }}>
                    <button onClick={() => toggleCorrect(i)}
                      title={o.correct ? "Correct answer" : "Mark as correct"}
                      style={{
                        width:22, height:22, borderRadius:"50%",
                        border:`1.5px solid ${o.correct ? "var(--green)" : "var(--line-strong)"}`,
                        background: o.correct ? "var(--green)" : "transparent",
                        color:"#fff", display:"grid", placeItems:"center",
                        cursor:"pointer", fontSize:11, fontWeight:700, padding:0,
                      }}>
                      {o.correct ? "✓" : ""}
                    </button>
                    <span className="mono" style={{ color:"var(--ink-2)", width:18, fontWeight:600 }}>({o.id})</span>
                    <input
                      className="input"
                      value={o.text}
                      onChange={e => setOption(i, { text: e.target.value })}
                      placeholder={`Option ${o.id}`}
                      disabled={q.type === "TF"}
                      style={{ flex:1, background:"transparent", border:"none", padding:"2px 4px", fontSize:13 }}/>
                    {q.type !== "TF" && (
                      <button className="btn sm ghost" onClick={() => removeOption(i)}
                        disabled={q.options.length <= 2}
                        style={{ padding:"2px 6px", opacity: q.options.length <= 2 ? 0.3 : 1 }}>
                        <MIcon name="trash" size={10}/>
                      </button>
                    )}
                  </div>
                ))}
              </div>
              {q.type !== "TF" && q.options.length < 6 && (
                <button className="btn sm" onClick={addOption} style={{ marginTop:6, alignSelf:"flex-start" }}>
                  <MIcon name="plus" size={10}/> Add option
                </button>
              )}
            </div>
          )}

          {/* Sub-questions for Long */}
          {q.type === "Long" && (
            <div className="field">
              <span className="label">Sub-questions</span>
              <div style={{ display:"flex", flexDirection:"column", gap:6 }}>
                {(q.subQuestions || []).map((sq, i) => (
                  <div key={sq.id} style={{ display:"flex", alignItems:"center", gap:8 }}>
                    <span className="mono" style={{ color:"var(--accent)", width:22, fontWeight:600 }}>{String.fromCharCode(97 + i)}.</span>
                    <input className="input" value={sq.text}
                      onChange={e => onUpdate({ subQuestions: q.subQuestions.map(x => x.id === sq.id ? { ...x, text: e.target.value } : x) })}
                      placeholder={`Sub-question ${String.fromCharCode(97 + i)}`}
                      style={{ flex:1 }}/>
                    <button className="btn sm ghost" style={{ padding:"2px 6px" }}
                      onClick={() => onUpdate({ subQuestions: q.subQuestions.filter(x => x.id !== sq.id) })}
                      disabled={q.subQuestions.length <= 1}>
                      <MIcon name="trash" size={10}/>
                    </button>
                  </div>
                ))}
              </div>
              <button className="btn sm" onClick={() => onUpdate({ subQuestions: [...(q.subQuestions || []), newSubQuestion()] })}
                style={{ marginTop:6, alignSelf:"flex-start" }}>
                <MIcon name="plus" size={10}/> Add sub-question
              </button>
            </div>
          )}

          {/* Expected answer for non-MCQ */}
          {!isMcq && (
            <div className="field">
              <span className="label" style={{ display:"flex", alignItems:"center", gap: 6 }}>
                <span>Expected answer / marking scheme · optional</span>
                <button
                  className="btn sm"
                  style={{ marginLeft:"auto", padding:"2px 8px", fontSize: 10 }}
                  disabled={!q.text.trim() || suggesting}
                  onClick={async () => {
                    if (!q.text.trim()) return;
                    setSuggesting(true);
                    setSuggestErr(null);
                    try {
                      const out = await window.KXApi.post("/questions/suggest-expected-answer", {
                        question_text: q.text,
                        question_type: q.type === "Long" ? "long" : q.type === "Short" ? "short" : q.type.toLowerCase(),
                        max_marks: q.marks ?? sectionMarks ?? 1,
                        language: "bn",
                        concepts: q.concepts || [],
                      });
                      // Compose marking scheme + model answer into the textarea.
                      const body = [
                        out.marking_scheme && `Marking scheme:\n${out.marking_scheme}`,
                        out.model_answer && `Model answer:\n${out.model_answer}`,
                      ].filter(Boolean).join("\n\n");
                      onUpdate({ expectedAnswer: body });
                    } catch (e) { setSuggestErr(e?.message || String(e)); }
                    setSuggesting(false);
                  }}>
                  {suggesting ? "Suggesting…" : "✨ Suggest with Gemini"}
                </button>
              </span>
              <textarea
                className="textarea"
                value={q.expectedAnswer}
                onChange={e => onUpdate({ expectedAnswer: e.target.value })}
                placeholder="Key points the model answer must cover. Used by AI grading to allocate partial credit. ✨ Suggest with Gemini autofills both."
                style={{ minHeight:80, fontSize:13, lineHeight:1.55 }}/>
              {suggestErr && <div style={{ marginTop: 4, fontSize: 11, color: "var(--red)" }}>{suggestErr}</div>}
            </div>
          )}

          {/* Concepts */}
          <div className="field">
            <span className="label">Concept tags</span>
            <div style={{ display:"flex", gap:6, flexWrap:"wrap", marginBottom:6 }}>
              {q.concepts.map(c => (
                <span key={c} className="pill mono" style={{ fontSize:11, paddingRight:4, display:"inline-flex", alignItems:"center", gap:4 }}>
                  {c}
                  <button onClick={() => removeConcept(c)} style={{ background:"none", border:"none", color:"var(--ink-3)", cursor:"pointer", padding:"0 2px", fontSize:11 }}>×</button>
                </span>
              ))}
              {q.concepts.length === 0 && <span className="muted" style={{ fontSize:11, fontStyle:"italic" }}>No concepts tagged.</span>}
            </div>
            <div style={{ display:"flex", gap:6 }}>
              <input className="input" value={conceptDraft} onChange={e => setConceptDraft(e.target.value)}
                onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addConcept(); } }}
                placeholder="e.g. chem.xi.hydrocarbons.unsaturation"
                style={{ flex:1 }}/>
              <button className="btn sm" onClick={addConcept}>Add</button>
            </div>
            <div className="muted" style={{ fontSize:10.5, marginTop:4 }}>
              Used by class-report misconception detection and personalised study guides.
            </div>
          </div>
        </div>

        <div style={{ padding:12, borderTop:"1px solid var(--line)", display:"flex", gap:8 }}>
          <button className="btn sm" style={{ color:"var(--red)" }} onClick={onDelete}>
            <MIcon name="trash" size={11}/> Delete question
          </button>
          <span style={{ marginLeft:"auto" }}>
            <button className="btn sm primary" onClick={onClose}>Done</button>
          </span>
        </div>
      </div>
    </div>
  );
};

/* ---------- Save-with-issues confirmation ---------- */
const SaveConfirmModal = ({ validation, onCancel, onSaveDraft }) => (
  <div onClick={onCancel} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.7)", zIndex:300, display:"grid", placeItems:"center", padding:30 }}>
    <div onClick={e => e.stopPropagation()} style={{ background:"var(--bg-1)", border:"1px solid var(--line-strong)", borderRadius:10, width:480, padding:20 }}>
      <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:12 }}>
        <MIcon name="warn" size={16}/>
        <span style={{ color:"var(--ink-0)", fontSize:15, fontWeight:600 }}>Can't publish this test yet</span>
      </div>
      <p className="muted" style={{ fontSize:13, lineHeight:1.55, margin:"0 0 14px" }}>
        Fix the issues below to mark this test as <b>Upcoming</b>, or save as <b>Draft</b> and continue later.
      </p>
      <ul style={{ margin:"0 0 16px", paddingLeft:18, fontSize:12.5, color:"var(--ink-1)", lineHeight:1.7 }}>
        {validation.emptyQ > 0 && <li style={{ color:"var(--red)" }}>{validation.emptyQ} question{validation.emptyQ === 1 ? "" : "s"} missing text</li>}
        {validation.mcqMissingCorrect > 0 && <li style={{ color:"var(--red)" }}>{validation.mcqMissingCorrect} MCQ{validation.mcqMissingCorrect === 1 ? "" : "s"} without a correct option marked</li>}
      </ul>
      <div style={{ display:"flex", gap:8, justifyContent:"flex-end" }}>
        <button className="btn sm" onClick={onCancel}>Back to fix</button>
        <button className="btn sm primary" onClick={onSaveDraft}>Save as draft</button>
      </div>
    </div>
  </div>
);

/* ====================================================================
   Chapter + topic multi-select chip picker
   --------------------------------------------------------------------
   Sourced from the admin-curated catalog (/catalog/chapters scoped to
   class × subject, /catalog/topics scoped to chapter). meta.chapter
   and meta.topic stay as comma-separated TEXT — back-compat with legacy
   single-name rows is automatic since a one-element list serialises to
   the original text. Topics span every selected chapter; deselecting a
   chapter auto-unselects any of its topics so the saved set stays
   internally consistent.
   ==================================================================== */
const parseCSV = (raw) => {
  if (raw == null) return [];
  const s = String(raw).trim();
  if (!s) return [];
  return s.split(/\s*,\s*/).map((x) => x.trim()).filter(Boolean);
};
const joinCSV = (xs) => {
  const arr = (xs || []).map((s) => String(s).trim()).filter(Boolean);
  if (arr.length === 0) return "";
  const seen = new Set(); const uniq = [];
  for (const x of arr) { if (!seen.has(x)) { seen.add(x); uniq.push(x); } }
  return uniq.join(", ");
};

const ChapterTopicFields = ({ meta, setMeta, subjects }) => {
  const subjectId = React.useMemo(
    () => subjects.find((s) => s.name === meta.subject)?.id || null,
    [subjects, meta.subject],
  );

  const [chapters, setChapters] = React.useState([]);
  // Per-chapter topic cache keyed by chapter_id. Lets us cheaply unite topics
  // across every selected chapter without refetching when the user toggles
  // chapters on/off.
  const [topicsByChapter, setTopicsByChapter] = React.useState({});
  const [loadingChapters, setLoadingChapters] = React.useState(false);

  // Chapter list keyed on (className × subjectId). Cleared on switch so stale
  // catalog rows never linger.
  React.useEffect(() => {
    if (!subjectId || !meta.className) { setChapters([]); return; }
    let aborted = false;
    setLoadingChapters(true);
    const q = new URLSearchParams({ subject_id: subjectId, class_level: meta.className });
    window.KXApi.get(`/catalog/chapters?${q.toString()}`)
      .then((rows) => { if (!aborted) setChapters(rows || []); })
      .catch(() => { if (!aborted) setChapters([]); })
      .finally(() => { if (!aborted) setLoadingChapters(false); });
    return () => { aborted = true; };
  }, [subjectId, meta.className]);

  // Parse the comma-joined selection back into arrays for the chip UI.
  const selectedChapterNames = React.useMemo(() => parseCSV(meta.chapter), [meta.chapter]);
  const selectedTopicNames   = React.useMemo(() => parseCSV(meta.topic),   [meta.topic]);

  // Match selected chapter names back to catalog rows so we know their IDs
  // (for the topic fetch) and can warn when a name is freeform (legacy).
  const selectedChapterRows = chapters.filter((c) => selectedChapterNames.includes(c.chapter_name));
  const freeformChapterNames = selectedChapterNames.filter(
    (n) => !chapters.some((c) => c.chapter_name === n),
  );

  // Fetch topics for any selected chapter we haven't seen yet. Cached
  // forever in this component instance — refetching on every toggle would
  // waste round-trips.
  React.useEffect(() => {
    const missing = selectedChapterRows.filter((c) => !(c.id in topicsByChapter));
    if (missing.length === 0) return;
    let aborted = false;
    Promise.all(missing.map((c) =>
      window.KXApi.get(`/catalog/topics?chapter_id=${encodeURIComponent(c.id)}`)
        .then((rows) => ({ id: c.id, rows: rows || [] }))
        .catch(() => ({ id: c.id, rows: [] })),
    )).then((results) => {
      if (aborted) return;
      setTopicsByChapter((prev) => {
        const next = { ...prev };
        for (const { id, rows } of results) next[id] = rows;
        return next;
      });
    });
    return () => { aborted = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedChapterRows.map((c) => c.id).join("|")]);

  // Flat list of available topics across every selected chapter, with the
  // chapter number prefixed so the same-named "Mole concept" topic in two
  // chapters can be told apart at a glance.
  const availableTopics = selectedChapterRows.flatMap((c) =>
    (topicsByChapter[c.id] || []).map((t) => ({
      chapterId: c.id, chapterNumber: c.chapter_number,
      topicId: t.id, topicNumber: t.topic_number, topicName: t.topic_name,
    })),
  );

  const toggleChapter = (name) => {
    const has = selectedChapterNames.includes(name);
    const nextChapterNames = has
      ? selectedChapterNames.filter((n) => n !== name)
      : [...selectedChapterNames, name];
    // When a chapter is removed, prune topics that only existed under it.
    // Topic names are unique within a chapter; we drop any topic whose name
    // doesn't appear in ANY still-selected chapter's topic catalog.
    let nextTopicNames = selectedTopicNames;
    if (has) {
      const stillValid = new Set();
      for (const c of chapters) {
        if (!nextChapterNames.includes(c.chapter_name)) continue;
        for (const t of (topicsByChapter[c.id] || [])) stillValid.add(t.topic_name);
      }
      nextTopicNames = selectedTopicNames.filter((n) => stillValid.has(n));
    }
    setMeta({ ...meta, chapter: joinCSV(nextChapterNames), topic: joinCSV(nextTopicNames) });
  };
  const toggleTopic = (name) => {
    const has = selectedTopicNames.includes(name);
    const next = has
      ? selectedTopicNames.filter((n) => n !== name)
      : [...selectedTopicNames, name];
    setMeta({ ...meta, topic: joinCSV(next) });
  };
  const removeFreeformChapter = (name) => {
    setMeta({
      ...meta,
      chapter: joinCSV(selectedChapterNames.filter((n) => n !== name)),
    });
  };

  const noPair = !subjectId || !meta.className;

  return (
    <>
      <div className="field" style={{ gridColumn: "1 / span 2" }}>
        <span className="label">
          Chapter
          <span className="muted" style={{ fontSize: 10, marginLeft: 6 }}>
            tap to {selectedChapterNames.length ? "toggle" : "add"} — multi-select
          </span>
        </span>
        {noPair ? (
          <span className="muted" style={{ fontSize: 11 }}>Pick a class + subject first.</span>
        ) : loadingChapters ? (
          <span className="muted" style={{ fontSize: 11 }}>Loading chapters…</span>
        ) : chapters.length === 0 ? (
          <span className="muted" style={{ fontSize: 11 }}>
            No chapters for class {meta.className} · {meta.subject}. Add some on the Chapters screen.
          </span>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
            {chapters.map((c) => {
              const on = selectedChapterNames.includes(c.chapter_name);
              return (
                <button
                  key={c.id} type="button"
                  className={`btn sm ${on ? "primary" : "ghost"}`}
                  style={{ padding: "4px 10px", fontSize: 11 }}
                  onClick={() => toggleChapter(c.chapter_name)}>
                  Ch {c.chapter_number}. {c.chapter_name}
                </button>
              );
            })}
            {freeformChapterNames.map((n) => (
              <span key={`legacy-${n}`}
                style={{
                  padding: "4px 8px", fontSize: 11, border: "1px dashed var(--line-strong)",
                  borderRadius: 4, color: "var(--ink-2)", display: "inline-flex", alignItems: "center", gap: 6,
                }}>
                {n} <small className="muted">(legacy)</small>
                <button className="btn ghost sm" style={{ padding: 0, fontSize: 11 }}
                  onClick={() => removeFreeformChapter(n)}>✕</button>
              </span>
            ))}
          </div>
        )}
      </div>
      <div className="field" style={{ gridColumn: "1 / span 2" }}>
        <span className="label">
          Topic
          <span className="muted" style={{ fontSize: 10, marginLeft: 6 }}>
            optional — multi-select across selected chapters
          </span>
        </span>
        {selectedChapterRows.length === 0 ? (
          <span className="muted" style={{ fontSize: 11 }}>Pick a chapter first.</span>
        ) : availableTopics.length === 0 ? (
          <span className="muted" style={{ fontSize: 11 }}>
            No topics under the selected chapter{selectedChapterRows.length === 1 ? "" : "s"} yet.
          </span>
        ) : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
            {availableTopics.map((t) => {
              const on = selectedTopicNames.includes(t.topicName);
              return (
                <button
                  key={`${t.chapterId}-${t.topicId}`} type="button"
                  className={`btn sm ${on ? "primary" : "ghost"}`}
                  style={{ padding: "4px 10px", fontSize: 11 }}
                  onClick={() => toggleTopic(t.topicName)}
                  title={`Ch ${t.chapterNumber}`}>
                  <span className="mono muted" style={{ fontSize: 9, marginRight: 4 }}>
                    Ch{t.chapterNumber}·
                  </span>
                  {t.topicNumber}. {t.topicName}
                </button>
              );
            })}
          </div>
        )}
      </div>
    </>
  );
};

/* ====================================================================
   Test Details modal — opens before manual authoring
   ==================================================================== */
const TestDetailsModal = ({ open, onClose, onContinue }) => {
  const classSections = window.KX?.CLASS_SECTIONS || [];
  const subjects = window.KX?.SUBJECTS || [];
  const defaultCs = classSections[0]?.id || "";
  const defaultSubject = subjects[0]?.name || "Chemistry";

  const [meta, setMeta] = useState({
    title: "",
    class_section_ids: defaultCs ? [defaultCs] : [],
    className: classSections[0]?.class_level || "",
    subject: defaultSubject,
    topic: "", chapter: "", date: "2026-05-20", language: "bn",
    durationMin: 60,
    // How students hand in descriptive answers: 'per_question' (upload during
    // the exam) | 'bulk' (upload all photos + an optional PDF after submit).
    // Only applies to tests with a descriptive (Short/Long) section — the
    // backend coerces it back to per_question otherwise.
    submissionMode: "per_question",
  });
  const [method, setMethod] = useState("manual"); // manual | paste | upload

  // Reset defaults when the catalog finishes loading after first render.
  React.useEffect(() => {
    if (meta.class_section_ids.length === 0 && defaultCs) {
      setMeta(m => ({
        ...m,
        class_section_ids: [defaultCs],
        className: classSections[0]?.class_level || "",
      }));
    }
  }, [defaultCs]);

  // Distinct class levels for the Class dropdown.
  const classLevels = Array.from(new Set(classSections.map(cs => cs.class_level))).sort();
  const pickClass = (cl) => {
    // When the class changes, pre-select its first section so the chip row
    // isn't empty; the teacher can toggle more on/off from there.
    const first = classSections.find(cs => cs.class_level === cl);
    setMeta({ ...meta, className: cl, class_section_ids: first ? [first.id] : [] });
  };

  if (!open) return null;
  const canContinue = meta.title.trim().length >= 3 && meta.class_section_ids.length > 0;

  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.78)", zIndex:200, display:"grid", placeItems:"center", padding:40 }}>
      <div onClick={e => e.stopPropagation()} style={{
        background:"var(--bg-1)", border:"1px solid var(--line-strong)", borderRadius:10,
        width:780, maxWidth:"100%", maxHeight:"90vh", display:"flex", flexDirection:"column",
      }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:10 }}>
          <MIcon name="plus" size={14}/>
          <span style={{ color:"var(--ink-0)", fontSize:14, fontWeight:600 }}>Create new test</span>
          <span className="muted" style={{ fontSize:11, marginLeft:8 }}>Set the details — you can edit anything later.</span>
          <button className="btn sm ghost" style={{ marginLeft:"auto" }} onClick={onClose}>✕</button>
        </div>

        <div style={{ padding:20, overflow:"auto", flex:1, display:"flex", flexDirection:"column", gap:16 }}>
          {/* Method picker */}
          <div>
            <div style={{ fontSize:11, color:"var(--ink-3)", letterSpacing:".08em", textTransform:"uppercase", marginBottom:8 }}>1 · How to author?</div>
            <div style={{ display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:8 }}>
              <MethodCard active={method==="manual"} onClick={()=>setMethod("manual")}
                icon="edit" title="Compose manually"
                sub="Add sections & questions one by one." />
              <MethodCard active={method==="paste"} onClick={()=>setMethod("paste")}
                icon="sparkle" title="Paste & structure"
                sub="Paste text, Gemini structures it." />
              <MethodCard active={method==="upload"} onClick={()=>setMethod("upload")}
                icon="upload" title="Upload PDF"
                sub="OCR + structure with Gemini." />
            </div>
          </div>

          {/* Meta form */}
          <div>
            <div style={{ fontSize:11, color:"var(--ink-3)", letterSpacing:".08em", textTransform:"uppercase", marginBottom:8 }}>2 · Test details</div>
            <div style={{ display:"grid", gridTemplateColumns:"1fr 1fr", gap:12 }}>
              <div className="field" style={{ gridColumn:"1 / span 2" }}>
                <span className="label">Test title <span style={{ color:"var(--red)" }}>*</span></span>
                <input className="input" placeholder="e.g. Hydrocarbons — Daily Pill 13" autoFocus
                  value={meta.title} onChange={e => setMeta({ ...meta, title: e.target.value })}/>
              </div>
              <div className="field">
                <span className="label">Class</span>
                <select className="select" value={meta.className} onChange={e => pickClass(e.target.value)}>
                  {classLevels.length === 0 && <option value="">— add a class first —</option>}
                  {classLevels.map(c => <option key={c} value={c}>{c}</option>)}
                </select>
              </div>
              <div className="field">
                <span className="label">Subject</span>
                <select className="select" value={meta.subject} onChange={e => setMeta({ ...meta, subject: e.target.value })}>
                  {subjects.length === 0 && <option value="">— add a subject first —</option>}
                  {subjects.map(s => <option key={s.id} value={s.name}>{s.name}</option>)}
                </select>
              </div>
              <div className="field" style={{ gridColumn: "1 / span 2" }}>
                <span className="label">Sections <span className="muted" style={{ fontSize: 10, marginLeft: 4 }}>tap to add — one test can target several</span></span>
                <window.KXUI.SectionChips classLevel={meta.className} selectedIds={meta.class_section_ids}
                  onChange={(ids) => setMeta({ ...meta, class_section_ids: ids })}/>
              </div>
              {/* Chapter + topic — cascading dropdowns sourced from the
                  admin-curated catalog. The chapter list narrows by
                  (className × subject); topics by chapter. Empty catalogs
                  fall back to "— none —" so the modal doesn't block test
                  creation when the catalog hasn't been populated yet. */}
              <ChapterTopicFields
                meta={meta} setMeta={setMeta}
                subjects={subjects}/>
              <div className="field">
                <span className="label">Scheduled date</span>
                <input className="input" type="date"
                  value={meta.date} onChange={e => setMeta({ ...meta, date: e.target.value })}/>
              </div>
              <div className="field">
                <span className="label">Duration (min)</span>
                <input className="input" type="number" min={10} max={300}
                  value={meta.durationMin} onChange={e => setMeta({ ...meta, durationMin: parseInt(e.target.value) || 60 })}/>
              </div>
              <div className="field" style={{ gridColumn:"1 / span 2" }}>
                <span className="label">Language</span>
                <select className="select" value={meta.language} onChange={e => setMeta({ ...meta, language: e.target.value })}>
                  <option value="bn">Bengali</option>
                  <option value="hi">Hindi</option>
                  <option value="en">English</option>
                  <option value="mix">Mixed</option>
                </select>
              </div>
              {/* Submission mode — applies to descriptive (Short/Long) tests,
                  where students hand in photos of their written answers. Ignored
                  (coerced to per-question) by the backend for all-MCQ tests. */}
              <div className="field" style={{ gridColumn:"1 / span 2" }}>
                <span className="label">Student answer submission <span className="muted" style={{ fontSize: 10, marginLeft: 4 }}>descriptive tests only</span></span>
                <select className="select" value={meta.submissionMode} onChange={e => setMeta({ ...meta, submissionMode: e.target.value })}>
                  <option value="per_question">Photo per question — uploaded during the exam</option>
                  <option value="bulk">Bulk at the end — all photos / a PDF after submission</option>
                </select>
              </div>
            </div>
          </div>
        </div>

        <div style={{ padding:12, borderTop:"1px solid var(--line)", display:"flex", gap:8, alignItems:"center" }}>
          <span className="muted" style={{ fontSize:11 }}>
            {method === "manual" && "→ Opens the manual authoring page."}
            {method === "paste" && "→ Opens the paste-text + Gemini parser."}
            {method === "upload" && "→ Opens the PDF upload flow."}
          </span>
          <span style={{ marginLeft:"auto", display:"flex", gap:6 }}>
            <button className="btn sm" onClick={onClose}>Cancel</button>
            <button className="btn sm primary"
              disabled={!canContinue}
              style={{ opacity: canContinue ? 1 : 0.55 }}
              onClick={() => onContinue(method, meta)}>
              Continue → <MIcon name="chevronRight" size={11}/>
            </button>
          </span>
        </div>
      </div>
    </div>
  );
};

const MethodCard = ({ active, onClick, icon, title, sub }) => (
  <button onClick={onClick}
    style={{
      padding:14, textAlign:"left", cursor:"pointer",
      background: active ? "rgba(255,186,90,0.10)" : "var(--bg-2)",
      border: `1px solid ${active ? "var(--accent)" : "var(--line-strong)"}`,
      borderRadius:8,
      display:"flex", flexDirection:"column", gap:6,
    }}>
    <div style={{ display:"flex", alignItems:"center", gap:8 }}>
      <span style={{ width:26, height:26, borderRadius:5, background: active ? "rgba(255,186,90,0.18)" : "var(--bg-3)", display:"grid", placeItems:"center", color: active ? "var(--accent)" : "var(--ink-2)" }}>
        <MIcon name={icon} size={13}/>
      </span>
      <span style={{ color: active ? "var(--accent)" : "var(--ink-0)", fontSize:12.5, fontWeight:600 }}>{title}</span>
    </div>
    <div className="muted" style={{ fontSize:11, lineHeight:1.45 }}>{sub}</div>
  </button>
);

/* ====================================================================
   Helper: convert Gemini-parsed JSON into ManualAuthoring sections
   ==================================================================== */
const parsedToManualSections = (parsed) => {
  const typeMap = { mcq:"MCQ", short:"Short", long:"Long", tf:"TF", fill:"Fill", match:"Match" };
  return (parsed?.sections || []).map((s, i) => {
    const firstQ = s.questions?.[0];
    const sectionType = typeMap[firstQ?.type?.toLowerCase?.()] || typeMap[firstQ?.type] || "MCQ";
    const sectionMarks = firstQ?.marks || 1;
    return {
      id: uid("s"),
      label: s.title || `Section ${SECTION_NAMES[i] || i + 1}`,
      type: sectionType,
      marks: sectionMarks,
      target: s.questions?.length || 1,
      instructions: s.instructions || "",
      questions: (s.questions || []).map(q => {
        const qType = typeMap[q.type?.toLowerCase?.()] || typeMap[q.type] || sectionType;
        const base = newQuestion(qType);
        const override = q.marks != null && q.marks !== sectionMarks ? q.marks : null;
        return {
          ...base,
          text: q.stem || q.text || "",
          marks: override,
          concepts: q.concepts || [],
          options: (q.options && q.options.length)
            ? q.options.map((o, idx) => ({
                id: o.id || String.fromCharCode(97 + idx),
                text: o.text || "",
                correct: !!o.correct,
              }))
            : base.options,
          subQuestions: (q.subQuestions && q.subQuestions.length)
            ? q.subQuestions.map(sq => ({ id: uid("sq"), text: sq.text || "" }))
            : (qType === "Long" ? base.subQuestions : []),
          expectedAnswer: q.expectedAnswer || q.markingScheme || "",
        };
      }),
    };
  });
};

/* ====================================================================
   exports
   ==================================================================== */
window.ManualAuthoring = ManualAuthoring;
window.TestDetailsModal = TestDetailsModal;
// Exported so screen-library.jsx can reuse the same chapter+topic picker
// without duplicating the cascading fetch logic.
window.ChapterTopicFields = ChapterTopicFields;
window.parsedToManualSections = parsedToManualSections;
