const { useState, useEffect, useCallback, useRef } = React;

/**
 * BH Content Creator — Clinical Review Queue
 * Production reviewer interface. v1.0 · 2026-08-15
 *
 * Talks to the API in api/. Set API_BASE, serve behind the same origin or a
 * CORS allowlist. This is not an artifact demo — it will show a connection
 * error until the API is running.
 *
 * WHAT THIS SCREEN IS
 *
 * Not a CMS. The reviewer's job is adjudicating assertions and then binding
 * their licence to a specific text. The nearest honest analogue is a document
 * execution page: enumerated exhibits, an explicit ruling on each, then a
 * signature that names what it covers.
 *
 * There IS an edit affordance, deliberately narrow: anyone holding draft.edit
 * (owner, operator, clinical director, compliance — never viewer) can revise
 * the body text in place. Saving recomputes the content hash on the server;
 * if the page was signed, that signature is voided on the record and the page
 * returns to the queue. A reviewer may sign their own revision; the record
 * states that they were also the editor.
 *
 * Three consequences run through the layout:
 *
 *   1. Claims lead, prose supports. The ledger is the left column and the
 *      draft is reference material on the right. A queue that opens on body
 *      copy invites proofreading, which is not the job.
 *
 *   2. The signature block is always visible and always states what is missing.
 *      It shows the reviewer's own licence and expiry, the content hash, and
 *      the number of unruled claims. It stays inert with a reason rather than
 *      going grey silently.
 *
 *   3. The content hash is rendered, in mono, as a seal. It is the answer to
 *      "what exactly did they read," and putting it on screen makes the binding
 *      legible instead of implied.
 *
 * There is no approve-all control, and adding one would defeat the queue.
 */

const API_BASE = "";

// ─── Design tokens ──────────────────────────────────────────────────────────
// Palette: cold document ink on cool paper, with an oxblood reserved solely for
// the act of signing. Oxblood appears nowhere else in the interface — it means
// "this binds you," and spending it on a secondary button would dilute the one
// signal that matters.
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&family=Public+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');

*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }

.rq {
  --ink:        #12191F;
  --ink-soft:   #48565F;
  --ink-faint:  #7C8B95;
  --paper:      #EEF1F4;
  --card:       #FFFFFF;
  --rule:       #D6DDE4;
  --rule-soft:  #E7ECF0;
  --seal:       #8C2F39;
  --seal-dim:   #8C2F390F;
  --sourced:    #1E6B4F;
  --sourced-dim:#1E6B4F14;
  --judgment:   #8A5B0B;
  --judgment-dim:#8A5B0B14;
  --blocked:    #8C2F39;
  --focus:      #1B3A5C;

  font-family: 'Public Sans', system-ui, sans-serif;
  color: var(--ink);
  background: var(--paper);
  min-height: 100vh;
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

.rq :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .rq *, .rq *::before, .rq *::after { animation: none !important; transition: none !important; }
}

.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
.serif { font-family: 'Source Serif 4', Georgia, serif; }

