/* ==================================================================
   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.AppUI;

// 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:      { tone: "grey",  label: "Draft",      fg: "var(--ink-3)", bg: "color-mix(in srgb, var(--ink-0) 12%, transparent)", bd: "color-mix(in srgb, var(--ink-0) 30%, transparent)" },
  entry_open: { tone: "amber", label: "Entry open", fg: "var(--amber)", bg: "color-mix(in srgb, var(--amber-solid) 14%, transparent)",  bd: "color-mix(in srgb, var(--amber-solid) 32%, transparent)" },
  published:  { tone: "green", label: "Published",  fg: "var(--green)", bg: "color-mix(in srgb, var(--green-solid) 16%, transparent)",   bd: "color-mix(in srgb, var(--green-solid) 36%, transparent)" },
};

// Status pill on the shared primitive. `outline` (icon + text) sits beside a
// page title; `solid` is for list rows; tint is the default.
const ExmPill = ({ tint, children, outline, solid }) => (
  <window.AppUI.StatusBadge tone={tint.tone || "grey"}
    variant={outline ? "outline" : solid ? "solid" : "tint"}
    icon={outline ? undefined : false}>
    {children ?? tint.label}
  </window.AppUI.StatusBadge>
);

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: 6, borderRadius: 999, background: "var(--track)", overflow: "hidden" }}>
        <div style={{ width: `${pct}%`, height: "100%", background: colour, transition: "width .2s" }} />
      </div>
      <span style={{ fontSize: 12.5, color: "var(--ink-2)", whiteSpace: "nowrap", fontVariantNumeric: "tabular-nums" }}>
        {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.AppApi.get("/admin/exams"),
        setup ? Promise.resolve(setup) : window.AppApi.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="cx-page">
      <div className="cx-card"><div className="muted">Loading examinations…</div></div>
    </div>
  );
  if (err) return (
    <div className="cx-page">
      <div className="cx-card">
        <div className="ui-alert red">{err}</div>
        <button className="btn sm" style={{ marginTop: 12 }} onClick={refresh}>
          <ExmIcon name="refresh" size={14}/>Retry
        </button>
      </div>
    </div>
  );

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

  return (
    <div className="cx-page">
      <div className="cx-page-head">
        <div>
          <div className="cx-eyebrow">Operations</div>
          <h1 className="cx-h1">Examinations</h1>
        </div>
      </div>
      <div style={{ marginBottom: 20 }}>
        <window.AppUI.Tabs value={tab} onChange={setTab} full ariaLabel="Examination sections"
          items={[
            { value: "exams", label: "Examinations", icon: "clipboard", count: exams.length },
            { value: "reports", label: "Progress reports", icon: "report" },
          ]}/>
      </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: 20 }}>
      {creating && (
        <ExmCreate
          setup={setup}
          onCancel={() => setCreating(false)}
          onCreated={(e) => { setCreating(false); onChanged(); onOpen(e.id); }}
        />
      )}

      <window.AppUI.Card flush
        title="All examinations"
        sub="Build an exam, set each paper's marks division, assign the teacher who enters them, then publish the result to parents."
        action={!creating && (
          <button className="btn primary" onClick={() => setCreating(true)}>
            <ExmIcon name="plus" size={15}/>New exam
          </button>
        )}>
        <div style={{ padding: "8px 0 4px" }}>
          {exams.length === 0 && !creating && (
            <window.AppUI.EmptyState compact icon="clipboard" title="No examinations yet"
              hint="“New exam” starts one."/>
          )}
          {exams.length > 0 && (
            <div className="ui-list" style={{ padding: "0 22px" }}>
              {exams.map((e) => {
                const tint = EXM_STATUS_TINT[e.status] || EXM_STATUS_TINT.draft;
                return (
                  <window.AppUI.ListRow key={e.id} onClick={() => onOpen(e.id)}
                    title={e.name}
                    sub={<>
                      {e.exam_type} · {e.academic_year}
                      {e.starts_on ? ` · ${exmDate(e.starts_on)}` : ""}
                      {e.ends_on && e.ends_on !== e.starts_on ? ` – ${exmDate(e.ends_on)}` : ""}
                    </>}
                    status={<ExmPill tint={tint} solid />}
                    amount={<span title="Papers submitted">{e.submitted}/{e.papers} <span className="muted" style={{ fontWeight: 400 }}>papers</span></span>}
                    trailing={<>
                      {e.unstaffed > 0 && (
                        <window.AppUI.StatusBadge tone="amber" icon="alertCircle">{e.unstaffed} unassigned</window.AppUI.StatusBadge>
                      )}
                      <span style={{ color: "var(--ink-4)", display: "inline-flex" }}><ExmIcon name="chevronRight" size={16}/></span>
                    </>}
                  />
                );
              })}
            </div>
          )}
        </div>
      </window.AppUI.Card>
    </div>
  );
};

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

const ExmField = ({ label, children, style }) => (
  <label className="field" style={{ display: "block", ...style }}>
    <div className="label" style={{ marginBottom: 6 }}>{label}</div>
    {children}
  </label>
);

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.AppApi.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 (
    <window.AppUI.Card title="New examination">
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(200px,1fr))", gap: 16 }}>
        <ExmField label="Name">
          <input className="input" style={{ width: "100%" }} value={f.name} autoFocus
            placeholder="Half-Yearly Examination"
            onChange={(e) => setF({ ...f, name: e.target.value })} />
        </ExmField>
        <ExmField label="Type">
          <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>
        </ExmField>
        <ExmField label="Academic year">
          <select className="select" 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>
        </ExmField>
        <ExmField label="Starts (optional)">
          <input className="input" type="date" style={{ width: "100%" }} value={f.starts_on}
            onChange={(e) => setF({ ...f, starts_on: e.target.value })} />
        </ExmField>
        <ExmField label="Ends (optional)">
          <input className="input" type="date" style={{ width: "100%" }} value={f.ends_on}
            onChange={(e) => setF({ ...f, ends_on: e.target.value })} />
        </ExmField>
        <ExmField label="Progress report row">
          <select className="select" 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>
        </ExmField>
      </div>
      {err && <div className="ui-alert red" style={{ marginTop: 14 }}>{err}</div>}
      <div className="cx-card-foot" style={{ justifyContent: "flex-end" }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn primary" disabled={busy || !f.name.trim() || !f.exam_type.trim()} onClick={save}>
          <ExmIcon name="check" size={15}/>{busy ? "Creating…" : "Create"}
        </button>
      </div>
    </window.AppUI.Card>
  );
};

// ══════════════════════════════════════════════════════════════════════
// 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.AppApi.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.AppApi.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.AppApi.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.AppApi.del(`/admin/exams/${examId}`); onBack(); }
    catch (e) { setErr(e.message || "Could not delete"); setBusy(false); }
  };

  if (!exam) {
    return (
      <div className="cx-page">
        <window.AppUI.BackLink onClick={onBack}>All examinations</window.AppUI.BackLink>
        <div className="cx-card" style={{ marginTop: 12 }}>
          {err ? <div className="ui-alert red">{err}</div> : <div className="muted">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";
  const unassigned = exam.papers.filter((p) => !p.assigned_to).length;

  return (
    <div className="cx-page">
      <window.AppUI.PageHeader
        onBack={onBack} backLabel="All examinations"
        title={exam.name}
        status={<ExmPill tint={tint} outline />}
        meta={[
          exam.exam_type,
          <span className="mono">{exam.academic_year}</span>,
          exam.starts_on
            ? `${exmDate(exam.starts_on)}${exam.ends_on && exam.ends_on !== exam.starts_on ? ` – ${exmDate(exam.ends_on)}` : ""}`
            : null,
          exam.published_at ? `published ${exmDate(exam.published_at)}` : null,
        ]}
        actions={<>
          {/* 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" title="All classes · one row per child per paper"
              onClick={() => exmDownloadCsv(examId, null, setErr)}>
              <ExmIcon name="download" size={15}/>Export CSV
            </button>
          )}
          {exam.status === "draft" && (
            <button className="btn primary" disabled={busy} onClick={() => move("entry_open")}>
              <ExmIcon name="penLine" size={15}/>Open for entry
            </button>
          )}
          {exam.status === "entry_open" && (
            <button className="btn primary" disabled={busy} onClick={() => move("published")}>
              <ExmIcon name="megaphone" size={15}/>Publish to parents
            </button>
          )}
          {exam.status === "published" && (
            <button className="btn" disabled={busy} onClick={() => move("entry_open")}>
              <ExmIcon name="eye" size={15}/>Unpublish
            </button>
          )}
        </>}
        toolbar={(exam.status === "entry_open" || exam.status === "draft") ? <>
          {exam.status === "entry_open" && (
            <button className="btn" disabled={busy} onClick={() => move("draft")}>
              <ExmIcon name="arrowLeft" size={15}/>Back to draft
            </button>
          )}
          {exam.status === "draft" && (
            <button className="btn ghost" disabled={busy} onClick={remove} style={{ color: "var(--red)" }}>
              <ExmIcon name="trash" size={15}/>Delete
            </button>
          )}
        </> : null}
      />

      <div className="cx-grid split" style={{ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1.4fr)", marginBottom: 20 }}>
        <window.AppUI.Card title="Status">
          <div className={`ui-alert ${exam.status === "published" ? "green" : exam.status === "entry_open" ? "amber" : "blue"}`}>
            {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>
          <window.AppUI.StatRow items={[
            { value: `${submitted}/${exam.papers.length}`, label: "papers submitted", tone: exam.papers.length && submitted === exam.papers.length ? "green" : undefined },
            { value: unassigned, label: "unassigned", tone: unassigned ? "amber" : undefined },
            { value: exam.papers.length ? `${Math.round((submitted / exam.papers.length) * 100)}%` : "—", label: "complete" },
          ]}/>
        </window.AppUI.Card>

        <window.AppUI.Card title="Details">
          <dl className="ui-details" style={{ "--cols": 2 }}>
            <div className="ui-detail"><div className="ui-detail-label">Type</div><div className="ui-detail-value">{exam.exam_type || "—"}</div></div>
            <div className="ui-detail"><div className="ui-detail-label">Academic year</div><div className="ui-detail-value mono">{exam.academic_year || "—"}</div></div>
            <div className="ui-detail"><div className="ui-detail-label">Starts</div><div className="ui-detail-value">{exmDate(exam.starts_on)}</div></div>
            <div className="ui-detail"><div className="ui-detail-label">Ends</div><div className="ui-detail-value">{exmDate(exam.ends_on)}</div></div>
            {/* Which row of the printed progress report this exam fills. A
                session has one of each, so the server refuses a clash. */}
            <div className="ui-detail" style={{ gridColumn: "1 / -1" }}>
              <div className="ui-detail-label">Progress report row</div>
              <div className="ui-detail-value" style={{ margin: 0 }}>
                <select className="select" style={{ minWidth: 220, width: "auto", marginTop: 4 }} disabled={busy}
                  aria-label="Progress report row"
                  value={exam.marksheet_row || ""}
                  onChange={async (e) => {
                    setBusy(true); setErr(null);
                    try {
                      setExam(await window.AppApi.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>
              </div>
            </div>
          </dl>
        </window.AppUI.Card>
      </div>

      {err && <div className="ui-alert red" style={{ marginBottom: 20 }}>{err}</div>}

      {notice?.kind === "confirm-publish" && (
        <div className="ui-alert amber" style={{ marginBottom: 20 }}>
          <div style={{ fontWeight: 600 }}>{notice.text}</div>
          <ul style={{ margin: "6px 0 10px 18px" }}>
            {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 style={{ marginBottom: 12, color: "var(--ink-2)" }}>
            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" onClick={() => setNotice(null)}>Cancel</button>
          </div>
        </div>
      )}

      <div style={{ display: "grid", gap: 20 }}>
        <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>
    </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.AppApi.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="cx-card">
        <div className="cx-card-head">
          <div style={{ minWidth: 0 }}>
            <div className="cx-card-title">Papers</div>
            <div className="cx-card-sub">
              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}>
              <ExmIcon name="check" size={15}/>{busy ? "Saving…" : dirty ? "Save papers" : "Saved"}
            </button>
          )}
        </div>

        {locked && (
          <div className="ui-alert blue">
            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 className="ui-alert red" style={{ marginTop: 12 }}>{err}</div>}
        {kept.length > 0 && (
          <div className="ui-alert amber" style={{ marginTop: 12 }}>
            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 && (
          <window.AppUI.EmptyState compact icon="layers" title="No papers yet" hint="Add a class and its subjects above."/>
        )}

        {grouped.map(([label, items]) => (
          <div key={label} className="cx-tablecard" style={{ marginTop: 18 }}>
            <div className="cx-tablecard-head" style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <span className="cx-tablecard-title">Class {label}</span>
              <span className="muted" style={{ fontSize: 12.5 }}>
                {items[0].r._roll != null ? `${items[0].r._roll} on roll` : ""}
              </span>
              <span style={{ flex: 1 }}/>
              {/* 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"
                  title="This class · one row per child"
                  onClick={() => exmDownloadCsv(exam.id, items[0].r.class_section_id, setErr)}>
                  <ExmIcon name="download" size={14}/>CSV
                </button>
              )}
            </div>
            <div className="cx-tablescroll">
              <table className="cx-table">
                <thead>
                  <tr>
                    <th style={{ minWidth: 160 }}>Subject · as examined</th>
                    <th style={{ width: 90 }}>Written</th>
                    <th style={{ width: 90 }}>Oral</th>
                    <th className="num" style={{ width: 70 }}>Total</th>
                    <th style={{ width: 118 }}>Report band</th>
                    <th style={{ width: 140 }}>Date</th>
                    <th style={{ minWidth: 150 }}>Entered by</th>
                    <th style={{ minWidth: 150 }}>Progress</th>
                    <th style={{ 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}`}>
                        <td>
                          {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: 11.5, marginTop: 3 }}>
                                  {registry} in the subject registry
                                </div>
                              )}
                              {!r.subject_id && (
                                <div className="muted" style={{ fontSize: 11.5, marginTop: 3 }}>
                                  exam only
                                </div>
                              )}
                            </>
                          )}
                        </td>
                        <td>
                          <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>
                          <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="num" style={{ fontWeight: 600, color: total > 0 ? "var(--ink-0)" : "var(--red)" }}>
                          {total || "—"}
                        </td>
                        <td>
                          {/* Which heading the subject prints under on the
                              progress report. "auto" reads it off the name. */}
                          <select className="select" 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>
                          <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>
                          <select className="select" 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>
                          {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} solid>In</ExmPill>}
                            </div>
                          ) : <window.AppUI.StatusBadge tone="blue" icon={false}>New</window.AppUI.StatusBadge>}
                        </td>
                        <td className="actions" style={{ whiteSpace: "nowrap", textAlign: "right" }}>
                          {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,
                            })}><ExmIcon name="eye" size={14}/>View</button>
                          )}
                          {" "}
                          {done && !locked && (
                            <button className="btn sm ghost" onClick={() => onReopen(r._id)}><ExmIcon name="refresh" size={14}/>Reopen</button>
                          )}
                          {!done && !locked && (
                            <button className="ui-iconbtn sm" onClick={() => drop(i)} aria-label="Remove paper"
                              title={r._entered > 0 ? "This paper holds marks — the server will refuse to remove it" : "Remove"}>
                              <ExmIcon name="x" size={14}/>
                            </button>
                          )}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </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 className="cx-tile" style={{ padding: 16 }}>
      <div className="ui-section-label" style={{ marginBottom: 10 }}>Add papers</div>
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
        <label className="field">
          <div className="label" style={{ marginBottom: 6 }}>Class</div>
          <select className="select" style={{ minWidth: 170 }} 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 className="field">
          <div className="label" style={{ marginBottom: 6 }}>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 className="field">
          <div className="label" style={{ marginBottom: 6 }}>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}>
          <ExmIcon name="plus" size={15}/>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 className="field" style={{ flex: 1, minWidth: 190 }}>
            <div className="label" style={{ marginBottom: 6 }}>
              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}>
            <ExmIcon name="plus" size={15}/>Add subject
          </button>
        </div>
      )}
      {customTaken && (
        <div style={{ fontSize: 12.5, color: "var(--amber)", marginTop: 8 }}>
          “{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 11px", minHeight: 28,
              borderRadius: 999, fontSize: 12.5, cursor: "pointer",
              border: `1px solid ${picked[s.id] ? "transparent" : "var(--line)"}`,
              background: picked[s.id] ? "var(--primary)" : "var(--surface)",
              color: picked[s.id] ? "var(--primary-ink)" : "var(--ink-1)",
            }}>
              <input type="checkbox" checked={!!picked[s.id]} style={{ margin: 0, accentColor: "var(--primary-ink)" }}
                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.AppApi.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="cx-card flush">
        <div className="cx-card-head" style={{ alignItems: "center" }}>
          <div style={{ minWidth: 0 }}>
            <div className="cx-card-title">{paper.class_label} · {paper.subject_name}</div>
            {data?.paper ? <div className="cx-card-sub">
              Out of {data.paper.max_total}
              {data.paper.max_oral > 0 ? ` (${data.paper.max_written} written + ${data.paper.max_oral} oral)` : ""}
            </div> : null}
          </div>
          <window.AppUI.IconButton icon="x" label="Close" onClick={onClose}/>
        </div>

        {err && <div className="ui-alert red" style={{ margin: "14px 20px" }}>{err}</div>}
        {!data && !err && <div className="muted" style={{ padding: "14px 20px" }}>Loading…</div>}

        {data && (
          <div className="cx-tablescroll" style={{ marginTop: 14 }}>
            <table className="cx-table">
              <thead>
                <tr>
                  <th style={{ width: 90 }}>Roll</th>
                  <th>Name</th>
                  {data.paper.max_written > 0 && <th className="num">Written</th>}
                  {data.paper.max_oral > 0 && <th className="num">Oral</th>}
                  <th className="num">Total</th>
                </tr>
              </thead>
              <tbody>
                {data.students.map((s) => (
                  <tr key={s.student_id}>
                    <td className="mono" style={{ color: "var(--ink-3)" }}>{s.roll_no}</td>
                    <td style={{ color: "var(--ink-0)", fontWeight: 500 }}>{s.name}</td>
                    {data.paper.max_written > 0 && (
                      <td className="num">
                        {s.is_absent ? "—" : (s.written ?? "·")}
                      </td>
                    )}
                    {data.paper.max_oral > 0 && (
                      <td className="num">
                        {s.is_absent ? "—" : (s.oral ?? "·")}
                      </td>
                    )}
                    <td className="num" style={{ fontWeight: 600 }}>
                      {s.is_absent
                        ? <window.AppUI.StatusBadge tone="amber" icon={false}>Absent</window.AppUI.StatusBadge>
                        : (s.total ?? <span className="muted" style={{ fontWeight: 400 }}>not entered</span>)}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </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 AppApi.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 AppApi, 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.AppApi.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.AppApi.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("app.exams.reportDate") || ""; } catch { return ""; }
  });
  const setReportDate = (v) => {
    setReportDateRaw(v);
    try { v ? localStorage.setItem("app.exams.reportDate", v) : localStorage.removeItem("app.exams.reportDate"); } catch {}
  };

  const load = React.useCallback(async (y) => {
    setErr(null);
    try {
      const d = await window.AppApi.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="cx-card"><div className="ui-alert red">{err}</div></div>
  );
  if (!data) return <div className="cx-card"><div className="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: 20 }}>
      <div className="cx-card">
          <div className="cx-card-head" style={{ flexWrap: "wrap" }}>
            <div style={{ minWidth: 0, flex: "1 1 320px" }}>
              <div className="cx-card-title">Progress reports</div>
              <div className="cx-card-sub">
                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 className="field">
                <div className="label" style={{ marginBottom: 6 }}>Session</div>
                <select className="select" 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 className="field">
                <div className="label" style={{ marginBottom: 6 }}>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 className="ui-alert amber" style={{ marginBottom: 14 }}>
              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 className="ui-list">
            {EXM_ROWS.filter((r) => r.key).map((r) => (
              <window.AppUI.ListRow key={r.key}
                title={r.label}
                sub={mapped[r.key] ? mapped[r.key].name : "Not set — this row prints blank"}
                status={mapped[r.key]
                  ? <ExmPill tint={EXM_STATUS_TINT[mapped[r.key].status] || EXM_STATUS_TINT.draft} solid />
                  : <window.AppUI.StatusBadge tone="grey" icon={false}>Blank</window.AppUI.StatusBadge>}
              />
            ))}
          </div>
          {missing.length > 0 && (
            <div className="muted" style={{ fontSize: 12.5, marginTop: 10 }}>
              Set a row on an exam from its own page (Examinations → open the exam).
            </div>
          )}
          {err && <div className="ui-alert red" style={{ marginTop: 12 }}>{err}</div>}
      </div>

      <ExmSignature />

      {(data.classes || []).length === 0 && (
        <window.AppUI.EmptyState card icon="report" title="No classes yet"
          hint="No class has papers in this session's marksheet exams yet."/>
      )}

      {(data.classes || []).map((c) => (
        <div key={c.id} className="cx-card">
            <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
              <window.AppUI.Avatar name={String(c.label)} size="lg"/>
              <div style={{ flex: 1, minWidth: 180 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                  <span className="cx-card-title">Class {c.label}</span>
                  {c.submitted < c.papers
                    ? <window.AppUI.StatusBadge tone="amber" icon="alertCircle">{c.papers - c.submitted} pending</window.AppUI.StatusBadge>
                    : <window.AppUI.StatusBadge tone="green" icon="checkCircle">All in</window.AppUI.StatusBadge>}
                </div>
                <div className="muted" style={{ fontSize: 12.5, marginTop: 3 }}>
                  {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"
                onClick={() => setOpenCls(openCls === c.id ? null : c.id)}>
                <ExmIcon name={openCls === c.id ? "chevronDown" : "users"} size={15}/>
                {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)}>
                <ExmIcon name="printer" size={15}/>{busy === c.id ? "Generating…" : `Generate all ${c.students}`}
              </button>
            </div>

            {c.submitted < c.papers && (
              <div style={{ fontSize: 12.5, color: "var(--amber)", marginTop: 12 }}>
                {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>
  );
};

// ── 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.AppApi.get("/admin/marksheet/signature?info=1");
      setInfo(i);
      if (i.present) {
        const { blobUrl } = await window.AppApi.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.AppApi.getToken();
      const r = await fetch(window.AppApi.base + "/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.AppApi.del("/admin/marksheet/signature"); await refresh(); }
    catch (e) { setErr(e.message || "Could not remove it"); }
    finally { setBusy(false); }
  };

  return (
    <div className="cx-card">
      <div style={{ display: "flex", gap: 18, alignItems: "center", flexWrap: "wrap" }}>
        <div style={{
          width: 190, height: 74, borderRadius: "var(--radius-tile)", flexShrink: 0,
          border: "1px dashed var(--line-strong)", background: "var(--bg-1)",
          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: 12.5 }}>No signature yet</span>}
        </div>
        <div style={{ flex: 1, minWidth: 220 }}>
          <div className="cx-card-title">Headmaster's signature</div>
          <div className="cx-card-sub">
            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: 12.5, marginTop: 6, display: "flex", alignItems: "center", gap: 6 }}>
              <ExmIcon name="clock" size={13}/>Updated {exmDate(info.updated_at)}
            </div>
          )}
          {err && <div className="ui-alert red" style={{ marginTop: 8 }}>{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()}>
            <ExmIcon name="upload" size={15}/>{busy ? "Saving…" : info?.present ? "Replace PNG" : "Upload PNG"}
          </button>
          {info?.present && (
            <button className="btn ghost" disabled={busy} onClick={remove} style={{ color: "var(--red)" }}>
              <ExmIcon name="trash" size={15}/>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.AppApi
      .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: 14, fontSize: 13 }}>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: 16, borderTop: "1px solid var(--line)", paddingTop: 16 }}>
      {roster.warnings?.length > 0 && (
        <div className="ui-alert amber" style={{ marginBottom: 12 }}>
          <ul style={{ margin: "0 0 0 16px", padding: 0 }}>
            {roster.warnings.map((w, i) => <li key={i}>{w}</li>)}
          </ul>
        </div>
      )}
      <div className="ui-search" style={{ width: "100%", maxWidth: 300, marginBottom: 12 }}>
        <ExmIcon name="search" size={14}/>
        <input className="input" style={{ width: "100%" }}
          placeholder="Find a name or roll number" value={q}
          onChange={(e) => setQ(e.target.value)} />
      </div>
      <div className="cx-tablecard"><div className="cx-tablescroll" style={{ maxHeight: 360, overflowY: "auto" }}>
        <table className="cx-table">
          <thead>
            <tr>
              <th style={{ width: 80 }}>Roll</th>
              <th>Name</th>
              <th className="num" style={{ width: 110 }}>Annual</th>
              <th style={{ width: 140 }}>Grade</th>
              <th style={{ width: 120 }}></th>
            </tr>
          </thead>
          <tbody>
            {rows.map((s) => {
              const key = `${classId}:${s.student_id}`;
              return (
                <tr key={s.student_id}>
                  <td className="mono" style={{ color: "var(--ink-3)" }}>{s.roll_no}</td>
                  <td style={{ color: "var(--ink-0)", fontWeight: 500 }}>{s.name}</td>
                  <td className="num">
                    {s.annual.pct == null
                      ? <span className="muted">no marks</span>
                      : `${s.annual.pct}%`}
                  </td>
                  <td>
                    {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={{ marginLeft: 6 }}><window.AppUI.StatusBadge tone="red" icon={false}>Not promoted</window.AppUI.StatusBadge></span>
                    )}
                  </td>
                  <td className="actions" style={{ textAlign: "right" }}>
                    <button className="btn sm" 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)}>
                      <ExmIcon name="printer" size={13}/>{busy === key ? "…" : "Generate"}
                    </button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
        {rows.length === 0 && (
          <div className="muted" style={{ fontSize: 13, padding: 16, textAlign: "center" }}>
            Nobody matches “{q}”.
          </div>
        )}
      </div></div>
    </div>
  );
};

window.ExamsScreen = ExamsScreen;
