/* EditorialReview — Editorial → Link Review (E2c). Assigns window.LedgerEditorialReview.
   Spec: specs/editorial-layer-v1-proposal.md (E2c handoff). Data: window.LedgerAPI.ed.

   The E2b linker proposes episode → matter links from names in episode titles. Nothing
   it proposes counts until a person here confirms it. Shape copied from the Content Ops
   Metadata desk (MetadataDesk.jsx): grouped by show, then matter; edit-then-approve;
   bulk with per-item results; a Decided list.

   "Edit" for a link means picking a different matter (the linker often finds a person's
   arrest matter when the trial matter is the right one) or setting the role. The RPC
   rejects the proposal and confirms the chosen matter, so the linker never proposes the
   wrong pair again.

   Who decides: a show's producer (staff_shows.is_link_reviewer) for that show; Content
   Ops and admins for every show. decide_piece_matter_link() checks it per link; this
   screen only loads the caller's shows. Admins assign producers on the Reviewers tab.

   The Material tab (E3b, EditorialMaterial.jsx) reviews what each confirmed episode used.
   Confirming a link here proposes that episode's material at once (0136). */
(function () {
  const DS = window.LawCrimeDesignSystem_e18d68;
  const T = window.titleize || ((s) => String(s || "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
  const icons = () => setTimeout(() => window.lucide && window.lucide.createIcons(), 30);
  // Icons sit in a span React owns, keyed on the name (see ledger-lucide-react-crash).
  const Ic = ({ n }) => <span className="ic-wrap" key={n}><i data-lucide={n} /></span>;
  const API = () => window.LedgerAPI.ed;

  const SOURCE = { title_match: "Title match", ai: "AI suggestion", human: "Picked by a person", request: "Asset request" };
  const FIELD = { title: "title", working_title: "working title", short_title: "short title",
                  description: "description", short_description: "short description",
                  // E5b: names and folders read from a show's master sheet.
                  show_master_defendant: "master sheet's Defendant column", show_master_victim: "master sheet's Victim column",
                  show_master_case: "master sheet's Case column", show_master_folder: "master sheet's server folder" };
  const MASTER_METHOD = { folder: "server folder", defendant: "defendant", case: "case name", victim: "victim" };
  const ROLES = ["primary", "secondary", "mention"];
  const PAGE = 40; // matter cards per page

  const fmtDate = (d) => (d ? window.LedgerTime.fmtDate(d) : "Undated");
  const fmtWhen = (iso) => (iso ? new Date(iso).toLocaleString("en-US",
    { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZone: "America/New_York" }) : "");
  const who = (email) => (email || "").split("@")[0] || "—";
  const conf = (r) => (r.confidence == null ? null : Number(r.confidence));
  const ev = (r) => r.evidence || {};
  // What "select all" may sweep up. An episode with two or more candidates, a match found
  // only in the description, or anything under 0.90 needs a person to look, so it can be
  // ticked by hand but never picked up by a bulk select.
  const needsLook = (r) => (conf(r) || 0) < 0.9 || r.piece_pending > 1 || ev(r).field === "description";
  const bulkEligible = (r) => !needsLook(r);

  function Conf({ value }) {
    if (value == null) return null;
    const v = Number(value);
    return <span className={"tr-conf tr-conf--" + (v >= 0.95 ? "high" : v >= 0.75 ? "mid" : "low")} title="Linker confidence">{v.toFixed(2)}</span>;
  }

  // Mark the matched name inside the episode text. Linker names are lower-case and
  // accent-free ("dustin christina"), so match word by word, case-insensitively.
  function Hi({ text, name }) {
    if (!text) return null;
    const words = String(name || "").split(/\s+/).filter((w) => w.length > 1)
      .map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
    if (!words.length) return <>{text}</>;
    const re = new RegExp("(" + words.join("[^A-Za-z]+") + ")", "i");
    const parts = String(text).split(re);
    return <>{parts.map((p, i) => (i % 2 ? <mark key={i} className="ed-lr-mark">{p}</mark> : <span key={i}>{p}</span>))}</>;
  }
  function snippet(text, name) {
    const t = String(text || "");
    const first = String(name || "").split(/\s+/).pop() || "";
    const at = first ? t.toLowerCase().indexOf(first.toLowerCase()) : -1;
    if (at < 0) return t.slice(0, 200) + (t.length > 200 ? "…" : "");
    const a = Math.max(0, at - 90), z = Math.min(t.length, at + 110);
    return (a ? "…" : "") + t.slice(a, z) + (z < t.length ? "…" : "");
  }

  // ------------------------------------------------------------------ matter picker
  function MatterPicker({ seed, current, onPick }) {
    const [q, setQ] = React.useState(seed || "");
    const [rows, setRows] = React.useState(null);
    const [err, setErr] = React.useState(null);
    React.useEffect(() => {
      const t = q.trim();
      if (t.length < 2) { setRows(null); return; }
      let live = true;
      const h = setTimeout(() => API().findMatters(t).then((r) => live && setRows(r)).catch((e) => live && setErr(e)), 250);
      return () => { live = false; clearTimeout(h); };
    }, [q]);
    return (
      <div className="ed-lr-picker">
        <input className="ed-input" autoFocus value={q} onChange={(e) => setQ(e.target.value)}
               placeholder="Find the right matter: name, title or LNC-…" />
        {err && <div className="login__err">{String(err.message || err)}</div>}
        {rows && rows.length === 0 && <div className="ed-muted">No matters match.</div>}
        {rows && rows.map((m) => (
          <button key={m.matter_id} type="button" disabled={m.matter_id === current}
                  className="ed-lr-pick" onClick={() => onPick(m)}>
            <b>{m.display_alias || m.working_title}</b>
            <span className="ed-muted"> {[m.display_id, m.crime_location_state, m.lane && T(m.lane)].filter(Boolean).join(" · ")}
              {m.matter_id === current ? " · proposed now" : ""}</span>
          </button>
        ))}
      </div>
    );
  }

  // ------------------------------------------------------------------ one proposal
  function Row({ r, siblings, selected, onToggle, onDone, onError, showShow }) {
    const { Button, Input } = DS;
    const [mode, setMode] = React.useState(null); // null | "reject" | "swap"
    const [why, setWhy] = React.useState("");
    const [role, setRole] = React.useState(r.role || "primary");
    const [pick, setPick] = React.useState(null);  // a matter chosen in the picker
    const [busy, setBusy] = React.useState(false);
    React.useEffect(() => { icons(); }, [mode, pick]);
    const e = ev(r);

    async function confirm() {
      setBusy(true);
      try {
        await API().decideLink(r.piece_matter_id, "confirm", {
          matterId: pick ? pick.matter_id : null, role: role !== r.role ? role : null, note: why || null });
        onDone(r, pick ? `Linked “${r.piece_title}” to ${pick.display_alias || pick.working_title} instead of ${r.matter_label}`
                       : `Confirmed: ${r.piece_title} → ${r.matter_label}`);
      } catch (x) { onError(x.message || String(x)); setBusy(false); }
    }
    async function reject() {
      setBusy(true);
      try {
        await API().decideLink(r.piece_matter_id, "reject", { note: why || null });
        onDone(r, `Rejected: ${r.piece_title} → ${r.matter_label}`);
      } catch (x) { onError(x.message || String(x)); setBusy(false); }
    }

    const titleField = e.field && e.field !== "description" && e.field !== "short_description";
    return (
      <div className={"ed-lr-row" + (selected ? " is-sel" : "")}>
        <label className="ed-lr-check" title={bulkEligible(r) ? "Select" : "Needs a look: under 0.90, several candidates, or found only in the description. Can still be ticked by hand."}>
          <input type="checkbox" checked={selected} onChange={() => onToggle(r)} disabled={busy} />
        </label>
        <div className="ed-lr-body">
          <div className="ed-lr-top">
            <a className="ed-lr-ep" href={`#ed/ep/${r.piece_display_id}`} title="Open the episode">
              {titleField ? <Hi text={r[e.field === "title" ? "piece_title" : "piece_" + e.field] || r.piece_title} name={e.name} /> : r.piece_title}
            </a>
            <span className="ed-lr-meta">
              <span className={"ed-lr-src ed-lr-src--" + r.link_source}>{e.via === "show_master" ? "Show master sheet" : (SOURCE[r.link_source] || T(r.link_source))}</span>
              <Conf value={r.confidence} />
            </span>
          </div>
          <div className="ed-muted">
            {[showShow && r.show_label, fmtDate(r.air_date), r.house_number, r.piece_display_id].filter(Boolean).join(" · ")}
          </div>
          <div className="ed-lr-ev">
            {e.name && e.field !== "show_master_folder" && <span>Found <b>“{T(e.name)}”</b> in the {FIELD[e.field] || T(e.field || "text")}. </span>}
            {e.via === "show_master" && !e.name && e.folder && <span>The matter's media folder matches the server folder in the master sheet. </span>}
            {e.via === "show_master" && e.folder && (
              <div className="ed-lr-quote" title="The server folder in the master sheet; the matter's media path ends in the same folder">{e.folder}</div>
            )}
            {e.via === "show_master" && (e.methods || []).length > 1 && (
              <div className="ed-muted">Agree: {(e.methods || []).map((m) => MASTER_METHOD[m] || m).join(" + ")}.</div>
            )}
            {e.field === "description" && r.piece_description && (
              <div className="ed-lr-quote"><Hi text={snippet(r.piece_description, e.name)} name={e.name} /></div>
            )}
            {e.why && <div className="ed-lr-why"><Ic n="sparkles" /> {e.why}</div>}
            {r.piece_pending > 1 && (
              <div className="ed-lr-warn"><Ic n="alert-triangle" /> {r.piece_pending} matters are proposed for this episode
                {siblings && siblings.length ? <> (also <b>{siblings.join(", ")}</b>)</> : ""}. Confirm the ones it covers.</div>
            )}
          </div>

          {mode === "swap" && (
            pick ? (
              <div className="ed-lr-chosen">
                Link to <b>{pick.display_alias || pick.working_title}</b> <span className="ed-muted">{pick.display_id}</span> instead.{" "}
                <button type="button" className="link-btn" onClick={() => setPick(null)} disabled={busy}>Choose again</button>
              </div>
            ) : (
              <MatterPicker seed={T(e.name || "")} current={r.matter_id} onPick={setPick} />
            )
          )}

          <div className="ed-lr-actions">
            {mode === "reject" ? (
              <>
                <Input value={why} onChange={(x) => setWhy(x.target.value)} placeholder="Why? (optional, kept on the record)" />
                <button type="button" className="link-btn" onClick={() => setMode(null)} disabled={busy}>Cancel</button>
                <Button size="sm" variant="secondary" icon="x" onClick={reject} disabled={busy}>{busy ? "…" : "Confirm reject"}</Button>
              </>
            ) : (
              <>
                <select className="lc-input ed-lr-role" value={role} onChange={(x) => setRole(x.target.value)} disabled={busy}
                        title="Primary: the episode is about this matter. Secondary: a second case it covers. Mention: in passing.">
                  {ROLES.map((x) => <option key={x} value={x}>{T(x)}</option>)}
                </select>
                <button type="button" className="link-btn" onClick={() => setMode("reject")} disabled={busy}>Reject</button>
                <button type="button" className="link-btn" disabled={busy}
                        onClick={() => { setPick(null); setMode(mode === "swap" ? null : "swap"); }}>
                  {mode === "swap" ? "Keep this matter" : "Different matter…"}
                </button>
                <Button size="sm" variant="primary" icon="check" onClick={confirm} disabled={busy || (mode === "swap" && !pick)}>
                  {busy ? "Saving…" : pick ? "Confirm new matter" : "Confirm"}
                </Button>
              </>
            )}
          </div>
        </div>
      </div>
    );
  }

  // A card is one matter: every proposed episode for it, judged together.
  function MatterCard({ g, siblingsOf, selected, onToggle, onSelectGroup, onBulk, bulkBusy, onDone, onError, onOpenMatter, showShow }) {
    const { Button } = DS;
    const [armed, setArmed] = React.useState(null);
    const boxRef = React.useRef(null);
    const sel = g.rows.filter((r) => selected.has(r.piece_matter_id));
    const allSel = sel.length === g.rows.length, some = sel.length > 0 && !allSel;
    React.useEffect(() => { if (boxRef.current) boxRef.current.indeterminate = some; }, [some]);
    React.useEffect(() => { if (!sel.length) setArmed(null); }, [sel.length]);
    const look = sel.filter(needsLook).length;
    return (
      <div className="ed-card ed-lr-card">
        <div className="ed-lr-mhead">
          <label className="ed-lr-check" title={allSel ? "Clear this matter" : "Select every proposal for this matter"}>
            <input ref={boxRef} type="checkbox" checked={allSel} disabled={bulkBusy} onChange={() => onSelectGroup(g.rows, !allSel)} />
          </label>
          <button type="button" className="ed-lr-mlink" onClick={() => onOpenMatter && onOpenMatter(g.matter_id)} title="Open the matter">
            {g.label || "Untitled matter"}
          </button>
          <span className="ed-muted">{[g.display_id, g.state, g.lane && T(g.lane)].filter(Boolean).join(" · ")}</span>
          {g.working_title && g.working_title !== g.label && <span className="ed-muted ed-lr-wt">“{g.working_title}”</span>}
          <span style={{ flex: 1 }} />
          {sel.length === 0 ? (
            <span className="ed-muted">{g.rows.length} episode{g.rows.length === 1 ? "" : "s"}</span>
          ) : (
            <span className="ed-lr-gact">
              <span className="ed-muted">{sel.length} selected{look > 0 && <b className="ed-lr-look"> · {look} need a look</b>}</span>
              {armed ? (
                <>
                  <button type="button" className="link-btn" onClick={() => setArmed(null)} disabled={bulkBusy}>Cancel</button>
                  <Button size="sm" variant={armed === "confirm" ? "primary" : "secondary"} icon={armed === "confirm" ? "check-check" : "x"}
                          onClick={async () => { await onBulk(armed, sel); setArmed(null); }} disabled={bulkBusy}>
                    {bulkBusy ? "Saving…" : `Yes, ${armed} ${sel.length}`}
                  </Button>
                </>
              ) : (
                <>
                  <button type="button" className="link-btn" onClick={() => setArmed("reject")} disabled={bulkBusy}>Reject {sel.length}</button>
                  <Button size="sm" variant="primary" icon="check-check" onClick={() => setArmed("confirm")} disabled={bulkBusy}>Confirm {sel.length}</Button>
                </>
              )}
            </span>
          )}
        </div>
        {g.rows.map((r) => (
          <Row key={r.piece_matter_id} r={r} siblings={siblingsOf(r)} selected={selected.has(r.piece_matter_id)} showShow={showShow}
               onToggle={onToggle} onDone={onDone} onError={onError} />
        ))}
      </div>
    );
  }

  // ------------------------------------------------------------------ decided
  function Decided({ rows, onOpenMatter }) {
    const [person, setPerson] = React.useState("");
    const people = Array.from(new Set(rows.map((r) => r.decided_email).filter(Boolean))).sort();
    const shown = person ? rows.filter((r) => r.decided_email === person) : rows;
    const label = (r) => (r.status === "confirmed" ? (r.link_source === "human" ? "Linked by hand" : "Confirmed")
                         : r.replaced_by ? "Replaced" : "Rejected");
    return (
      <div>
        <div className="ed-filters">
          <select className="ed-input" value={person} onChange={(e) => setPerson(e.target.value)} aria-label="Decided by">
            <option value="">Everyone</option>
            {people.map((p) => <option key={p} value={p}>{p}</option>)}
          </select>
          <span className="ed-muted">Last 7 days · {shown.length}</span>
        </div>
        {shown.length === 0 ? (
          <div className="ledger-empty"><Ic n="inbox" /><b>No decisions yet</b><span>Confirmations and rejections from the last 7 days show here.</span></div>
        ) : (
          <div className="ed-tablewrap">
            <table className="ed-table">
              <thead><tr><th style={{ width: 130 }}>When</th><th style={{ width: 110 }}>Who</th><th>Episode</th><th style={{ width: "28%" }}>Matter</th><th style={{ width: 120 }}>Decision</th></tr></thead>
              <tbody>
                {shown.map((r) => (
                  <tr key={r.piece_matter_id}>
                    <td className="ed-muted">{fmtWhen(r.decided_at)}</td>
                    <td>{who(r.decided_email)}</td>
                    <td><a href={`#ed/ep/${r.piece_display_id}`} className="ed-lab">{r.piece_title}</a>
                      <div className="ed-muted">{r.show_label} · {fmtDate(r.air_date)}</div>
                      {r.decision_note && <div className="ed-muted">“{r.decision_note}”</div>}</td>
                    <td><a href="#" className="ed-lab" onClick={(e) => { e.preventDefault(); onOpenMatter && onOpenMatter(r.matter_id); }}>{r.matter_label}</a>
                      <div className="ed-muted">{r.matter_display_id}{r.role && r.role !== "primary" ? ` · ${T(r.role)}` : ""}</div></td>
                    <td><span className={"ed-pill " + (r.status === "confirmed" ? "ok" : "n")}>{label(r)}</span></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    );
  }

  // ------------------------------------------------------------------ reviewers
  function Reviewers({ shows, isAdmin }) {
    const { Button } = DS;
    const [rows, setRows] = React.useState(null);
    const [people, setPeople] = React.useState([]);
    const [err, setErr] = React.useState(null);
    const [msg, setMsg] = React.useState(null);
    const [draft, setDraft] = React.useState({});
    const [busy, setBusy] = React.useState(false);
    const load = React.useCallback(() => { API().reviewers().then(setRows).catch(setErr); }, []);
    React.useEffect(() => {
      load();
      // Admins read the allow-list for the email suggestions; nobody else can.
      if (isAdmin && window.LedgerAPI.listAccess) window.LedgerAPI.listAccess().then((a) => setPeople(a.map((x) => x.email))).catch(() => {});
    }, [load, isAdmin]);
    React.useEffect(() => { icons(); });

    async function set(email, show, on) {
      setBusy(true); setErr(null); setMsg(null);
      try {
        const r = await API().setReviewer(email, show, on);
        setMsg(on ? `${r.email} now reviews ${show.replace(/_/g, " ")}${r.staff_created ? " (added to the staff directory)" : ""}.`
                  : `${email} no longer reviews ${show.replace(/_/g, " ")}.`);
        setDraft((d) => ({ ...d, [show]: "" }));
        load();
      } catch (e) { setErr(e.message || String(e)); }
      setBusy(false);
    }

    const by = {};
    (rows || []).forEach((r) => { (by[r.show_key] = by[r.show_key] || []).push(r); });
    const list = shows.filter((s) => s.enabled);
    return (
      <div>
        <p className="ed-lede">
          A show's producers confirm that show's links. Content Operations can confirm every show and backs them up.
          {isAdmin ? " Add a producer by email. They also need Workspace and Editorial on the Access tab." : " Admins assign producers."}
        </p>
        {err && <div className="login__err" style={{ marginBottom: 12 }}>{String(err.message || err)}</div>}
        {msg && <div className="ledger-ok" style={{ marginBottom: 12 }}>{msg}</div>}
        {rows === null && !err && <div className="ed-muted">Loading…</div>}
        {isAdmin && <datalist id="ed-lr-people">{people.map((p) => <option key={p} value={p} />)}</datalist>}
        {rows && (
          <div className="ed-tablewrap">
            <table className="ed-table">
              <thead><tr><th style={{ width: 200 }}>Show</th><th>Producers</th>{isAdmin && <th style={{ width: 330 }}>Add</th>}</tr></thead>
              <tbody>
                {list.map((s) => (
                  <tr key={s.show_key}>
                    <td><b>{s.label}</b><div className="ed-muted">{(s.proposed || 0).toLocaleString()} episodes with proposals</div></td>
                    <td>
                      {(by[s.show_key] || []).length === 0 && <span className="ed-muted">None. Content Operations covers it.</span>}
                      {(by[s.show_key] || []).map((r) => (
                        <span key={r.email} className="ed-lr-person" title={r.email}>
                          {r.name || r.email}
                          {(!r.has_workspace || !r.has_editorial) && (
                            <span className="ed-lr-warnchip" title="Can't use Link Review until granted on the Access tab">
                              needs {[!r.has_workspace && "Workspace", !r.has_editorial && "Editorial"].filter(Boolean).join(" + ")}
                            </span>
                          )}
                          {isAdmin && <button type="button" className="ed-lr-x" disabled={busy} title="Remove" onClick={() => set(r.email, s.show_key, false)}>×</button>}
                        </span>
                      ))}
                    </td>
                    {isAdmin && (
                      <td>
                        <div className="ed-lr-add">
                          <input className="ed-input" list="ed-lr-people" placeholder="name@lawnewz.com" value={draft[s.show_key] || ""}
                                 onChange={(e) => setDraft((d) => ({ ...d, [s.show_key]: e.target.value }))} />
                          <Button size="sm" variant="secondary" icon="user-plus" disabled={busy || !(draft[s.show_key] || "").includes("@")}
                                  onClick={() => set(draft[s.show_key].trim(), s.show_key, true)}>Add</Button>
                        </div>
                      </td>
                    )}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    );
  }

  // ------------------------------------------------------------------ root
  function LinkReview({ onOpenMatter }) {
    const { Button } = DS;
    const caps = window.LedgerCaps || {};
    const [tab, setTab] = React.useState("pending");
    const [scope, setScope] = React.useState(null);
    const [shows, setShows] = React.useState([]);
    const [rows, setRows] = React.useState(null);
    const [decided, setDecided] = React.useState(null);
    const [counts, setCounts] = React.useState(null);
    const [matCounts, setMatCounts] = React.useState(null); // Material tab badge (E3b)
    const [show, setShow] = React.useState("");
    const [src, setSrc] = React.useState("all");
    const [floor, setFloor] = React.useState(0);
    const [lookOnly, setLookOnly] = React.useState(false);
    const [q, setQ] = React.useState("");
    const [shown, setShown] = React.useState(PAGE);
    const [selected, setSelected] = React.useState(() => new Set());
    const [armed, setArmed] = React.useState(null);
    const [bulkBusy, setBulkBusy] = React.useState(false);
    const [err, setErr] = React.useState(null);
    const [msg, setMsg] = React.useState(null);

    const refreshCounts = React.useCallback(() => { API().linkCounts().then(setCounts).catch(() => {}); }, []);
    React.useEffect(() => {
      API().shows().then(setShows).catch(() => {});
      API().reviewScope().then((s) => {
        setScope(s);
        return Promise.all([API().linkQueue({ status: "pending", scope: s }), API().linkCounts()]);
      }).then(([r, c]) => { setRows(r); setCounts(c); }).catch((e) => setErr(e.message || String(e)));
      if (API().usageCounts) API().usageCounts().then(setMatCounts).catch(() => {});
    }, []);
    React.useEffect(() => {
      if (tab !== "decided" || !scope) return;
      setDecided(null);
      API().linkQueue({ status: "decided", scope }).then(setDecided).catch((e) => setErr(e.message || String(e)));
    }, [tab, scope]);
    React.useEffect(() => { icons(); });
    React.useEffect(() => { setShown(PAGE); }, [show, src, floor, lookOnly, q]);
    React.useEffect(() => { setMsg(null); setErr(null); }, [tab]);

    const label = Object.fromEntries(shows.map((s) => [s.show_key, s.label]));
    const filtered = React.useMemo(() => {
      const needle = q.trim().toLowerCase();
      return (rows || []).filter((r) => (!show || r.show_key === show) && (src === "all" || r.link_source === src) &&
        (conf(r) || 0) >= floor &&
        (!lookOnly || needsLook(r)) &&
        (!needle || [r.piece_title, r.piece_working_title, r.matter_label, r.matter_display_id, r.piece_display_id, r.house_number]
          .join(" ").toLowerCase().includes(needle)));
    }, [rows, show, src, floor, lookOnly, q]);

    // Show, then matter. Inside a show, the matters with the most episodes first.
    const sections = React.useMemo(() => {
      const byShow = new Map();
      filtered.forEach((r) => {
        let s = byShow.get(r.show_key);
        if (!s) { s = { show_key: r.show_key, label: r.show_label, groups: new Map(), n: 0 }; byShow.set(r.show_key, s); }
        let g = s.groups.get(r.matter_id);
        if (!g) { g = { matter_id: r.matter_id, label: r.matter_label, working_title: r.matter_working_title, display_id: r.matter_display_id, state: r.state, lane: r.lane, rows: [] }; s.groups.set(r.matter_id, g); }
        g.rows.push(r); s.n++;
      });
      return Array.from(byShow.values()).sort((a, b) => b.n - a.n)
        .map((s) => ({ ...s, groups: Array.from(s.groups.values()).sort((a, b) => b.rows.length - a.rows.length) }));
    }, [filtered]);
    // Page by cards across sections.
    let budget = shown;
    const paged = sections.map((s) => { const g = s.groups.slice(0, Math.max(0, budget)); budget -= g.length; return { ...s, groups: g }; })
      .filter((s) => s.groups.length);
    const totalCards = sections.reduce((a, s) => a + s.groups.length, 0);

    // The other matters proposed for the same episode, named on the row's warning.
    const byPiece = React.useMemo(() => {
      const m = new Map();
      (rows || []).forEach((r) => { if (!m.has(r.piece_id)) m.set(r.piece_id, []); m.get(r.piece_id).push(r); });
      return m;
    }, [rows]);
    const siblingsOf = (r) => (byPiece.get(r.piece_id) || []).filter((o) => o.piece_matter_id !== r.piece_matter_id).map((o) => o.matter_label);
    const eligible = filtered.filter(bulkEligible);
    const selRows = (rows || []).filter((r) => selected.has(r.piece_matter_id));

    function toggle(r) {
      setSelected((s) => { const n = new Set(s); n.has(r.piece_matter_id) ? n.delete(r.piece_matter_id) : n.add(r.piece_matter_id); return n; });
      setArmed(null);
    }
    function selectGroup(list, on) {
      setSelected((s) => { const n = new Set(s); list.forEach((r) => (on ? n.add(r.piece_matter_id) : n.delete(r.piece_matter_id))); return n; });
      setArmed(null);
    }
    function removeIds(ids) {
      const drop = new Set(ids);
      setRows((rs) => (rs || []).filter((r) => !drop.has(r.piece_matter_id)));
      setSelected((s) => { const n = new Set(s); ids.forEach((id) => n.delete(id)); return n; });
    }
    async function done(r, how) {
      removeIds([r.piece_matter_id]);
      setMsg(how);
      refreshCounts();
      if (API().usageCounts) API().usageCounts().then(setMatCounts).catch(() => {});
      if (r.piece_pending > 1) {
        // Siblings' piece_pending is now stale and one may have been confirmed; reload them.
        try {
          const fresh = await API().linkQueue({ status: "pending", scope });
          setRows(fresh);
        } catch (e) { /* the next load catches up */ }
      }
    }
    async function runBulk(decision, target) {
      const list = target || selRows;
      setBulkBusy(true); setErr(null); setMsg(null);
      try {
        const res = await API().decideLinks(list.map((r) => ({ id: r.piece_matter_id, decision })));
        const ok = res.filter((x) => x.ok).map((x) => x.id);
        const bad = res.filter((x) => !x.ok);
        removeIds(ok);
        setMsg(`${ok.length} ${decision === "confirm" ? "confirmed" : "rejected"}.`);
        if (bad.length) setErr(`${bad.length} not saved: ` + Array.from(new Set(bad.map((x) => x.error))).slice(0, 3).join("; "));
        if (list.some((r) => r.piece_pending > 1)) setRows(await API().linkQueue({ status: "pending", scope }));
      } catch (e) { setErr(e.message || String(e)); }
      setBulkBusy(false); setArmed(null);
      refreshCounts();
      if (API().usageCounts) API().usageCounts().then(setMatCounts).catch(() => {});
    }

    const canSeeReviewers = !!caps.isAdmin || !!(scope && scope.all);
    const myShows = scope ? (scope.all ? null : scope.shows) : [];
    const showKeys = Object.keys((counts && counts.by_show) || {}).sort((a, b) => counts.by_show[b] - counts.by_show[a]);

    return (
      <div className="ed-lr">
        <div className="page-head">
          <div>
            <div className="eyebrow">Editorial</div>
            <h1 className="page-title">Link Review<window.LedgerComment section="Link Review" /></h1>
            <div className="page-lede">
              Matters the linker thinks each episode covers, found from names in the episode's title. A link counts only
              once someone here confirms it. Pick a different matter when it found the right person on the wrong case.
              {myShows && myShows.length > 0 && <> You review <b>{myShows.map((k) => label[k] || T(k)).join(", ")}</b>.</>}
            </div>
          </div>
          {counts && (
            <div className="ed-lr-stats">
              <div><b>{counts.pending.toLocaleString()}</b><span>to review</span></div>
              <div><b>{counts.decided_today_me || 0}</b><span>by you today</span></div>
              <div><b>{counts.decided_today || 0}</b><span>team today</span></div>
            </div>
          )}
        </div>

        <div className="ed-lr-tabs">
          <button type="button" className={"tagopt" + (tab === "pending" ? " tagopt--on" : "")} onClick={() => setTab("pending")}>Needs a decision</button>
          <button type="button" className={"tagopt" + (tab === "decided" ? " tagopt--on" : "")} onClick={() => setTab("decided")}>Decided</button>
          {window.LedgerEditorialMaterial && (
            <button type="button" className={"tagopt" + (tab === "material" ? " tagopt--on" : "")} onClick={() => setTab("material")}
                    title="What material each confirmed episode used">
              Material{matCounts && matCounts.pending ? ` (${matCounts.pending.toLocaleString()})` : ""}</button>
          )}
          {canSeeReviewers && <button type="button" className={"tagopt" + (tab === "reviewers" ? " tagopt--on" : "")} onClick={() => setTab("reviewers")}>Reviewers</button>}
        </div>

        {err && <div className="login__err" style={{ marginBottom: 12 }}>{err}</div>}
        {msg && <div className="ledger-ok" style={{ marginBottom: 12 }}>{msg}</div>}

        {tab === "reviewers" ? <Reviewers shows={shows} isAdmin={!!caps.isAdmin} />
         : tab === "material" ? (scope ? <window.LedgerEditorialMaterial scope={scope} shows={shows} onOpenMatter={onOpenMatter} onCountsChanged={setMatCounts} />
                                       : <div className="ed-muted">Loading…</div>)
         : tab === "decided" ? (decided === null ? <div className="ed-muted">Loading…</div> : <Decided rows={decided} onOpenMatter={onOpenMatter} />)
         : (
          <>
            {showKeys.length > 1 && (
              <div className="ed-filters">
                <button type="button" className={"ed-chip" + (!show ? " on" : "")} onClick={() => setShow("")}>All shows <b>{counts.pending}</b></button>
                {showKeys.map((k) => (
                  <button key={k} type="button" className={"ed-chip" + (show === k ? " on" : "")} onClick={() => setShow(show === k ? "" : k)}>
                    {label[k] || T(k)} <b>{counts.by_show[k]}</b>
                  </button>
                ))}
              </div>
            )}
            <div className="ed-filters">
              <select className="ed-input" value={src} onChange={(e) => setSrc(e.target.value)} aria-label="How it was found">
                <option value="all">Any source</option>
                <option value="title_match">Title match (one matter carries the name)</option>
                <option value="ai">AI suggestion (it chose between matters)</option>
              </select>
              <select className="ed-input" value={floor} onChange={(e) => setFloor(Number(e.target.value))} aria-label="Minimum confidence"
                      title="Minimum confidence. Title matches score 0.90 at most, so 0.95 leaves only AI suggestions.">
                <option value={0}>Any confidence</option>
                <option value={0.9}>≥ 0.90</option>
                <option value={0.95}>≥ 0.95</option>
              </select>
              <button type="button" className={"ed-chip" + (lookOnly ? " on" : "")} onClick={() => setLookOnly((v) => !v)}
                      title="Under 0.90, several candidates for one episode, or found only in the description">Needs a look</button>
              <input className="ed-input ed-lr-find" value={q} onChange={(e) => setQ(e.target.value)} placeholder="Find an episode or matter…" />
              {rows && <span className="ed-muted">{filtered.length.toLocaleString()} proposal{filtered.length === 1 ? "" : "s"} · {totalCards.toLocaleString()} matter{totalCards === 1 ? "" : "s"}</span>}
            </div>

            <div className="ed-lr-bulk">
              <div>
                {selRows.length > 0
                  ? <span><b>{selRows.length}</b> selected. <button type="button" className="link-btn" onClick={() => { setSelected(new Set()); setArmed(null); }}>Clear</button></span>
                  : <span><b>{eligible.length.toLocaleString()}</b> here are 0.90 or better, the only candidate for their episode, and found in a title. They can be selected together.</span>}
              </div>
              <span className="ed-lr-bulk-act">
                {selRows.length === 0 ? (
                  <Button size="sm" variant="secondary" icon="check-square" disabled={!eligible.length}
                          onClick={() => setSelected(new Set(eligible.map((r) => r.piece_matter_id)))}>Select {eligible.length.toLocaleString()}</Button>
                ) : armed ? (
                  <>
                    <button type="button" className="link-btn" onClick={() => setArmed(null)} disabled={bulkBusy}>Cancel</button>
                    <Button size="sm" variant={armed === "confirm" ? "primary" : "secondary"} icon={armed === "confirm" ? "check-check" : "x"}
                            onClick={() => runBulk(armed)} disabled={bulkBusy}>{bulkBusy ? "Saving…" : `Yes, ${armed} ${selRows.length}`}</Button>
                  </>
                ) : (
                  <>
                    <button type="button" className="link-btn" onClick={() => setArmed("reject")}>Reject selected</button>
                    <Button size="sm" variant="primary" icon="check-check" onClick={() => setArmed("confirm")}>Confirm selected ({selRows.length})</Button>
                  </>
                )}
              </span>
            </div>

            {rows === null && !err && <div className="ed-muted">Loading…</div>}
            {rows && sections.length === 0 && (
              <div className="ledger-empty"><Ic n="check-check" /><b>Nothing waiting here</b>
                <span>Every proposal in this view has been decided. New episodes are linked nightly.</span></div>
            )}
            {paged.map((s) => (
              <div key={s.show_key} className="ed-lr-show">
                {(!show && sections.length > 1) && <h3 className="ed-sec">{s.label} <span className="ed-muted">{s.n} proposal{s.n === 1 ? "" : "s"}</span></h3>}
                {s.groups.map((g) => (
                  <MatterCard key={g.matter_id} g={g} siblingsOf={siblingsOf} selected={selected} onToggle={toggle} onSelectGroup={selectGroup}
                              onBulk={runBulk} bulkBusy={bulkBusy} onDone={done} onError={setErr} onOpenMatter={onOpenMatter} showShow={false} />
                ))}
              </div>
            ))}
            {totalCards > shown && (
              <div style={{ textAlign: "center", margin: "14px 0" }}>
                <Button size="sm" variant="secondary" onClick={() => setShown((n) => n + PAGE)}>Show more ({(totalCards - shown).toLocaleString()} matters left)</Button>
              </div>
            )}
          </>
        )}
      </div>
    );
  }

  window.LedgerEditorialReview = LinkReview;
})();