/* ── Masthead ── */
.rq-head {
  background: var(--ink); color: #EEF1F4;
  padding: 0 22px; height: 54px;
  display: flex; align-items: center; gap: 18px;
  position: sticky; top: 0; z-index: 30;
}
.rq-mark { font-family: 'Source Serif 4', serif; font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }
.rq-mark span { color: #8FA3AF; font-weight: 400; }
.rq-facility { font-size: 12px; color: #8FA3AF; border-left: 1px solid #33424C; padding-left: 18px; }
.rq-head-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 12px; }
.rq-who { text-align: right; line-height: 1.3; }
.rq-who b { display: block; font-weight: 600; font-size: 12.5px; }
.rq-who span { color: #8FA3AF; font-size: 11px; }

/* ── Capacity strip ── */
.rq-capacity {
  background: #1B242C; color: #A9B8C2;
  padding: 6px 22px; font-size: 11.5px;
  display: flex; gap: 20px; align-items: center;
  border-bottom: 1px solid #2A343D;
}
.rq-capacity b { color: #EEF1F4; font-weight: 600; }
.rq-capacity .warn { color: #E0A458; }

/* ── Shell ── */
.rq-shell { display: grid; grid-template-columns: 268px 1fr; min-height: calc(100vh - 54px); }

/* ── Worklist ── */
.rq-list { background: var(--card); border-right: 1px solid var(--rule); overflow-y: auto; }
.rq-list-head {
  padding: 14px 18px 10px; border-bottom: 1px solid var(--rule);
  position: sticky; top: 0; background: var(--card);
}
.rq-list-head h2 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-faint); margin: 0 0 3px; font-weight: 600; }
.rq-list-head p { margin: 0; font-size: 13px; color: var(--ink-soft); }

.rq-item {
  display: block; width: 100%; text-align: left;
  padding: 13px 18px; border: 0; border-bottom: 1px solid var(--rule-soft);
  border-left: 3px solid transparent;
  background: transparent; cursor: pointer; font: inherit;
}
.rq-item:hover { background: #F7F9FB; }
.rq-item[aria-current="true"] { background: #F2F6F9; border-left-color: var(--ink); }
.rq-item-type { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-faint); font-weight: 600; }
.rq-item-title { font-family: 'Source Serif 4', serif; font-size: 15px; font-weight: 600; margin: 2px 0 4px; line-height: 1.3; }
.rq-item-meta { font-size: 11.5px; color: var(--ink-faint); display: flex; gap: 10px; }
.rq-item-meta .stale { color: var(--judgment); font-weight: 600; }

/* ── Packet ── */
.rq-packet { display: grid; grid-template-rows: auto 1fr auto; min-width: 0; }
.rq-packet-head { background: var(--card); border-bottom: 1px solid var(--rule); padding: 18px 26px 14px; }
.rq-eyebrow { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-faint); font-weight: 600; }
.rq-title { font-family: 'Source Serif 4', serif; font-size: 25px; font-weight: 600; margin: 4px 0 8px; line-height: 1.2; letter-spacing: -0.01em; }
.rq-facts { display: flex; flex-wrap: wrap; gap: 8px 20px; font-size: 12px; color: var(--ink-soft); }
.rq-facts b { font-weight: 600; color: var(--ink); }

.rq-cols { display: grid; grid-template-columns: minmax(0,1.05fr) minmax(0,1fr); gap: 0; min-height: 0; }
.rq-ledger { padding: 20px 26px 40px; overflow-y: auto; border-right: 1px solid var(--rule); }
.rq-draft { padding: 20px 26px 40px; overflow-y: auto; background: #F7F9FB; }

.rq-sec { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-faint); font-weight: 600; margin: 0 0 12px; }

/* ── Claims ledger ──
   Numbered because these ARE an enumerated evidentiary record — exhibit
   numbers, not decoration. The reviewer refers to "claim 3" when they request
   changes, and the number has to mean something. */
.rq-claim {
  background: var(--card); border: 1px solid var(--rule);
  border-radius: 3px; padding: 13px 15px; margin-bottom: 10px;
  display: grid; grid-template-columns: 26px 1fr; gap: 12px;
}
.rq-claim.ruled { border-color: var(--rule-soft); background: #FCFDFD; }
.rq-claim-n { font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: var(--ink-faint); padding-top: 2px; }
.rq-claim-text { font-family: 'Source Serif 4', serif; font-size: 15.5px; line-height: 1.45; margin: 0 0 8px; }
.rq-claim-ask { font-size: 12.5px; color: var(--ink-soft); margin: 0 0 10px; }
.rq-tag {
  display: inline-block; font-size: 10px; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 2px; margin-right: 6px;
}
.tag-sourced { background: var(--sourced-dim); color: var(--sourced); }
.tag-judgment { background: var(--judgment-dim); color: var(--judgment); }
.tag-blocked { background: var(--seal-dim); color: var(--blocked); }
.rq-source { font-size: 12px; color: var(--ink-soft); margin: 0 0 10px; }
.rq-source a { color: var(--focus); }
.rq-source .q { font-family: 'Source Serif 4', serif; font-style: italic; }

.rq-rulings { display: flex; gap: 6px; }
.rq-rule-btn {
  font: inherit; font-size: 12px; font-weight: 600;
  padding: 5px 11px; border-radius: 2px; cursor: pointer;
  border: 1px solid var(--rule); background: var(--card); color: var(--ink-soft);
}
.rq-rule-btn:hover { border-color: var(--ink-soft); }
.rq-rule-btn[aria-pressed="true"].accept { background: var(--sourced); border-color: var(--sourced); color: #fff; }
.rq-rule-btn[aria-pressed="true"].reject { background: var(--seal); border-color: var(--seal); color: #fff; }

/* ── Draft body ── */
.rq-body { font-family: 'Source Serif 4', serif; font-size: 16px; line-height: 1.62; color: #1B242C; }
.rq-body h2 { font-size: 17px; font-weight: 700; margin: 22px 0 7px; }
.rq-body p { margin: 0 0 13px; }

/* ── Signature block — the signature element of this interface ──
   Always visible, always states what is missing. It shows the reviewer their
   own licence and the hash of the exact text they are binding it to. */
.rq-sign {
  background: var(--card); border-top: 2px solid var(--ink);
  padding: 14px 26px; display: flex; align-items: center; gap: 22px;
  position: sticky; bottom: 0;
}
.rq-sign-id { min-width: 0; }
.rq-sign-id b { font-size: 13.5px; font-weight: 600; display: block; }
.rq-sign-id span { font-size: 11.5px; color: var(--ink-soft); }
.rq-seal {
  border-left: 1px solid var(--rule); padding-left: 22px;
  font-size: 11px; color: var(--ink-faint); line-height: 1.4;
}
.rq-seal .h { font-family: 'IBM Plex Mono', monospace; font-size: 12.5px; color: var(--ink); letter-spacing: 0.02em; }
.rq-sign-state { margin-left: auto; text-align: right; font-size: 12px; color: var(--ink-soft); max-width: 260px; }
.rq-sign-state.blocked { color: var(--judgment); }

.rq-btn {
  font: inherit; font-weight: 600; font-size: 14px;
  padding: 11px 22px; border-radius: 2px; cursor: pointer; border: 1px solid transparent;
}
.rq-btn-sign { background: var(--seal); color: #fff; }
.rq-btn-sign:hover:not(:disabled) { background: #7A2831; }
.rq-btn-sign:disabled { background: #C9D1D8; color: #7C8B95; cursor: not-allowed; }
.rq-btn-ghost { background: transparent; border-color: var(--rule); color: var(--ink-soft); }
.rq-btn-ghost:hover { border-color: var(--ink-soft); color: var(--ink); }

/* ── Notices ── */
.rq-notice {
  padding: 11px 15px; border-radius: 3px; font-size: 13px;
  margin-bottom: 14px; border: 1px solid;
}
.notice-warn { background: var(--judgment-dim); border-color: #D8B87A; color: #6B4708; }
.notice-stop { background: var(--seal-dim); border-color: #D7A2A8; color: #6E242C; }
.notice-info { background: #EDF3F8; border-color: #B9CEDE; color: #244258; }
.rq-notice b { font-weight: 600; }

/* ── Elevation dialog ── */
.rq-scrim {
  position: fixed; inset: 0; background: rgba(18,25,31,0.55);
  display: grid; place-items: center; z-index: 60; padding: 20px;
}
.rq-dialog {
  background: var(--card); border-radius: 4px; padding: 26px; width: 100%; max-width: 430px;
  box-shadow: 0 18px 50px rgba(18,25,31,0.28);
}
.rq-dialog h2 { font-family: 'Source Serif 4', serif; font-size: 21px; margin: 0 0 8px; font-weight: 600; }
.rq-dialog p { font-size: 13.5px; color: var(--ink-soft); margin: 0 0 16px; }
.rq-field { display: block; margin-bottom: 14px; }
.rq-field span { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-faint); font-weight: 600; margin-bottom: 5px; }
.rq-input {
  width: 100%; font: inherit; font-size: 14px; padding: 9px 11px;
  border: 1px solid var(--rule); border-radius: 2px; background: #FBFCFD;
}
.rq-input:focus { border-color: var(--focus); }
.rq-dialog-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }
.rq-err { font-size: 13px; color: var(--blocked); margin: 0 0 12px; }

/* ── Sign-in ── */
.rq-signin { min-height: 100vh; display: grid; place-items: center; padding: 20px; }
.rq-signin-card { background: var(--card); border: 1px solid var(--rule); border-radius: 4px; padding: 34px; width: 100%; max-width: 400px; }
.rq-signin-card h1 { font-family: 'Source Serif 4', serif; font-size: 24px; margin: 0 0 4px; font-weight: 600; }
.rq-signin-card > p { font-size: 13px; color: var(--ink-soft); margin: 0 0 22px; }

/* ── Empty ── */
.rq-empty { display: grid; place-items: center; padding: 70px 26px; text-align: center; }
.rq-empty p { max-width: 380px; color: var(--ink-soft); font-size: 14px; }
.rq-empty h2 { font-family: 'Source Serif 4', serif; font-size: 21px; font-weight: 600; margin: 0 0 6px; }

/* Readiness */
.rq-ready { display: flex; gap: 22px; align-items: center; margin-top: 14px; padding: 12px 14px; border: 1px solid var(--rule); border-radius: 6px; background: var(--card); }
.rq-ready-num { min-width: 92px; }
.rq-ready-num b { font-family: 'Source Serif 4', serif; font-size: 30px; font-weight: 700; color: var(--ink); line-height: 1; }
.rq-ready-num span { font-size: 13px; color: var(--ink-faint); margin-left: 2px; }
.rq-ready-cap { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-faint); font-weight: 600; margin-top: 4px; }
.rq-ready-dims { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 6px 22px; }
.rq-dim { display: grid; grid-template-columns: 120px 1fr 32px; gap: 10px; align-items: center; font-size: 12px; color: var(--ink-soft); }
.rq-dim.weakest .rq-dim-label { color: var(--ink); font-weight: 600; }
.rq-dim-bar { height: 6px; background: var(--rule-soft); border-radius: 3px; overflow: hidden; }
.rq-dim-bar i { display: block; height: 100%; background: var(--sourced); }
.rq-dim.weakest .rq-dim-bar i { background: #B7791F; }
.rq-dim-val { text-align: right; color: var(--ink); }

/* Record */
.rq-record { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--rule); }
.rq-rec { padding: 10px 12px; border: 1px solid var(--rule); border-radius: 6px; margin-bottom: 8px; background: var(--card); font-size: 12px; color: var(--ink-soft); }
.rq-rec-head { display: flex; justify-content: space-between; gap: 12px; color: var(--ink); }
.rq-rec-head b { font-weight: 600; }
.rq-rec-who { margin-top: 3px; }
.rq-rec-hash { margin-top: 3px; color: var(--ink-faint); font-size: 11px; }
.rq-rec-note { margin-top: 5px; font-style: italic; }
.rq-rec.rec-approved { border-left: 3px solid var(--sourced); }
.rq-rec.rec-voided { border-left: 3px solid var(--seal); background: var(--seal-dim); }
.rq-rec.rec-voided .rq-rec-head b { color: var(--seal); }
.tag-editor { background: #B7791F1A; color: #7A4F0E; margin-left: 8px; }

/* Editing */
.rq-draft-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.rq-draft-head .rq-sec { margin: 0; }
.rq-edit-actions { display: inline-flex; gap: 8px; }
.rq-btn-sm { padding: 6px 12px; font-size: 12px; }
.rq-btn-edit { background: var(--ink); color: #fff; border-color: var(--ink); }
.rq-btn-edit:disabled { opacity: 0.55; }
.rq-textarea { width: 100%; font-family: 'Source Serif 4', serif; font-size: 16px; line-height: 1.62; color: #1B242C; padding: 10px 12px; border: 1px solid var(--rule); border-radius: 6px; background: var(--card); resize: vertical; margin: 0 0 13px; }
.rq-textarea:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
.rq-list-head-2 { margin-top: 14px; border-top: 1px solid var(--rule); }

@media (max-width: 1080px) {
  .rq-shell { grid-template-columns: 1fr; }
  .rq-list { border-right: 0; border-bottom: 1px solid var(--rule); max-height: 210px; }
  .rq-cols { grid-template-columns: 1fr; }
  .rq-ledger { border-right: 0; border-bottom: 1px solid var(--rule); }
  .rq-sign { flex-wrap: wrap; gap: 12px; }
  .rq-sign-state { margin-left: 0; text-align: left; max-width: none; }
}
`;

// ─── API client ─────────────────────────────────────────────────────────────
// One place that knows what each status code means. The interface reacts to
// `action` rather than re-deriving intent from the status everywhere.
function useApi(token, onSignOut, onNeedElevation) {
  return useCallback(async (path, { method = "GET", body } = {}) => {
    const res = await fetch(`${API_BASE}${path}`, {
      method,
      headers: {
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...(body ? { "Content-Type": "application/json" } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    const data = await res.json().catch(() => ({}));
    if (res.ok) return data;

    // A missing elevation is NOT a sign-out. Prompt in place and keep their spot.
    if (data.action === "prompt_password") {
      onNeedElevation?.();
      const err = new Error(data.error);
      err.code = data.code;
      err.needsElevation = true;
      throw err;
    }
    if (data.action === "sign_in") { onSignOut?.(data.error); }
    const err = new Error(data.error || `Request failed (${res.status})`);
    err.code = data.code;
    err.status = res.status;
    throw err;
  }, [token, onSignOut, onNeedElevation]);
}

// ─── Sign in ────────────────────────────────────────────────────────────────
function SignIn({ onSignedIn }) {
  // An org-scoped invitation link can carry the organization id (?org=…) so a
  // reviewer is not asked to type a UUID. Nothing else is read from the URL.
  const [orgId, setOrgId] = useState(() => {
    try { return new URLSearchParams(window.location.search).get("org") || ""; } catch { return ""; }
  });
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(null);
  const [busy, setBusy] = useState(false);

  const submit = async () => {
    setBusy(true); setError(null);
    try {
      const res = await fetch(`${API_BASE}/api/auth/login`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ orgId, email, password }),
      });
      const data = await res.json();
      if (!res.ok) { setError(data.error || "Sign in failed"); setBusy(false); return; }
      onSignedIn(data);
    } catch {
      setError("Cannot reach the review service. Check that the API is running.");
    }
    setBusy(false);
  };

  return (
    <div className="rq-signin">
      <div className="rq-signin-card">
        <h1 className="serif">Clinical review</h1>
        <p>Sign in to review and approve pages before they publish.</p>
        {error && <p className="rq-err">{error}</p>}
        <label className="rq-field">
          <span>Organization ID</span>
          <input className="rq-input mono" value={orgId} onChange={(e) => setOrgId(e.target.value)} autoComplete="off" />
        </label>
        <label className="rq-field">
          <span>Email</span>
          <input className="rq-input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="username" />
        </label>
        <label className="rq-field">
          <span>Password</span>
          <input className="rq-input" type="password" value={password} onChange={(e) => setPassword(e.target.value)}
            autoComplete="current-password" onKeyDown={(e) => e.key === "Enter" && submit()} />
        </label>
        <button className="rq-btn rq-btn-sign" style={{ width: "100%" }} onClick={submit} disabled={busy || !orgId || !email || !password}>
          {busy ? "Signing in…" : "Sign in"}
        </button>
      </div>
    </div>
  );
}

// ─── Elevation dialog ───────────────────────────────────────────────────────
function ElevationDialog({ onConfirm, onCancel, title, error, busy }) {
  const ref = useRef(null);
  const [password, setPassword] = useState("");
  useEffect(() => { ref.current?.focus(); }, []);

  return (
    <div className="rq-scrim" role="dialog" aria-modal="true" aria-labelledby="elev-title"
      onKeyDown={(e) => e.key === "Escape" && onCancel()}>
      <div className="rq-dialog">
        <h2 id="elev-title" className="serif">Confirm your identity</h2>
        <p>
          You are about to attach your licence to <b>{title}</b>. Enter your password
          to sign. This confirms the signature is yours and not an open browser session.
        </p>
        {error && <p className="rq-err">{error}</p>}
        <label className="rq-field">
          <span>Password</span>
          <input ref={ref} className="rq-input" type="password" value={password} autoComplete="current-password"
            onChange={(e) => setPassword(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && password && onConfirm(password)} />
        </label>
        <div className="rq-dialog-actions">
          <button className="rq-btn rq-btn-ghost" onClick={onCancel}>Cancel</button>
          <button className="rq-btn rq-btn-sign" onClick={() => onConfirm(password)} disabled={busy || !password}>
            {busy ? "Signing…" : "Sign page"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── Claims ledger ──────────────────────────────────────────────────────────
function ClaimsLedger({ claims, rulings, onRule }) {
  return (
    <>
      <h2 className="rq-sec">Claims to adjudicate · {claims.length}</h2>
      {claims.map((c, i) => {
        const ruled = rulings[i];
        const tag = c.resolution === "PASS" ? ["tag-sourced", "Sourced"]
          : c.resolution === "BLOCK" ? ["tag-blocked", "Blocked"]
          : ["tag-judgment", "Needs judgment"];
        return (
          <div key={i} className={`rq-claim${ruled ? " ruled" : ""}`}>
            <div className="rq-claim-n">{String(i + 1).padStart(2, "0")}</div>
            <div>
              <p className="rq-claim-text">{c.claim}</p>
              <div>
                <span className={`rq-tag ${tag[0]}`}>{tag[1]}</span>
                <span className="rq-tag tag-judgment" style={{ background: "transparent", color: "var(--ink-faint)" }}>
                  {c.class === "facility" ? "About us" : "External"}
                </span>
              </div>
              <p className="rq-claim-ask">{c.ask}</p>
              {c.boundTo && (
                <p className="rq-source">Checked against your profile field <span className="mono">{c.boundTo}</span></p>
              )}
              {c.url && (
                <p className="rq-source">
                  {c.source} — <a href={c.url} target="_blank" rel="noreferrer noopener">view source</a>
                  {c.quote && <><br /><span className="q">“{c.quote}”</span></>}
                </p>
              )}
              <div className="rq-rulings">
                <button className="rq-rule-btn accept" aria-pressed={ruled === "accept"}
                  onClick={() => onRule(i, ruled === "accept" ? null : "accept")}>Accurate</button>
                <button className="rq-rule-btn reject" aria-pressed={ruled === "reject"}
                  onClick={() => onRule(i, ruled === "reject" ? null : "reject")}>Needs change</button>
              </div>
            </div>
          </div>
        );
      })}
    </>
  );
}

// ─── Readiness ──────────────────────────────────────────────────────────────
// The composite is shown WITH its four dimensions. A single number invites
// trust-and-skim; the weakest dimension is what a reviewer should look at.
function Readiness({ readiness }) {
  if (!readiness || typeof readiness.composite !== "number") return null;
  const dims = readiness.dimensions || {};
  return (
    <div className="rq-ready" aria-label="Publish readiness">
      <div className="rq-ready-num">
        <b>{readiness.composite}</b><span>/100</span>
        <div className="rq-ready-cap">
          {readiness.capped ? "Capped: a dimension scored zero" : "Publish readiness"}
        </div>
      </div>
      <div className="rq-ready-dims">
        {Object.entries(dims).map(([k, d]) => (
          <div key={k} className={`rq-dim${readiness.weakest === k ? " weakest" : ""}`}>
            <span className="rq-dim-label">{d.label}</span>
            <span className="rq-dim-bar"><i style={{ width: `${Math.max(0, Math.min(100, d.value))}%` }} /></span>
            <span className="rq-dim-val mono">{d.value}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Record ─────────────────────────────────────────────────────────────────
// The evidentiary chain for this draft, oldest first. Every row names the hash
// it covered, so a voided approval is visibly a signature against text that no
// longer exists — not an erased one.
function Record({ history }) {
  if (!history || !history.length) return null;
  const label = { approved: "Signed", voided: "Signature voided", changes_requested: "Sent back", declined: "Declined" };
  return (
    <div className="rq-record">
      <h2 className="rq-sec">Record · {history.length}</h2>
      {history.map((h, i) => (
        <div key={i} className={`rq-rec rec-${h.action}`}>
          <div className="rq-rec-head">
            <b>{label[h.action] || h.action}</b>
            <span className="mono">{new Date(h.at).toLocaleString()}</span>
          </div>
          <div className="rq-rec-who">
            {h.action === "voided" ? "system" : `${h.by}${h.credentials ? `, ${h.credentials}` : ""}${h.licence ? ` · licence ${h.licence}` : ""}`}
            {h.signerWasEditor && <span className="rq-tag tag-editor">Signer also edited this revision</span>}
          </div>
          <div className="rq-rec-hash mono">hash {h.contentHash}</div>
          {h.note && <div className="rq-rec-note">{h.note}</div>}
        </div>
      ))}
    </div>
  );
}

// ─── Main ───────────────────────────────────────────────────────────────────
function ReviewQueue() {
  const [session, setSession] = useState(null);
  const [me, setMe] = useState(null);
  const [capacity, setCapacity] = useState(null);
  const [items, setItems] = useState([]);
  const [resolved, setResolved] = useState([]);
  const [activeId, setActiveId] = useState(null);
  const [packet, setPacket] = useState(null);
  const [rulings, setRulings] = useState({});
  const [elevating, setElevating] = useState(false);
  const [elevError, setElevError] = useState(null);
  const [busy, setBusy] = useState(false);
  const [banner, setBanner] = useState(null);
  // Editing. `draftBody` is the working copy of the body while an edit is open.
  const [editing, setEditing] = useState(false);
  const [draftBody, setDraftBody] = useState(null);

  const signOut = useCallback((reason) => {
    setSession(null); setMe(null); setPacket(null); setItems([]); setResolved([]);
    if (reason) setBanner({ kind: "info", text: reason });
  }, []);

  // Stable callbacks, deliberately: useApi memoizes on these, and an inline
  // arrow here would give every render a new client, re-arm every effect that
  // depends on it, and cancel the previous load before it could set state.
  const needElevation = useCallback(() => setElevating(true), []);
  const api = useApi(session?.token, signOut, needElevation);

  const loadQueue = useCallback(async (facilityId) => {
    const [queue, ent] = await Promise.all([
      api(`/api/facilities/${facilityId}/queue`),
      api("/api/entitlements").catch(() => null),
    ]);
    setItems(queue.items);
    setResolved(queue.resolved || []);
    setCapacity(ent);
    return queue;
  }, [api]);

  const loadPacket = useCallback(async (id) => {
    const p = await api(`/api/review/${id}`);
    setPacket(p);
    return p;
  }, [api]);

  // Load identity and worklist
  useEffect(() => {
    if (!session) return;
    let cancelled = false;
    (async () => {
      try {
        const meRes = await api("/api/me");
        if (cancelled) return;
        setMe(meRes);
        const facilityId = meRes.facilities[0];
        if (!facilityId) { setBanner({ kind: "warn", text: "No facility is assigned to your account. Ask your administrator to add one." }); return; }
        const queue = await loadQueue(facilityId);
        if (cancelled) return;
        setActiveId((cur) => cur || queue.items[0]?.id || null);
      } catch (e) { if (!cancelled) setBanner({ kind: "stop", text: e.message }); }
    })();
    return () => { cancelled = true; };
  }, [session, api, loadQueue]);

  // Load the active packet
  useEffect(() => {
    if (!activeId || !session) { setPacket(null); return; }
    let cancelled = false;
    (async () => {
      try {
        const p = await api(`/api/review/${activeId}`);
        if (cancelled) return;
        setPacket(p); setRulings({}); setEditing(false); setDraftBody(null);
      } catch (e) { if (!cancelled) setBanner({ kind: "stop", text: e.message }); }
    })();
    return () => { cancelled = true; };
  }, [activeId, session, api]);

  const attest = async (action, elevationPassword) => {
    if (!packet) return;
    setBusy(true); setElevError(null);
    try {
      if (elevationPassword) {
        const res = await fetch(`${API_BASE}/api/auth/elevate`, {
          method: "POST",
          headers: { Authorization: `Bearer ${session.token}`, "Content-Type": "application/json" },
          body: JSON.stringify({ password: elevationPassword }),
        });
        if (!res.ok) {
          const d = await res.json().catch(() => ({}));
          setElevError(d.error || "That password was not accepted."); setBusy(false); return;
        }
      }
      const r = await api(`/api/review/${packet.id}/attest`, {
        method: "POST",
        body: { action, contentHash: packet.contentHash },
      });
      setElevating(false);
      // Keep the page on screen after signing. The reviewer should see the
      // signature land on the record — and see it voided if the text moves.
      await Promise.all([loadQueue(me.facilities[0]), loadPacket(packet.id)]);
      setBanner({
        kind: "info",
        text: action === "approved"
          ? `Page signed. Your licence is recorded against hash ${r.contentHash}.`
          : "Sent back for changes. The page returns to the queue once it is revised.",
      });
    } catch (e) {
      if (e.needsElevation) { setBusy(false); return; }  // dialog is already open
      setBanner({ kind: "stop", text: e.message });
      setElevating(false);
    }
    setBusy(false);
  };

  const beginEdit = () => {
    setDraftBody((packet.content?.body || []).map((b) => ({ ...b })));
    setEditing(true);
  };

  const saveEdit = async () => {
    if (!packet || !draftBody) return;
    setBusy(true);
    try {
      const content = { ...packet.content, body: draftBody };
      const r = await api(`/api/review/${packet.id}/edit`, { method: "POST", body: { content } });
      setEditing(false); setDraftBody(null); setRulings({});
      await Promise.all([loadQueue(me.facilities[0]), loadPacket(packet.id)]);
      setBanner({ kind: r.voided ? "warn" : "info", text: r.message });
    } catch (e) {
      setBanner({ kind: "stop", text: e.message });
    }
    setBusy(false);
  };

  if (!session) {
    return (<><style>{CSS}</style><div className="rq"><SignIn onSignedIn={setSession} /></div></>);
  }

  const total = packet?.claims?.length || 0;
  const ruled = Object.values(rulings).filter(Boolean).length;
  const rejected = Object.values(rulings).filter((r) => r === "reject").length;
  const allRuled = total > 0 && ruled === total;
  const alreadySigned = packet?.state === "approved";
  const lastRecord = packet?.history?.[packet.history.length - 1];
  const recordLeads = alreadySigned || lastRecord?.action === "voided";
  const canSign = packet?.preflight?.canSign && allRuled && rejected === 0 && !alreadySigned && !editing;

  let signState;
  if (!packet) signState = "Select a page from the queue.";
  else if (alreadySigned) signState = "Signed. Any edit to the text voids this signature and returns the page to the queue.";
  else if (editing) signState = "Finish or cancel the edit before signing.";
  else if (!packet.preflight.canSign) signState = packet.preflight.reason;
  else if (!allRuled) signState = `${total - ruled} of ${total} claims still need a ruling.`;
  else if (rejected > 0) signState = `${rejected} claim${rejected > 1 ? "s" : ""} marked for change. Send it back instead of signing.`;
  else if (packet.editedBySelf) signState = "Every claim ruled. You authored this revision; the record will say so.";
  else signState = "Every claim ruled. Ready to sign.";

  const ListItem = ({ i }) => (
    <button key={i.id} className="rq-item" aria-current={i.id === activeId}
      onClick={() => setActiveId(i.id)}>
      <div className="rq-item-type">{(i.pageType || "page").replace(/_/g, " ")}</div>
      <div className="rq-item-title">{i.title || i.draftId.slice(0, 8)}</div>
      <div className="rq-item-meta">
        {i.state === "approved" ? <span>Signed</span> : i.state === "declined" ? <span>Declined</span> : (
          <span className={i.daysWaiting >= 7 ? "stale" : ""}>
            {i.daysWaiting < 1 ? "Today" : `${Math.round(i.daysWaiting)}d waiting`}
          </span>
        )}
        {i.state === "in_review" && <span>In review</span>}
        {i.dualAttestation && <span>Two signatures</span>}
      </div>
    </button>
  );

  return (
    <>
      <style>{CSS}</style>
      <div className="rq">
        <header className="rq-head">
          <div className="rq-mark">BH Content Creator <span>· Clinical review</span></div>
          <div className="rq-facility">{items.length} awaiting review</div>
          <div className="rq-head-right">
            <div className="rq-who">
              <b>{me?.name}</b>
              <span>{me?.credentials || me?.role} {me?.licence ? `· ${me.licence}` : ""}</span>
            </div>
            <button className="rq-btn rq-btn-ghost" style={{ padding: "6px 12px", fontSize: 12, borderColor: "#33424C", color: "#A9B8C2" }}
              onClick={() => { api("/api/auth/logout", { method: "POST" }).catch(() => {}); signOut(); }}>
              Sign out
            </button>
          </div>
        </header>

        {capacity && (
          <div className="rq-capacity">
            <span><b>{capacity.usage.newPages}</b> of <b>{capacity.usage.newPagesLimit}</b> pages generated this period</span>
            <span>Plan <b style={{ textTransform: "capitalize" }}>{capacity.entitlements.tier}</b></span>
            {capacity.usage.newPagesRemaining === 0 && (
              <span className="warn">Page limit reached — review continues as normal.</span>
            )}
          </div>
        )}

        <div className="rq-shell">
          <nav className="rq-list" aria-label="Pages awaiting review">
            <div className="rq-list-head">
              <h2>Queue</h2>
              <p>{items.length === 0 ? "Nothing waiting" : `${items.length} page${items.length > 1 ? "s" : ""}`}</p>
            </div>
            {items.map((i) => <ListItem key={i.id} i={i} />)}
            {resolved.length > 0 && (
              <>
                <div className="rq-list-head rq-list-head-2">
                  <h2>Signed or declined</h2>
                  <p>{resolved.length} page{resolved.length > 1 ? "s" : ""}</p>
                </div>
                {resolved.map((i) => <ListItem key={i.id} i={i} />)}
              </>
            )}
          </nav>

          <main className="rq-packet">
            {!packet ? (
              <div className="rq-empty">
                <div>
                  <h2 className="serif">{items.length ? "Select a page" : "Queue is clear"}</h2>
                  <p>{items.length
                    ? "Choose a page from the queue to review its claims."
                    : "Nothing is waiting for your signature. New drafts appear here as they are generated."}</p>
                </div>
              </div>
            ) : (
              <>
                <div className="rq-packet-head">
                  <div className="rq-eyebrow">{(packet.pageType || "").replace(/_/g, " ")} · {(packet.state || "").replace(/_/g, " ")}</div>
                  <h1 className="rq-title">{packet.title}</h1>
                  <div className="rq-facts">
                    <span>Target query <b>{packet.targetQuery}</b></span>
                    <span>Routed to <b>{(packet.route || []).join(" or ").replace(/_/g, " ")}</b></span>
                    {packet.dualAttestation && <span><b>Two signatures required</b></span>}
                    {packet.editedBy && <span>Current revision edited by <b>{packet.editedBy}</b></span>}
                  </div>
                  <Readiness readiness={packet.readiness} />
                </div>

                <div className="rq-cols">
                  <section className="rq-ledger">
                    {banner && (
                      <div className={`rq-notice notice-${banner.kind}`} role="status">{banner.text}</div>
                    )}
                    {packet.preflight?.warning && (
                      <div className="rq-notice notice-warn" role="status">{packet.preflight.warning}</div>
                    )}
                    {!packet.preflight?.canSign && (
                      <div className="rq-notice notice-stop" role="alert">
                        <b>You cannot sign this page.</b> {packet.preflight.reason}
                      </div>
                    )}
                    {/* Claims lead — unless the page's signature is the news.
                        A page that is signed, or whose signature was just
                        voided, should show the record before the exhibits. */}
                    {recordLeads && <Record history={packet.history} />}
                    <ClaimsLedger claims={packet.claims} rulings={rulings}
                      onRule={(i, v) => setRulings((r) => ({ ...r, [i]: v }))} />
                    {!recordLeads && <Record history={packet.history} />}
                  </section>

                  <section className="rq-draft">
                    <div className="rq-draft-head">
                      <h2 className="rq-sec">{editing ? "Editing the page text" : "The page as it will publish"}</h2>
                      {packet.canEdit && !editing && (
                        <button className="rq-btn rq-btn-ghost rq-btn-sm" disabled={busy} onClick={beginEdit}>Edit text</button>
                      )}
                      {editing && (
                        <span className="rq-edit-actions">
                          <button className="rq-btn rq-btn-ghost rq-btn-sm" disabled={busy}
                            onClick={() => { setEditing(false); setDraftBody(null); }}>Cancel</button>
                          <button className="rq-btn rq-btn-edit rq-btn-sm" disabled={busy} onClick={saveEdit}>
                            {busy ? "Saving…" : "Save revision"}
                          </button>
                        </span>
                      )}
                    </div>
                    {editing && (
                      <div className="rq-notice notice-warn" role="status">
                        {alreadySigned
                          ? "This page is signed. Saving any change to the text voids that signature and returns the page to the queue."
                          : "Saving changes the content hash. A reviewer holding the current packet will be refused on sign and must reload."}
                      </div>
                    )}
                    <div className="rq-body">
                      {!editing && (packet.content?.body || []).map((b, i) => (
                        <div key={i}>
                          {b.heading && <h2>{b.heading}</h2>}
                          <p>{b.content}</p>
                        </div>
                      ))}
                      {editing && (draftBody || []).map((b, i) => (
                        <div key={i} className="rq-edit-block">
                          {b.heading && <h2>{b.heading}</h2>}
                          <textarea className="rq-textarea" value={b.content || ""} rows={Math.max(3, Math.ceil((b.content || "").length / 90))}
                            onChange={(e) => setDraftBody((cur) => cur.map((x, j) => j === i ? { ...x, content: e.target.value } : x))} />
                        </div>
                      ))}
                      {!(packet.content?.body || []).length && (
                        <p style={{ color: "var(--ink-faint)" }}>No rendered body on this draft.</p>
                      )}
                    </div>
                  </section>
                </div>

                <div className="rq-sign">
                  <div className="rq-sign-id">
                    <b>{me?.name}{me?.credentials ? `, ${me.credentials}` : ""}</b>
                    <span>
                      {me?.licence ? `Licence ${me.licence}` : "No licence on file"}
                      {me?.licenceExpires ? ` · expires ${me.licenceExpires}` : ""}
                    </span>
                  </div>
                  <div className="rq-seal">
                    {alreadySigned ? "Signed against this exact text" : "Signing this exact text"}<br />
                    <span className="h">{packet.contentHash}</span>
                  </div>
                  <div className={`rq-sign-state${canSign ? "" : " blocked"}`}>{signState}</div>
                  <button className="rq-btn rq-btn-ghost" disabled={busy || alreadySigned || editing}
                    onClick={() => attest("changes_requested")}>Send back</button>
                  <button className="rq-btn rq-btn-sign" disabled={!canSign || busy}
                    onClick={() => setElevating(true)}>Sign page</button>
                </div>
              </>
            )}
          </main>
        </div>

        {elevating && (
          <ElevationDialog title={packet?.title} busy={busy} error={elevError}
            onCancel={() => { setElevating(false); setElevError(null); }}
            onConfirm={(pw) => attest("approved", pw)} />
        )}
      </div>
    </>
  );
}


ReactDOM.createRoot(document.getElementById("root")).render(<ReviewQueue />);
