/* ==================================================================
   Operations → Examinations
   ------------------------------------------------------------------
   Where the office builds an exam, staffs it, and releases the result.

   The lifecycle this screen drives (backend/src/routes/exams.ts):

     draft ──► entry_open ──► published
       ▲            │             │
       └────────────┘             │   (unpublish)
                    ◄─────────────┘

     • draft       the paper list is still being laid out. Teachers see
                   nothing at all.
     • entry_open  the assigned teachers can fill and submit. Parents
                   still see nothing.
     • published   THE one-way door that matters: every submitted paper
                   becomes visible in the Parents app. Reversible.

   A "paper" is one class-section × one subject, carrying its own
   written and oral maximums — because Class III English out of 50 and
   Class IX English out of 100 are different papers in the same exam.

   Admin / HoD only, enforced server-side on /admin/exams*.

   NOTE ON NAMING. Every top-level binding in this file is prefixed
   Exm/EXM_ on purpose: the panel's screens are Babel-compiled into ONE
   shared global scope, so a top-level `const` that collides with
   another screen's silently kills whichever file loads second.

   Endpoints: /api/admin/exams*, /api/admin/exam-setup
   ================================================================== */

const { Icon: ExmIcon } = window.KXUI;

// Offered as suggestions in the create form. Free text server-side —
// schools invent exam types faster than a migration can add enum values.
const EXM_TYPES = [
  "Unit Test", "First Summative", "Half-Yearly", "Second Summative",
  "Pre-Annual", "Annual", "Model", "Pre-Board",
];

// The three rows of the printed progress report. An exam either fills one of
// them or is simply not on the marksheet (a unit test, a model paper).
const EXM_ROWS = [
  { key: "",                label: "Not on the marksheet" },
  { key: "first_terminal",  label: "1st Terminal Examination" },
  { key: "second_terminal", label: "2nd Terminal Examination" },
  { key: "annual",          label: "Annual Examination" },
];
const EXM_ROW_LABEL = Object.fromEntries(EXM_ROWS.map((r) => [r.key, r.label]));

// Which band a subject prints under. Blank means "work it out from the name",
// which is right for every ordinary subject.
const EXM_GROUPS = [
  { key: "",               label: "auto" },
  { key: "language",       label: "Language" },
  { key: "science",        label: "Science" },
  { key: "social_science", label: "Social Science" },
  { key: "co_curricular",  label: "Co-curricular" },
];

const EXM_STATUS_TINT = {
  draft:      { label: "Draft",      fg: "var(--ink-3)", bg: "rgba(138,138,147,.12)", bd: "rgba(138,138,147,.30)" },
  entry_open: { label: "Entry open", fg: "var(--amber)", bg: "rgba(255,159,10,.14)",  bd: "rgba(255,159,10,.32)" },
  published:  { label: "Published",  fg: "var(--green)", bg: "rgba(47,190,95,.16)",   bd: "rgba(47,190,95,.36)" },
};

const ExmPill = ({ tint, children }) => (
  <span style={{
    display: "inline-block", padding: "2px 8px", borderRadius: 999,
    fontSize: 11, fontWeight: 600, color: tint.fg, background: tint.bg,
    border: `1px solid ${tint.bd}`, whiteSpace: "nowrap",
  }}>{children ?? tint.label}</span>
);

const exmDate = (d) => {
  if (!d) return "—";
  try {
    return new Date(d).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" });
  } catch { return String(d).slice(0, 10); }
};

// A paper's roster progress. Grey until someone starts, amber part-way,
// green when the sheet is in — so an exam's bottleneck is visible without
// opening a single paper.
const ExmProgress = ({ entered, roll, status }) => {
  const pct = roll > 0 ? Math.round((entered / roll) * 100) : 0;
  const done = status === "submitted";
  const colour = done ? "var(--green)" : entered === 0 ? "var(--ink-3)" : "var(--amber)";
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 116 }}>
      <div style={{ flex: 1, height: 5, borderRadius: 999, background: "rgba(138,138,147,.18)", overflow: "hidden" }}>
        <div style={{ width: `${pct}%`, height: "100%", background: colour, transition: "width .2s" }} />
      </div>
      <span className="mono" style={{ fontSize: 11, color: "var(--ink-2)", whiteSpace: "nowrap" }}>
        {entered}/{roll}
      </span>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════════
// Root — list ⇄ detail
// ══════════════════════════════════════════════════════════════════════

const ExamsScreen = () => {
  const [exams, setExams]   = React.useState([]);
  const [setup, setSetup]   = React.useState(null);
  const [openId, setOpenId] = React.useState(null);
  const [tab, setTab]       = React.useState("exams");
  const [loading, setLoad]  = React.useState(true);
  const [err, setErr]       = React.useState(null);

  const refresh = React.useCallback(async () => {
    setErr(null);
    try {
      const [xs, st] = await Promise.all([
        window.KXApi.get("/admin/exams"),
        setup ? Promise.resolve(setup) : window.KXApi.get("/admin/exam-setup"),
      ]);
      setExams(xs); setSetup(st);
    } catch (e) { setErr(e.message || "Failed to load"); }
    finally { setLoad(false); }
  }, [setup]);

  React.useEffect(() => { refresh(); }, []);   // eslint-disable-line

  if (loading) return <div className="card"><div className="card-body muted">Loading examinations…</div></div>;
  if (err) return (
    <div className="card"><div className="card-body">
      <div style={{ color: "var(--red)" }}>{err}</div>
      <button className="btn sm" style={{ marginTop: 10 }} onClick={refresh}>Retry</button>
    </div></div>
  );

  if (openId) {
    return <ExmDetail
      examId={openId} setup={setup}
      onBack={() => { setOpenId(null); refresh(); }}
      onChanged={refresh}
    />;
  }

  return (
    <div style={{ display: "grid", gap: 14 }}>
      <div style={{ display: "flex", gap: 6 }}>
        {[["exams", "Examinations"], ["reports", "Progress reports"]].map(([k, label]) => (
          <button key={k} className={`btn sm ${tab === k ? "primary" : "ghost"}`}
            onClick={() => setTab(k)}>{label}</button>
        ))}
      </div>
      {tab === "exams"
        ? <ExmList exams={exams} setup={setup} onOpen={setOpenId} onChanged={refresh} />
        : <ExmMarksheets />}
    </div>
  );
};

// ── The list ──────────────────────────────────────────────────────────

