/* ADMIN — in-app admin dashboard (Phase 3).

   No-build Babel JSX, brand-consistent with the suite (dark theme + gold).
   AdminRoot injects ChairsideStyle; AdminApp fetches /api/me, gates visibility
   (super-admin / practice admin), and wires to the /api/admin/* actions
   (the single dynamic [action] route from Phase 1/2). Confirms destructive
   actions and retains the legacy Cloudflare resync only during migration.
   No new serverless functions — this is static jsx. */

const { useState, useEffect, useCallback } = React;

// Mirrors PRACTICE_ROLES in api/_lib/identity-shared.ts, with friendly labels.
const ROLE_OPTIONS = [
  { value: "TC", label: "Treatment Coordinator" },
  { value: "FrontDesk", label: "Front Desk" },
  { value: "Hygienist", label: "Hygienist" },
  { value: "Doctor", label: "Doctor" },
  { value: "Assistant", label: "Dental Assistant" },
  { value: "OfficeManager", label: "Office Manager" },
  { value: "Owner", label: "Owner" },
];
const ROLE_LABEL = Object.fromEntries(ROLE_OPTIONS.map((r) => [r.value, r.label]));

// Course SKUs — mirrors ALL_COURSE_KEYS in api/_lib/identity-shared.ts, with
// friendly labels for the plan editor. An org with an empty stored list is
// fail-open (owns everything); the editor treats "all four on" as that state.
const COURSE_OPTIONS = [
  { key: "praxia", label: "Praxia" },
  { key: "eq", label: "Praxia EQ" },
  { key: "chairside", label: "Chairside" },
  { key: "lead", label: "Lead" },
];
const ALL_COURSE_KEYS_UI = COURSE_OPTIONS.map((c) => c.key);

// ─── fetch helpers ───────────────────────────────────────────────────────
async function safeJson(res) {
  try { return await res.json(); } catch (e) { return null; }
}
async function apiGet(path) {
  const res = await fetch(path, { credentials: "same-origin" });
  return { ok: res.ok, status: res.status, data: await safeJson(res) };
}
async function apiPost(path, body) {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "same-origin",
    body: JSON.stringify(body || {}),
  });
  return { ok: res.ok, status: res.status, data: await safeJson(res) };
}

function fmtDate(iso) {
  if (!iso) return "—";
  try { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); }
  catch (e) { return "—"; }
}

// Normalize any admin response into a user-facing notice. Partial success
// (Redis ok, Cloudflare not synced) surfaces as a "warn", not an error.
function noticeFromResult(okText, res) {
  if (!res.ok) {
    const msg = (res.data && res.data.error) || "Something went wrong. Try again.";
    return { kind: "error", text: msg };
  }
  if (res.data && res.data.cloudflareSynced === false && res.data.message) {
    return { kind: "warn", text: res.data.message };
  }
  return { kind: "ok", text: okText };
}

// ─── small presentational pieces ─────────────────────────────────────────
function StatusBadge({ status, tokens, accent }) {
  const map = {
    active: { label: "Active", fg: accent.c, bg: accent.soft },
    deactivated: { label: "Deactivated", fg: "#E08A8A", bg: "rgba(224,138,138,0.14)" },
    pending: { label: "Pending", fg: tokens.mute, bg: tokens.surface2 },
    ready: { label: "Ready", fg: accent.c, bg: accent.soft },
    needs_attention: { label: "Needs attention", fg: "#E0C07A", bg: "rgba(224,192,122,0.12)" },
  };
  const s = map[status] || map.pending;
  return (
    <span className="mono" style={{
      display: "inline-block", padding: "3px 9px", borderRadius: 999,
      fontSize: 10, letterSpacing: "0.1em", textTransform: "uppercase",
      color: s.fg, background: s.bg, border: `1px solid ${s.fg}33`,
    }}>{s.label}</span>
  );
}

function Notice({ notice, onClose, tokens }) {
  if (!notice) return null;
  const palette = {
    ok: { fg: "#9CC79C", bg: "rgba(120,180,120,0.12)", bd: "rgba(120,180,120,0.4)" },
    warn: { fg: "#E0C07A", bg: "rgba(224,192,122,0.12)", bd: "rgba(224,192,122,0.45)" },
    error: { fg: "#E08A8A", bg: "rgba(224,138,138,0.12)", bd: "rgba(224,138,138,0.45)" },
  };
  const p = palette[notice.kind] || palette.ok;
  return (
    <div role="status" style={{
      margin: "0 0 20px", padding: "12px 16px", borderRadius: 12,
      background: p.bg, border: `1px solid ${p.bd}`, color: p.fg,
      display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12, fontSize: 14, lineHeight: 1.5,
    }}>
      <span>{notice.text}</span>
      <button onClick={onClose} aria-label="Dismiss" style={{
        background: "none", border: "none", color: p.fg, cursor: "pointer", fontSize: 16, lineHeight: 1, padding: 0,
      }}>×</button>
    </div>
  );
}

