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

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

// Hand-rolled .ui-modal shell for this file's dialogs. Not portalled and no
// Esc handler, so open/close behaviour is exactly what it was; the original
// z-index of each dialog is passed through so stacking is unchanged.
const LibModalShell = ({ onClose, size = "", zIndex = 200, eyebrow, title, sub, icon, headExtra, bar, children, footer, bodyStyle, width }) => (
  <div className="ui-scrim" onClick={onClose} style={{ zIndex }}>
    <div className={`ui-modal ${size}`} role="dialog" aria-modal="true"
      aria-label={typeof title === "string" ? title : undefined}
      onClick={e => e.stopPropagation()} style={width ? { width: `min(${width}px, 100%)` } : undefined}>
      <div className="ui-modal-head">
        {icon && <span className="ui-modal-icon"><LIcon name={icon} size={16}/></span>}
        <div className="ui-modal-titleblock" style={{ minWidth: 0, flex: 1 }}>
          {eyebrow && <div className="ui-section-label" style={{ margin: "0 0 3px" }}>{eyebrow}</div>}
          <div className="ui-modal-title">{title}</div>
          {sub && <div className="ui-modal-sub">{sub}</div>}
        </div>
        {headExtra && <div className="ui-modal-extra" style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>{headExtra}</div>}
        <button type="button" className="ui-iconbtn ui-modal-close" aria-label="Close" onClick={onClose} style={{ marginLeft: headExtra ? 4 : "auto" }}>
          <LIcon name="x" size={16}/>
        </button>
      </div>
      {bar && <div className="ui-modal-bar">{bar}</div>}
      <div className="ui-modal-body" style={bodyStyle}>{children}</div>
      {footer && <div className="ui-modal-foot">{footer}</div>}
    </div>
  </div>
);

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.APP.TESTS];
  // refreshTick keeps `allTests` reactive when window.APP.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.AppUI.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.APP.OPEN_UPLOAD_SHEETS_FOR
  // = <display_id>. When the library remounts, reopen that test's roster modal.
  useEffect(() => {
    const targetId = window.APP.OPEN_UPLOAD_SHEETS_FOR;
    if (!targetId) return;
    window.APP.OPEN_UPLOAD_SHEETS_FOR = null;
    (async () => {
      try {
        window.APP.TESTS = await window.AppApi.get("/tests");
        const t = window.APP.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.AppApi.post(`/tests/${newTest._payload.meta.display_id}/structure`,
          { sections, as_draft, submission_mode });
      } else {
        await window.AppApi.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.APP.TESTS = await window.AppApi.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.APP.TESTS = await window.AppApi.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.AppApi.del(`/tests/${test.id}`);
      await refreshTests();
    } catch (e) { alert("Could not delete: " + (e?.message || e)); }
  };

  const handleSaveEdit = async (display_id, updates) => {
    await window.AppApi.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.AppApi.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 || "en",
        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 className="cx-page">
      <window.AppUI.PageHeader
        eyebrow={window.APP.CURRENT_SCHOOL ? `${window.APP.CURRENT_SCHOOL.name} · ${window.APP.CURRENT_SCHOOL.academic_year}` : "—"}
        title="Test Library"
        meta={[`${allTests.length} ${allTests.length === 1 ? "test" : "tests"}`, `${allTests.filter(t => t.status === "grading").length} in active grading`]}
        actions={<>
          <button className="btn" onClick={()=>setScreen("online-tests")}>
            <LIcon name="monitor" size={15}/>Online test
          </button>
          <button className="btn primary" onClick={()=>setCreateOpen(true)}><LIcon name="plus" size={15}/>Create test</button>
        </>}
      />

      <div className="cx-catalog-split narrow" style={{ gap: 20 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <div className="cx-card" style={{ alignSelf: "start", width: "100%" }}>
            <div className="cx-card-head">
              <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
                <LIcon name="filter" size={15} style={{ color: "var(--ink-3)" }}/>
                <span className="cx-card-title">Filters</span>
              </span>
              <span className="muted" style={{ fontSize: 12.5 }}>Reset</span>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              {[
                { k: "class",   label: "Class",   opts: ["All", ...(window.APP?.CLASS_OPTIONS   || ["IX","X"])] },
                { k: "subject", label: "Subject", opts: ["All", ...(window.APP?.SUBJECT_OPTIONS || [])] },
                { k: "status",  label: "Status",  opts: ["All", "draft", "upcoming", "optimised", "grading", "closed"] },
              ].map((f) =>
                <div key={f.k} className="field" style={{ margin: 0 }}>
                  <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" style={{ margin: 0 }}>
                <span className="label">Date range</span>
                <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                  <input className="input" type="date"/>
                  <input className="input" type="date"/>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div className="cx-tablecard" style={{ display: "flex", flexDirection: "column" }}>
          <div className="cx-tablecard-head" style={{ gap: 10 }}>
            <div className="ui-search" style={{ flex: "1 1 220px" }}>
              <LIcon name="search" size={14}/>
              <input className="input"
                placeholder="Search tests, IDs, topics… (⌘K)" value={search} onChange={(e) => setSearch(e.target.value)}/>
            </div>
            <span className="muted tabular" style={{ fontSize: 12.5 }}>{filtered.length} of {allTests.length}</span>
            <window.AppUI.Tabs size="sm" value="All" onChange={() => {}} ariaLabel="Ownership"
              items={[{ value: "All", label: "All" }, { value: "Mine", label: "Mine" }, { value: "Shared", label: "Shared" }]}/>
          </div>

          <div className="cx-tablescroll">
            <table className="tests" style={{ minWidth: 1080 }}>
              <thead><tr>
                <th style={{ width: 80 }}>ID</th><th>Test name</th>
                <th style={{ width: 64 }}>Class</th><th style={{ width: 110 }}>Subject</th>
                <th style={{ width: 110 }}>Date</th><th style={{ width: 120 }}>Status</th>
                <th style={{ width: 140 }}>Progress</th><th className="actions" style={{ width: 200 }}>Action</th>
              </tr></thead>
              <tbody>
                {filtered.map((t) => {
                  const isActive = selected === t.id;
                  // Online 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(--surface-active)" } : {}}>
                      <td><span className="ui-id">{t.id}</span></td>
                      <td>
                        <div className="t-main">{t.title}</div>
                        <div className="t-sub">
                          {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="muted tabular" style={{ fontSize: 12.5, whiteSpace: "nowrap" }} title={t.date}>{/^\d{4}-\d{2}-\d{2}T/.test(t.date || "") ? new Date(t.date).toLocaleDateString("en-CA") : 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="muted tabular" style={{ fontSize: 11.5 }}>
                              {numer}/{t.students} {isOnline ? "submitted" : "reviewed"}
                            </span>
                          </div> : <span className="muted">—</span>}
                      </td>
                      <td className="actions" 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>
                  );
                })}
                {filtered.length === 0 && (
                  <tr><td colSpan="8"><window.AppUI.EmptyState compact icon="library" title="No tests match"
                    hint={allTests.length ? "Try clearing a filter or the search." : "Create your first test to get started."}/></td></tr>
                )}
              </tbody>
            </table>
          </div>

          <div style={{ borderTop: "1px solid var(--line)", padding: "12px 20px", display: "flex", alignItems: "center", fontSize: 12.5, color: "var(--ink-3)" }}>
            <span className="tabular">Showing 1–{filtered.length} of {filtered.length}</span>
            <span className="tabular" 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.APP.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 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.AppUI.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:"inline-flex", gap: 4, justifyContent: "flex-end" }}>
      <button className={`btn sm ${primary.cls || ""}`} onClick={primary.onClick}>{primary.label}</button>
      <button className="btn sm icon-only" onClick={onEngineConfig} title="Engine config" aria-label="Engine config">
        <LIcon name="settings" size={14}/>
      </button>
      <button className="btn sm icon-only" onClick={onOpen} title="More actions" aria-label="More actions" aria-expanded={open}>
        <LIcon name="more" size={14}/>
      </button>
      {open && (
        <div className="ui-menu" role="menu" style={{
          position:"absolute", right: 0, top: "100%", marginTop: 6, zIndex: 50,
          minWidth: 200, padding: "4px 0",
        }}>
          {items.map((it, i) => (
            <button key={i} type="button" role="menuitem" disabled={it.disabled}
              className={`ui-menu-item${it.danger ? " danger" : ""}`}
              onClick={()=>!it.disabled && it.onClick()}
              style={it.disabled ? { color: "var(--ink-4)", cursor: "not-allowed", background: "transparent" } : undefined}>
              {it.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

/* ---------- Test preview modal (clicking test name) ---------- */
const TestPreviewModal = ({ test, onClose }) => {
  if (!test) return null;
  const questions = window.APP.QUESTIONS;
  return (
    <LibModalShell onClose={onClose} width={820}
      eyebrow={`${test.id} · ${test.className} · ${test.subject}`}
      title={test.title}
      headExtra={<>
        <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>
      </>}
      bodyStyle={{ padding: 0, display: "flex", flexDirection: "column" }}
      footer={<>
        <button className="btn" onClick={onClose}>Close</button>
        <button className="btn"><LIcon name="edit" size={15}/>Edit paper</button>
        <button className="btn primary"><LIcon name="download" size={15}/>Download PDF</button>
      </>}>
      <div style={{ padding: "18px 24px", flex: 1, background:"var(--paper)", color:"var(--ink-0)", fontFamily:"var(--font-sans)", borderTop: "1px solid var(--line)" }}>
        <div style={{ display:"flex", justifyContent:"space-between", gap: 12, flexWrap: "wrap", borderBottom:"1px solid var(--line-strong)", paddingBottom: 10, marginBottom: 16, fontSize: 12, color:"var(--ink-2)" }}>
          <span>{test.chapter} · {/^\d{4}-\d{2}-\d{2}T/.test(test.date || "") ? new Date(test.date).toLocaleDateString("en-CA") : 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 className="mono" style={{ color:"var(--ink-3)", fontSize: 11.5, 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 && (
          <window.AppUI.EmptyState compact icon="edit" title="Still a draft" hint="This test is still a draft — no questions added yet."/>
        )}
      </div>
    </LibModalShell>
  );
};

/* ---------- Upload / Create test modal ---------- */
const UploadTestModal = ({ open, onClose, onSave, onOpenManual }) => {
  const classSections = window.APP?.CLASS_SECTIONS || [];
  const subjects = window.APP?.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: "en",
  });
  // 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.AppApi.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(window.AppApi.base + "/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 className="ui-scrim" onClick={onClose} style={{ zIndex: 200 }}>
      <div className="ui-modal" role="dialog" aria-modal="true" aria-label="Upload question paper"
        onClick={e=>e.stopPropagation()} style={{ width: "min(880px, 100%)", maxHeight: "90vh" }}>
        <div className="ui-modal-head" style={{ alignItems: "center", borderBottom: "1px solid var(--line)" }}>
          <span className="ui-modal-icon"><LIcon name="upload" size={16}/></span>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div className="ui-modal-title">Upload question paper · Create new test</div>
          </div>
          <window.AppUI.StatusBadge tone="blue" icon={false}>Step {step} of 2</window.AppUI.StatusBadge>
          <button type="button" className="ui-iconbtn" aria-label="Close" onClick={onClose}><LIcon name="x" size={16}/></button>
        </div>

        {step === 1 && (
          <>
            <div style={{ padding: 20, overflow:"auto", flex: 1, display:"grid", gridTemplateColumns:"minmax(0, 260px) minmax(0, 1fr)", gap: 20, alignItems: "start" }}>
              <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: 11.5, marginLeft: 4 }}>tap to add</span></span>
                  <window.AppUI.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 style={{ marginBottom: 12 }}>
                  <window.AppUI.Tabs value={sourceMode} onChange={setSourceMode} ariaLabel="Paper source"
                    items={[{ value: "text", label: "Paste text", icon: "clipboard" }, { value: "pdf", label: "Upload PDF", icon: "upload" }]}/>
                </div>

                {sourceMode === "text" && (
                  <>
                    <div style={{ display:"flex", alignItems:"center", marginBottom: 6 }}>
                      <span className="label">Paste question paper text</span>
                      <span className="muted tabular" style={{ marginLeft:"auto", fontSize: 12 }}>{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:"var(--font-mono)", fontSize: 12.5, resize:"vertical" }}/>
                    <div className="cx-note" style={{ marginTop: 10, fontSize: 12.5, display: "flex", gap: 8, alignItems: "center" }}>
                      <LIcon name="sparkle" size={14}/> 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:`1.5px dashed ${pdfFile ? "var(--accent)" : "var(--line-strong)"}`,
                        borderRadius: "var(--radius-card)", padding: 30, textAlign:"center", cursor:"pointer",
                        background: pdfFile ? "var(--accent-bg)" : "var(--surface-hover)",
                      }}>
                      {pdfFile ? (
                        <div>
                          <div className="ui-empty-mark" style={{ margin: "0 auto 12px" }}><LIcon name="report" size={18}/></div>
                          <div style={{ color:"var(--ink-0)", fontSize: 14, fontWeight: 600 }}>{pdfFile.name}</div>
                          <div className="muted tabular" style={{ fontSize: 12.5, marginTop: 4 }}>{(pdfFile.size/1024/1024).toFixed(1)} MB · click to replace</div>
                        </div>
                      ) : (
                        <div>
                          <div className="ui-empty-mark" style={{ margin: "0 auto 12px" }}><LIcon name="upload" size={18}/></div>
                          <div style={{ color:"var(--ink-0)", fontSize: 14, fontWeight: 600 }}>Drop a question-paper PDF here</div>
                          <div className="muted" style={{ fontSize: 12.5, marginTop: 4 }}>or click to browse · max 25 MB</div>
                        </div>
                      )}
                    </div>
                    <div className="cx-note" style={{ marginTop: 10, fontSize: 12.5, display: "flex", gap: 8, alignItems: "center" }}>
                      <LIcon name="sparkle" size={14}/> Gemini Vision reads the PDF directly — handwriting, formulas, tables. Typical: 5–20 s for a 2-page paper.
                    </div>
                  </>
                )}
              </div>
            </div>
            <div className="ui-modal-foot" style={{ justifyContent: "flex-start" }}>
              <span className="muted" style={{ fontSize: 12.5, 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" onClick={()=>save(true)}>Save as draft</button>
                {sourceMode === "text" ? (
                  <button className="btn 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={15}/>Structure with Gemini<LIcon name="arrowRight" size={15}/></>}
                  </button>
                ) : (
                  <button className="btn primary" disabled={!pdfFile || parsing}
                    onClick={parsePdfNow}
                    style={{ opacity: (!pdfFile || parsing) ? 0.55 : 1 }}>
                    {parsing ? "Parsing PDF…" : <><LIcon name="sparkle" size={15}/>Structure PDF with Gemini<LIcon name="arrowRight" size={15}/></>}
                  </button>
                )}
              </span>
            </div>
          </>
        )}

        {step === 2 && parsed && (
          <>
            <div style={{ padding: "16px 22px 0", display:"flex", alignItems:"center", gap: 8, flexWrap: "wrap" }}>
              <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}><LIcon name="list" size={14}/>Format JSON</button>
                <button className="btn sm" onClick={() => navigator.clipboard?.writeText(jsonText)}><LIcon name="copy" size={14}/>Copy</button>
              </span>
            </div>
            <div style={{ padding: "8px 22px 0" }}>
              <h3 className="cx-card-title" style={{ margin:"8px 0 4px", fontSize: 18 }}>Gemini output · review & edit</h3>
              <p className="muted" style={{ margin:"0 0 8px", fontSize: 13 }}>
                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:"var(--font-mono)", fontSize: 12, lineHeight: 1.55,
                  color:"var(--ink-0)",
                  border: jsonError ? "1px solid var(--red)" : "1px solid var(--line-strong)",
                  background:"var(--ink-0)", padding: 12, resize:"vertical", borderRadius: "var(--radius-tile)",
                }}/>
              <div className={`ui-alert ${jsonError ? "red" : "green"}`} style={{ marginTop: 10, fontSize: 12.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 className="ui-modal-foot" style={{ justifyContent: "flex-start", marginTop: 12 }}>
              <button className="btn" onClick={()=>setStep(1)}><LIcon name="arrowLeft" size={15}/>Back to edit</button>
              <button className="btn" onClick={parseNow}><LIcon name="sparkle" size={15}/>Regenerate</button>
              <span style={{ marginLeft:"auto", display:"flex", gap: 8 }}>
                <button className="btn" disabled={!!jsonError} onClick={()=>save(true)}>Save as draft</button>
                <button className="btn primary" disabled={!!jsonError} onClick={()=>save(false)}><LIcon name="calendarCheck" size={15}/>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.APP.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 (
    <LibModalShell onClose={onClose} width={960}
      eyebrow={`${test.id} · Students`}
      title={test.title}
      headExtra={<>
        <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>
      </>}
      bar={<>
        <div className="ui-search" style={{ width: 260 }}>
          <LIcon name="search" size={14}/>
          <input className="input" placeholder="Search by roll or name…"/>
        </div>
        <select className="select" defaultValue="all" style={{ width: 170 }}>
          <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: 8 }}>
          <button className="btn sm"><LIcon name="upload" size={14}/>Bulk upload scripts</button>
          <button className="btn sm primary" onClick={() => {onClose(); setScreen("evaluate");}}>
            <LIcon name="sparkle" size={14}/>Evaluate next pending
          </button>
        </span>
      </>}
      bodyStyle={{ padding: 0 }}
      footer={<>
        <span className="muted" style={{ fontSize: 12.5, marginRight: "auto" }}>{roster.length} students · roster from class {test.className}</span>
        <button className="btn" onClick={onClose}>Close</button>
      </>}>
      <div className="cx-tablescroll">
        <table className="tests" style={{ width: "100%" }}>
          <thead><tr>
            <th style={{ width: 90 }}>Roll No.</th><th>Student name</th>
            <th style={{ width: 140 }}>Status</th><th className="num" style={{ width: 90 }}>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><span className="ui-id">{r.roll}</span></td>
                <td>
                  <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                    <window.AppUI.Avatar name={r.name} size="sm"/>
                    <span className="t-main">{r.name}</span>
                  </div>
                </td>
                <td><StudentStatus status={r.status}/></td>
                <td className="num">{r.score != null ?
                  <span style={{ color: "var(--ink-0)", fontWeight: 600 }}>{r.score}<span style={{ color: "var(--ink-3)", fontWeight: 400 }}>/{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={14}/>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={14}/>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<LIcon name="arrowRight" size={14}/></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={14}/>View
                  </button>
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    </LibModalShell>
  );
};

const StudentStatus = ({ status }) => {
  const map = {
    "evaluated":    { tone: "green", label: "Evaluated" },
    "in-progress":  { tone: "amber", label: "In progress" },
    "uploaded":     { tone: "blue",  label: "Uploaded" },
    "not-uploaded": { tone: "grey",  label: "Not uploaded" },
  }[status];
  return <window.AppUI.StatusBadge tone={map.tone} variant="outline">{map.label}</window.AppUI.StatusBadge>;
};

/* ---------- 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.APP.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 className="ui-scrim" onClick={onClose} style={{ zIndex: 210 }}>
      <div className="ui-modal" role="dialog" aria-modal="true" aria-label="Student report"
        onClick={e=>e.stopPropagation()} style={{ width: "min(980px, 100%)", maxHeight: "92vh" }}>
        <div className="ui-modal-head" style={{ alignItems: "center", borderBottom: "1px solid var(--line)", flexWrap: "wrap" }}>
          <window.AppUI.Avatar name={student.name} size="lg"/>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div className="ui-section-label" style={{ margin: "0 0 3px" }}>{test.id} · Student report</div>
            <div className="ui-modal-title" style={{ fontSize: 18 }}>{student.name} <span className="ui-id" style={{ marginLeft: 6 }}>{student.roll}</span></div>
          </div>
          <span style={{ display:"flex", alignItems:"center", gap: 8, flexWrap: "wrap" }}>
            <span className="pill">{test.title}</span>
            <button className="btn sm"><LIcon name="download" size={14}/>Download PDF</button>
            <button className="btn sm"><LIcon name="message" size={14}/>Share with parent</button>
            <button type="button" className="ui-iconbtn" aria-label="Close" onClick={onClose}><LIcon name="x" size={16}/></button>
          </span>
        </div>

        <div style={{ flex:1, overflow:"auto", padding: 20, display:"flex", flexDirection:"column", gap: 20 }}>
          {/* Score summary */}
          <div className="cx-card tight">
            <window.AppUI.StatRow flush items={[
              { value: `${obtained}/${max}`, label: "total score", tone: "blue" },
              { value: `${pct}%`, label: "percentage", tone: pct >= 75 ? "green" : pct >= 50 ? "amber" : "red" },
              { value: "—", label: "class rank" },
              { value: `${perQ.filter(r=>r.marks===r.q.maxMarks).length} / ${perQ.filter(r=>r.marks===0).length}`, label: "strong / weak" },
            ]}/>
          </div>

          {/* 1. Per-question marks */}
          <Section2 title="1 · Question-wise performance">
            <div className="cx-tablecard cx-tablescroll">
              <table className="tests" style={{ width:"100%" }}>
                <thead><tr><th style={{ width:50 }}>Q</th><th>Question</th><th className="num" 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: 13.5 }}>{q.english}</div></td>
                        <td className="num"><span style={{ color: full ? "var(--green)" : zero ? "var(--red)" : "var(--amber)", fontWeight: 600 }}>{marks}/{q.maxMarks}</span></td>
                        <td>{full
                          ? <window.AppUI.StatusBadge tone="green" variant="solid">Full</window.AppUI.StatusBadge>
                          : zero ? <window.AppUI.StatusBadge tone="red" variant="solid">Missed</window.AppUI.StatusBadge>
                          : <window.AppUI.StatusBadge tone="amber" variant="solid">Partial</window.AppUI.StatusBadge>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </Section2>

          {/* 2. Per-question suggestions */}
          <Section2 title="2 · Question-based suggestions">
            <div className="cx-card tight">
              <div className="ui-list">
                {perQ.filter(r => r.marks < r.q.maxMarks).map(({q}) => (
                  <div key={q.id} className="ui-row" style={{ alignItems: "flex-start", gap: 12 }}>
                    <span className="mono" style={{ color:"var(--accent)", fontWeight: 600, fontSize: 12.5, minWidth: 30, paddingTop: 1 }}>Q{q.num}</span>
                    <div style={{ flex: 1, fontSize: 13.5, color:"var(--ink-1)", lineHeight: 1.5 }}>{suggestionsByQ[q.num] || "Review this concept and try a similar problem."}</div>
                  </div>
                ))}
              </div>
              {perQ.every(r => r.marks === r.q.maxMarks) && (
                <div className="ui-alert green">
                  Perfect score — no targeted suggestions needed. Move to next-level problems.
                </div>
              )}
            </div>
          </Section2>

          {/* 3. Misconceptions */}
          <Section2 title="3 · Potential misconceptions">
            <div className="cx-card tight">
              {misconceptions.length === 0 && <div className="muted" style={{ fontSize: 13 }}>No misconceptions flagged.</div>}
              <div className="ui-list">
                {misconceptions.map(m => (
                  <window.AppUI.ListRow key={m.code}
                    leading={<span className="pill violet">{m.code}</span>}
                    title={<span style={{ whiteSpace: "normal", fontWeight: 400, color: "var(--ink-1)" }}>{m.note}</span>}
                    status={<span className="pill">{m.weight}</span>}/>
                ))}
              </div>
            </div>
          </Section2>

          {/* 4. Study guide */}
          <Section2 title="4 · Personalised study guide">
            <div className="cx-card tight">
              {studyGuide.length === 0 && <div className="muted" style={{ fontSize: 13 }}>No study guide yet.</div>}
              <div className="ui-list">
                {studyGuide.map((g, i) => (
                  <window.AppUI.ListRow key={i}
                    leading={<span className="mono" style={{ color:"var(--ink-3)", fontSize: 12, minWidth: 22 }}>{String(i+1).padStart(2,"0")}</span>}
                    title={g.topic}
                    status={<span className={`pill ${g.priority === "high" ? "red" : g.priority === "medium" ? "amber" : ""}`}>{g.priority}</span>}
                    amount={g.time}/>
                ))}
              </div>
            </div>
          </Section2>
        </div>

        <div className="ui-modal-foot">
          <button className="btn" onClick={onClose}>Close</button>
          <button className="btn primary"><LIcon name="mail" size={15}/>Email to student</button>
        </div>
      </div>
    </div>
  );
};

const Stat2 = ({ label, value, accent }) => (
  <div className="cx-tile">
    <div className="cx-tile-label">{label}</div>
    <div className="cx-tile-value tabular" style={{ color: accent }}>{value}</div>
  </div>
);

const Section2 = ({ title, children }) => (
  <div>
    <div className="cx-card-title" style={{ marginBottom: 10 }}>{title}</div>
    {children}
  </div>
);

/* ---------- Edit test modal — full CRUD over the test header ---------- */
const EditTestModal = ({ test, onClose, onSave }) => {
  const classSections = window.APP?.CLASS_SECTIONS || [];
  const subjects = window.APP?.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 || "en",
      author: window.APP.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 (
    <LibModalShell onClose={onClose} width={640} icon="edit"
      title={<>Edit test <span className="ui-id" style={{ marginLeft: 6, fontWeight: 400 }}>{test.id}</span></>}
      footer={<>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn primary" disabled={saving || !form.title?.trim()} onClick={submit}>
          <LIcon name="check" size={15}/>{saving ? "Saving…" : "Save changes"}
        </button>
      </>}>
        <div style={{ display:"grid", gridTemplateColumns:"minmax(0, 1fr) minmax(0, 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: 11.5, marginLeft: 4 }}>tap to add</span></span>
            <window.AppUI.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>
    </LibModalShell>
  );
};

/* ---------- 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.AppApi.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(`${window.AppApi.base}/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 (
    <LibModalShell onClose={onClose} width={920} icon="upload"
      eyebrow={`${test.id} · ${test.className} · ${test.subject}`}
      title={`Upload answer sheets — ${test.title}`}
      headExtra={roster && (
        <span className="muted tabular" style={{ fontSize: 12.5 }}>
          {roster.filter(r=>r.uploaded).length}/{roster.length} uploaded · {roster.filter(r=>r.finalised).length}/{roster.length} finalised
        </span>
      )}
      bodyStyle={{ padding: 0, borderTop: "1px solid var(--line)" }}
      footer={<>
        <span className="muted" style={{ fontSize: 12.5, marginRight: "auto" }}>
          Once every enrolled student is evaluated, this test will move to <b>Completed</b>.
        </span>
        <button className="btn" onClick={onClose}>Done</button>
      </>}>
        <div className="cx-tablescroll">
          <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 className="num" style={{ width:90 }}>Score</th>
                <th className="actions" style={{ width:190 }}></th>
              </tr>
            </thead>
            <tbody>
              {(roster || []).map(r => (
                <tr key={r.student_id}>
                  <td><span className="ui-id">{r.roll_no}</span></td>
                  <td>
                    <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                      <window.AppUI.Avatar name={r.name} size="sm"/>
                      <span className="t-main">{r.name}</span>
                    </div>
                  </td>
                  <td>
                    {r.uploaded
                      ? <window.AppUI.StatusBadge tone="green" variant="outline">Uploaded</window.AppUI.StatusBadge>
                      : <window.AppUI.StatusBadge tone="grey" variant="outline">Not uploaded</window.AppUI.StatusBadge>}
                  </td>
                  <td>
                    {r.finalised
                      ? <window.AppUI.StatusBadge tone="green" variant="solid">Finalised</window.AppUI.StatusBadge>
                      : r.evaluated
                          ? <window.AppUI.StatusBadge tone="amber">Draft</window.AppUI.StatusBadge>
                          : (r.uploaded ? <window.AppUI.StatusBadge tone="grey">Not started</window.AppUI.StatusBadge>
                                        : <span className="muted">—</span>)}
                  </td>
                  <td className="num">
                    {r.evaluation_count > 0
                      ? <span style={{ color: r.finalised ? "var(--green)" : "var(--amber)", fontWeight: 600 }}>
                          {r.score}<span className="muted" style={{ fontWeight: 400 }}>/{r.max_score || "—"}</span>
                        </span>
                      : <span className="muted">—</span>}
                  </td>
                  <td className="actions"><div style={{ display:"inline-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)}>
                      <LIcon name={r.uploaded ? "refresh" : "upload"} size={14}/>
                      {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>
                  </div></td>
                </tr>
              ))}
              {roster && roster.length === 0 && (
                <tr><td colSpan="6"><window.AppUI.EmptyState compact icon="users" title="No students enrolled" hint="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>
    </LibModalShell>
  );
};