const ExmList = ({ exams, setup, onOpen, onChanged }) => {
  const [creating, setCreating] = React.useState(false);

  return (
    <div style={{ display: "grid", gap: 14 }}>
      <div className="card">
        <div className="card-body" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
          <div>
            <div className="cx-card-title">Examinations</div>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>
              Build an exam, set each paper's marks division, assign the teacher who enters
              them, then publish the result to parents.
            </div>
          </div>
          <button className="btn primary" onClick={() => setCreating(true)}>New exam</button>
        </div>
      </div>

      {creating && (
        <ExmCreate
          setup={setup}
          onCancel={() => setCreating(false)}
          onCreated={(e) => { setCreating(false); onChanged(); onOpen(e.id); }}
        />
      )}

      {exams.length === 0 && !creating && (
        <div className="card"><div className="card-body muted" style={{ textAlign: "center", padding: 28 }}>
          No examinations yet. “New exam” starts one.
        </div></div>
      )}

      {exams.map((e) => {
        const tint = EXM_STATUS_TINT[e.status] || EXM_STATUS_TINT.draft;
        return (
          <div key={e.id} className="card" style={{ cursor: "pointer" }} onClick={() => onOpen(e.id)}>
            <div className="card-body" style={{ display: "flex", alignItems: "center", gap: 14 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                  <span style={{ fontSize: 15, fontWeight: 600, color: "var(--ink-0)" }}>{e.name}</span>
                  <ExmPill tint={tint} />
                  <span className="muted" style={{ fontSize: 12 }}>{e.exam_type}</span>
                </div>
                <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>
                  {e.academic_year}
                  {e.starts_on ? ` · ${exmDate(e.starts_on)}` : ""}
                  {e.ends_on && e.ends_on !== e.starts_on ? ` – ${exmDate(e.ends_on)}` : ""}
                </div>
              </div>
              <div style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                <div className="mono" style={{ fontSize: 13, color: "var(--ink-1)" }}>
                  {e.submitted}/{e.papers} papers in
                </div>
                {e.unstaffed > 0 && (
                  <div style={{ fontSize: 11.5, color: "var(--amber)", marginTop: 2 }}>
                    {e.unstaffed} unassigned
                  </div>
                )}
              </div>
              <ExmIcon name="chevron-right" />
            </div>
          </div>
        );
      })}
    </div>
  );
};

// ── Create ────────────────────────────────────────────────────────────

const ExmCreate = ({ setup, onCancel, onCreated }) => {
  // Default to the academic year the classes are actually in, so the exam
  // lands against this year's roster rather than a year nobody teaches.
  const years = [...new Set((setup?.classes || []).map((c) => c.academic_year))];
  const [f, setF] = React.useState({
    name: "", exam_type: "Half-Yearly", academic_year: years[0] || "",
    starts_on: "", ends_on: "", marksheet_row: "",
  });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  const save = async () => {
    setBusy(true); setErr(null);
    try {
      onCreated(await window.KXApi.post("/admin/exams", {
        ...f,
        starts_on: f.starts_on || null,
        ends_on: f.ends_on || null,
        marksheet_row: f.marksheet_row || null,
      }));
    } catch (e) { setErr(e.message || "Could not create"); setBusy(false); }
  };

  return (
    <div className="card">
      <div className="card-body">
        <div className="cx-card-title" style={{ marginBottom: 12 }}>New examination</div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(190px,1fr))", gap: 12 }}>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>NAME</div>
            <input className="input" style={{ width: "100%" }} value={f.name} autoFocus
              placeholder="Half-Yearly Examination"
              onChange={(e) => setF({ ...f, name: e.target.value })} />
          </label>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>TYPE</div>
            <input className="input" style={{ width: "100%" }} value={f.exam_type} list="exm-types"
              onChange={(e) => setF({ ...f, exam_type: e.target.value })} />
            <datalist id="exm-types">
              {EXM_TYPES.map((t) => <option key={t} value={t} />)}
            </datalist>
          </label>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>ACADEMIC YEAR</div>
            <select className="input" style={{ width: "100%" }} value={f.academic_year}
              onChange={(e) => setF({ ...f, academic_year: e.target.value })}>
              {years.map((y) => <option key={y} value={y}>{y}</option>)}
            </select>
          </label>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>STARTS (OPTIONAL)</div>
            <input className="input" type="date" style={{ width: "100%" }} value={f.starts_on}
              onChange={(e) => setF({ ...f, starts_on: e.target.value })} />
          </label>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>ENDS (OPTIONAL)</div>
            <input className="input" type="date" style={{ width: "100%" }} value={f.ends_on}
              onChange={(e) => setF({ ...f, ends_on: e.target.value })} />
          </label>
          <label style={{ display: "block" }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>PROGRESS REPORT ROW</div>
            <select className="input" style={{ width: "100%" }} value={f.marksheet_row}
              onChange={(e) => setF({ ...f, marksheet_row: e.target.value })}>
              {EXM_ROWS.map((r) => <option key={r.key} value={r.key}>{r.label}</option>)}
            </select>
          </label>
        </div>
        {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 10 }}>{err}</div>}
        <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
          <button className="btn primary" disabled={busy || !f.name.trim() || !f.exam_type.trim()} onClick={save}>
            {busy ? "Creating…" : "Create"}
          </button>
          <button className="btn ghost" onClick={onCancel}>Cancel</button>
        </div>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════════
// Detail — the paper list, and the state machine
// ══════════════════════════════════════════════════════════════════════

const ExmDetail = ({ examId, setup, onBack, onChanged }) => {
  const [exam, setExam] = React.useState(null);
  const [err, setErr]   = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  const [sheet, setSheet] = React.useState(null);      // read-only marks viewer

  const load = React.useCallback(async () => {
    try { setExam(await window.KXApi.get(`/admin/exams/${examId}`)); }
    catch (e) { setErr(e.message || "Failed to load"); }
  }, [examId]);
  React.useEffect(() => { load(); }, [load]);

  const move = async (status, force) => {
    setBusy(true); setErr(null); setNotice(null);
    try {
      setExam(await window.KXApi.post(`/admin/exams/${examId}/status`, { status, force }));
      onChanged();
    } catch (e) {
      // The publish gate answers 409 with the list of outstanding papers —
      // name them rather than saying "something went wrong".
      const pending = e.body?.pending;
      if (pending?.length) {
        setNotice({
          kind: "confirm-publish",
          text: `${pending.length} paper(s) have not been submitted:`,
          list: pending,
        });
      } else setErr(e.message || "Could not change status");
    } finally { setBusy(false); }
  };

  const reopen = async (paperId) => {
    setBusy(true); setErr(null);
    try { setExam(await window.KXApi.post(`/admin/exams/${examId}/papers/${paperId}/reopen`, {})); onChanged(); }
    catch (e) { setErr(e.message || "Could not reopen"); }
    finally { setBusy(false); }
  };

  const remove = async () => {
    if (!window.confirm("Delete this exam? Only possible while it is a draft with no marks.")) return;
    setBusy(true); setErr(null);
    try { await window.KXApi.del(`/admin/exams/${examId}`); onBack(); }
    catch (e) { setErr(e.message || "Could not delete"); setBusy(false); }
  };

  if (!exam) {
    return (
      <div className="card"><div className="card-body">
        <button className="btn sm ghost" onClick={onBack}>← Back</button>
        <div className="muted" style={{ marginTop: 10 }}>{err || "Loading…"}</div>
      </div></div>
    );
  }

  const tint = EXM_STATUS_TINT[exam.status] || EXM_STATUS_TINT.draft;
  const submitted = exam.papers.filter((p) => p.status === "submitted").length;
  const locked = exam.status === "published";

  return (
    <div style={{ display: "grid", gap: 14 }}>
      <div className="card">
        <div className="card-body">
          <button className="btn sm ghost" onClick={onBack}>← All examinations</button>
          <div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginTop: 10, flexWrap: "wrap" }}>
            <div style={{ flex: 1, minWidth: 220 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                <span style={{ fontSize: 18, fontWeight: 650, color: "var(--ink-0)" }}>{exam.name}</span>
                <ExmPill tint={tint} />
              </div>
              <div className="muted" style={{ fontSize: 12.5, marginTop: 3 }}>
                {exam.exam_type} · {exam.academic_year}
                {exam.starts_on ? ` · ${exmDate(exam.starts_on)}` : ""}
                {exam.ends_on && exam.ends_on !== exam.starts_on ? ` – ${exmDate(exam.ends_on)}` : ""}
              </div>
              <div className="muted" style={{ fontSize: 12.5, marginTop: 3 }}>
                {submitted} of {exam.papers.length} papers submitted
                {exam.published_at ? ` · published ${exmDate(exam.published_at)}` : ""}
              </div>
              {/* Which row of the printed progress report this exam fills. A
                  session has one of each, so the server refuses a clash. */}
              <label style={{ display: "inline-flex", alignItems: "center", gap: 7, marginTop: 8 }}>
                <span className="muted" style={{ fontSize: 11.5 }}>PROGRESS REPORT ROW</span>
                <select className="input" style={{ minWidth: 190 }} disabled={busy}
                  value={exam.marksheet_row || ""}
                  onChange={async (e) => {
                    setBusy(true); setErr(null);
                    try {
                      setExam(await window.KXApi.patch(`/admin/exams/${examId}`,
                        { marksheet_row: e.target.value || null }));
                      onChanged();
                    } catch (x) { setErr(x.message || "Could not set the row"); }
                    finally { setBusy(false); }
                  }}>
                  {EXM_ROWS.map((r) => <option key={r.key} value={r.key}>{r.label}</option>)}
                </select>
              </label>
            </div>
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
              {exam.status === "draft" && (
                <button className="btn primary" disabled={busy} onClick={() => move("entry_open")}>
                  Open for entry
                </button>
              )}
              {exam.status === "entry_open" && (
                <>
                  <button className="btn primary" disabled={busy} onClick={() => move("published")}>
                    Publish to parents
                  </button>
                  <button className="btn ghost sm" disabled={busy} onClick={() => move("draft")}>
                    Back to draft
                  </button>
                </>
              )}
              {exam.status === "published" && (
                <button className="btn ghost" disabled={busy} onClick={() => move("entry_open")}>
                  Unpublish
                </button>
              )}
              {exam.status === "draft" && (
                <button className="btn ghost sm" disabled={busy} onClick={remove}>Delete</button>
              )}
              {/* Every class, one row per child per paper — the shape that
                  holds classes with different subjects in one sheet. */}
              {exam.papers.length > 0 && (
                <button className="btn ghost" title="All classes · one row per child per paper"
                  onClick={() => exmDownloadCsv(examId, null, setErr)}>Export CSV</button>
              )}
            </div>
          </div>

          <div style={{
            marginTop: 12, padding: "9px 12px", borderRadius: 8, fontSize: 12.5,
            background: "rgba(59,123,246,.08)", border: "1px solid rgba(59,123,246,.20)", color: "var(--ink-1)",
          }}>
            {exam.status === "draft" &&
              "Draft — teachers cannot see this exam yet. Lay out the papers below, assign a teacher to each, then open it for entry."}
            {exam.status === "entry_open" &&
              "Open for entry — the assigned teachers can fill and submit their papers. Parents still see nothing until you publish."}
            {exam.status === "published" &&
              "Published — every submitted paper is now visible in the Parents app. Unpublish to take it back off."}
          </div>

          {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 10 }}>{err}</div>}

          {notice?.kind === "confirm-publish" && (
            <div style={{
              marginTop: 12, padding: 12, borderRadius: 8,
              background: "rgba(255,159,10,.10)", border: "1px solid rgba(255,159,10,.30)",
            }}>
              <div style={{ fontSize: 13, color: "var(--ink-0)", fontWeight: 600 }}>{notice.text}</div>
              <ul style={{ margin: "6px 0 10px 18px", fontSize: 12.5, color: "var(--ink-1)" }}>
                {notice.list.slice(0, 12).map((p, i) => <li key={i}>{p}</li>)}
                {notice.list.length > 12 && <li>…and {notice.list.length - 12} more</li>}
              </ul>
              <div className="muted" style={{ fontSize: 12, marginBottom: 10 }}>
                Publishing now shows parents only the subjects that ARE in. The missing ones
                appear later, the moment you publish again after they are submitted.
              </div>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="btn sm primary" disabled={busy}
                  onClick={() => { setNotice(null); move("published", true); }}>
                  Publish anyway
                </button>
                <button className="btn sm ghost" onClick={() => setNotice(null)}>Cancel</button>
              </div>
            </div>
          )}
        </div>
      </div>

      <ExmPapers
        exam={exam} setup={setup} locked={locked}
        onSaved={(e) => { setExam(e); onChanged(); }}
        onReopen={reopen}
        onViewSheet={setSheet}
      />

      {sheet && (
        <ExmSheetViewer examId={examId} paper={sheet} onClose={() => setSheet(null)} />
      )}
    </div>
  );
};