function Spinner({ tokens }) {
  return <div className="mono" style={{ color: tokens.mute, fontSize: 12, letterSpacing: "0.16em", padding: "40px 0" }}>LOADING…</div>;
}

// ─── Plan editor (entitlements + seats) ─────────────────────────────────────
// Lives on the org detail view. Grants/revokes course access and adjusts the
// seat cap after practice creation — the highest-leverage admin gap. Writes go
// through /api/admin/set-entitlements and /api/admin/set-seats, which re-check
// the admin guard server-side.
//
// Fail-open contract: a server that returns all four courses is treated as
// "default open" (no scoping). Turning every course ON therefore saves an
// empty list — the same as no restriction — rather than an explicit all-four
// list, keeping the stored value canonical.
function PlanEditor({ tokens, accent, isPhone, orgId, initialEntitlements, initialSeats, seatUsage, onSaved, onNotice }) {
  // Empty stored list = owns everything → show all toggles on.
  const startOwned = (initialEntitlements && initialEntitlements.length > 0)
    ? initialEntitlements
    : ALL_COURSE_KEYS_UI;
  const [owned, setOwned] = useState(() => new Set(startOwned));
  const [seats, setSeats] = useState(initialSeats != null ? String(initialSeats) : "");
  const [savingCourses, setSavingCourses] = useState(false);
  const [savingSeats, setSavingSeats] = useState(false);

  const toggle = (key) => {
    setOwned((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key); else next.add(key);
      return next;
    });
  };

  const saveCourses = async () => {
    // All-on → save [] (canonical fail-open). Otherwise the explicit subset.
    const list = owned.size === ALL_COURSE_KEYS_UI.length
      ? []
      : ALL_COURSE_KEYS_UI.filter((k) => owned.has(k));
    setSavingCourses(true);
    const body = { entitlements: list };
    if (orgId) body.orgId = orgId;
    const res = await apiPost("/api/admin/set-entitlements", body);
    onNotice(noticeFromResult("Updated the practice's courses.", res));
    setSavingCourses(false);
    if (res.ok && onSaved) onSaved();
  };

  const saveSeats = async () => {
    const trimmed = seats.trim();
    const body = { seats: trimmed === "" ? null : Number(trimmed) };
    if (orgId) body.orgId = orgId;
    setSavingSeats(true);
    const res = await apiPost("/api/admin/set-seats", body);
    onNotice(noticeFromResult(trimmed === "" ? "Removed the seat cap." : `Set the seat cap to ${trimmed}.`, res));
    setSavingSeats(false);
    if (res.ok && onSaved) onSaved();
  };

  const allOn = owned.size === ALL_COURSE_KEYS_UI.length;
  const usedTotal = seatUsage ? seatUsage.total : null;
  const overCap = seats.trim() !== "" && usedTotal != null && usedTotal > Number(seats.trim());

  return (
    <div className="card" style={{ padding: 20, marginBottom: 22, display: "grid", gap: 18 }}>
      <div>
        <div className="eyebrow" style={{ color: accent.c, marginBottom: 4 }}>Plan</div>
        <div className="mono" style={{ color: tokens.mute, fontSize: 11 }}>
          What this practice can open. All four on = full access.
        </div>
      </div>

      {/* Courses */}
      <div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 12 }}>
          {COURSE_OPTIONS.map((c) => {
            const on = owned.has(c.key);
            return (
              <button key={c.key} onClick={() => toggle(c.key)} type="button" style={{
                padding: "8px 14px", borderRadius: 999, fontSize: 13, cursor: "pointer",
                border: `1px solid ${on ? accent.c : tokens.line}`,
                background: on ? accent.soft : tokens.surface2,
                color: on ? accent.c : tokens.mute, fontWeight: 500,
              }}>
                {on ? "✓ " : ""}{c.label}
              </button>
            );
          })}
        </div>
        {owned.size === 0 && (
          <div className="mono" style={{ color: "#C77A2A", fontSize: 11, marginBottom: 10 }}>
            No courses selected turns the whole catalog back on (fail-open). Pick a subset to scope it.
          </div>
        )}
        <button className="btn accent" style={{ padding: "8px 16px", fontSize: 13 }} onClick={saveCourses} disabled={savingCourses}>
          {savingCourses ? "Saving…" : allOn ? "Save (full access)" : "Save courses"}
        </button>
      </div>

      {/* Seats */}
      <div style={{ borderTop: `1px solid ${tokens.line}`, paddingTop: 16 }}>
        <div className="mono" style={{ color: tokens.mute, fontSize: 11, marginBottom: 8 }}>
          Seat cap{usedTotal != null ? ` — ${usedTotal} in use` : ""}. Blank = unlimited.
        </div>
        <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          <input type="number" min="1" placeholder="Unlimited" value={seats}
            onChange={(e) => setSeats(e.target.value)} style={{ width: isPhone ? "100%" : 140 }} />
          <button className="btn" style={{ padding: "8px 16px", fontSize: 13 }} onClick={saveSeats} disabled={savingSeats}>
            {savingSeats ? "Saving…" : "Save seats"}
          </button>
        </div>
        {overCap && (
          <div className="mono" style={{ color: "#C77A2A", fontSize: 11, marginTop: 8 }}>
            {usedTotal} in use is above this cap — existing members keep access; new invites/reactivations are blocked until usage drops.
          </div>
        )}
      </div>
    </div>
  );
}

