/* ==================================================================
   Admin → Students
   ------------------------------------------------------------------
   CRUD over the `students` table. Mirrors the Teachers screen — the
   class+section filter on the left, a roster grid, and a right-hand
   detail panel for edit / move section / issue or reset portal login.
   ================================================================== */

const { Icon: StIcon } = window.AppUI;

const StudentsScreen = () => {
  const [students, setStudents] = React.useState([]);
  const [classSections, setClassSections] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [err, setErr] = React.useState(null);
  const [filterCs, setFilterCs] = React.useState("");
  // Residential-type filter ("", "boarder", "day_scholar"). Applied client-
  // side over the already-fetched roster so it's instant and doesn't add a
  // round-trip per change.
  const [filterType, setFilterType] = React.useState("");
  const [search, setSearch] = React.useState("");
  const [selectedId, setSelectedId] = React.useState(null);
  const [credentialFlash, setCredentialFlash] = React.useState(null);

  const loadStudents = React.useCallback(async () => {
    setLoading(true); setErr(null);
    try {
      const qs = filterCs ? `?class_section_id=${encodeURIComponent(filterCs)}` : "";
      setStudents(await window.AppApi.get(`/admin/students${qs}`));
    } catch (e) { setErr(String(e.message || e)); }
    finally { setLoading(false); }
  }, [filterCs]);

  React.useEffect(() => { loadStudents(); }, [loadStudents]);
  React.useEffect(() => {
    window.AppApi.get("/admin/class-sections").then(setClassSections).catch(() => {});
  }, []);

  const filtered = students.filter(s => {
    if (filterType && s.student_type !== filterType) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return (s.name || "").toLowerCase().includes(q)
        || (s.roll_no || "").toLowerCase().includes(q)
        || (s.email || "").toLowerCase().includes(q)
        || (s.username || "").toLowerCase().includes(q)
        // Searchable by the finance-system id: the office reads it off a
        // receipt or a Ledger screen and needs to find the child here.
        || (s.ledger_student_uid || "").toLowerCase().includes(q);
  });

  const selected = selectedId ? students.find(s => s.id === selectedId) : null;

  // No height:100% / overflow:hidden on the split any more. The app shell is
  // min-height:100vh, so that height never resolved and the pane simply grew
  // with the page — and `overflow: hidden` on an ancestor silently disables
  // position:sticky on the detail pane inside it.
  return (
    <div className={`cx-roster-split${selected ? " has-detail" : ""}`}>
      <div className="cx-page" style={{ minWidth: 0, width: "100%" }}>
        <window.AppUI.PageHeader
          eyebrow="Admin · roster"
          title="Students"
          meta={["The roster mirrors the Ledger — students are admitted, corrected and discontinued there, and appear here on the next sync. Portal logins and everything in a student's record beyond these fields are managed here."]}
          actions={
            /* No "Add student" here by design — admissions happen in the
               Ledger so the child gets a fee record and a permanent id. */
            <a className="btn" href={`${window.BRAND.links.ledger}/students/new`} target="_blank" rel="noreferrer"
               title="Students are admitted in the Ledger; they appear here after the next roster sync">
              <StIcon name="arrowUpRight" size={15}/>Admit in Ledger
            </a>
          }
        />

        {credentialFlash && (
          <div className="ui-alert green" style={{ marginBottom: 20, display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
            <div style={{ display: "flex", gap: 10, minWidth: 0 }}>
              <StIcon name="key" size={16} style={{ marginTop: 2, flexShrink: 0 }}/>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontWeight: 600, marginBottom: 4 }}>Portal credentials — copy now, won't be shown again</div>
                <div className="mono" style={{ fontSize: 13, color: "var(--ink-0)" }}>
                  Class: <b>{credentialFlash.class_label}</b>
                  &nbsp;&nbsp;·&nbsp;&nbsp;
                  Roll: <b>{credentialFlash.roll_no}</b>
                  &nbsp;&nbsp;·&nbsp;&nbsp;
                  Password: <b>{credentialFlash.temp_password}</b>
                </div>
              </div>
            </div>
            <window.AppUI.IconButton icon="x" label="Dismiss" className="sm" onClick={() => setCredentialFlash(null)}/>
          </div>
        )}

        <div className="cx-tablecard">
          <div className="cx-tablecard-head" style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
            <span className="cx-tablecard-title">{students.length} {students.length === 1 ? "student" : "students"}</span>
            <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", flex: 1, justifyContent: "flex-end" }}>
              <select className="select" value={filterCs} onChange={e => setFilterCs(e.target.value)} aria-label="Class"
                style={{ width: "auto", minWidth: 150, flex: "0 0 auto" }}>
                <option value="">All classes</option>
                {classSections.map(cs => (
                  <option key={cs.id} value={cs.id}>{cs.label} · {cs.academic_year}</option>
                ))}
              </select>
              <select className="select" value={filterType} onChange={e => setFilterType(e.target.value)}
                title="Filter by residential type" aria-label="Residential type"
                style={{ width: "auto", minWidth: 130, flex: "0 0 auto" }}>
                <option value="">All types</option>
                <option value="boarder">Boarders</option>
                <option value="day_scholar">Day Scholars</option>
              </select>
              <div className="ui-search" style={{ flex: "1 1 200px", maxWidth: 320 }}>
                <StIcon name="search" size={14}/>
                <input className="input" placeholder="Search name, roll, email…" value={search} onChange={e => setSearch(e.target.value)}/>
              </div>
            </div>
          </div>

          {!loading && filtered.length > 0 && window.matchMedia("(min-width: 761px)").matches && (
            <div className="cx-roster-row ui-section-label" style={{ padding: "10px 20px", gap: 12, borderBottom: "1px solid var(--line)", margin: 0 }}>
              <span>Roll</span><span>Name</span><span>Class</span><span>Type</span><span>Login</span><span style={{ textAlign: "right" }}>Activity</span>
            </div>
          )}

          <div>
            {loading && <div style={{ padding: 28, color: "var(--ink-3)", fontSize: 13 }}>Loading…</div>}
            {err && <div className="ui-alert red" style={{ margin: 16 }}>{err}</div>}
            {!loading && filtered.length === 0 && (
              <window.AppUI.EmptyState compact icon="student"
                title={students.length === 0 ? "No students yet" : "No matches"}
                hint={students.length === 0 ? "Admit students in the Ledger; they appear here after the next sync." : null}/>
            )}
            {filtered.map(s => (
              <div key={s.id}
                onClick={() => setSelectedId(s.id)}
                className="cx-roster-row ui-row clickable"
                style={{
                  display: "grid", padding: "11px 20px", gap: 12,
                  background: selectedId === s.id ? "var(--surface-active)" : undefined,
                }}>
                <span className="ui-id">{s.roll_no}</span>
                <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
                  <window.AppUI.Avatar name={s.name} size="sm"/>
                  <div style={{ minWidth: 0 }}>
                    <div className="ui-row-title" style={{ display: "flex", alignItems: "center", gap: "2px 6px", flexWrap: "wrap", whiteSpace: "normal" }}>
                      <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{s.name}</span>
                      {s.username && !s.password_used_at && <window.AppUI.StatusBadge tone="grey">Never used</window.AppUI.StatusBadge>}
                      {/* Only the MISSING case is loud. A linked student is the norm and
                          needs no decoration; an unlinked one means their parent's fee
                          screen cannot resolve, which is invisible everywhere else. */}
                      {!s.ledger_student_uid && <window.AppUI.StatusBadge tone="amber" title="No link to the finance system — this student's fees cannot be shown to their parent">No fee link</window.AppUI.StatusBadge>}
                    </div>
                    <div className="ui-row-sub">{s.email || "—"}</div>
                  </div>
                </div>
                <div style={{ fontSize: 13, color: "var(--ink-1)" }}>{s.class_label}</div>
                <StudentTypePill type={s.student_type}/>
                <div className="ui-id" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{s.username || <span style={{ color: "var(--ink-3)", fontFamily: "inherit" }}>no login</span>}</div>
                <div className="muted tabular" style={{ fontSize: 13, textAlign: "right" }}>
                  {s.attempt_count + s.sheet_count > 0 ? `${s.attempt_count + s.sheet_count} acts` : "—"}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {selected && (
        <StudentDetail
          key={selected.id}
          studentSummary={selected}
          classSections={classSections}
          onClose={() => setSelectedId(null)}
          onChanged={async (flashCreds) => { if (flashCreds) setCredentialFlash(flashCreds); await loadStudents(); }}
        />
      )}
    </div>
  );
};

/* ---------- detail panel ---------- */

// A Ledger-owned value, rendered as text. Deliberately not a disabled input:
// a greyed-out box still reads as "editable, just not right now".
const ReadOnly = ({ value, mono }) => (
  <div style={{
    fontSize: mono ? 13 : 13.5, color: value ? "var(--ink-0)" : "var(--ink-3)",
    fontFamily: mono ? "var(--font-mono)" : "inherit", overflowWrap: "anywhere",
  }}>{value || "—"}</div>
);

const REQUESTABLE = [
  { key: "name", label: "Name" },
  { key: "roll_no", label: "Roll no" },
  { key: "email", label: "Email" },
  { key: "class_section_id", label: "Class / section" },
  { key: "student_type", label: "Boarder / day scholar" },
  { key: "avails_transport", label: "Avails transport" },
];

// Asks the office to change something in the Ledger. Resolving the request does
// not write here — the office edits the Ledger and the roster sync brings it
// back, so these fields only ever change by one route.
const EditRequest = ({ student, onDone }) => {
  const [field, setField] = React.useState("name");
  const [value, setValue] = React.useState("");
  const [reason, setReason] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [sent, setSent] = React.useState(false);

  const submit = async () => {
    setBusy(true); setErr(null);
    try {
      await window.AppApi.post(`/admin/students/${student.id}/edit-request`, {
        field, requested_value: value.trim(), reason: reason.trim() || null,
      });
      setSent(true);
      setTimeout(onDone, 1600);
    } catch (e) { setErr(String(e.message || e)); }
    finally { setBusy(false); }
  };

  if (sent) return (
    <div className="ui-alert green" style={{ marginTop: 12 }}>
      Sent to the office. It will appear here once they update the Ledger.
    </div>
  );

  return (
    <div className="cx-tile" style={{ marginTop: 12, display: "grid", gap: 8 }}>
      <FormRow label="Field">
        <select className="select" value={field} onChange={e => setField(e.target.value)} style={{ width: "100%" }}>
          {REQUESTABLE.map(f => <option key={f.key} value={f.key}>{f.label}</option>)}
        </select>
      </FormRow>
      <FormRow label="Should be">
        <input className="input" value={value} onChange={e => setValue(e.target.value)}
               placeholder={field === "class_section_id" ? "e.g. VI-B" : "correct value"} style={{ width: "100%" }}/>
      </FormRow>
      <FormRow label="Why">
        <input className="input" value={reason} onChange={e => setReason(e.target.value)}
               placeholder="optional — helps the office check" style={{ width: "100%" }}/>
      </FormRow>
      {err && <div className="ui-alert red">{err}</div>}
      <div style={{ display: "flex", justifyContent: "flex-end", gap: 6, marginTop: 4 }}>
        <button className="btn sm" onClick={onDone}>Cancel</button>
        <button className="btn sm primary" disabled={busy || !value.trim()} onClick={submit}>
          <StIcon name="mail" size={14}/>{busy ? "Sending…" : "Send to office"}
        </button>
      </div>
    </div>
  );
};

const StudentDetail = ({ studentSummary, classSections, onClose, onChanged }) => {
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [showRequest, setShowRequest] = React.useState(false);
  // Issued set-password link. Shown once — only its hash is stored server-side.
  const [pwLink, setPwLink] = React.useState(null);
  const [copied, setCopied] = React.useState(false);

  React.useEffect(() => { setShowRequest(false); setPwLink(null); }, [studentSummary.id]);

  const issueLogin = async () => {
    setBusy(true); setErr(null);
    try {
      const out = await window.AppApi.post(`/admin/students/${studentSummary.id}/credentials`, {});
      await onChanged({
        username: out.username, temp_password: out.temp_password,
        class_label: studentSummary.class_label, roll_no: studentSummary.roll_no,
      });
    } catch (e) { setErr(String(e.message || e)); }
    finally { setBusy(false); }
  };

  const createPasswordLink = async () => {
    setBusy(true); setErr(null); setCopied(false);
    try {
      const out = await window.AppApi.post(`/admin/students/${studentSummary.id}/password-link`, {});
      setPwLink(out);
      // A first link also creates the login, so the roster needs the username.
      if (!studentSummary.username) await onChanged();
    } catch (e) { setErr(String(e.message || e)); }
    finally { setBusy(false); }
  };

  const copyPasswordLink = async () => {
    try {
      await navigator.clipboard.writeText(pwLink.url);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch { /* clipboard blocked — the input below is selectable as a fallback */ }
  };

  const removeLogin = async () => {
    if (!window.confirm("Disable portal login for this student?")) return;
    setBusy(true); setErr(null);
    try {
      await window.AppApi.del(`/admin/students/${studentSummary.id}/credentials`);
      await onChanged();
    } catch (e) { setErr(String(e.message || e)); }
    finally { setBusy(false); }
  };

  // While the pane is a bottom sheet the page behind it must not scroll —
  // otherwise a drag on the sheet's own content scrolls the 881-row roster
  // underneath and the sheet appears to drift. Guarded to the phone layout,
  // since on desktop the pane is just a column and the page should scroll.
  useEffect(() => {
    const sheet = window.matchMedia("(max-width: 1000px)");
    if (!sheet.matches) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener("keydown", onKey);
    };
  }, [onClose]);

  return (
    <>
    {/* Backdrop — only painted on the phone layout, where the pane is a
        bottom sheet rather than a column. */}
    <div className="cx-roster-scrim" onClick={onClose} aria-hidden="true"/>
    <aside className="cx-roster-detail">
      {/* Grab handle: the affordance that says "this sheet closes downward".
          Hidden on desktop, where the pane is a column with a × button. */}
      <div className="cx-sheet-grip" aria-hidden="true"/>
      <div className="cx-roster-detail-head" style={{ padding: "18px 20px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "flex-start", gap: 12 }}>
        <window.AppUI.Avatar name={studentSummary.name} size="lg"/>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ color: "var(--ink-0)", fontSize: 16, fontWeight: 600, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
            {studentSummary.name}
            <StudentTypePill type={studentSummary.student_type}/>
          </div>
          <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>{studentSummary.class_label} · <span className="mono">{studentSummary.roll_no}</span></div>
          {/* The finance-system identity. Read-only here on purpose: the Ledger
              owns it, and a value typed in this panel would be a guess that
              silently points a family's fees at the wrong child. */}
          <div className="mono muted" style={{ fontSize: 11.5, marginTop: 4 }}
               title={studentSummary.ledger_student_uid
                 ? "Permanent student ID in the finance system — set at admission, unchanged by promotions"
                 : "Not linked to the finance system. Fees cannot be shown to this student's parent."}>
            {studentSummary.ledger_student_uid
              ? <>Ledger: {studentSummary.ledger_student_uid}</>
              : <span style={{ color: "var(--amber)" }}>Ledger: not linked</span>}
          </div>
        </div>
        <window.AppUI.IconButton icon="x" label="Close" onClick={onClose}/>
      </div>

      {err && <div className="ui-alert red" style={{ margin: "14px 20px 0" }}>{err}</div>}

      {/* These fields belong to the Ledger. Shown as values, not inputs — an
          editable box that silently reverts overnight is worse than no box,
          because the clerk believes the correction stuck. */}
      <section style={{ padding: "18px 20px", borderBottom: "1px solid var(--line)" }}>
        <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 10, marginBottom: 10 }}>
          <div className="ui-section-label" style={{ margin: 0 }}>Profile</div>
          <span className="muted" style={{ fontSize: 11.5 }}>from the Ledger · read-only</span>
        </div>
        <FormRow label="Name"><ReadOnly value={studentSummary.name}/></FormRow>
        <FormRow label="Roll no"><ReadOnly value={studentSummary.roll_no} mono/></FormRow>
        <FormRow label="Email"><ReadOnly value={studentSummary.email}/></FormRow>
        <FormRow label="Class"><ReadOnly value={studentSummary.class_label}/></FormRow>
        <FormRow label="Type"><ReadOnly value={studentSummary.student_type === "day_scholar" ? "Day Scholar" : "Boarder"}/></FormRow>
        <FormRow label="Transport"><ReadOnly value={studentSummary.avails_transport ? "Avails transport" : "—"}/></FormRow>
        {/* The gate card. Registered in the Ledger like every other field
            here — the roster sync carries the UID across. */}
        <FormRow label="Gate card">
          <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
            {studentSummary.rfid_card_uid
              ? <span className="mono" style={{ fontSize: 13, color: "var(--ink-0)" }}>{studentSummary.rfid_card_uid}</span>
              : <window.AppUI.StatusBadge tone="amber">No gate card</window.AppUI.StatusBadge>}
            {studentSummary.ledger_student_uid && (
              <a className="btn xs ghost"
                 href={`${window.BRAND.links.ledger}/students/uid/${studentSummary.ledger_student_uid}`}
                 target="_blank" rel="noreferrer"
                 title="Cards are registered on the student's edit screen in the Ledger; the next roster sync brings the UID here">
                Register in Ledger<StIcon name="arrowUpRight" size={12}/>
              </a>
            )}
          </div>
        </FormRow>
        <p className="muted" style={{ fontSize: 12.5, margin: "12px 0 0", lineHeight: 1.5 }}>
          Students are admitted and corrected in the Ledger; this roster reflects it.
          Spotted something wrong?
        </p>
        <button className="btn sm" style={{ marginTop: 8 }}
          onClick={() => setShowRequest(v => !v)}><StIcon name="penLine" size={14}/>Request a correction</button>
        {showRequest && <EditRequest student={studentSummary} onDone={() => setShowRequest(false)}/>}
      </section>

      <section style={{ padding: "18px 20px", borderBottom: "1px solid var(--line)" }}>
        <div className="ui-section-label" style={{ marginBottom: 10 }}>Portal login</div>
        <FormRow label="Username">
          <span className="ui-id">{studentSummary.username || <span className="muted" style={{ fontFamily: "inherit" }}>no login</span>}</span>
        </FormRow>
        {studentSummary.username && (
          <FormRow label="Last login">
            {studentSummary.password_used_at
              ? <span style={{ fontSize: 13.5, color: "var(--ink-0)" }} className="tabular">{new Date(studentSummary.password_used_at).toLocaleString()}</span>
              : <window.AppUI.StatusBadge tone="grey">Never used</window.AppUI.StatusBadge>
            }
          </FormRow>
        )}
        <div style={{ display: "flex", gap: 6, marginTop: 14, flexWrap: "wrap" }}>
          <button className="btn sm" disabled={busy} onClick={createPasswordLink}
            title="A one-time link the student opens to choose their own password">
            <StIcon name="mail" size={14}/>Password link
          </button>
          <button className="btn sm" disabled={busy} onClick={issueLogin}>
            <StIcon name="key" size={14}/>{studentSummary.username ? "Issue new password" : "Create login"}
          </button>
          {studentSummary.username && (
            <button className="btn sm danger" disabled={busy} onClick={removeLogin}><StIcon name="ban" size={14}/>Disable login</button>
          )}
        </div>

        {pwLink && (
          <div className="ui-alert green" style={{ marginTop: 14 }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, marginBottom: 8 }}>
              <b>Set-password link — copy now, shown once</b>
              <window.AppUI.IconButton icon="x" label="Dismiss" className="sm" onClick={() => setPwLink(null)}/>
            </div>
            <div style={{ display: "flex", gap: 6 }}>
              {/* readOnly input rather than plain text: it stays selectable
                  when the clipboard API is unavailable. */}
              <input className="input mono" readOnly value={pwLink.url}
                onFocus={e => e.target.select()}
                style={{ flex: 1, minWidth: 0, fontSize: 12 }}/>
              <button className="btn" onClick={copyPasswordLink}>
                <StIcon name={copied ? "check" : "copy"} size={14}/>{copied ? "Copied" : "Copy"}
              </button>
            </div>
            <div style={{ fontSize: 12, marginTop: 8, lineHeight: 1.5, color: "var(--ink-2)" }}>
              {studentSummary.name} opens it, chooses a password, and is signed straight in;
              after that they sign in with their class, roll number and that password. Works once, expires in {pwLink.expires_in_hours} hours;
              creating another link cancels this one. Their current password keeps working until the link is used.
            </div>
          </div>
        )}
      </section>

      <section style={{ padding: "18px 20px" }}>
        <div className="ui-section-label" style={{ marginBottom: 4 }}>Activity</div>
        <window.AppUI.StatRow flush align="left" items={[
          { value: String(studentSummary.sheet_count ?? 0), label: "sheets" },
          { value: String(studentSummary.attempt_count ?? 0), label: "attempts" },
        ]}/>
        {/* No delete. A departure is recorded in the Ledger, where the fees,
            deposit and refund are settled; the record here is kept because it
            holds this child's attempts and results. */}
        <p className="muted" style={{ fontSize: 12.5, margin: "14px 0 0", lineHeight: 1.5 }}>
          Left the school? Mark them discontinued in the Ledger — their results stay here.
        </p>
      </section>
    </aside>
    </>
  );
};

const FormRow = ({ label, children }) => (
  <div style={{ display: "grid", gridTemplateColumns: "96px minmax(0, 1fr)", gap: 12, alignItems: "center", padding: "7px 0", fontSize: 13.5, borderBottom: "1px solid var(--line-soft)" }}>
    <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>{label}</span>
    <div style={{ minWidth: 0 }}>{children}</div>
  </div>
);

// Residential-type badge for the roster row. Subtle background tint keeps the
// list scannable without drawing attention away from name/class. Defaults to
// boarder if the field is unset — the migration backfilled every existing row
// to boarder so the null-coalesce here is belt-and-braces.
const StudentTypePill = ({ type }) => {
  const isDay = type === "day_scholar";
  return (
    <window.AppUI.StatusBadge tone={isDay ? "blue" : "grey"} className="st-type">
      {isDay ? "Day" : "Boarder"}
    </window.AppUI.StatusBadge>
  );
};

/* ---------- add modal ---------- */

