/* ==================================================================
   Test Library — with Test preview / Upload-paper / Student report modals
   ================================================================== */

const { Icon: LIcon, StatusPill: LStatusPill } = window.KXUI;

const TestLibrary = ({ setScreen, onOpenManual }) => {
  const [filters, setFilters] = useState({ class: "All", subject: "All", topic: "All", status: "All" });
  const [search, setSearch] = useState("");
  const [selected, setSelected] = useState(null);
  const [studentsForTest, setStudentsForTest] = useState(null);
  const [previewTest, setPreviewTest] = useState(null);
  const [uploadOpen, setUploadOpen] = useState(false);
  const [createOpen, setCreateOpen] = useState(false);
  const [openActionMenu, setOpenActionMenu] = useState(null);
  const [studentReport, setStudentReport] = useState(null); // {test, student}
  const [extraTests, setExtraTests] = useState([]);
  const [refreshTick, setRefreshTick] = useState(0);
  const [editTest, setEditTest] = useState(null);          // test row being edited
  const [uploadSheetsTest, setUploadSheetsTest] = useState(null); // test for upload-sheets modal
  const [engineConfigTest, setEngineConfigTest] = useState(null); // test for engine-config modal

  const allTests = [...extraTests, ...window.KX.TESTS];
  // refreshTick keeps `allTests` reactive when window.KX.TESTS is replaced after save.
  // eslint-disable-next-line no-unused-vars
  const _r = refreshTick;
  const filtered = allTests.filter((t) => {
    if (filters.class !== "All" && t.className !== filters.class) return false;
    if (filters.subject !== "All" && t.subject !== filters.subject) return false;
    if (filters.status !== "All" && window.KXUI.effectiveStatus(t.status, t.date) !== filters.status) return false;
    if (search && !t.title.toLowerCase().includes(search.toLowerCase()) && !t.id.toLowerCase().includes(search.toLowerCase())) return false;
    return true;
  });

  // Close action menu on outside click
  useEffect(() => {
    const close = () => setOpenActionMenu(null);
    if (openActionMenu) {
      document.addEventListener("click", close);
      return () => document.removeEventListener("click", close);
    }
  }, [openActionMenu]);

  // Cockpit's "Finalise report" hands off via window.KX.OPEN_UPLOAD_SHEETS_FOR
  // = <display_id>. When the library remounts, reopen that test's roster modal.
  useEffect(() => {
    const targetId = window.KX.OPEN_UPLOAD_SHEETS_FOR;
    if (!targetId) return;
    window.KX.OPEN_UPLOAD_SHEETS_FOR = null;
    (async () => {
      try {
        window.KX.TESTS = await window.KXApi.get("/tests");
        const t = window.KX.TESTS.find(x => x.id === targetId);
        if (t) {
          setRefreshTick(n => n + 1);
          setUploadSheetsTest(t);
        }
      } catch {}
    })();
  }, []);

  const onSaveTest = async (newTest) => {
    try {
      const sections = newTest._payload?.sections;
      const title = (newTest.title || "").trim() || "Untitled test";
      // `asDraft` controls status: true → keeps test in 'draft'; false → promotes
      // to 'live' once it has questions. The editor's two save buttons set this.
      const as_draft = !!newTest._payload?.asDraft;
      // How descriptive answers are collected: 'per_question' (legacy) | 'bulk'.
      // The backend coerces 'bulk' back to per_question for all-objective tests.
      const submission_mode = newTest._payload?.meta?.submissionMode || "per_question";
      // If we're updating an existing test (Continue authoring / Edit questions),
      // reuse its display_id and just replace the structure.
      if (newTest._payload?.meta?.display_id) {
        await window.KXApi.post(`/tests/${newTest._payload.meta.display_id}/structure`,
          { sections, as_draft, submission_mode });
      } else {
        await window.KXApi.post("/tests", {
          title,
          submission_mode,
          // Prefer the explicit class_section_ids from the multi-section
          // picker; fall back to className-only for old code paths.
          class_section_ids: (newTest.class_section_ids && newTest.class_section_ids.length)
            ? newTest.class_section_ids : undefined,
          className: newTest.className,
          subject: newTest.subject,
          topic: (!newTest.topic || newTest.topic === "—") ? undefined : newTest.topic,
          chapter: (!newTest.chapter || newTest.chapter === "—") ? undefined : newTest.chapter,
          // Treat empty / missing dates as omitted. Postgres DATE rejects "".
          scheduled_date: (newTest.date && newTest.date.trim()) ? newTest.date : undefined,
          language: newTest.language,
          sections,
          as_draft,
        });
      }
      window.KX.TESTS = await window.KXApi.get("/tests");
      setExtraTests([]);
      setRefreshTick(t => t + 1);
      setUploadOpen(false);
      setCreateOpen(false);
    } catch (e) {
      const msg = e?.message || String(e);
      // Show inline in the upload modal if it's open; fall back to alert otherwise.
      if (uploadOpen && typeof window.__KX_LIB_SAVE_ERR === "function") {
        window.__KX_LIB_SAVE_ERR(msg);
      } else {
        alert("Could not save test: " + msg);
      }
      throw e;
    }
  };

  const handleCreateContinue = (method, meta) => {
    setCreateOpen(false);
    if (method === "manual") {
      onOpenManual && onOpenManual(meta, onSaveTest);
    } else {
      // paste / upload — open the existing structured-upload modal
      setUploadOpen(true);
    }
  };

  const refreshTests = async () => {
    window.KX.TESTS = await window.KXApi.get("/tests");
    setRefreshTick(t => t + 1);
  };

  const handleDeleteTest = async (test) => {
    if (!window.confirm(`Delete "${test.title}"? This removes all its questions, sheets, and evaluations.`)) return;
    try {
      await window.KXApi.del(`/tests/${test.id}`);
      await refreshTests();
    } catch (e) { alert("Could not delete: " + (e?.message || e)); }
  };

  const handleSaveEdit = async (display_id, updates) => {
    await window.KXApi.put(`/tests/${display_id}`, updates);
    await refreshTests();
    setEditTest(null);
  };

  // "Continue authoring" on a draft test row — fetch its current structure and
  // open the manual editor prefilled with meta + sections + questions.
  const handleContinueAuthoring = async (test) => {
    try {
      const full = await window.KXApi.get(`/tests/${test.id}/full`);
      const initialSections = dbSectionsToEditorSections(full.sections);
      onOpenManual && onOpenManual({
        title: full.meta.title,
        class_section_ids: Array.isArray(full.meta.class_section_ids) && full.meta.class_section_ids.length
          ? full.meta.class_section_ids
          : (full.meta.class_section_id ? [full.meta.class_section_id] : []),
        className: full.meta.className,
        subject: full.meta.subject,
        topic: full.meta.topic || "",
        chapter: full.meta.chapter || "",
        date: full.meta.date || "",
        language: full.meta.language || "bn",
        submissionMode: full.meta.submission_mode || "per_question",
        display_id: full.meta.id,
      }, onSaveTest, initialSections);
    } catch (e) {
      alert("Could not load test: " + (e?.message || e));
    }
  };

  return (
    <div style={{ padding: "20px 28px", maxWidth: 1400, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", marginBottom: 20 }}>
        <div>
          <div className="muted" style={{ fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 6 }}>
            {window.KX.CURRENT_SCHOOL ? `${window.KX.CURRENT_SCHOOL.name} · ${window.KX.CURRENT_SCHOOL.academic_year}` : "—"}
          </div>
          <h1 style={{ margin: 0, color: "var(--ink-0)", fontSize: 22, fontWeight: 600, letterSpacing: "-0.01em" }}>Test Library</h1>
          <div style={{ marginTop: 6, fontSize: 12, color: "var(--ink-2)" }}>
            {allTests.length} {allTests.length === 1 ? "test" : "tests"} · {allTests.filter(t => t.status === "grading").length} in active grading
          </div>
        </div>
        <div style={{ display: "flex", gap: 8 }}>
          <button className="btn" onClick={()=>setScreen("kinetix")}
                  style={{ background:"rgba(46,91,255,0.12)", borderColor:"rgba(46,91,255,0.4)", color:"#5C7CFF" }}>
            <LIcon name="sparkle" size={13}/> KinetiX online test
          </button>
          <button className="btn primary" onClick={()=>setCreateOpen(true)}><LIcon name="plus" size={13}/> Create test</button>
        </div>
      </div>

      <div className="cx-catalog-split narrow">
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <div className="card">
            <div className="card-head"><LIcon name="filter" size={13}/><span className="card-title">Filters</span><span className="muted" style={{ marginLeft: "auto", fontSize: 11 }}>Reset</span></div>
            <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              {[
                { k: "class",   label: "Class",   opts: ["All", ...(window.KX?.CLASS_OPTIONS   || ["IX","X"])] },
                { k: "subject", label: "Subject", opts: ["All", ...(window.KX?.SUBJECT_OPTIONS || [])] },
                { k: "status",  label: "Status",  opts: ["All", "draft", "upcoming", "optimised", "grading", "closed"] },
              ].map((f) =>
                <div key={f.k} className="field">
                  <span className="label">{f.label}</span>
                  <select className="select" value={filters[f.k]} onChange={(e) => setFilters({ ...filters, [f.k]: e.target.value })}>
                    {f.opts.map((o) => <option key={o}>{o}</option>)}
                  </select>
                </div>
              )}
              <div className="field">
                <span className="label">Date range</span>
                <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                  <input className="input" type="date" style={{ padding: "6px 8px" }}/>
                  <input className="input" type="date" style={{ padding: "6px 8px" }}/>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div className="card" style={{ overflow: "hidden", display: "flex", flexDirection: "column" }}>
          <div className="card-head" style={{ gap: 10 }}>
            <div style={{ position: "relative", flex: 1 }}>
              <span style={{ position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", color: "var(--ink-3)" }}><LIcon name="search" size={13}/></span>
              <input className="input" style={{ paddingLeft: 30, background: "var(--bg-2)" }}
                placeholder="Search tests, IDs, topics… (⌘K)" value={search} onChange={(e) => setSearch(e.target.value)}/>
            </div>
            <span className="muted" style={{ fontSize: 11 }}>{filtered.length} of {allTests.length}</span>
            <div className="seg"><button className="active">All</button><button>Mine</button><button>Shared</button></div>
          </div>

          <div style={{ overflow: "auto" }}>
            <table className="tests">
              <thead><tr>
                <th style={{ width: 70 }}>ID</th><th>Test name</th>
                <th style={{ width: 60 }}>Class</th><th style={{ width: 110 }}>Subject</th>
                <th style={{ width: 110 }}>Date</th><th style={{ width: 110 }}>Status</th>
                <th style={{ width: 140 }}>Progress</th><th style={{ width: 160 }}>Action</th>
              </tr></thead>
              <tbody>
                {filtered.map((t) => {
                  const isActive = selected === t.id;
                  // Online (KinetiX) tests track submissions (auto-graded MCQ);
                  // paper tests track teacher-reviewed evaluations.
                  const isOnline = !!t.type;
                  const numer = isOnline ? (t.submitted ?? 0) : t.reviewed;
                  const pct = t.students ? (numer / t.students) * 100 : 0;
                  return (
                    <tr key={t.id} onClick={() => setSelected(t.id)} style={isActive ? { background: "var(--bg-2)" } : {}}>
                      <td className="mono muted" style={{ fontSize: 11 }}>{t.id}</td>
                      <td>
                        <div style={{ color: "var(--ink-0)", fontWeight: 500 }}>{t.title}</div>
                        <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>
                          {t.chapter || "—"} · {t.questions ? `${t.questions} Q` : "—"} · lang {t.language.toUpperCase()}
                        </div>
                      </td>
                      <td>{t.classLabel || t.className}</td>
                      <td>{t.subject}</td>
                      <td className="mono muted" style={{ fontSize: 11 }}>{t.date}</td>
                      <td><LStatusPill status={t.status} scheduledDate={t.date} isExpired={t.is_expired}/></td>
                      <td>
                        {t.students > 0 ?
                          <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
                            <div className="progress" style={{ width: 110 }}><div style={{ width: pct + "%", background: t.status === "closed" ? "var(--green)" : "var(--accent)" }}></div></div>
                            <span className="mono muted" style={{ fontSize: 10 }}>
                              {numer}/{t.students} {isOnline ? "submitted" : "reviewed"}
                            </span>
                          </div> : <span className="muted">—</span>}
                      </td>
                      <td onClick={e=>e.stopPropagation()}>
                        <ActionMenu test={t} open={openActionMenu === t.id}
                          onOpen={()=>setOpenActionMenu(openActionMenu === t.id ? null : t.id)}
                          onPreview={()=>{ setPreviewTest(t); setOpenActionMenu(null); }}
                          onStudents={()=>{ setStudentsForTest(t); setOpenActionMenu(null); }}
                          onContinueAuthoring={()=>{ setOpenActionMenu(null); handleContinueAuthoring(t); }}
                          onUploadSheets={()=>{ setOpenActionMenu(null); setUploadSheetsTest(t); }}
                          onEdit={()=>{ setOpenActionMenu(null); setEditTest(t); }}
                          onEngineConfig={()=>{ setOpenActionMenu(null); setEngineConfigTest(t); }}
                          onDelete={()=>{ setOpenActionMenu(null); handleDeleteTest(t); }}
                          setScreen={setScreen}/>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>

          <div className="card-head" style={{ borderTop: "1px solid var(--line)", borderBottom: 0, fontSize: 11, color: "var(--ink-3)" }}>
            <span>Showing 1–{filtered.length} of {filtered.length}</span>
            <span style={{ marginLeft: "auto" }}>Page 1 / 1</span>
          </div>
        </div>
      </div>

      <StudentsModal test={studentsForTest} onClose={() => setStudentsForTest(null)} setScreen={setScreen}
        onViewReport={(student) => { setStudentsForTest(null); setStudentReport({ test: studentsForTest, student }); }}/>
      <TestPreviewModal test={previewTest} onClose={()=>setPreviewTest(null)}/>
      <UploadTestModal open={uploadOpen} onClose={()=>setUploadOpen(false)} onSave={onSaveTest} onOpenManual={onOpenManual}/>
      <EditTestModal test={editTest} onClose={()=>setEditTest(null)} onSave={handleSaveEdit}/>
      {uploadSheetsTest?.type ? (
        <window.OnlineSubmissionsModal test={uploadSheetsTest} onClose={()=>setUploadSheetsTest(null)}/>
      ) : (
        <UploadSheetsModal test={uploadSheetsTest} onClose={()=>setUploadSheetsTest(null)} onRefresh={refreshTests}
          onOpenCockpit={({ test, student }) => {
            window.KX.EVAL_CONTEXT = { test, student };
            setScreen("evaluate");
          }}/>
      )}
      {typeof window.TestDetailsModal !== "undefined" && (
        <window.TestDetailsModal open={createOpen} onClose={()=>setCreateOpen(false)} onContinue={handleCreateContinue}/>
      )}
      <StudentReportModal data={studentReport} onClose={()=>setStudentReport(null)}/>
      {typeof window.EngineConfigModal !== "undefined" && (
        <window.EngineConfigModal test={engineConfigTest} onClose={()=>setEngineConfigTest(null)}/>
      )}
    </div>
  );
};

window.TestLibrary = TestLibrary;

// DB shape (snake_case, plural rows) → ManualAuthoring shape (camelCase, nested).
function dbSectionsToEditorSections(sections) {
  const TYPE_MAP = { mcq: "MCQ", short: "Short", long: "Long", tf: "TF", fill: "Fill", match: "Match", numerical: "Short", derivation: "Long", diagram: "Long" };
  return sections.map(sec => ({
    id: sec.id,
    label: sec.title ? `Section ${sec.section_label} · ${sec.title}` : `Section ${sec.section_label}`,
    type: TYPE_MAP[sec.questions[0]?.type] || "MCQ",
    marks: sec.marks_per_question ?? sec.questions[0]?.marks ?? 1,
    target: Math.max(sec.questions.length, 5),
    instructions: sec.instructions || "",
    questions: sec.questions.map(q => ({
      id: q.id,
      text: q.text,
      type: TYPE_MAP[q.type] || "Short",
      options: (q.options || []).map(o => ({ id: o.letter, text: o.text, correct: !!o.is_correct })),
      subQuestions: (q.sub_questions || []).map(sq => ({ id: sq.id, text: sq.text })),
      expectedAnswer: q.expected_answer || "",
      concepts: q.concepts || [],
      marks: q.marks,
    })),
  }));
}
window.dbSectionsToEditorSections = dbSectionsToEditorSections;

/* ---------- Action menu (dropdown) ---------- */
const ActionMenu = ({ test, open, onOpen, onPreview, onStudents, onContinueAuthoring, onUploadSheets, onEdit, onEngineConfig, onDelete, setScreen }) => {
  const t = test;
  // Online (KinetiX) tests are auto-graded — the "Upload sheets" / "Evaluate"
  // wording becomes "View submissions" / hidden respectively.
  const isOnline = !!t.type;
  const submissionsLabel = isOnline ? "View submissions" : "Upload sheets";
  // Three user-facing states: draft → live → completed. All intermediate / legacy
  // states (optimised, ready_for_review, published) act as "live" for the primary action.
  // Future-scheduled tests collapse to "upcoming" even if the row says live, so
  // the teacher sees "Open paper" (preview) rather than a submissions action
  // that won't have any data yet.
  const status = window.KXUI.effectiveStatus(t.status, t.date);
  const primary = {
    draft:     { label: "Continue authoring", onClick: onContinueAuthoring },
    live:      { label: submissionsLabel,     onClick: onUploadSheets, cls: "primary" },
    completed: { label: "View report",        onClick: () => setScreen("report"), cls: "primary" },
    // Legacy fallbacks
    optimised:        { label: submissionsLabel, onClick: onUploadSheets, cls: "primary" },
    ready_for_review: { label: submissionsLabel, onClick: onUploadSheets, cls: "primary" },
    published:        { label: submissionsLabel, onClick: onUploadSheets, cls: "primary" },
    closed:           { label: "View report",   onClick: () => setScreen("report"), cls: "primary" },
    upcoming:         { label: "Open paper",    onClick: onPreview },
    grading:          { label: isOnline ? submissionsLabel : "Resume grading", onClick: onUploadSheets, cls: "primary" },
  }[status] || { label: "Open", onClick: onPreview };

  const items = [
    { label: "Edit details",      onClick: onEdit },
    { label: "Edit questions",    onClick: onContinueAuthoring },
    { label: submissionsLabel,    onClick: onUploadSheets, disabled: t.status === "draft" },
    { label: "Students table",    onClick: onStudents, disabled: t.status === "draft" },
    // "Open evaluation" is the teacher's paper-grading cockpit — meaningless
    // for auto-graded online tests, so it's hidden in that case.
    ...(isOnline ? [] : [{ label: "Open evaluation", onClick: () => setScreen("evaluate"), disabled: t.status === "draft" }]),
    { label: "Class report",      onClick: () => setScreen("report"),   disabled: t.status === "draft" },
    { label: "Delete test",       onClick: onDelete, danger: true },
  ];

  return (
    <div style={{ position:"relative", display:"flex", gap: 4 }}>
      <button className={`btn sm ${primary.cls || ""}`} onClick={primary.onClick}>{primary.label}</button>
      <button className="btn sm" style={{ padding:"4px 8px" }} onClick={onEngineConfig} title="Engine config">⚙</button>
      <button className="btn sm" style={{ padding:"4px 6px" }} onClick={onOpen} title="More actions">▾</button>
      {open && (
        <div style={{
          position:"absolute", right: 0, top: "100%", marginTop: 4, zIndex: 50,
          background:"var(--bg-1)", border:"1px solid var(--line-strong)", borderRadius: 6,
          boxShadow:"0 8px 24px rgba(0,0,0,0.5)", minWidth: 180, padding: 4,
        }}>
          {items.map((it, i) => (
            <div key={i} onClick={()=>!it.disabled && it.onClick()}
              style={{
                padding:"6px 10px", fontSize: 12, borderRadius: 4,
                color: it.disabled ? "var(--ink-4)" : (it.danger ? "var(--red)" : "var(--ink-1)"),
                cursor: it.disabled ? "not-allowed" : "pointer",
                background: "transparent",
              }}
              onMouseEnter={(e)=>!it.disabled && (e.currentTarget.style.background="var(--bg-3)")}
              onMouseLeave={(e)=>(e.currentTarget.style.background="transparent")}>
              {it.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

/* ---------- Test preview modal (clicking test name) ---------- */
const TestPreviewModal = ({ test, onClose }) => {
  if (!test) return null;
  const questions = window.KX.QUESTIONS;
  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.75)", 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: 820, maxWidth:"100%", maxHeight:"88vh", display:"flex", flexDirection:"column" }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap: 12 }}>
          <div>
            <div style={{ fontSize: 10, color:"var(--ink-3)", letterSpacing:".08em", textTransform:"uppercase" }}>{test.id} · {test.className} · {test.subject}</div>
            <div style={{ color:"var(--ink-0)", fontSize: 16, fontWeight: 600 }}>{test.title}</div>
          </div>
          <span style={{ marginLeft:"auto", display:"flex", gap: 6, alignItems:"center" }}>
            <LStatusPill status={test.status} scheduledDate={test.date} isExpired={test.is_expired}/>
            <span className="pill mono">{test.questions || 0} Q</span>
            <span className="pill mono">{test.language?.toUpperCase()}</span>
            <button className="btn sm ghost" onClick={onClose}>✕</button>
          </span>
        </div>
        <div style={{ padding: "16px 22px", overflow:"auto", flex: 1, background:"#f7f3ea", color:"#1a1a1a", fontFamily:"'Hind Siliguri', serif" }}>
          <div style={{ display:"flex", justifyContent:"space-between", borderBottom:"1px solid #c8c0aa", paddingBottom: 8, marginBottom: 14, fontFamily:"'IBM Plex Sans', sans-serif", fontSize: 11, color:"#666" }}>
            <span>{test.chapter} · {test.date}</span>
            <span>Total: {test.questions || 0} questions · Time: 30 min</span>
          </div>
          {(test.questions ? questions : []).map((q, i) => (
            <div key={q.id} style={{ marginBottom: 18, fontSize: 14.5, lineHeight: 1.6 }}>
              <b>{i + 1}.</b> {q.text}
              <span style={{ color:"#888", fontFamily:"'IBM Plex Mono', monospace", fontSize: 11, marginLeft: 8 }}>[{q.maxMarks}]</span>
              {q.type === "mcq" && (
                <div style={{ marginTop: 4, marginLeft: 18, fontSize: 13.5 }}>
                  (a) ……  (b) ……  (c) ……  (d) ……
                </div>
              )}
            </div>
          ))}
          {!test.questions && (
            <div style={{ padding: 40, textAlign:"center", color:"#888", fontStyle:"italic" }}>
              This test is still a draft — no questions added yet.
            </div>
          )}
        </div>
        <div style={{ padding: 12, borderTop:"1px solid var(--line)", display:"flex", gap: 8 }}>
          <span style={{ marginLeft:"auto", display:"flex", gap: 6 }}>
            <button className="btn sm" onClick={onClose}>Close</button>
            <button className="btn sm">Edit paper</button>
            <button className="btn sm primary">Download PDF</button>
          </span>
        </div>
      </div>
    </div>
  );
};

/* ---------- Upload / Create test modal ---------- */
const UploadTestModal = ({ open, onClose, onSave, onOpenManual }) => {
  const classSections = window.KX?.CLASS_SECTIONS || [];
  const subjects = window.KX?.SUBJECTS || [];
  const defaultCs = classSections[0] || null;

  const [step, setStep] = useState(1); // 1=meta+paste, 2=structured preview
  const [meta, setMeta] = useState({
    title: "",
    class_section_ids: defaultCs ? [defaultCs.id] : [],
    className: defaultCs?.class_level || "",
    subject: subjects[0]?.name || "",
    topic: "", chapter: "", date: "", language: "bn",
  });
  // Keep meta in sync with the bootstrapped catalog (it may load after mount).
  React.useEffect(() => {
    if (meta.class_section_ids.length === 0 && defaultCs) {
      setMeta(m => ({ ...m, class_section_ids: [defaultCs.id], className: defaultCs.class_level }));
    }
    if (!meta.subject && subjects[0]) {
      setMeta(m => ({ ...m, subject: subjects[0].name }));
    }
  }, [defaultCs?.id, subjects.length]);

  const classLevels = Array.from(new Set(classSections.map(c => c.class_level))).sort();
  const pickClass = (cl) => {
    const first = classSections.find(c => c.class_level === cl);
    setMeta({ ...meta, className: cl, class_section_ids: first ? [first.id] : [] });
  };
  const [text, setText] = useState("");
  const [parsing, setParsing] = useState(false);
  const [parsed, setParsed] = useState(null);
  const [jsonText, setJsonText] = useState("");
  const [jsonError, setJsonError] = useState(null);
  const [sourceMode, setSourceMode] = useState("text"); // "text" | "pdf"
  const [pdfFile, setPdfFile] = useState(null);
  const pdfInputRef = React.useRef(null);
  const [saveErr, setSaveErr] = React.useState(null);

  // Let the parent's onSaveTest forward error messages here.
  React.useEffect(() => {
    if (!open) return;
    window.__KX_LIB_SAVE_ERR = (msg) => setSaveErr(msg);
    return () => { window.__KX_LIB_SAVE_ERR = null; };
  }, [open]);

  if (!open) return null;

  const parseNow = async () => {
    if (!text.trim()) { setJsonError("Paste some paper text first."); return; }
    setParsing(true);
    setSaveErr(null);
    try {
      // Hit the live backend → Gemini parser.
      const response = await window.KXApi.post("/papers/parse", {
        title: meta.title || "Untitled preview",
        content: text,
        type: "text",
        language: meta.language,
      });
      // response.structured matches the parser system-prompt schema:
      //   { sections: [{ section_id, title, marks_per_question, total_questions,
      //                  section_total_marks, questions: [{ q_no, text, type, options, prerequisite_tags }] }],
      //     parsing_flags: [...] }
      const editorParsed = backendToEditorParsed(response.structured, meta);
      setParsed(editorParsed);
      setJsonText(JSON.stringify(response.structured, null, 2));
      setJsonError(null);
      setParsing(false);

      // Hand off to ManualAuthoring with sections pre-filled.
      if (typeof onOpenManual === "function" && typeof window.parsedToManualSections === "function") {
        const initialSections = window.parsedToManualSections(editorParsed);
        const handoffMeta = {
          title: meta.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, language: meta.language,
          source: "gemini-parsed",
          parseModel: "gemini-2.5-flash",
        };
        onOpenManual(handoffMeta, onSave, initialSections);
        setStep(1); setText(""); setParsed(null); setJsonText(""); setJsonError(null);
        onClose && onClose();
        return;
      }
      setStep(2);
    } catch (err) {
      setParsing(false);
      setJsonError(err?.message || String(err));
    }
  };

  // PDF variant: upload a question-paper PDF, let Gemini Vision parse it,
  // hand off to the manual editor pre-filled.
  const parsePdfNow = async () => {
    if (!pdfFile) { setJsonError("Pick a PDF first."); return; }
    setParsing(true);
    try {
      const form = new FormData();
      form.append("file", pdfFile, pdfFile.name);
      if (meta.title) form.append("title", meta.title);
      if (meta.language) form.append("language", meta.language);
      const r = await fetch("/api/papers/parse-pdf", { method: "POST", body: form });
      if (!r.ok) throw new Error(`parse-pdf ${r.status}: ${await r.text()}`);
      const response = await r.json();
      const editorParsed = backendToEditorParsed(response.structured, meta);
      setParsed(editorParsed);
      setJsonText(JSON.stringify(response.structured, null, 2));
      setJsonError(null);
      setParsing(false);

      if (typeof onOpenManual === "function" && typeof window.parsedToManualSections === "function") {
        const initialSections = window.parsedToManualSections(editorParsed);
        const handoffMeta = {
          title: meta.title || (pdfFile.name || "").replace(/\.pdf$/i, "") || "Untitled test",
          class_section_ids: meta.class_section_ids,
          className: meta.className,
          subject: meta.subject,
          topic: meta.topic, chapter: meta.chapter,
          date: meta.date, language: meta.language,
          source: "gemini-pdf",
          parseModel: "gemini-2.5-flash",
        };
        onOpenManual(handoffMeta, onSave, initialSections);
        setStep(1); setText(""); setPdfFile(null); setParsed(null); setJsonText(""); setJsonError(null);
        setSourceMode("text");
        onClose && onClose();
        return;
      }
      setStep(2);
    } catch (err) {
      setParsing(false);
      setJsonError(err?.message || String(err));
    }
  };

  // Backend parser JSON → the shape `parsedToManualSections` expects.
  function backendToEditorParsed(structured, meta) {
    const sections = (structured?.sections || []).map(sec => ({
      title: sec.title ? `Section ${sec.section_id} · ${sec.title}` : `Section ${sec.section_id}`,
      instructions: "",
      questions: (sec.questions || []).map(q => ({
        stem: q.text,
        type: q.type, // mcq | short | long | numerical | derivation
        marks: q.marks ?? sec.marks_per_question
          ?? (sec.section_total_marks && sec.total_questions ? sec.section_total_marks / sec.total_questions : 1),
        options: q.options?.length
          ? q.options.map((t, i) => ({ id: "abcd"[i], text: t, correct: false }))
          : undefined,
        concepts: q.prerequisite_tags || [],
      })),
    }));
    return {
      meta: { title: meta.title, class: meta.className, subject: meta.subject, language: meta.language },
      sections,
      summary: {
        questions: sections.reduce((a, s) => a + s.questions.length, 0),
        sections: sections.length,
        total_marks: sections.reduce((a, s) => a + s.questions.reduce((b, q) => b + (q.marks || 0), 0), 0),
      },
    };
  }

  const validateJson = (txt) => {
    try {
      const obj = JSON.parse(txt);
      setParsed(obj);
      setJsonError(null);
      return obj;
    } catch (e) {
      setJsonError(e.message);
      return null;
    }
  };

  const formatJson = () => {
    const obj = validateJson(jsonText);
    if (obj) setJsonText(JSON.stringify(obj, null, 2));
  };

  const save = async (asDraft) => {
    setSaveErr(null);
    const qCount = parsed?.summary?.questions ?? (parsed?.sections || []).reduce((a, s) => a + (s.questions?.length || 0), 0);
    const newTest = {
      title: meta.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: qCount || 0, students: 0, reviewed: 0,
      language: meta.language,
      _payload: { asDraft },
    };
    try {
      await onSave(newTest);
      setStep(1); setText(""); setParsed(null); setJsonText(""); setJsonError(null);
      setMeta({ ...meta, title:"", topic:"", chapter:"" });
    } catch { /* error already surfaced via setSaveErr inline */ }
  };

  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: 880, maxWidth:"100%", maxHeight:"90vh", display:"flex", flexDirection:"column" }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap: 10 }}>
          <LIcon name="upload" size={14}/>
          <span style={{ color:"var(--ink-0)", fontSize: 14, fontWeight: 600 }}>Upload question paper · Create new test</span>
          <span className="pill mono" style={{ marginLeft: 6 }}>Step {step} of 2</span>
          <button className="btn sm ghost" style={{ marginLeft:"auto" }} onClick={onClose}>✕</button>
        </div>

        {step === 1 && (
          <>
            <div style={{ padding: 18, overflow:"auto", flex: 1, display:"grid", gridTemplateColumns:"260px 1fr", gap: 16 }}>
              <div style={{ display:"flex", flexDirection:"column", gap: 10 }}>
                <div className="field"><span className="label">Test title</span>
                  <input className="input" placeholder="e.g. Hydrocarbons — Pill 13" 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"><span className="label">Sections <span className="muted" style={{ fontSize: 10, marginLeft: 4 }}>tap to add</span></span>
                  <window.KXUI.SectionChips classLevel={meta.className} selectedIds={meta.class_section_ids}
                    onChange={(ids) => setMeta({ ...meta, class_section_ids: ids })}/>
                </div>
                {/* Chapter + topic dropdowns sourced from the admin catalog.
                    Defined in screen-manual.jsx — both screens share the
                    same picker so the UX is identical. */}
                <window.ChapterTopicFields meta={meta} setMeta={setMeta} subjects={subjects}/>
                <div className="field"><span className="label">Scheduled date</span>
                  <input className="input" type="date" style={{ colorScheme:"dark" }} value={meta.date} onChange={e=>setMeta({...meta, date:e.target.value})}/></div>
                <div className="field"><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></select></div>
              </div>
              <div style={{ display:"flex", flexDirection:"column", minHeight: 0 }}>
                {/* Source tab */}
                <div className="seg" style={{ marginBottom: 8, width:"fit-content" }}>
                  <button className={sourceMode === "text" ? "active" : ""} onClick={()=>setSourceMode("text")}>Paste text</button>
                  <button className={sourceMode === "pdf" ? "active" : ""} onClick={()=>setSourceMode("pdf")}>Upload PDF</button>
                </div>

                {sourceMode === "text" && (
                  <>
                    <div style={{ display:"flex", alignItems:"center", marginBottom: 6 }}>
                      <span className="label">Paste question paper text</span>
                      <span className="muted" style={{ marginLeft:"auto", fontSize: 11 }}>{text.length.toLocaleString()} chars · {text.split(/\n\s*\n/).filter(Boolean).length} blocks</span>
                    </div>
                    <textarea className="textarea" value={text} onChange={e=>setText(e.target.value)}
                      spellCheck={false}
                      placeholder={"1. Question stem ...\n   (a) ...  (b) ...  (c) ...  (d) ...\n\n2. Question stem ...   [3]\n\n3. ..."}
                      style={{ minHeight: 320, fontFamily:"'IBM Plex Mono', monospace", fontSize: 12.5, resize:"vertical" }}/>
                    <div style={{ marginTop: 8, padding:"8px 10px", background:"var(--bg-2)", border:"1px solid var(--line-soft)", borderRadius: 6, fontSize: 11, color:"var(--ink-3)" }}>
                      <LIcon name="sparkle" size={11}/> Gemini will structure your paste into questions, options, marks, and concept tags (JSON schema · 2–4s).
                    </div>
                  </>
                )}

                {sourceMode === "pdf" && (
                  <>
                    <input ref={pdfInputRef} type="file" accept="application/pdf,.pdf" style={{ display:"none" }}
                      onChange={e => setPdfFile(e.target.files?.[0] || null)}/>
                    <div onClick={() => pdfInputRef.current?.click()}
                      style={{
                        minHeight: 320, display:"grid", placeItems:"center",
                        border:`2px dashed ${pdfFile ? "var(--accent)" : "var(--line-strong)"}`,
                        borderRadius: 8, padding: 30, textAlign:"center", cursor:"pointer",
                        background: pdfFile ? "rgba(255,186,90,0.06)" : "var(--bg-2)",
                      }}>
                      {pdfFile ? (
                        <div>
                          <div style={{ fontSize: 30, marginBottom: 8 }}><LIcon name="report" size={36}/></div>
                          <div style={{ color:"var(--ink-0)", fontSize: 13, fontWeight: 500 }}>{pdfFile.name}</div>
                          <div className="muted" style={{ fontSize: 11, marginTop: 4 }}>{(pdfFile.size/1024/1024).toFixed(1)} MB · click to replace</div>
                        </div>
                      ) : (
                        <div>
                          <div style={{ fontSize: 30, color:"var(--ink-3)", marginBottom: 8 }}><LIcon name="upload" size={36}/></div>
                          <div style={{ color:"var(--ink-0)", fontSize: 13, fontWeight: 500 }}>Drop a question-paper PDF here</div>
                          <div className="muted" style={{ fontSize: 11, marginTop: 4 }}>or click to browse · max 25 MB</div>
                        </div>
                      )}
                    </div>
                    <div style={{ marginTop: 8, padding:"8px 10px", background:"var(--bg-2)", border:"1px solid var(--line-soft)", borderRadius: 6, fontSize: 11, color:"var(--ink-3)" }}>
                      <LIcon name="sparkle" size={11}/> Gemini Vision reads the PDF directly — handwriting, formulas, tables. Typical: 5–20 s for a 2-page paper.
                    </div>
                  </>
                )}
              </div>
            </div>
            <div style={{ padding: 12, borderTop:"1px solid var(--line)", display:"flex", gap: 8, alignItems:"center" }}>
              <span className="muted" style={{ fontSize: 11, maxWidth: 600, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>
                {saveErr && <span style={{ color:"var(--red)" }}>Save failed: {saveErr}</span>}
                {!saveErr && jsonError && <span style={{ color:"var(--red)" }}>{jsonError}</span>}
              </span>
              <span style={{ marginLeft:"auto", display:"flex", gap: 6 }}>
                <button className="btn sm" onClick={()=>save(true)}>Save as draft</button>
                {sourceMode === "text" ? (
                  <button className="btn sm primary" disabled={text.trim().length < 20 || parsing}
                    onClick={parseNow}
                    style={{ opacity: (text.trim().length < 20 || parsing) ? 0.55 : 1 }}>
                    {parsing ? "Parsing…" : <><LIcon name="sparkle" size={11}/> Structure with Gemini →</>}
                  </button>
                ) : (
                  <button className="btn sm primary" disabled={!pdfFile || parsing}
                    onClick={parsePdfNow}
                    style={{ opacity: (!pdfFile || parsing) ? 0.55 : 1 }}>
                    {parsing ? "Parsing PDF…" : <><LIcon name="sparkle" size={11}/> Structure PDF with Gemini →</>}
                  </button>
                )}
              </span>
            </div>
          </>
        )}

        {step === 2 && parsed && (
          <>
            <div style={{ padding: "16px 22px 0", display:"flex", alignItems:"center", gap: 10 }}>
              <span className="pill green"><span className="swatch"></span>{Math.round((parsed?.meta?.parse_confidence || 0.94) * 100)}% confidence</span>
              <span className="pill mono">{parsed?.meta?.model || "gemini-2.5-flash"}</span>
              <span className="pill mono">{(parsed?.summary?.questions ?? 0)} Q · {(parsed?.summary?.sections ?? 0)} sections · {(parsed?.summary?.total_marks ?? 0)} marks</span>
              <span style={{ marginLeft:"auto", display:"flex", gap: 6 }}>
                <button className="btn sm" onClick={formatJson} disabled={!!jsonError}>Format JSON</button>
                <button className="btn sm" onClick={() => navigator.clipboard?.writeText(jsonText)}>Copy</button>
              </span>
            </div>
            <div style={{ padding: "8px 22px 0" }}>
              <h3 style={{ margin:"6px 0 2px", color:"var(--ink-0)", fontFamily:"'Instrument Serif', serif", fontWeight:400, fontSize: 22 }}>Gemini output · review & edit</h3>
              <p className="muted" style={{ margin:"0 0 8px", fontSize: 12.5 }}>
                Raw structured JSON returned by Gemini. Edit any field — stems, options, marks, concepts, correct-answer flags — before saving. Live-validated.
              </p>
            </div>

            <div style={{ padding: "8px 22px", flex: 1, minHeight: 0, display:"flex", flexDirection:"column" }}>
              <textarea
                className="textarea"
                value={jsonText}
                onChange={(e) => { setJsonText(e.target.value); validateJson(e.target.value); }}
                spellCheck={false}
                style={{
                  flex: 1, minHeight: 320,
                  fontFamily:"'IBM Plex Mono', monospace", fontSize: 12, lineHeight: 1.55,
                  color:"var(--ink-0)",
                  border: jsonError ? "1px solid var(--red)" : "1px solid var(--line-strong)",
                  background:"#0e0e10", padding: 12, resize:"vertical", borderRadius: 6,
                }}/>
              <div style={{ marginTop: 8, padding:"8px 10px", border:"1px solid var(--line-soft)", borderRadius: 6,
                background: jsonError ? "rgba(255,107,107,0.08)" : "var(--bg-2)",
                color: jsonError ? "var(--red)" : "var(--ink-3)", fontSize: 11.5 }}>
                {jsonError
                  ? <><b>JSON error:</b> {jsonError}</>
                  : <>✓ Valid JSON · {(parsed?.sections || []).reduce((a, s) => a + (s.questions?.length || 0), 0)} questions across {(parsed?.sections || []).length} sections · schema: <span className="mono">paper.v1</span></>}
              </div>
            </div>

            <div style={{ padding: 12, borderTop:"1px solid var(--line)", display:"flex", gap: 8, alignItems:"center" }}>
              <button className="btn sm" onClick={()=>setStep(1)}>← Back to edit</button>
              <button className="btn sm" onClick={parseNow}><LIcon name="sparkle" size={11}/> Regenerate</button>
              <span style={{ marginLeft:"auto", display:"flex", gap: 6 }}>
                <button className="btn sm" disabled={!!jsonError} onClick={()=>save(true)}>Save as draft</button>
                <button className="btn sm primary" disabled={!!jsonError} onClick={()=>save(false)}>Save & schedule (Upcoming)</button>
              </span>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

/* ---------- Students-for-Test modal ---------- */
const StudentsModal = ({ test, onClose, setScreen, onViewReport }) => {
  if (!test) return null;
  const roster = window.KX.EVALUATIONS_Q3.map((e, i) => {
    const status = i < test.reviewed ? "evaluated" :
      i === test.reviewed ? "in-progress" :
        i < test.students - 4 ? "uploaded" : "not-uploaded";
    return { roll: e.roll, name: e.studentName, status,
      score: i < test.reviewed ? e.finalScore : null, max: 12 };
  }).slice(0, test.students);

  const counts = {
    evaluated: roster.filter((r) => r.status === "evaluated").length,
    inProgress: roster.filter((r) => r.status === "in-progress").length,
    uploaded: roster.filter((r) => r.status === "uploaded").length,
    notUploaded: roster.filter((r) => r.status === "not-uploaded").length,
  };

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.75)", 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: 960, maxWidth: "100%", maxHeight: "88vh", display: "flex", flexDirection: "column" }}>
        <div style={{ padding: "14px 18px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12 }}>
          <div>
            <div style={{ fontSize: 10, color: "var(--ink-3)", letterSpacing: ".08em", textTransform: "uppercase" }}>{test.id} · Students</div>
            <div style={{ color: "var(--ink-0)", fontSize: 15, fontWeight: 600 }}>{test.title}</div>
          </div>
          <span style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
            <span className="pill green">{counts.evaluated} evaluated</span>
            <span className="pill amber">{counts.inProgress} in progress</span>
            <span className="pill blue">{counts.uploaded} uploaded</span>
            <span className="pill">{counts.notUploaded} not uploaded</span>
            <button className="btn sm ghost" onClick={onClose} style={{ marginLeft: 6 }}>✕</button>
          </span>
        </div>

        <div style={{ padding: "10px 18px", borderBottom: "1px solid var(--line)", display: "flex", gap: 8, alignItems: "center" }}>
          <input className="input" placeholder="Search by roll or name…" style={{ width: 240, padding: "5px 10px", fontSize: 12 }}/>
          <select className="select" defaultValue="all" style={{ width: 160, padding: "5px 10px", fontSize: 12 }}>
            <option value="all">All statuses</option>
            <option>Evaluated</option><option>In progress</option><option>Uploaded</option><option>Not uploaded</option>
          </select>
          <span style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
            <button className="btn sm"><LIcon name="upload" size={11}/> Bulk upload scripts</button>
            <button className="btn sm primary" onClick={() => {onClose(); setScreen("evaluate");}}>
              <LIcon name="sparkle" size={11}/> Evaluate next pending
            </button>
          </span>
        </div>

        <div style={{ flex: 1, overflow: "auto" }}>
          <table className="tests" style={{ width: "100%" }}>
            <thead><tr>
              <th style={{ width: 90 }}>Roll No.</th><th>Student name</th>
              <th style={{ width: 130 }}>Status</th><th style={{ width: 110 }}>Score</th>
              <th style={{ width: 320 }}>Actions</th><th style={{ width: 90 }}>Report</th>
            </tr></thead>
            <tbody>
              {roster.map((r) =>
                <tr key={r.roll}>
                  <td className="mono" style={{ fontSize: 11.5, color: "var(--ink-1)" }}>{r.roll}</td>
                  <td><div style={{ color: "var(--ink-0)", fontWeight: 500 }}>{r.name}</div></td>
                  <td><StudentStatus status={r.status}/></td>
                  <td>{r.score != null ?
                    <span className="mono" style={{ color: "var(--ink-0)", fontSize: 13 }}>{r.score}<span style={{ color: "var(--ink-3)", fontSize: 11 }}>/{r.max}</span></span> :
                    <span className="muted">—</span>}</td>
                  <td><div style={{ display: "flex", gap: 6 }}>
                    <button className="btn sm" disabled={r.status !== "not-uploaded"} style={{ opacity: r.status === "not-uploaded" ? 1 : 0.45 }}>
                      <LIcon name="upload" size={11}/> Upload
                    </button>
                    <button className="btn sm primary" disabled={r.status === "not-uploaded" || r.status === "evaluated"}
                      style={{ opacity: r.status === "uploaded" ? 1 : 0.45 }}
                      onClick={() => {onClose(); setScreen("evaluate");}}>
                      <LIcon name="sparkle" size={11}/> Evaluate
                    </button>
                    <button className="btn sm" disabled={r.status !== "in-progress"} style={{ opacity: r.status === "in-progress" ? 1 : 0.45 }}
                      onClick={() => {onClose(); setScreen("evaluate");}}>Resume →</button>
                  </div></td>
                  <td>
                    <button className="btn sm ghost" disabled={r.status !== "evaluated"}
                      style={{ opacity: r.status === "evaluated" ? 1 : 0.4 }}
                      onClick={() => onViewReport ? onViewReport(r) : setScreen("report")}>
                      <LIcon name="report" size={11}/> View
                    </button>
                  </td>
                </tr>
              )}
            </tbody>
          </table>
        </div>

        <div style={{ padding: "10px 18px", borderTop: "1px solid var(--line)", display: "flex", alignItems: "center", fontSize: 11, color: "var(--ink-3)" }}>
          <span>{roster.length} students · roster from class {test.className}</span>
          <span style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
            <button className="btn sm" onClick={onClose}>Close</button>
          </span>
        </div>
      </div>
    </div>
  );
};

const StudentStatus = ({ status }) => {
  const map = {
    "evaluated": { cls: "pill green", label: "Evaluated", dot: "var(--green)" },
    "in-progress": { cls: "pill amber", label: "In progress", dot: "var(--amber)" },
    "uploaded": { cls: "pill blue", label: "Uploaded", dot: "var(--blue)" },
    "not-uploaded": { cls: "pill", label: "Not uploaded", dot: "var(--ink-4)" },
  }[status];
  return <span className={map.cls}><span className="swatch" style={{ background: map.dot }}></span>{map.label}</span>;
};

/* ---------- Student-specific report ---------- */
const StudentReportModal = ({ data, onClose }) => {
  if (!data) return null;
  const { test, student } = data;
  // Backend populates:
  //   student.evaluations    → [{ question_id, marks, max_marks }]
  //   student.suggestions    → { [q_no]: string }
  //   student.misconceptions → [{ code, note, weight }]
  //   student.studyGuide     → [{ topic, time, priority }]
  const questions = window.KX.QUESTIONS || [];
  const evalByQ = Object.fromEntries((student.evaluations || []).map(e => [e.question_id, e]));
  const perQ = questions.map(q => ({ q, marks: evalByQ[q.id]?.marks ?? 0 }));
  const obtained = perQ.reduce((a, r) => a + r.marks, 0);
  const max = perQ.reduce((a, r) => a + r.q.maxMarks, 0);
  const pct = max ? Math.round((obtained / max) * 100) : 0;

  const suggestionsByQ = student.suggestions || {};
  const misconceptions = student.misconceptions || [];
  const studyGuide = student.studyGuide || [];

  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.78)", zIndex: 210, 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: 980, maxWidth:"100%", maxHeight:"92vh", display:"flex", flexDirection:"column" }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap: 12 }}>
          <div>
            <div style={{ fontSize: 10, color:"var(--ink-3)", letterSpacing:".08em", textTransform:"uppercase" }}>{test.id} · Student report</div>
            <div style={{ color:"var(--ink-0)", fontSize: 16, fontWeight: 600 }}>{student.name} <span className="mono muted" style={{ fontSize: 12, marginLeft: 6 }}>{student.roll}</span></div>
          </div>
          <span style={{ marginLeft:"auto", display:"flex", alignItems:"center", gap: 8 }}>
            <span className="pill mono">{test.title}</span>
            <button className="btn sm">Download PDF</button>
            <button className="btn sm">Share with parent</button>
            <button className="btn sm ghost" onClick={onClose}>✕</button>
          </span>
        </div>

        <div style={{ flex:1, overflow:"auto", padding: 20, display:"flex", flexDirection:"column", gap: 18 }}>
          {/* Score summary */}
          <div style={{ display:"grid", gridTemplateColumns:"repeat(4, 1fr)", gap: 12 }}>
            <Stat2 label="Total score" value={`${obtained}/${max}`} accent="var(--accent)"/>
            <Stat2 label="Percentage" value={`${pct}%`} accent={pct >= 75 ? "var(--green)" : pct >= 50 ? "var(--accent)" : "var(--red)"}/>
            <Stat2 label="Class rank" value="—" accent="var(--ink-1)"/>
            <Stat2 label="Strong / weak" value={`${perQ.filter(r=>r.marks===r.q.maxMarks).length} / ${perQ.filter(r=>r.marks===0).length}`} accent="var(--blue)"/>
          </div>

          {/* 1. Per-question marks */}
          <Section2 title="1 · Question-wise performance">
            <div className="card">
              <table className="tests" style={{ width:"100%" }}>
                <thead><tr><th style={{ width:50 }}>Q</th><th>Question</th><th style={{ width: 100 }}>Marks</th><th style={{ width: 140 }}>Result</th></tr></thead>
                <tbody>
                  {perQ.map(({q, marks}) => {
                    const full = marks === q.maxMarks;
                    const zero = marks === 0;
                    return (
                      <tr key={q.id}>
                        <td className="mono" style={{ color:"var(--accent)", fontWeight: 600 }}>Q{q.num}</td>
                        <td><div style={{ color:"var(--ink-1)", fontSize: 12.5 }}>{q.english}</div></td>
                        <td><span className="mono" style={{ color: full ? "var(--green)" : zero ? "var(--red)" : "var(--amber)", fontSize: 13, fontWeight: 600 }}>{marks}/{q.maxMarks}</span></td>
                        <td>{full ? <span className="pill green">Full</span> : zero ? <span className="pill red">Missed</span> : <span className="pill amber">Partial</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </Section2>

          {/* 2. Per-question suggestions */}
          <Section2 title="2 · Question-based suggestions">
            <div style={{ display:"flex", flexDirection:"column", gap: 8 }}>
              {perQ.filter(r => r.marks < r.q.maxMarks).map(({q}) => (
                <div key={q.id} style={{ padding: 12, background:"var(--bg-2)", border:"1px solid var(--line)", borderRadius: 6, display:"flex", gap: 12 }}>
                  <span className="mono" style={{ color:"var(--accent)", fontWeight: 600, fontSize: 12, minWidth: 28 }}>Q{q.num}</span>
                  <div style={{ flex: 1, fontSize: 13, color:"var(--ink-1)", lineHeight: 1.5 }}>{suggestionsByQ[q.num] || "Review this concept and try a similar problem."}</div>
                </div>
              ))}
              {perQ.every(r => r.marks === r.q.maxMarks) && (
                <div style={{ padding: 14, background:"var(--green-bg)", border:"1px solid rgba(75,201,123,0.25)", borderRadius: 6, color:"var(--green)", fontSize: 13 }}>
                  Perfect score — no targeted suggestions needed. Move to next-level problems.
                </div>
              )}
            </div>
          </Section2>

          {/* 3. Misconceptions */}
          <Section2 title="3 · Potential misconceptions">
            <div style={{ display:"flex", flexDirection:"column", gap: 8 }}>
              {misconceptions.map(m => (
                <div key={m.code} style={{ padding: 12, background:"rgba(180,140,255,0.06)", border:"1px solid rgba(180,140,255,0.25)", borderRadius: 6 }}>
                  <div style={{ display:"flex", alignItems:"center", gap: 8, marginBottom: 4 }}>
                    <span className="pill violet">{m.code}</span>
                    <span className="pill" style={{ marginLeft: 6, fontSize: 10 }}>{m.weight}</span>
                  </div>
                  <div style={{ fontSize: 12.5, color:"var(--ink-1)" }}>{m.note}</div>
                </div>
              ))}
            </div>
          </Section2>

          {/* 4. Study guide */}
          <Section2 title="4 · Personalised study guide">
            <div className="card">
              {studyGuide.map((g, i) => (
                <div key={i} style={{ padding:"10px 14px", borderBottom: i < studyGuide.length - 1 ? "1px solid var(--line-soft)" : "none", display:"flex", alignItems:"center", gap: 12 }}>
                  <span className="mono" style={{ color:"var(--ink-3)", fontSize: 11, minWidth: 22 }}>{String(i+1).padStart(2,"0")}</span>
                  <span style={{ flex: 1, color:"var(--ink-0)", fontSize: 13 }}>{g.topic}</span>
                  <span className="pill mono" style={{ fontSize: 10 }}>{g.time}</span>
                  <span className={`pill ${g.priority === "high" ? "red" : g.priority === "medium" ? "amber" : ""}`} style={{ fontSize: 10 }}>{g.priority}</span>
                </div>
              ))}
            </div>
          </Section2>
        </div>

        <div style={{ padding: 12, borderTop:"1px solid var(--line)", display:"flex", gap: 8 }}>
          <span style={{ marginLeft:"auto", display:"flex", gap: 6 }}>
            <button className="btn sm" onClick={onClose}>Close</button>
            <button className="btn sm primary">Email to student</button>
          </span>
        </div>
      </div>
    </div>
  );
};

const Stat2 = ({ label, value, accent }) => (
  <div style={{ padding: 14, background:"var(--bg-2)", border:"1px solid var(--line)", borderRadius: 8 }}>
    <div className="muted" style={{ fontSize: 10, letterSpacing:".08em", textTransform:"uppercase", marginBottom: 4 }}>{label}</div>
    <div className="mono" style={{ fontSize: 24, fontWeight: 600, color: accent }}>{value}</div>
  </div>
);

const Section2 = ({ title, children }) => (
  <div>
    <div style={{ fontSize: 12, color:"var(--ink-0)", fontWeight: 600, marginBottom: 8, letterSpacing:".02em" }}>{title}</div>
    {children}
  </div>
);

/* ---------- Edit test modal — full CRUD over the test header ---------- */
const EditTestModal = ({ test, onClose, onSave }) => {
  const classSections = window.KX?.CLASS_SECTIONS || [];
  const subjects = window.KX?.SUBJECTS || [];
  const [form, setForm] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  React.useEffect(() => {
    if (!test) { setForm(null); return; }
    setForm({
      title: test.title || "",
      class_section_ids: Array.isArray(test.class_section_ids) && test.class_section_ids.length
        ? test.class_section_ids
        : (test.class_section_id ? [test.class_section_id] : []),
      className: test.className || "",
      subject: test.subject || (subjects[0]?.name || ""),
      topic: test.topic || "",
      chapter: test.chapter || "",
      date: (test.date || "").slice(0, 10),
      language: test.language || "bn",
      author: window.KX.CURRENT_USER?.name || "",
    });
  }, [test]);
  if (!test || !form) return null;

  const classLevels = Array.from(new Set(classSections.map(c => c.class_level))).sort();
  const pickClass = (cl) => {
    const first = classSections.find(c => c.class_level === cl);
    setForm({ ...form, className: cl, class_section_ids: first ? [first.id] : [] });
  };

  const submit = async () => {
    setSaving(true);
    try {
      // Pass `null` (not undefined) so an empty selection actually clears
      // the column — `undefined` would be JSON-stripped from the body and
      // the backend's "field absent ⇒ keep current value" rule would kick
      // in, silently preserving the old text.
      await onSave(test.id, {
        title: form.title,
        class_section_ids: form.class_section_ids.length ? form.class_section_ids : undefined,
        className: form.className,
        subject: form.subject,
        topic: form.topic ? form.topic : null,
        chapter: form.chapter ? form.chapter : null,
        scheduled_date: form.date || undefined,
        language: form.language,
      });
    } catch (e) { alert("Could not save: " + (e?.message || e)); }
    finally { setSaving(false); }
  };

  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:640, maxWidth:"100%", display:"flex", flexDirection:"column" }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:10 }}>
          <span style={{ color:"var(--ink-0)", fontSize:14, fontWeight:600 }}>Edit test · <span className="mono muted">{test.id}</span></span>
          <button className="btn sm ghost" style={{ marginLeft:"auto" }} onClick={onClose}>✕</button>
        </div>
        <div style={{ padding:20, display:"grid", gridTemplateColumns:"1fr 1fr", gap:12 }}>
          <div className="field" style={{ gridColumn:"1 / span 2" }}>
            <span className="label">Test title</span>
            <input className="input" value={form.title} onChange={e=>setForm({...form, title:e.target.value})}/>
          </div>
          <div className="field">
            <span className="label">Class</span>
            <select className="select" value={form.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={form.subject} onChange={e=>setForm({...form, 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</span></span>
            <window.KXUI.SectionChips classLevel={form.className} selectedIds={form.class_section_ids}
              onChange={(ids) => setForm({ ...form, class_section_ids: ids })}/>
          </div>
          {/* Chapter + topic — multi-select chip picker shared with the
              create modal (defined in screen-manual.jsx). The picker reads
              + writes `chapter` and `topic` as comma-separated TEXT, so the
              existing submit() path that POSTs those columns keeps working
              with no further changes. */}
          <window.ChapterTopicFields meta={form} setMeta={setForm} subjects={subjects}/>
          <div className="field"><span className="label">Scheduled date</span>
            <input className="input" type="date" style={{ colorScheme:"dark" }} value={form.date} onChange={e=>setForm({...form, date:e.target.value})}/></div>
          <div className="field">
            <span className="label">Language</span>
            <select className="select" value={form.language} onChange={e=>setForm({...form, language:e.target.value})}>
              <option value="bn">Bengali</option><option value="hi">Hindi</option><option value="en">English</option>
            </select>
          </div>
          <div className="field" style={{ gridColumn:"1 / span 2" }}>
            <span className="label">Author</span>
            <input className="input" value={form.author} disabled style={{ opacity:0.6 }}/>
          </div>
        </div>
        <div style={{ padding:12, borderTop:"1px solid var(--line)", display:"flex", gap:8 }}>
          <span style={{ marginLeft:"auto", display:"flex", gap:6 }}>
            <button className="btn sm" onClick={onClose}>Cancel</button>
            <button className="btn sm primary" disabled={saving || !form.title?.trim()} onClick={submit}>
              {saving ? "Saving…" : "Save changes"}
            </button>
          </span>
        </div>
      </div>
    </div>
  );
};

/* ---------- Upload sheets modal — roster with per-row upload + evaluate ---------- */
const UploadSheetsModal = ({ test, onClose, onRefresh, onOpenCockpit }) => {
  const [roster, setRoster] = React.useState(null);
  const [busy, setBusy] = React.useState({}); // student_id → "uploading"
  const fileInputs = React.useRef({});         // student_id → <input ref>

  const load = React.useCallback(async () => {
    if (!test) return;
    try { setRoster(await window.KXApi.get(`/tests/${test.id}/students`)); }
    catch (e) { alert("Could not load roster: " + (e?.message || e)); }
  }, [test]);

  React.useEffect(() => { load(); }, [load]);

  if (!test) return null;

  const onPickFile = (student) => {
    fileInputs.current[student.student_id]?.click();
  };

  const onFileChosen = async (student, file) => {
    if (!file) return;
    if (file.type && file.type !== "application/pdf") {
      alert("Please choose a PDF file."); return;
    }
    setBusy(b => ({ ...b, [student.student_id]: "uploading" }));
    try {
      const form = new FormData();
      form.append("file", file, file.name);
      const r = await fetch(`/api/tests/${test.id}/students/${student.student_id}/sheet`, {
        method: "POST", body: form,
      });
      if (!r.ok) throw new Error(`upload ${r.status}: ${await r.text()}`);
      await load();
    } catch (e) { alert("Upload failed: " + (e?.message || e)); }
    setBusy(b => { const c = { ...b }; delete c[student.student_id]; return c; });
  };

  // Evaluate now opens the Paper Evaluation cockpit with the student's PDF in the middle panel.
  const onEvaluate = (student) => {
    if (!student.sheet_id) return;
    onOpenCockpit?.({ test, student });
    onClose?.();
  };

  return (
    <div onClick={onClose} style={{ position:"fixed", inset:0, background:"rgba(0,0,0,0.78)", zIndex:200, 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:920, maxWidth:"100%", maxHeight:"86vh", display:"flex", flexDirection:"column" }}>
        <div style={{ padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:10 }}>
          <div>
            <div style={{ fontSize:10, color:"var(--ink-3)", letterSpacing:".08em", textTransform:"uppercase" }}>{test.id} · {test.className} · {test.subject}</div>
            <div style={{ color:"var(--ink-0)", fontSize:15, fontWeight:600 }}>Upload answer sheets — {test.title}</div>
          </div>
          <span style={{ marginLeft:"auto", display:"flex", gap:8, alignItems:"center" }}>
            {roster && <span className="muted" style={{ fontSize:11 }}>{roster.filter(r=>r.uploaded).length}/{roster.length} uploaded · {roster.filter(r=>r.finalised).length}/{roster.length} finalised</span>}
            <button className="btn sm ghost" onClick={onClose}>✕</button>
          </span>
        </div>
        <div style={{ flex:1, overflow:"auto" }}>
          <table className="tests" style={{ width:"100%" }}>
            <thead>
              <tr>
                <th style={{ width:90 }}>Roll no.</th>
                <th>Name</th>
                <th style={{ width:160 }}>Sheet</th>
                <th style={{ width:160 }}>Evaluation</th>
                <th style={{ width:110 }}>Score</th>
                <th style={{ width:140 }}></th>
              </tr>
            </thead>
            <tbody>
              {(roster || []).map(r => (
                <tr key={r.student_id}>
                  <td className="mono" style={{ fontSize:11.5 }}>{r.roll_no}</td>
                  <td>{r.name}</td>
                  <td>
                    {r.uploaded
                      ? <span className="pill green"><span className="swatch" style={{ background:"var(--green)" }}></span>Uploaded</span>
                      : <span className="pill"><span className="swatch"></span>Not uploaded</span>}
                  </td>
                  <td>
                    {r.finalised
                      ? <span className="pill green"><span className="swatch" style={{ background:"var(--green)" }}></span>Finalised</span>
                      : r.evaluated
                          ? <span className="pill amber"><span className="swatch" style={{ background:"var(--amber)" }}></span>Draft</span>
                          : (r.uploaded ? <span className="pill"><span className="swatch"></span>Not started</span>
                                        : <span className="pill"><span className="swatch"></span>—</span>)}
                  </td>
                  <td>
                    {r.evaluation_count > 0
                      ? <span className="mono" style={{ color: r.finalised ? "var(--green)" : "var(--amber)" }}>
                          {r.score}<span className="muted">/{r.max_score || "—"}</span>
                        </span>
                      : <span className="muted mono">—</span>}
                  </td>
                  <td style={{ display:"flex", gap:6 }}>
                    <input
                      ref={el => { fileInputs.current[r.student_id] = el; }}
                      type="file"
                      accept="application/pdf,.pdf"
                      style={{ display:"none" }}
                      onChange={e => onFileChosen(r, e.target.files?.[0])}
                    />
                    <button className="btn sm" disabled={!!busy[r.student_id]} onClick={()=>onPickFile(r)}>
                      {busy[r.student_id]==="uploading" ? "…" : (r.uploaded ? "Replace" : "Upload PDF")}
                    </button>
                    <button className="btn sm primary" disabled={!r.uploaded} onClick={()=>onEvaluate(r)}
                      style={{ opacity: !r.uploaded ? 0.5 : 1 }}>
                      Evaluate
                    </button>
                  </td>
                </tr>
              ))}
              {roster && roster.length === 0 && (
                <tr><td colSpan="6" style={{ padding:30, textAlign:"center", color:"var(--ink-3)", fontSize:13 }}>No students enrolled in this test.</td></tr>
              )}
              {!roster && (
                <tr><td colSpan="6" style={{ padding:30, textAlign:"center", color:"var(--ink-3)", fontSize:13 }}>Loading…</td></tr>
              )}
            </tbody>
          </table>
        </div>
        <div style={{ padding:12, borderTop:"1px solid var(--line)", display:"flex", gap:8 }}>
          <span className="muted" style={{ fontSize:11 }}>
            Once every enrolled student is evaluated, this test will move to <b>Completed</b>.
          </span>
          <span style={{ marginLeft:"auto" }}>
            <button className="btn sm" onClick={onClose}>Done</button>
          </span>
        </div>
      </div>
    </div>
  );
};