function BulkInvitePanel({ tokens, accent, orgId, onNotice, onDone }) {
  const [csv, setCsv] = useState("email,fullName,role\n");
  const [busy, setBusy] = useState(false);
  const rows = csv.split(/\r?\n/).slice(1).map((line) => {
    const [email, fullName, role] = line.split(",").map((value) => (value || "").trim());
    return { email, fullName: fullName || undefined, role: role || undefined };
  }).filter((row) => row.email);
  const submit = async () => {
    if (!rows.length) return;
    setBusy(true);
    const body = { members: rows };
    if (orgId) body.orgId = orgId;
    const res = await apiPost("/api/admin/bulk-invite", body);
    setBusy(false);
    if (!res.ok) return onNotice({ kind: "error", text: (res.data && res.data.error) || "Roster import failed." });
    const failed = res.data.failed || 0;
    onNotice({ kind: failed ? "warn" : "ok", text: `Sent ${res.data.succeeded} invitation${res.data.succeeded === 1 ? "" : "s"}${failed ? `; ${failed} row${failed === 1 ? "" : "s"} need attention` : ""}.` });
    if (!failed) setCsv("email,fullName,role\n");
    onDone();
  };
  return (
    <div className="card" style={{ padding: 20, marginBottom: 22, display: "grid", gap: 12 }}>
      <div>
        <div className="eyebrow" style={{ color: accent.c }}>Import roster</div>
        <p style={{ color: tokens.mute, margin: "6px 0 0", fontSize: 13 }}>One person per line: email, full name, role. Valid roles include Doctor, Hygienist, Assistant, FrontDesk, TC, OfficeManager, and Owner.</p>
      </div>
      <textarea value={csv} onChange={(e) => setCsv(e.target.value)} rows={8} spellCheck="false"
        style={{ width: "100%", resize: "vertical", fontFamily: "monospace", lineHeight: 1.6 }} />
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <button className="btn accent" onClick={submit} disabled={busy || !rows.length}>{busy ? "Sending…" : `Review & invite ${rows.length || ""}`}</button>
        <span className="mono" style={{ color: tokens.mute, fontSize: 11 }}>{rows.length} parsed row{rows.length === 1 ? "" : "s"}</span>
      </div>
    </div>
  );
}