// ── The paper grid ────────────────────────────────────────────────────

const ExmPapers = ({ exam, setup, locked, onSaved, onReopen, onViewSheet }) => {
  // Local working copy. The admin edits freely and saves the whole grid as a
  // set — which is also how a paper gets removed (leave it out).
  const [rows, setRows] = React.useState([]);
  const [dirty, setDirty] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [kept, setKept] = React.useState([]);

  React.useEffect(() => {
    setRows(exam.papers.map((p) => ({
      class_section_id: p.class_section_id,
      // Either may be absent: a paper is named by a registry subject, by its
      // own label, or both (a registry subject renamed for this exam).
      subject_id: p.subject_id || null,
      subject_label: p.subject_label || "",
      subject_group: p.subject_group || "",
      max_written: p.max_written, max_oral: p.max_oral,
      exam_date: p.exam_date ? String(p.exam_date).slice(0, 10) : "",
      assigned_to: p.assigned_to || "",
      // Read-only, for display only — never sent back.
      _id: p.id, _status: p.status, _entered: p.entered, _roll: p.roll,
      _class: p.class_label, _subject: p.subject_name,
      _registry: p.registry_subject_name || "",
      _submitted_by: p.submitted_by_name,
    })));
    setDirty(false); setKept([]);
  }, [exam]);

  const classById = React.useMemo(
    () => Object.fromEntries((setup?.classes || []).map((c) => [c.id, c])), [setup]);
  const subjectById = React.useMemo(
    () => Object.fromEntries((setup?.subjects || []).map((s) => [s.id, s])), [setup]);

  const patch = (i, k, v) => {
    setRows((rs) => rs.map((r, j) => (j === i ? { ...r, [k]: v } : r)));
    setDirty(true);
  };
  const drop = (i) => { setRows((rs) => rs.filter((_, j) => j !== i)); setDirty(true); };

  const save = async () => {
    setBusy(true); setErr(null); setKept([]);
    try {
      const out = await window.KXApi.put(`/admin/exams/${exam.id}/papers`, {
        papers: rows.map((r) => ({
          class_section_id: r.class_section_id,
          subject_id: r.subject_id || null,
          subject_label: (r.subject_label || "").trim() || null,
          subject_group: r.subject_group || null,
          max_written: Number(r.max_written) || 0, max_oral: Number(r.max_oral) || 0,
          exam_date: r.exam_date || null, assigned_to: r.assigned_to || null,
        })),
      });
      setKept(out.kept_because_marked || []);
      onSaved(out);
    } catch (e) { setErr(e.message || "Could not save"); }
    finally { setBusy(false); }
  };

  // Group for display; the payload stays flat.
  const grouped = React.useMemo(() => {
    const g = new Map();
    rows.forEach((r, i) => {
      const label = r._class || classById[r.class_section_id]?.label || "—";
      if (!g.has(label)) g.set(label, []);
      g.get(label).push({ r, i });
    });
    return [...g.entries()].sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true }));
  }, [rows, classById]);

  return (
    <div className="card">
      <div className="card-body">
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
          <div>
            <div className="cx-card-title">Papers</div>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>
              One row per class and subject. The written and oral maximums are what the
              teacher's entry screen enforces.
            </div>
          </div>
          {!locked && (
            <button className="btn primary" disabled={!dirty || busy} onClick={save}>
              {busy ? "Saving…" : dirty ? "Save papers" : "Saved"}
            </button>
          )}
        </div>

        {locked && (
          <div className="muted" style={{ fontSize: 12.5, marginTop: 10 }}>
            This exam is published — unpublish it to change the paper list.
          </div>
        )}

        {!locked && (
          <ExmAddPapers
            setup={setup} rows={rows}
            onAdd={(added) => { setRows((rs) => [...rs, ...added]); setDirty(true); }}
          />
        )}

        {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 10 }}>{err}</div>}
        {kept.length > 0 && (
          <div style={{
            marginTop: 10, padding: 10, borderRadius: 8, fontSize: 12.5,
            background: "rgba(255,159,10,.10)", border: "1px solid rgba(255,159,10,.30)", color: "var(--ink-1)",
          }}>
            Kept — these papers already hold marks and were not removed:
            <ul style={{ margin: "5px 0 0 18px" }}>{kept.map((k, i) => <li key={i}>{k}</li>)}</ul>
          </div>
        )}

        {rows.length === 0 && (
          <div className="muted" style={{ fontSize: 13, textAlign: "center", padding: 22 }}>
            No papers yet. Add a class and its subjects above.
          </div>
        )}

        {grouped.map(([label, items]) => (
          <div key={label} style={{ marginTop: 16 }}>
            <div style={{
              fontSize: 12, fontWeight: 700, color: "var(--ink-1)", textTransform: "uppercase",
              letterSpacing: ".06em", marginBottom: 6,
            }}>
              Class {label}
              <span className="muted" style={{ fontWeight: 400, textTransform: "none", letterSpacing: 0, marginLeft: 8 }}>
                {items[0].r._roll != null ? `${items[0].r._roll} on roll` : ""}
              </span>
              {/* One row per child, subjects across — the result-sheet shape.
                  Only once the class's papers are saved: the export reads
                  what is in the database, not this unsaved grid. */}
              {items.some(({ r }) => r._id) && (
                <button className="btn sm ghost" style={{ marginLeft: 10, textTransform: "none", letterSpacing: 0 }}
                  title="This class · one row per child"
                  onClick={() => exmDownloadCsv(exam.id, items[0].r.class_section_id, setErr)}>CSV</button>
              )}
            </div>
            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
                <thead>
                  <tr style={{ textAlign: "left", color: "var(--ink-3)", fontSize: 11 }}>
                    <th style={{ padding: "6px 8px", minWidth: 160 }}>SUBJECT · AS EXAMINED</th>
                    <th style={{ padding: "6px 8px", width: 90 }}>WRITTEN</th>
                    <th style={{ padding: "6px 8px", width: 90 }}>ORAL</th>
                    <th style={{ padding: "6px 8px", width: 70 }}>TOTAL</th>
                    <th style={{ padding: "6px 8px", width: 118 }}>REPORT BAND</th>
                    <th style={{ padding: "6px 8px", width: 140 }}>DATE</th>
                    <th style={{ padding: "6px 8px", minWidth: 150 }}>ENTERED BY</th>
                    <th style={{ padding: "6px 8px", minWidth: 150 }}>PROGRESS</th>
                    <th style={{ padding: "6px 8px", width: 90 }}></th>
                  </tr>
                </thead>
                <tbody>
                  {items.map(({ r, i }) => {
                    const total = (Number(r.max_written) || 0) + (Number(r.max_oral) || 0);
                    // What this paper is CALLED: the exam-specific label if one
                    // was given, otherwise the registry subject's own name.
                    const registry = r._registry || subjectById[r.subject_id]?.name || "";
                    const shown = r.subject_label || registry;
                    const renamed = !!r.subject_label && !!registry && r.subject_label !== registry;
                    const subj = shown || "—";
                    const done = r._status === "submitted";
                    return (
                      <tr key={r._id || `${r.class_section_id}:${r.subject_id || r.subject_label}`}
                          style={{ borderTop: "1px solid var(--line, rgba(138,138,147,.18))" }}>
                        <td style={{ padding: "7px 8px" }}>
                          {locked ? (
                            <span style={{ color: "var(--ink-0)" }}>{subj}</span>
                          ) : (
                            <>
                              {/* Editable on every row: this is the name that
                                  goes on the exam and the marksheet, which is
                                  not always what the registry calls it. */}
                              <input className="input" style={{ width: "100%", minWidth: 120 }}
                                value={shown}
                                placeholder="Subject name"
                                onChange={(e) => {
                                  const v = e.target.value;
                                  // Typing the registry name back clears the
                                  // override rather than storing a redundant
                                  // copy that would drift if the subject is
                                  // ever renamed centrally.
                                  patch(i, "subject_label", v === registry ? "" : v);
                                }} />
                              {renamed && (
                                <div className="muted" style={{ fontSize: 10.5, marginTop: 2 }}>
                                  {registry} in the subject registry
                                </div>
                              )}
                              {!r.subject_id && (
                                <div className="muted" style={{ fontSize: 10.5, marginTop: 2 }}>
                                  exam only
                                </div>
                              )}
                            </>
                          )}
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          <input className="input" type="number" min="0" step="0.5" style={{ width: 74 }}
                            value={r.max_written} disabled={locked}
                            onChange={(e) => patch(i, "max_written", e.target.value)} />
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          <input className="input" type="number" min="0" step="0.5" style={{ width: 74 }}
                            value={r.max_oral} disabled={locked}
                            onChange={(e) => patch(i, "max_oral", e.target.value)} />
                        </td>
                        <td className="mono" style={{ padding: "7px 8px", color: total > 0 ? "var(--ink-1)" : "var(--red)" }}>
                          {total || "—"}
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          {/* Which heading the subject prints under on the
                              progress report. "auto" reads it off the name. */}
                          <select className="input" style={{ width: 112 }}
                            value={r.subject_group || ""} disabled={locked}
                            onChange={(e) => patch(i, "subject_group", e.target.value)}>
                            {EXM_GROUPS.map((g) => (
                              <option key={g.key} value={g.key}>{g.label}</option>
                            ))}
                          </select>
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          <input className="input" type="date" style={{ width: 132 }}
                            value={r.exam_date} disabled={locked}
                            onChange={(e) => patch(i, "exam_date", e.target.value)} />
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          <select className="input" style={{ width: "100%", minWidth: 140 }}
                            value={r.assigned_to} disabled={locked}
                            onChange={(e) => patch(i, "assigned_to", e.target.value)}>
                            <option value="">— unassigned —</option>
                            {(setup?.teachers || []).map((t) => (
                              <option key={t.id} value={t.id}>{t.name}</option>
                            ))}
                          </select>
                        </td>
                        <td style={{ padding: "7px 8px" }}>
                          {r._id ? (
                            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                              <ExmProgress entered={r._entered || 0} roll={r._roll || 0} status={r._status} />
                              {done && <ExmPill tint={EXM_STATUS_TINT.published}>In</ExmPill>}
                            </div>
                          ) : <span className="muted" style={{ fontSize: 11.5 }}>new</span>}
                        </td>
                        <td style={{ padding: "7px 8px", whiteSpace: "nowrap" }}>
                          {r._id && (r._entered > 0 || done) && (
                            <button className="btn sm ghost" onClick={() => onViewSheet({
                              id: r._id, class_label: r._class, subject_name: subj, status: r._status,
                            })}>View</button>
                          )}
                          {" "}
                          {done && !locked && (
                            <button className="btn sm ghost" onClick={() => onReopen(r._id)}>Reopen</button>
                          )}
                          {!done && !locked && (
                            <button className="btn sm ghost" onClick={() => drop(i)}
                              title={r._entered > 0 ? "This paper holds marks — the server will refuse to remove it" : "Remove"}>
                              ✕
                            </button>
                          )}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

// ── Bulk add: one class, many subjects, one set of maximums ───────────

const ExmAddPapers = ({ setup, rows, onAdd }) => {
  const [classId, setClassId] = React.useState("");
  const [picked, setPicked] = React.useState({});
  const [written, setWritten] = React.useState(80);
  const [oral, setOral] = React.useState(20);
  const [custom, setCustom] = React.useState("");

  // Mirrors the server's exam_papers.subject_key, so a paper already on this
  // class is greyed out here rather than refused on save.
  const taken = React.useMemo(() => new Set(rows.map((r) =>
    `${r.class_section_id}:${r.subject_id || `label:${(r.subject_label || "").trim().toLowerCase()}`}`,
  )), [rows]);

  const subjects = (setup?.subjects || []).filter((s) => !taken.has(`${classId}:${s.id}`));
  const chosen = subjects.filter((s) => picked[s.id]);

  const cls = (setup?.classes || []).find((c) => c.id === classId);

  const add = () => {
    onAdd(chosen.map((s) => ({
      class_section_id: classId, subject_id: s.id, subject_label: "", subject_group: "",
      max_written: written, max_oral: oral, exam_date: "",
      // Pre-filled from the school's standing assignment matrix — a
      // suggestion the admin can override on any row before saving.
      assigned_to: setup?.suggested?.[`${classId}:${s.id}`] || "",
      _class: cls?.label, _subject: s.name, _registry: s.name, _roll: cls?.students,
    })));
    setPicked({});
  };

  // A subject the school examines but the registry has never heard of —
  // Drawing, Work Education, Moral Science. It gets no registry link and no
  // suggested teacher, because there is no assignment matrix row to read.
  const customName = custom.trim();
  const customTaken = !!customName
    && taken.has(`${classId}:label:${customName.toLowerCase()}`);
  const addCustom = () => {
    if (!classId || !customName || customTaken) return;
    onAdd([{
      class_section_id: classId, subject_id: null, subject_label: customName, subject_group: "",
      max_written: written, max_oral: oral, exam_date: "", assigned_to: "",
      _class: cls?.label, _subject: customName, _registry: "", _roll: cls?.students,
    }]);
    setCustom("");
  };

  return (
    <div style={{
      marginTop: 12, padding: 12, borderRadius: 8,
      background: "var(--surface-2, rgba(138,138,147,.06))",
      border: "1px solid rgba(138,138,147,.20)",
    }}>
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
        <label>
          <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>CLASS</div>
          <select className="input" style={{ minWidth: 150 }} value={classId}
            onChange={(e) => { setClassId(e.target.value); setPicked({}); }}>
            <option value="">— pick a class —</option>
            {(setup?.classes || []).map((c) => (
              <option key={c.id} value={c.id}>{c.label} ({c.students})</option>
            ))}
          </select>
        </label>
        <label>
          <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>WRITTEN</div>
          <input className="input" type="number" min="0" step="0.5" style={{ width: 80 }}
            value={written} onChange={(e) => setWritten(e.target.value)} />
        </label>
        <label>
          <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>ORAL</div>
          <input className="input" type="number" min="0" step="0.5" style={{ width: 80 }}
            value={oral} onChange={(e) => setOral(e.target.value)} />
        </label>
        <button className="btn" disabled={!classId || chosen.length === 0} onClick={add}>
          Add {chosen.length || ""} subject{chosen.length === 1 ? "" : "s"}
        </button>
      </div>

      {/* Not every examined subject is in the registry. That list drives test
          authoring, chapters and the routine, so a paper the school only ever
          examines belongs here and not there. */}
      {classId && (
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "flex-end", marginTop: 10 }}>
          <label style={{ flex: 1, minWidth: 190 }}>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>
              OR A SUBJECT NOT IN THE LIST
            </div>
            <input
              className="input" style={{ width: "100%" }} value={custom}
              list="exm-custom-subjects"
              placeholder="Drawing, Work Education, Moral Science…"
              onChange={(e) => setCustom(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addCustom(); } }}
            />
            {/* Names this school has already examined under, so the second
                class reuses one instead of retyping it into a typo. */}
            <datalist id="exm-custom-subjects">
              {(setup?.custom_subjects || []).map((n) => <option key={n} value={n} />)}
            </datalist>
          </label>
          <button className="btn" disabled={!customName || customTaken} onClick={addCustom}>
            Add subject
          </button>
        </div>
      )}
      {customTaken && (
        <div style={{ fontSize: 11.5, color: "var(--amber)", marginTop: 6 }}>
          “{customName}” is already on this class.
        </div>
      )}

      {classId && (
        <div style={{ marginTop: 10, display: "flex", gap: 8, flexWrap: "wrap" }}>
          {subjects.length === 0 && (
            <span className="muted" style={{ fontSize: 12.5 }}>
              Every registry subject is already on this class's paper list — add one of
              your own below.
            </span>
          )}
          {subjects.map((s) => (
            <label key={s.id} style={{
              display: "inline-flex", alignItems: "center", gap: 6, padding: "4px 10px",
              borderRadius: 999, fontSize: 12.5, cursor: "pointer",
              border: `1px solid ${picked[s.id] ? "var(--blue)" : "rgba(138,138,147,.30)"}`,
              background: picked[s.id] ? "rgba(59,123,246,.12)" : "transparent",
              color: picked[s.id] ? "var(--blue)" : "var(--ink-1)",
            }}>
              <input type="checkbox" checked={!!picked[s.id]} style={{ margin: 0 }}
                onChange={(e) => setPicked({ ...picked, [s.id]: e.target.checked })} />
              {s.name}
            </label>
          ))}
          {subjects.length > 1 && (
            <button className="btn sm ghost"
              onClick={() => setPicked(Object.fromEntries(subjects.map((s) => [s.id, true])))}>
              Select all
            </button>
          )}
        </div>
      )}
    </div>
  );
};

// ── Read-only sheet viewer ────────────────────────────────────────────
// So the office can check a submitted paper before publishing without
// signing in as the teacher.

const ExmSheetViewer = ({ examId, paper, onClose }) => {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    let alive = true;
    window.KXApi.get(`/admin/exams/${examId}/papers/${paper.id}/marks`)
      .then((d) => { if (alive) setData(d); })
      .catch((e) => { if (alive) setErr(e.message || "Failed to load"); });
    return () => { alive = false; };
  }, [examId, paper.id]);

  return (
    <div className="card">
      <div className="card-body">
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
          <div className="cx-card-title">
            {paper.class_label} · {paper.subject_name}
            {data?.paper ? <span className="muted" style={{ fontWeight: 400, fontSize: 12.5 }}>
              {"  "}out of {data.paper.max_total}
              {data.paper.max_oral > 0 ? ` (${data.paper.max_written} written + ${data.paper.max_oral} oral)` : ""}
            </span> : null}
          </div>
          <button className="btn sm ghost" onClick={onClose}>Close</button>
        </div>

        {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 10 }}>{err}</div>}
        {!data && !err && <div className="muted" style={{ marginTop: 10 }}>Loading…</div>}

        {data && (
          <div style={{ overflowX: "auto", marginTop: 10 }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
              <thead>
                <tr style={{ textAlign: "left", color: "var(--ink-3)", fontSize: 11 }}>
                  <th style={{ padding: "6px 8px" }}>ROLL</th>
                  <th style={{ padding: "6px 8px" }}>NAME</th>
                  {data.paper.max_written > 0 && <th style={{ padding: "6px 8px", textAlign: "right" }}>WRITTEN</th>}
                  {data.paper.max_oral > 0 && <th style={{ padding: "6px 8px", textAlign: "right" }}>ORAL</th>}
                  <th style={{ padding: "6px 8px", textAlign: "right" }}>TOTAL</th>
                </tr>
              </thead>
              <tbody>
                {data.students.map((s) => (
                  <tr key={s.student_id} style={{ borderTop: "1px solid var(--line, rgba(138,138,147,.18))" }}>
                    <td className="mono" style={{ padding: "6px 8px", color: "var(--ink-2)" }}>{s.roll_no}</td>
                    <td style={{ padding: "6px 8px", color: "var(--ink-0)" }}>{s.name}</td>
                    {data.paper.max_written > 0 && (
                      <td className="mono" style={{ padding: "6px 8px", textAlign: "right" }}>
                        {s.is_absent ? "—" : (s.written ?? "·")}
                      </td>
                    )}
                    {data.paper.max_oral > 0 && (
                      <td className="mono" style={{ padding: "6px 8px", textAlign: "right" }}>
                        {s.is_absent ? "—" : (s.oral ?? "·")}
                      </td>
                    )}
                    <td className="mono" style={{ padding: "6px 8px", textAlign: "right", fontWeight: 600 }}>
                      {s.is_absent
                        ? <span style={{ color: "var(--amber)", fontWeight: 500 }}>Absent</span>
                        : (s.total ?? <span className="muted">not entered</span>)}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════════
// Progress reports — generating the printed marksheet
// ══════════════════════════════════════════════════════════════════════

// Opens the generated PDF in a new tab rather than downloading it: the
// browser's own viewer is where somebody hits Print, which is the whole point
// of the file. The session is a Bearer token, so a plain <a href> would fetch
// it unauthenticated — hence KXApi.download and a blob URL.
// Downloads the exam's marks as CSV. Bearer-token session, so a plain
// <a href> would fetch unauthenticated — fetch via KXApi, then hand the
// browser a blob with the server's filename.
async function exmDownloadCsv(examId, classSectionId, setErr) {
  try {
    const qs = classSectionId ? `?class_section_id=${classSectionId}` : "";
    const { blobUrl, filename } = await window.KXApi.download(`/admin/exams/${examId}/marks.csv${qs}`);
    const a = document.createElement("a");
    a.href = blobUrl; a.download = filename || "marks.csv";
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
  } catch (e) { setErr(e.message || "Could not export the marks"); }
}

async function exmOpenPdf(qs, setBusy, setErr, key) {
  setBusy(key); setErr(null);
  try {
    const { blobUrl } = await window.KXApi.download(`/admin/marksheet.pdf?${qs}`);
    const w = window.open(blobUrl, "_blank");
    if (!w) setErr("Your browser blocked the pop-up — allow pop-ups for this site to open the report.");
    // Revoked late: revoking immediately races the new tab's own load.
    setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
  } catch (e) {
    setErr(e.message || "Could not generate the report");
  } finally { setBusy(null); }
}

const ExmMarksheets = () => {
  const [data, setData]   = React.useState(null);
  const [year, setYear]   = React.useState(null);
  const [err, setErr]     = React.useState(null);
  const [busy, setBusy]   = React.useState(null);
  const [openCls, setOpenCls] = React.useState(null);
  // The result's publication date, printed in every report's Date field. No
  // default: it is almost never the day Generate is pressed, so the office
  // has to choose it. Remembered in this browser between sessions.
  const [reportDate, setReportDateRaw] = React.useState(() => {
    try { return localStorage.getItem("kx.exams.reportDate") || ""; } catch { return ""; }
  });
  const setReportDate = (v) => {
    setReportDateRaw(v);
    try { v ? localStorage.setItem("kx.exams.reportDate", v) : localStorage.removeItem("kx.exams.reportDate"); } catch {}
  };

  const load = React.useCallback(async (y) => {
    setErr(null);
    try {
      const d = await window.KXApi.get(
        `/admin/marksheet/sessions${y ? `?academic_year=${encodeURIComponent(y)}` : ""}`);
      setData(d); setYear(d.academic_year);
    } catch (e) { setErr(e.message || "Failed to load"); }
  }, []);
  React.useEffect(() => { load(null); }, [load]);

  if (err && !data) return (
    <div className="card"><div className="card-body">
      <div style={{ color: "var(--red)" }}>{err}</div>
    </div></div>
  );
  if (!data) return <div className="card"><div className="card-body muted">Loading…</div></div>;

  const mapped = Object.fromEntries((data.rows || []).map((r) => [r.marksheet_row, r]));
  const missing = EXM_ROWS.filter((r) => r.key && !mapped[r.key]);

  return (
    <div style={{ display: "grid", gap: 14 }}>
      <div className="card">
        <div className="card-body">
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
            <div>
              <div className="cx-card-title">Progress reports</div>
              <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>
                One sheet per child for the whole session — the three terminal rows, every
                subject, and the annual grade. Prints on Legal paper, landscape.
              </div>
            </div>
            <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
              <label>
                <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>SESSION</div>
                <select className="input" value={year || ""} onChange={(e) => load(e.target.value)}>
                  {(data.sessions || []).map((x) => (
                    <option key={x.academic_year} value={x.academic_year}>{x.academic_year}</option>
                  ))}
                </select>
              </label>
              <label>
                <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>DATE ON THE REPORTS</div>
                <input className="input" type="date" value={reportDate}
                  style={{ borderColor: reportDate ? undefined : "var(--amber)" }}
                  onChange={(e) => setReportDate(e.target.value)} />
              </label>
            </div>
          </div>
          {!reportDate && (
            <div style={{ fontSize: 12, color: "var(--amber)", marginTop: 8 }}>
              Choose the date the result is published — it is printed on every report.
              Generating stays off until you do.
            </div>
          )}

          {/* Which exam fills which row — the one thing that has to be right
              before anything is printed. */}
          <div style={{ marginTop: 12, display: "grid", gap: 6 }}>
            {EXM_ROWS.filter((r) => r.key).map((r) => (
              <div key={r.key} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5 }}>
                <span style={{ minWidth: 190, color: "var(--ink-2)" }}>{r.label}</span>
                {mapped[r.key] ? (
                  <>
                    <span style={{ color: "var(--ink-0)" }}>{mapped[r.key].name}</span>
                    <ExmPill tint={EXM_STATUS_TINT[mapped[r.key].status] || EXM_STATUS_TINT.draft} />
                  </>
                ) : (
                  <span className="muted">not set — this row prints blank</span>
                )}
              </div>
            ))}
          </div>
          {missing.length > 0 && (
            <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>
              Set a row on an exam from its own page (Examinations → open the exam).
            </div>
          )}
          {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 10 }}>{err}</div>}
        </div>
      </div>

      <ExmSignature />

      {(data.classes || []).length === 0 && (
        <div className="card"><div className="card-body muted" style={{ textAlign: "center", padding: 26 }}>
          No class has papers in this session's marksheet exams yet.
        </div></div>
      )}

      {(data.classes || []).map((c) => (
        <div key={c.id} className="card">
          <div className="card-body">
            <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
              <div style={{ flex: 1, minWidth: 180 }}>
                <div style={{ fontSize: 15, fontWeight: 600, color: "var(--ink-0)" }}>
                  Class {c.label}
                </div>
                <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
                  {c.students} student{c.students === 1 ? "" : "s"} · {c.subjects} subject
                  {c.subjects === 1 ? "" : "s"} · {c.submitted}/{c.papers} papers submitted
                </div>
              </div>
              <button className="btn sm ghost"
                onClick={() => setOpenCls(openCls === c.id ? null : c.id)}>
                {openCls === c.id ? "Hide students" : "One student…"}
              </button>
              <button className="btn primary" disabled={busy === c.id || !c.students || !reportDate}
                title={reportDate ? "" : "Choose the date on the reports first"}
                onClick={() => exmOpenPdf(
                  `class_section_id=${c.id}&academic_year=${encodeURIComponent(year)}&date=${reportDate}`,
                  setBusy, setErr, c.id)}>
                {busy === c.id ? "Generating…" : `Generate all ${c.students}`}
              </button>
            </div>

            {c.submitted < c.papers && (
              <div style={{ fontSize: 11.5, color: "var(--amber)", marginTop: 8 }}>
                {c.papers - c.submitted} paper(s) not submitted — marks already typed into
                them still print, so this report can change.
              </div>
            )}

            {openCls === c.id && (
              <ExmMarksheetRoster
                classId={c.id} year={year} busy={busy} setBusy={setBusy} setErr={setErr}
                reportDate={reportDate}
              />
            )}
          </div>
        </div>
      ))}
    </div>
  );
};

// ── The headmaster's signature, printed on every report ───────────────
// One PNG for the school, stored server-side (backend/data/marksheet). PNG so
// a scanned signature keeps its transparency and sits on the ruled line
// without a white box around it.
const ExmSignature = () => {
  const [info, setInfo] = React.useState(null);
  const [preview, setPreview] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const fileRef = React.useRef(null);

  const refresh = React.useCallback(async () => {
    setErr(null);
    try {
      const i = await window.KXApi.get("/admin/marksheet/signature?info=1");
      setInfo(i);
      if (i.present) {
        const { blobUrl } = await window.KXApi.download(`/admin/marksheet/signature?t=${Date.now()}`);
        setPreview((old) => { if (old) URL.revokeObjectURL(old); return blobUrl; });
      } else setPreview((old) => { if (old) URL.revokeObjectURL(old); return null; });
    } catch (e) { setErr(e.message || "Could not load the signature"); }
  }, []);
  React.useEffect(() => { refresh(); }, [refresh]);

  const upload = async (file) => {
    if (!file) return;
    setErr(null);
    if (file.type !== "image/png") { setErr("Use a PNG image — it keeps the transparency around the signature."); return; }
    if (file.size > 2 * 1024 * 1024) { setErr("That image is larger than 2 MB."); return; }
    setBusy(true);
    try {
      const form = new FormData();
      form.append("file", file, file.name);
      const token = window.KXApi.getToken();
      const r = await fetch("/api/admin/marksheet/signature", {
        method: "PUT", body: form,
        headers: token ? { authorization: `Bearer ${token}` } : {},
      });
      if (!r.ok) {
        let msg = `upload failed (${r.status})`;
        try { const j = await r.json(); msg = j.error || j.message || msg; } catch {}
        throw new Error(msg);
      }
      await refresh();
    } catch (e) { setErr(e.message || "Upload failed"); }
    finally { setBusy(false); if (fileRef.current) fileRef.current.value = ""; }
  };

  const remove = async () => {
    if (!window.confirm("Remove the headmaster's signature? Reports will print with an empty signature block.")) return;
    setBusy(true); setErr(null);
    try { await window.KXApi.del("/admin/marksheet/signature"); await refresh(); }
    catch (e) { setErr(e.message || "Could not remove it"); }
    finally { setBusy(false); }
  };

  return (
    <div className="card">
      <div className="card-body" style={{ display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
        <div style={{
          width: 190, height: 74, borderRadius: 8, flexShrink: 0,
          border: "1px dashed rgba(138,138,147,.45)", background: "#fff",
          display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden",
        }}>
          {preview
            ? <img src={preview} alt="Headmaster's signature" style={{ maxWidth: "92%", maxHeight: "86%" }} />
            : <span className="muted" style={{ fontSize: 11.5 }}>No signature yet</span>}
        </div>
        <div style={{ flex: 1, minWidth: 220 }}>
          <div className="cx-card-title">Headmaster's signature</div>
          <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>
            Printed above the line in the headmaster's block on every report. A PNG with a
            transparent background works best; up to 2 MB.
          </div>
          {info?.present && (
            <div className="muted" style={{ fontSize: 11.5, marginTop: 4 }}>
              Updated {exmDate(info.updated_at)}
            </div>
          )}
          {err && <div style={{ color: "var(--red)", fontSize: 12.5, marginTop: 6 }}>{err}</div>}
        </div>
        <div style={{ display: "flex", gap: 8 }}>
          <input ref={fileRef} type="file" accept="image/png" style={{ display: "none" }}
            onChange={(e) => upload(e.target.files?.[0])} />
          <button className="btn" disabled={busy} onClick={() => fileRef.current?.click()}>
            {busy ? "Saving…" : info?.present ? "Replace PNG" : "Upload PNG"}
          </button>
          {info?.present && (
            <button className="btn ghost" disabled={busy} onClick={remove}>Remove</button>
          )}
        </div>
      </div>
    </div>
  );
};

const ExmMarksheetRoster = ({ classId, year, busy, setBusy, setErr, reportDate }) => {
  const [roster, setRoster] = React.useState(null);
  const [q, setQ] = React.useState("");

  React.useEffect(() => {
    let alive = true;
    window.KXApi
      .get(`/admin/marksheet/roster?class_section_id=${classId}&academic_year=${encodeURIComponent(year)}`)
      .then((d) => { if (alive) setRoster(d); })
      .catch((e) => { if (alive) setErr(e.message || "Could not load the roster"); });
    return () => { alive = false; };
  }, [classId, year, setErr]);

  if (!roster) return <div className="muted" style={{ marginTop: 10, fontSize: 12.5 }}>Loading roster…</div>;

  const t = q.trim().toLowerCase();
  const rows = t
    ? roster.students.filter((s) =>
        s.name.toLowerCase().includes(t) || (s.roll_no || "").toLowerCase().includes(t))
    : roster.students;

  return (
    <div style={{ marginTop: 12, borderTop: "1px solid rgba(138,138,147,.20)", paddingTop: 10 }}>
      {roster.warnings?.length > 0 && (
        <ul style={{ margin: "0 0 10px 16px", fontSize: 11.5, color: "var(--amber)" }}>
          {roster.warnings.map((w, i) => <li key={i}>{w}</li>)}
        </ul>
      )}
      <input className="input" style={{ width: "100%", maxWidth: 280, marginBottom: 8 }}
        placeholder="Find a name or roll number" value={q}
        onChange={(e) => setQ(e.target.value)} />
      <div style={{ maxHeight: 320, overflowY: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
          <thead>
            <tr style={{ textAlign: "left", color: "var(--ink-3)", fontSize: 11 }}>
              <th style={{ padding: "5px 8px", width: 70 }}>ROLL</th>
              <th style={{ padding: "5px 8px" }}>NAME</th>
              <th style={{ padding: "5px 8px", width: 110, textAlign: "right" }}>ANNUAL</th>
              <th style={{ padding: "5px 8px", width: 70 }}>GRADE</th>
              <th style={{ padding: "5px 8px", width: 110 }}></th>
            </tr>
          </thead>
          <tbody>
            {rows.map((s) => {
              const key = `${classId}:${s.student_id}`;
              return (
                <tr key={s.student_id} style={{ borderTop: "1px solid rgba(138,138,147,.16)" }}>
                  <td className="mono" style={{ padding: "5px 8px", color: "var(--ink-2)" }}>{s.roll_no}</td>
                  <td style={{ padding: "5px 8px", color: "var(--ink-0)" }}>{s.name}</td>
                  <td className="mono" style={{ padding: "5px 8px", textAlign: "right" }}>
                    {s.annual.pct == null
                      ? <span className="muted">no marks</span>
                      : `${s.annual.pct}%`}
                  </td>
                  <td style={{ padding: "5px 8px" }}>
                    {s.annual.grade || "—"}
                    {/* Promotion is the Annual examination's verdict; before
                        the Annual row exists there is no verdict to show. */}
                    {s.annual.promoted === false
                      && s.rows?.find((r) => r.row === "annual")?.total != null && (
                      <span style={{ color: "var(--red)", fontSize: 11 }}> · not promoted</span>
                    )}
                  </td>
                  <td style={{ padding: "5px 8px", textAlign: "right" }}>
                    <button className="btn sm ghost" disabled={busy === key || !reportDate}
                      title={reportDate ? "" : "Choose the date on the reports first"}
                      onClick={() => exmOpenPdf(
                        `class_section_id=${classId}&academic_year=${encodeURIComponent(year)}&student_id=${s.student_id}&date=${reportDate}`,
                        setBusy, setErr, key)}>
                      {busy === key ? "…" : "Generate"}
                    </button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
        {rows.length === 0 && (
          <div className="muted" style={{ fontSize: 12.5, padding: 14, textAlign: "center" }}>
            Nobody matches “{q}”.
          </div>
        )}
      </div>
    </div>
  );
};

window.ExamsScreen = ExamsScreen;