function StructurePanel({ tokens, accent, orgId, data, onNotice, onDone }) {
  const [locationName, setLocationName] = useState("");
  const [teamName, setTeamName] = useState("");
  const [teamLocation, setTeamLocation] = useState("");
  const post = async (action, body, success) => {
    if (orgId) body.orgId = orgId;
    const res = await apiPost(`/api/admin/${action}`, body);
    onNotice(noticeFromResult(success, res));
    if (res.ok) onDone();
  };
  const addLocation = async (e) => {
    e.preventDefault(); if (!locationName.trim()) return;
    await post("save-location", { name: locationName.trim(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }, "Added the location.");
    setLocationName("");
  };
  const addTeam = async (e) => {
    e.preventDefault(); if (!teamName.trim()) return;
    await post("save-team", { name: teamName.trim(), locationId: teamLocation || undefined }, "Added the team.");
    setTeamName("");
  };
  const assign = (row, field, value) => post("set-member-assignments", {
    userId: row.userId,
    locationIds: field === "location" ? (value ? [value] : []) : row.locationIds,
    teamIds: field === "team" ? (value ? [value] : []) : row.teamIds,
  }, `Updated ${row.email}.`);
  return (
    <div className="card" style={{ padding: 20, marginBottom: 22, display: "grid", gap: 22 }}>
      <div><div className="eyebrow" style={{ color: accent.c }}>Locations & teams</div><p style={{ color: tokens.mute, margin: "6px 0 0", fontSize: 13 }}>Organize reporting and course assignments inside this practice.</p></div>
      <form onSubmit={addLocation} style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        <input value={locationName} onChange={(e) => setLocationName(e.target.value)} placeholder="New location name" />
        <button className="btn" type="submit">Add location</button>
      </form>
      {!!data.locations.length && <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>{data.locations.map((location) => <span className="pill" key={location.locationId}>{location.name} <button aria-label={`Delete ${location.name}`} onClick={() => post("delete-location", { locationId: location.locationId }, "Removed the location.")} style={{ background: "none", border: 0, color: "inherit", cursor: "pointer" }}>×</button></span>)}</div>}
      <form onSubmit={addTeam} style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        <input value={teamName} onChange={(e) => setTeamName(e.target.value)} placeholder="New team name" />
        <select value={teamLocation} onChange={(e) => setTeamLocation(e.target.value)}><option value="">All locations</option>{data.locations.map((location) => <option key={location.locationId} value={location.locationId}>{location.name}</option>)}</select>
        <button className="btn" type="submit">Add team</button>
      </form>
      {!!data.teams.length && <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>{data.teams.map((team) => <span className="pill" key={team.teamId}>{team.name} <button aria-label={`Delete ${team.name}`} onClick={() => post("delete-team", { teamId: team.teamId }, "Removed the team.")} style={{ background: "none", border: 0, color: "inherit", cursor: "pointer" }}>×</button></span>)}</div>}
      {(data.locations.length > 0 || data.teams.length > 0) && <div style={{ display: "grid", gap: 8 }}>
        {data.members.filter((row) => row.userId && row.status !== "deactivated").map((row) => <div key={row.userId} style={{ display: "grid", gridTemplateColumns: "minmax(160px,1fr) 1fr 1fr", gap: 8, alignItems: "center" }}>
          <span style={{ fontSize: 13 }}>{row.displayName || row.email}</span>
          <select value={(row.locationIds && row.locationIds[0]) || ""} onChange={(e) => assign(row, "location", e.target.value)}><option value="">No location</option>{data.locations.map((location) => <option key={location.locationId} value={location.locationId}>{location.name}</option>)}</select>
          <select value={(row.teamIds && row.teamIds[0]) || ""} onChange={(e) => assign(row, "team", e.target.value)}><option value="">No team</option>{data.teams.map((team) => <option key={team.teamId} value={team.teamId}>{team.name}</option>)}</select>
        </div>)}
      </div>}
    </div>
  );
}

function AuditPanel({ tokens, orgId, onNotice }) {
  const [records, setRecords] = useState(null);
  useEffect(() => { (async () => {
    const res = await apiGet("/api/admin/audit" + (orgId ? "?orgId=" + encodeURIComponent(orgId) : ""));
    if (res.ok) setRecords(res.data.records || []); else onNotice({ kind: "error", text: (res.data && res.data.error) || "Couldn't load activity." });
  })(); }, [orgId, onNotice]);
  if (!records) return <Spinner tokens={tokens} />;
  return <div className="card" style={{ marginBottom: 22, overflow: "hidden" }}>
    {records.length === 0 && <div style={{ padding: 22, color: tokens.mute }}>No provisioning activity recorded yet.</div>}
    {records.map((record, index) => <div key={record.auditId} style={{ padding: "14px 20px", borderTop: index ? `1px solid ${tokens.line}` : "none" }}>
      <div style={{ fontSize: 13 }}>{record.action.replaceAll(".", " ")} {record.targetEmail ? `· ${record.targetEmail}` : ""}</div>
      <div className="mono" style={{ color: tokens.mute, fontSize: 10, marginTop: 4 }}>{record.actorEmail} · {fmtDate(record.createdAt)}</div>
    </div>)}
  </div>;
}

// ─── Members view ──────────────────────────────────────────────────────────
function MembersView({ tokens, accent, isPhone, me, orgId, onNotice }) {
  // orgId is undefined for a practice admin (server uses their identity org);
  // a super-admin drilling into a practice passes an explicit orgId.
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  const [busyEmail, setBusyEmail] = useState(null);
  const [showAdd, setShowAdd] = useState(false);
  const [showBulk, setShowBulk] = useState(false);
  const [showPlan, setShowPlan] = useState(false);
  const [showStructure, setShowStructure] = useState(false);
  const [showAudit, setShowAudit] = useState(false);
  const [addEmail, setAddEmail] = useState("");
  const [addName, setAddName] = useState("");
  const [addRole, setAddRole] = useState("");
  const [adding, setAdding] = useState(false);

  const path = "/api/admin/members" + (orgId ? "?orgId=" + encodeURIComponent(orgId) : "");

  const reload = useCallback(async () => {
    setLoading(true);
    const res = await apiGet(path);
    if (res.ok) setData(res.data);
    else onNotice({ kind: "error", text: (res.data && res.data.error) || "Couldn't load members." });
    setLoading(false);
  }, [path, onNotice]);

  useEffect(() => { reload(); }, [reload]);

  const doInvite = async (e) => {
    e.preventDefault();
    if (!addEmail.trim()) return;
    setAdding(true);
    const body = { email: addEmail.trim(), role: addRole || undefined, fullName: addName.trim() || undefined };
    if (orgId) body.orgId = orgId;
    const res = await apiPost("/api/admin/invite", body);
    onNotice(noticeFromResult(`Invited ${addEmail.trim()}.`, res));
    setAdding(false);
    if (res.ok) {
      setAddEmail(""); setAddName(""); setAddRole(""); setShowAdd(false);
      reload();
    }
  };

  const doDeactivate = async (row) => {
    if (!window.confirm(`Deactivate ${row.email}? They'll lose access to the app immediately.`)) return;
    setBusyEmail(row.email);
    const res = await apiPost("/api/admin/deactivate", row.userId ? { userId: row.userId } : { email: row.email });
    onNotice(noticeFromResult(`Deactivated ${row.email}.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };

  const doReactivate = async (row) => {
    setBusyEmail(row.email);
    const res = await apiPost("/api/admin/reactivate", row.userId ? { userId: row.userId } : { email: row.email });
    onNotice(noticeFromResult(`Reactivated ${row.email}.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };

  const doSetRole = async (row, role) => {
    if (!row.userId || !role || role === row.role) return;
    setBusyEmail(row.email);
    const res = await apiPost("/api/admin/set-role", { userId: row.userId, role });
    onNotice(noticeFromResult(`Updated ${row.email} to ${ROLE_LABEL[role] || role}.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };

  const orgBody = (body) => orgId ? { ...body, orgId } : body;
  const doInvitationAction = async (row, action) => {
    setBusyEmail(row.email);
    const res = await apiPost(`/api/admin/${action}-invitation`, orgBody({ email: row.email }));
    onNotice(noticeFromResult(action === "resend" ? `Sent a new invitation to ${row.email}.` : `Revoked ${row.email}'s invitation.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };
  const doRemove = async (row) => {
    if (!window.confirm(`Remove ${row.email} from this practice?`)) return;
    setBusyEmail(row.email);
    const res = await apiPost("/api/admin/remove-member", orgBody(row.userId ? { userId: row.userId } : { email: row.email }));
    onNotice(noticeFromResult(`Removed ${row.email}.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };
  const doTransfer = async (row) => {
    if (!window.confirm(`Transfer practice ownership to ${row.email}?`)) return;
    setBusyEmail(row.email);
    const res = await apiPost("/api/admin/transfer-ownership", orgBody({ userId: row.userId }));
    onNotice(noticeFromResult(`Transferred ownership to ${row.email}.`, res));
    setBusyEmail(null);
    if (res.ok) reload();
  };
  const doSyncClerk = async () => {
    const res = await apiPost("/api/admin/sync-clerk", orgBody({}));
    onNotice(res.ok && res.data.status === "ready"
      ? { kind: "ok", text: "Practice is linked to Clerk. Existing signed-in members were synchronized." }
      : { kind: "warn", text: (res.data && (res.data.error || res.data.message)) || "Clerk sync needs attention." });
    if (res.ok) reload();
  };

  if (loading && !data) return <Spinner tokens={tokens} />;
  if (!data) return null;

  const seatLabel = data.seats != null
    ? `${data.seatUsage ? data.seatUsage.total : "?"} / ${data.seats} seats`
    : null;

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap", marginBottom: 18 }}>
        <div>
          <h2 className="display" style={{ fontSize: 28 }}>{data.orgName}</h2>
          <div className="mono" style={{ color: tokens.mute, marginTop: 6 }}>
            {data.members.length} member{data.members.length === 1 ? "" : "s"}{seatLabel ? ` · ${seatLabel}` : ""}
          </div>
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <button className="btn ghost" onClick={() => setShowAudit((s) => !s)}>{showAudit ? "Hide activity" : "Activity"}</button>
          <button className="btn ghost" onClick={() => setShowStructure((s) => !s)}>{showStructure ? "Hide teams" : "Teams"}</button>
          <button className="btn ghost" onClick={() => setShowPlan((s) => !s)}>{showPlan ? "Hide plan" : "Edit plan"}</button>
          <button className="btn ghost" onClick={() => setShowBulk((s) => !s)}>{showBulk ? "Cancel import" : "Import roster"}</button>
          <button className="btn accent" onClick={() => setShowAdd((s) => !s)}>{showAdd ? "Cancel" : "+ Add member"}</button>
        </div>
      </div>

      {showPlan && (
        <PlanEditor
          tokens={tokens} accent={accent} isPhone={isPhone}
          orgId={orgId}
          initialEntitlements={data.entitlements}
          initialSeats={data.seats}
          seatUsage={data.seatUsage}
          onSaved={reload}
          onNotice={onNotice}
        />
      )}

      {showBulk && <BulkInvitePanel tokens={tokens} accent={accent} orgId={orgId} onNotice={onNotice} onDone={reload} />}
      {showStructure && <StructurePanel tokens={tokens} accent={accent} orgId={orgId} data={data} onNotice={onNotice} onDone={reload} />}
      {showAudit && <AuditPanel tokens={tokens} orgId={orgId} onNotice={onNotice} />}

      {showAdd && (
        <form onSubmit={doInvite} className="card" style={{ padding: 20, marginBottom: 22, display: "grid", gap: 12 }}>
          <div style={{ display: "grid", gridTemplateColumns: isPhone ? "1fr" : "1.4fr 1fr 1fr", gap: 12 }}>
            <input type="email" required placeholder="email@practice.com" value={addEmail} onChange={(e) => setAddEmail(e.target.value)} />
            <input type="text" placeholder="Full name (optional)" value={addName} onChange={(e) => setAddName(e.target.value)} />
            <select value={addRole} onChange={(e) => setAddRole(e.target.value)}>
              <option value="">Role (optional)</option>
              {ROLE_OPTIONS.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
            </select>
          </div>
          <div style={{ display: "flex", gap: 10 }}>
            <button type="submit" className="btn accent" disabled={adding}>{adding ? "Adding…" : "Add to practice"}</button>
            <span className="mono" style={{ color: tokens.mute, alignSelf: "center", fontSize: 11 }}>
              {me && me.authProvider === "clerk"
                ? "Sends a Clerk invitation and adds them to the practice roster."
                : "Adds them to Cloudflare access + the roster."}
            </span>
          </div>
        </form>
      )}

      <div className="card" style={{ overflow: "hidden" }}>
        {data.members.length === 0 && (
          <div style={{ padding: 28, color: tokens.soft }}>No members yet. Add the first one above.</div>
        )}
        {data.members.map((row, i) => (
          <div key={row.userId || `pending:${row.email}`} style={{
            display: "grid",
            gridTemplateColumns: isPhone ? "1fr" : "1.6fr 1fr auto",
            gap: isPhone ? 8 : 16, alignItems: "center",
            padding: isPhone ? "16px 18px" : "16px 22px",
            borderTop: i === 0 ? "none" : `1px solid ${tokens.line}`,
            opacity: row.status === "deactivated" ? 0.62 : 1,
          }}>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontWeight: 500, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                {row.displayName || row.email.split("@")[0]}
                {row.isOwner && <span className="pill">Owner</span>}
                <StatusBadge status={row.status} tokens={tokens} accent={accent} />
              </div>
              <div className="mono" style={{ color: tokens.mute, marginTop: 4, fontSize: 11, overflow: "hidden", textOverflow: "ellipsis" }}>
                {row.email} · last seen {fmtDate(row.lastSeenAt)}
              </div>
              {row.invitation && <div className="mono" style={{ color: row.invitation.status === "failed" ? "#E08A8A" : tokens.mute, marginTop: 3, fontSize: 10 }}>
                Invite {row.invitation.status}{row.invitation.expiresAt ? ` · expires ${fmtDate(row.invitation.expiresAt)}` : ""}
              </div>}
            </div>

            <div>
              {row.status === "pending" ? (
                <span style={{ color: tokens.mute, fontSize: 13 }}>{row.role ? ROLE_LABEL[row.role] || row.role : "—"}</span>
              ) : (
                <select
                  value={row.role || ""}
                  disabled={busyEmail === row.email}
                  onChange={(e) => doSetRole(row, e.target.value)}
                  style={{ padding: "8px 10px" }}
                >
                  <option value="">No role</option>
                  {ROLE_OPTIONS.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
                </select>
              )}
            </div>

            <div style={{ display: "flex", gap: 8, justifyContent: isPhone ? "flex-start" : "flex-end" }}>
              {row.status === "pending" && <>
                <button className="btn" style={{ padding: "8px 12px", fontSize: 12 }} disabled={busyEmail === row.email} onClick={() => doInvitationAction(row, "resend")}>Resend</button>
                {row.invitation && row.invitation.url && <button className="btn ghost" style={{ padding: "8px 12px", fontSize: 12 }} onClick={() => navigator.clipboard.writeText(row.invitation.url).then(() => onNotice({ kind: "ok", text: "Copied the invitation link." }))}>Copy link</button>}
                <button className="btn ghost" style={{ padding: "8px 12px", fontSize: 12 }} disabled={busyEmail === row.email} onClick={() => doInvitationAction(row, "revoke")}>Revoke</button>
              </>}
              {row.status === "active" && (
                <button className="btn ghost" style={{ padding: "8px 14px", fontSize: 13, color: "#E08A8A" }}
                  disabled={busyEmail === row.email} onClick={() => doDeactivate(row)}>Deactivate</button>
              )}
              {row.status === "deactivated" && (
                <button className="btn" style={{ padding: "8px 14px", fontSize: 13 }}
                  disabled={busyEmail === row.email} onClick={() => doReactivate(row)}>Reactivate</button>
              )}
              {row.status === "active" && !row.isOwner && <button className="btn ghost" style={{ padding: "8px 12px", fontSize: 12 }} disabled={busyEmail === row.email} onClick={() => doTransfer(row)}>Make owner</button>}
              {!row.isOwner && <button className="btn ghost" style={{ padding: "8px 12px", fontSize: 12, color: "#E08A8A" }} disabled={busyEmail === row.email} onClick={() => doRemove(row)}>Remove</button>}
            </div>
          </div>
        ))}
      </div>
      <div style={{ marginTop: 14, display: "flex", justifyContent: "flex-end" }}>
        <button className="btn ghost" onClick={doSyncClerk}>Repair Clerk sync</button>
      </div>
    </div>
  );
}

// ─── Practices view (super-admin) ────────────────────────────────────────
function PracticesView({ tokens, accent, isPhone, authProvider, onOpenOrg, onNotice }) {
  const [loading, setLoading] = useState(true);
  const [orgs, setOrgs] = useState([]);
  const [showCreate, setShowCreate] = useState(false);
  const [name, setName] = useState("");
  const [ownerEmail, setOwnerEmail] = useState("");
  const [seats, setSeats] = useState("");
  const [courses, setCourses] = useState(() => [...ALL_COURSE_KEYS_UI]);
  const [creating, setCreating] = useState(false);
  const [resyncing, setResyncing] = useState(false);

  const reload = useCallback(async () => {
    setLoading(true);
    const res = await apiGet("/api/admin/orgs");
    if (res.ok && res.data) setOrgs(res.data.orgs || []);
    else onNotice({ kind: "error", text: (res.data && res.data.error) || "Couldn't load practices." });
    setLoading(false);
  }, [onNotice]);

  useEffect(() => { reload(); }, [reload]);

  const doCreate = async (e) => {
    e.preventDefault();
    if (!name.trim() || !ownerEmail.trim()) return;
    setCreating(true);
    const body = { orgName: name.trim(), ownerEmail: ownerEmail.trim() };
    if (seats.trim()) body.seats = Number(seats.trim());
    body.entitlements = courses.length === ALL_COURSE_KEYS_UI.length ? [] : courses;
    const res = await apiPost("/api/admin/create-practice", body);
    onNotice(noticeFromResult(`Created ${name.trim()} and invited ${ownerEmail.trim()} as owner.`, res));
    setCreating(false);
    if (res.ok) { setName(""); setOwnerEmail(""); setSeats(""); setCourses([...ALL_COURSE_KEYS_UI]); setShowCreate(false); reload(); }
  };

  const doResync = async () => {
    if (!window.confirm("Rebuild the Cloudflare allowlist from the roster? This repairs drift between Redis and Cloudflare.")) return;
    setResyncing(true);
    const res = await apiPost("/api/admin/resync-cloudflare", {});
    if (res.ok) {
      onNotice(res.data && res.data.cloudflareSynced === false
        ? { kind: "warn", text: (res.data.message) || "Rebuilt the set; Cloudflare update failed — retry." }
        : { kind: "ok", text: `Resynced ${res.data ? res.data.emailCount : "?"} emails across ${res.data ? res.data.orgsIndexed : "?"} practices.` });
    } else {
      onNotice({ kind: "error", text: (res.data && res.data.error) || "Resync failed." });
    }
    setResyncing(false);
  };

  if (loading) return <Spinner tokens={tokens} />;

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap", marginBottom: 18 }}>
        <div>
          <h2 className="display" style={{ fontSize: 28 }}>Practices</h2>
          <div className="mono" style={{ color: tokens.mute, marginTop: 6 }}>{orgs.length} practice{orgs.length === 1 ? "" : "s"}</div>
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          {authProvider !== "clerk" && (
            <button className="btn ghost" onClick={doResync} disabled={resyncing} title="Repair Redis ↔ Cloudflare drift">
              {resyncing ? "Resyncing…" : "Resync Cloudflare"}
            </button>
          )}
          <button className="btn accent" onClick={() => setShowCreate((s) => !s)}>{showCreate ? "Cancel" : "+ Create practice"}</button>
        </div>
      </div>

      {showCreate && (
        <form onSubmit={doCreate} className="card" style={{ padding: 20, marginBottom: 22, display: "grid", gap: 12 }}>
          <div><div className="eyebrow" style={{ color: accent.c }}>New practice</div><p style={{ color: tokens.mute, margin: "6px 0 2px", fontSize: 13 }}>Create its Clerk workspace, set access, and email the owner.</p></div>
          <div style={{ display: "grid", gridTemplateColumns: isPhone ? "1fr" : "1.5fr 1.5fr 0.7fr", gap: 12 }}>
            <input type="text" required placeholder="Practice name" value={name} onChange={(e) => setName(e.target.value)} />
            <input type="email" required placeholder="Owner email" value={ownerEmail} onChange={(e) => setOwnerEmail(e.target.value)} />
            <input type="number" min="1" placeholder="Seats" value={seats} onChange={(e) => setSeats(e.target.value)} />
          </div>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>{COURSE_OPTIONS.map((course) => {
            const selected = courses.includes(course.key);
            return <button key={course.key} type="button" className={selected ? "chip active" : "chip"} onClick={() => setCourses((current) => selected ? current.filter((key) => key !== course.key) : [...current, course.key])}>{selected ? "✓ " : ""}{course.label}</button>;
          })}</div>
          <div>
            <button type="submit" className="btn accent" disabled={creating}>{creating ? "Creating…" : "Create & invite owner"}</button>
          </div>
        </form>
      )}

      <div className="card" style={{ overflow: "hidden" }}>
        {orgs.length === 0 && <div style={{ padding: 28, color: tokens.soft }}>No practices yet. Create one above.</div>}
        {orgs.map((o, i) => (
          <button key={o.orgId} onClick={() => onOpenOrg(o)} style={{
            width: "100%", textAlign: "left", background: "none", border: "none",
            borderTop: i === 0 ? "none" : `1px solid ${tokens.line}`,
            padding: "18px 22px", cursor: "pointer", color: tokens.text,
            display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16,
          }}>
            <div>
              <div className="display" style={{ fontSize: 19 }}>{o.name}</div>
              <div className="mono" style={{ color: tokens.mute, marginTop: 4, fontSize: 11 }}>
                {o.ownerEmail} · {o.memberCount} active · {o.pendingCount || 0} pending{o.seats != null ? ` · ${o.seats} seats` : ""}
              </div>
            </div>
            <span style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <StatusBadge status={o.provisioningStatus} tokens={tokens} accent={accent} />
              <span style={{ color: accent.c, fontSize: 16 }}>→</span>
            </span>
          </button>
        ))}
      </div>
    </div>
  );
}

// ─── App ────────────────────────────────────────────────────────────────
function AdminApp({ tokens, accent }) {
  const isPhone = (window.useIsPhone || (() => false))();
  const [phase, setPhase] = useState("loading"); // loading | ready | forbidden | offline
  const [me, setMe] = useState(null);
  const [notice, setNotice] = useState(null);
  // view: { name: "practices" } | { name: "members", orgId?, orgName? }
  const [view, setView] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      let res;
      try { res = await fetch("/api/me", { credentials: "same-origin" }); }
      catch (e) { if (!cancelled) setPhase("offline"); return; }

      if (res.status === 401 || res.status === 403 || res.status === 404) {
        if (!cancelled) setPhase("forbidden");
        return;
      }
      if (!res.ok) { if (!cancelled) setPhase("offline"); return; }
      const data = await safeJson(res);
      if (!data) { if (!cancelled) setPhase("offline"); return; }
      if (cancelled) return;

      const isSuper = !!data.isSuperAdmin;
      const isPracticeAdmin = !!data.isOwner || data.role === "Owner" || data.role === "OfficeManager";
      if (!isSuper && !isPracticeAdmin) { setPhase("forbidden"); setMe(data); return; }

      setMe(data);
      setView(isSuper ? { name: "practices" } : { name: "members" });
      setPhase("ready");
    })();
    return () => { cancelled = true; };
  }, []);

  const wrap = (children) => (
    <div style={{ background: tokens.bg, minHeight: "100vh" }}>
      <ChairsideHeader product="Admin" tokens={tokens} accent={accent} currentHref="Chairside-Admin.html" />
      <main style={{ maxWidth: 980, margin: "0 auto", padding: isPhone ? "28px 18px 96px" : "44px 28px 120px" }}>
        {children}
      </main>
    </div>
  );

  if (phase === "loading") return wrap(<Spinner tokens={tokens} />);

  if (phase === "offline") {
    return wrap(
      <div style={{ textAlign: "center", padding: "80px 20px", color: tokens.soft }}>
        <h2 className="display" style={{ fontSize: 30, marginBottom: 12 }}>Admin is unavailable</h2>
        <p>Couldn't reach the server. Open this page through praxia.school while signed in.</p>
      </div>,
    );
  }

  if (phase === "forbidden") {
    return wrap(
      <div style={{ textAlign: "center", padding: "80px 20px", color: tokens.soft }}>
        <div className="eyebrow" style={{ color: accent.c, marginBottom: 14 }}>Restricted</div>
        <h2 className="display" style={{ fontSize: 30, marginBottom: 12 }}>You don't have admin access</h2>
        <p>This area is for practice owners, office managers, and platform admins. If that's you, contact your practice owner.</p>
      </div>,
    );
  }

  const isSuper = !!(me && me.isSuperAdmin);

  return wrap(
    <div className="fade-up">
      <Notice notice={notice} onClose={() => setNotice(null)} tokens={tokens} />

      {/* Sub-nav: super-admin gets Practices + (when drilled in) the org's Members */}
      {isSuper && (
        <div style={{ display: "flex", gap: 8, marginBottom: 22, flexWrap: "wrap" }}>
          <button className={"chip" + (view && view.name === "practices" ? " active" : "")}
            onClick={() => setView({ name: "practices" })}>Practices</button>
          {view && view.name === "members" && (
            <button className="chip active">Members · {view.orgName || view.orgId}</button>
          )}
        </div>
      )}

      {view && view.name === "practices" && (
        <PracticesView
          tokens={tokens} accent={accent} isPhone={isPhone}
          authProvider={me && me.authProvider}
          onOpenOrg={(o) => setView({ name: "members", orgId: o.orgId, orgName: o.name })}
          onNotice={setNotice}
        />
      )}

      {view && view.name === "members" && (
        <div>
          {isSuper && (
            <button className="btn ghost" style={{ marginBottom: 14, padding: "8px 14px", fontSize: 13 }}
              onClick={() => setView({ name: "practices" })}>← All practices</button>
          )}
          <MembersView
            tokens={tokens} accent={accent} isPhone={isPhone} me={me}
            orgId={isSuper ? view.orgId : undefined}
            onNotice={setNotice}
          />
        </div>
      )}
    </div>,
  );
}

function AdminRoot() {
  const tokens = CHAIRSIDE_THEMES.dark;
  // Gold-on-dark — the platform/seller surface. Matches the praxia.school gold.
  const accent = { c: "#C9A65A", c2: "#D9BC7A", soft: "rgba(201,166,90,0.16)" };
  return (
    <>
      <ChairsideStyle tokens={tokens} accent={accent} />
      <AdminApp tokens={tokens} accent={accent} />
    </>
  );
}

Object.assign(window, { AdminRoot, AdminApp });
