/* OpsQueue — the cross-matter Asset Requests worklist, first sub-tab of the
   Content Operations app. The sidebar and route guard show it to `isContentOps`
   (content-center AND workspace, admins included). The data gate is RLS on
   asset_requests, which is is_workspace(): the capability decides who sees the
   desk, not who may read the rows. Filters (status/type/show/
   assignee/overdue), inline advance + assign, KPI tiles. Assigns window.LedgerOpsQueue.
   Reads the merged asset_requests table (migrations 0048/0051) via LedgerAPI.

   SERVER-SIDE SINCE 0092. Filters, counts, ordering and paging all run in
   Postgres. Before that the desk fetched a page and filtered inside it, which
   was invisible at the ~5 rows this table has held and would have been a desk
   quietly describing an arbitrary slice of A4's 9,655 requests. Pre-A4 work,
   done before the volume rather than after — the same call C1 made. */
(function () {
  const DS = window.LawCrimeDesignSystem_e18d68;
  const T = window.titleize;
  const V = window.LEDGER_VOCAB;
  const K = window.LedgerRequestKit;
  const icons = () => setTimeout(() => window.lucide && window.lucide.createIcons(), 30);

  // `startOfWeek` / `sameMonth` lived here to tally the KPI tiles in the browser.
  // Migration 0092 computes all four in Postgres, so both are gone — a rolling
  // 7-day window and a calendar month are now defined once, on the server.

  // THE DUE-DATE RAIL, folded in from Worklist.jsx on 2026-09-11. It used to be a
  // separate surface — the Police Videos lane home — reachable only by switching
  // the chrome's lane, which also renamed the sidebar tab and replaced the page.
  // The lane is a filter again (see app.jsx), so the rail moved to the desk that
  // owns these rows. The binary "Overdue only" toggle it replaces could only ask
  // one of the five questions the desk actually asks.
  //
  // `tone` drives the chip colour: overdue is the only one that is a problem, so
  // it is the only one that shouts. "Open" is first because it is the unfiltered
  // desk, and a rail with no way back to everything is a trap.
  const BUCKETS = [
    { id: "all",       label: "Open",        icon: "inbox",          tone: "neutral" },
    { id: "overdue",   label: "Overdue",     icon: "alarm-clock",    tone: "bad" },
    { id: "today",     label: "Today",       icon: "calendar-check", tone: "hot" },
    { id: "this_week", label: "This week",   icon: "calendar-days",  tone: "active" },
    { id: "later",     label: "Later",       icon: "calendar",       tone: "neutral" },
    { id: "no_due",    label: "No due date", icon: "calendar-off",   tone: "muted" },
  ];

  function Kpi({ label, value, tone }) {
    return (
      <div className={"opsq-kpi" + (tone ? " opsq-kpi--" + tone : "")}>
        <div className="opsq-kpi-val">{value}</div>
        <div className="opsq-kpi-lbl">{label}</div>
      </div>
    );
  }

  // `embedded`: rendered as the Asset Requests calendar's List view (Content Ops Phase 1),
  // which owns the page heading. `initialFilter` seeds the filters from there
  // (owner, due bucket), so "Open in list" lands on the same slice.
  function OpsQueue({ onOpenMatter, embedded, initialFilter }) {
    const { Select } = DS;
    const [rows, setRows] = React.useState(null);
    const [staff, setStaff] = React.useState([]);          // everyone: names for history
    const [assignable, setAssignable] = React.useState([]); // Content Center, Assignable (0137)
    const [shows, setShows] = React.useState([]);
    const [err, setErr] = React.useState(null);
    const [flt, setFlt] = React.useState(() => ({ status: "all", request_type: "all", show: "all", assignee: "all", due: "all", ...(initialFilter || {}) }));
    const [dueCounts, setDueCounts] = React.useState(null);
    const SortTh = window.LedgerSortKit.Th;
    // Unchanged default: soonest due first, which is the order the desk is worked in.
    const { sort, toggle } = window.LedgerSortKit.useSort("due_date", "asc");
    const [savingId, setSavingId] = React.useState(null);
    const [openId, setOpenId] = React.useState(null);   // request detail open on the desk

    const [total, setTotal] = React.useState(0);
    const [page, setPage] = React.useState(0);
    const [kpis, setKpis] = React.useState(null);
    const PAGE_SIZE = 50;

    // A filter change re-queries from the first page. Leaving `page` where it was
    // lands you on page 4 of a 2-page result and reads as an empty desk.
    React.useEffect(() => { setPage(0); }, [flt, sort]);

    const load = React.useCallback(() => {
      // Every filter below is applied by Postgres, not by the browser (0092 +
      // Worklist.jsx's precedent). Until this change the desk fetched 200 rows and
      // filtered within them, so at A4's 9,655 requests "Overdue only" would have
      // shown the overdue rows *of an arbitrary 200* and looked authoritative.
      window.LedgerAPI.listAssetRequestQueue({
        status: flt.status,
        request_type: flt.request_type,
        show: flt.show,
        // "Unassigned" is an IS NULL, not an equality — the API takes it as a
        // distinct token rather than a staff id.
        assignee: flt.assignee,
        due: flt.due,
        // An explicit status choice wins over "open only". With no status chosen
        // the desk is OPEN requests — which is a change: it used to mean literally
        // every row, terminal ones included. It has to match, because the rail
        // above counts open requests, and a bucket whose number disagrees with the
        // list under it is worse than no number. The status select says "Open
        // requests" rather than "All statuses" so the default is stated, and every
        // terminal status is still individually selectable.
        openOnly: flt.status === "all",
        sort,
        page, pageSize: PAGE_SIZE,
      })
        .then((res) => { setRows(res.rows); setTotal(res.total); })
        .catch((e) => { setErr(e.message || String(e)); setRows([]); });
    }, [flt, sort, page]);
    React.useEffect(() => { load(); }, [load]);

    // Desk-wide tiles, one round trip, independent of the filters above.
    const loadKpis = React.useCallback(() => {
      window.LedgerAPI.assetRequestKpis().then(setKpis).catch(() => setKpis(null));
    }, []);
    React.useEffect(() => { loadKpis(); }, [loadKpis]);
    // The rail follows the OWNER filter, because "what is overdue" and "what is
    // overdue for me" are different questions and the numbers should answer
    // whichever one is selected. It deliberately does not follow status / type /
    // show: those narrow the list, and a rail that shrank with every filter would
    // stop being the desk's backlog and start being a restatement of the table.
    React.useEffect(() => {
      let live = true;
      window.LedgerAPI.requestDueCounts({ openOnly: true, assignee: flt.assignee })
        .then((c) => { if (live) setDueCounts(c); })
        .catch(() => { if (live) setDueCounts(null); });
      return () => { live = false; };
    }, [flt.assignee]);

    React.useEffect(() => {
      window.LedgerAPI.listStaff().then(setStaff).catch(() => setStaff([]));
      window.LedgerAPI.listAssignableStaff().then(setAssignable).catch(() => setAssignable([]));
      window.LedgerAPI.showTaxonomy().then(setShows).catch(() => setShows([]));
    }, []);
    React.useEffect(() => { icons(); }, [rows, flt, savingId, dueCounts, sort]);

    const showLabel = K.showLabelMap(shows);
    const staffName = (id) => { const s = staff.find((x) => x.staff_id === id); return s ? `${s.first_name || ""} ${s.last_name || ""}`.trim() : "—"; };

    async function patchRow(id, patch) {
      setSavingId(id); setErr(null);
      try {
        const updated = await window.LedgerAPI.updateAssetRequest(id, patch);
        setRows((rs) => (rs || []).map((r) => (r.id === id ? { ...r, ...updated,
          // keep the client-side enrichment fields the update RPC doesn't return
          matter_title: r.matter_title, matter_display_id: r.matter_display_id,
          agency_name: r.agency_name, assignee_name: patch.assignee !== undefined ? staffName(patch.assignee) : r.assignee_name } : r)));
        // Advancing a request to a terminal status changes Open and Overdue. The
        // row is left in place on purpose — yanking it out from under the cursor
        // the instant you set its status is hostile — but the tiles must not lie.
        loadKpis();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setSavingId(null); }
    }

    // `filtered` is gone: the rows arriving here are already the filtered page.
    const all = rows || [];

    const set = (k) => (e) => setFlt((s) => ({ ...s, [k]: e.target.value }));
    // "Open requests", not "All statuses" — see openOnly in load() above.
    const statusFilterOpts = [{ value: "all", label: "Open requests" }].concat(V.request_status.map((s) => ({ value: s, label: T(s) })));
    const typeFilterOpts = [{ value: "all", label: "All types" }].concat(V.request_type.map((s) => ({ value: s, label: T(s) })));
    const showFilterOpts = [{ value: "all", label: "All shows" }].concat((shows || []).slice().sort((a, b) => (a.label || "").localeCompare(b.label || "")).map((s) => ({ value: s.show_key, label: s.label })));
    // Only Content Center staff marked Assignable can be picked (0137; the DB refuses
    // anyone else). A row still held by someone outside that list (closed history)
    // shows their name as a disabled option rather than going blank.
    const personOpt = (s) => ({ value: s.staff_id, label: `${s.first_name || ""} ${s.last_name || ""}`.trim() });
    const assignableIds = new Set(assignable.map((s) => s.staff_id));
    const assigneeFilterOpts = [{ value: "all", label: "All assignees" }, { value: "unassigned", label: "Unassigned" }]
      .concat(assignable.map(personOpt));
    // Inline per-row controls
    const statusRowOpts = V.request_status.map((s) => ({ value: s, label: T(s) }));
    const assigneeRowOpts = [{ value: "", label: "— unassigned —" }].concat(assignable.map(personOpt));
    const rowOptsFor = (r) => (r.assignee && !assignableIds.has(r.assignee))
      ? assigneeRowOpts.concat({ value: r.assignee, label: staffName(r.assignee) + " (not assignable)", disabled: true })
      : assigneeRowOpts;

    // Clicking a request opens its editable detail (fields + agency playbook + the cost /
    // deliveries ops panel) — the desk is where Content Center + Media Ops work a request.
    const openRow = openId ? all.find((r) => r.id === openId) : null;
    if (openRow) {
      return (
        <div className="opsq">
          <K.RequestDetail request={openRow}
            onBack={() => { setOpenId(null); load(); loadKpis(); }}
            onChanged={() => { load(); loadKpis(); }}
            onOpenMatter={onOpenMatter} />
        </div>
      );
    }

    return (
      <div className="opsq">
        {!embedded && <div className="page-head">
          <div>
            <div className="eyebrow">Content Operations</div>
            <h1 className="page-title">Asset Requests<window.LedgerComment section="Asset Requests list" /></h1>
            <p className="page-sub">Every FOIA / asset request across matters — assign, advance, and work the desk.</p>
          </div>
        </div>}

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

        {/* The "showing the first N of M" notice that stood here is retired: the
            filters and the tiles now both run over the whole desk, so there is no
            longer a slice to disclose. The pager below states the range instead. */}
        {/* Two tiles, not four. `Open` and `Overdue` are now the first two chips on
            the rail below, and printing the same number twice on one screen is how a
            desk starts to look like more information than it carries. What is left is
            what the rail cannot answer: arrival and completion, which are not due
            dates. `assetRequestKpis()` still returns all four — `open` feeds the
            rail's first chip. */}
        <div className="opsq-kpis opsq-kpis--pair">
          <Kpi label="New this week" value={kpis ? kpis.newWeek.toLocaleString() : "—"} tone="active" />
          <Kpi label="Received this month" value={kpis ? kpis.received.toLocaleString() : "—"} tone="done" />
        </div>

        {/* Bucket rail — the counts ARE the worklist; the table below is the drill-in.
            Due date is the work-shaping predicate on this desk, not a nicety: "if you
            don't give it a due date, it doesn't go anywhere" (Lacey Lecroy, 08-19). */}
        <div className="buckets">
          {BUCKETS.map((b) => (
            <button key={b.id} type="button"
              className={"bucket bucket--" + b.tone + (flt.due === b.id ? " bucket--on" : "")}
              aria-pressed={flt.due === b.id}
              onClick={() => setFlt((s) => ({ ...s, due: b.id }))}>
              <i data-lucide={b.icon} />
              <span className="bucket__n">
                {/* ⚠️ "Open" is NOT the sum of the other five. The buckets overlap —
                    `today` is [d0,d0] and `this_week` is [d0,d0+7], so d0 is inside
                    both — and summing them counts today's requests twice. It comes
                    from asset_request_kpis() instead, which is the same open-status
                    predicate the list below is filtered by, so the chip and the
                    pager under it can never disagree. */}
                {b.id === "all"
                  ? (kpis ? kpis.open.toLocaleString() : "—")
                  : (dueCounts ? (dueCounts[b.id] || 0).toLocaleString() : "—")}
              </span>
              <span className="bucket__l">{b.label}</span>
            </button>
          ))}
        </div>

        <div className="opsq-filters">
          <Select value={flt.status} onChange={set("status")} options={statusFilterOpts} />
          <Select value={flt.request_type} onChange={set("request_type")} options={typeFilterOpts} />
          <Select value={flt.show} onChange={set("show")} options={showFilterOpts} />
          <Select value={flt.assignee} onChange={set("assignee")} options={assigneeFilterOpts} />
        </div>

        {rows === null && <div style={{ color: "var(--text-muted)", fontSize: 13 }}>Loading…</div>}
        {rows && all.length === 0 && (
          <div className="ledger-empty"><i data-lucide="clipboard-check" />
            <b>Nothing {(BUCKETS.find((b) => b.id === flt.due) || {}).label ? (flt.due === "all" ? "on the desk" : (BUCKETS.find((b) => b.id === flt.due).label.toLowerCase())) : "here"}</b>
            <span>Try another bucket or owner, or file a request from a matter's Asset Requests tab.</span></div>
        )}
        {rows && all.length > 0 && (
          <DS.Card><DS.Card.Body style={{ padding: 0 }}>
            <div className="opsq-table-wrap">
              <table className="opsq-table">
                {/* ⚠️ FOUR OF THESE EIGHT COLUMNS CANNOT BE SORTED, and they say so by
                    rendering as plain headers rather than as dead buttons. `Matter`,
                    `Agency` and `Assignee` are joined in the browser below, after the
                    page is fetched, so Postgres has nothing to ORDER BY — offering the
                    control would sort the visible 50 of 11,007 and look authoritative.
                    `Content` is an array column (content_kind), which has no ordering
                    a reader would predict. The rest are real columns and sort across
                    the whole desk. Fixing the three joins means embedding those tables
                    in the PostgREST query; worth doing, not worth faking. */}
                <thead>
                  <tr>
                    <SortTh k="display_id" sort={sort} onSort={toggle}>Request</SortTh>
                    <SortTh>Matter</SortTh>
                    <SortTh>Content</SortTh>
                    <SortTh k="requesting_show" sort={sort} onSort={toggle}>Show</SortTh>
                    <SortTh>Agency</SortTh>
                    <SortTh k="due_date" sort={sort} onSort={toggle} firstDir="asc">Due</SortTh>
                    <SortTh>Assignee</SortTh>
                    <SortTh k="status" sort={sort} onSort={toggle}>Status</SortTh>
                  </tr>
                </thead>
                <tbody>
                  {all.map((r) => {
                    const overdue = K.isOverdue(r);
                    return (
                      <tr key={r.id} className={"opsq-row--click" + (savingId === r.id ? " opsq-row--saving" : "")}
                        onClick={() => setOpenId(r.id)}>
                        <td>
                          <div className="opsq-id">{r.display_id}</div>
                          {r.request_type && <span className="chip chip--basis">{T(r.request_type)}</span>}
                        </td>
                        <td>
                          {r.matter_id
                            ? <button type="button" className="link-btn opsq-matter" onClick={(e) => { e.stopPropagation(); onOpenMatter && onOpenMatter(r.matter_id); }}>{r.matter_title || r.matter_display_id || "Matter"}</button>
                            : <span style={{ color: "var(--text-faint)" }}>— unlinked —</span>}
                        </td>
                        <td>
                          <div className="opsq-kinds">
                            {(r.content_kind || []).map((k) => <span key={k} className="chip">{T(k)}</span>)}
                            {!(r.content_kind || []).length && <span style={{ color: "var(--text-faint)" }}>—</span>}
                          </div>
                        </td>
                        <td>{r.requesting_show ? (r.requesting_shows && r.requesting_shows.length ? r.requesting_shows : [r.requesting_show]).map(showLabel).join(" · ") : <span style={{ color: "var(--text-faint)" }}>—</span>}</td>
                        <td>{r.agency_name || <span style={{ color: "var(--text-faint)" }}>—</span>}</td>
                        <td className={overdue ? "overdue" : ""}>{r.due_date ? (overdue ? `${Math.abs(K.daysUntil(r.due_date))}d over` : r.due_date) : "—"}</td>
                        <td>
                          <select className="opsq-inline" value={r.assignee || ""} disabled={savingId === r.id}
                            onClick={(e) => e.stopPropagation()}
                            onChange={(e) => patchRow(r.id, { assignee: e.target.value || null })}>
                            {rowOptsFor(r).map((o) => <option key={o.value} value={o.value} disabled={!!o.disabled}>{o.label}</option>)}
                          </select>
                        </td>
                        <td>
                          <select className={"opsq-inline opsq-status opsq-status--" + K.requestTone(r.status)} value={r.status} disabled={savingId === r.id}
                            onClick={(e) => e.stopPropagation()}
                            onChange={(e) => patchRow(r.id, { status: e.target.value })}>
                            {statusRowOpts.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
                          </select>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </DS.Card.Body></DS.Card>
        )}

        {/* Same pager as the Worklist, deliberately — one desk should not count
            its rows differently from the other. */}
        {rows && total > 0 && (
          <div className="pager">
            <span className="pager__count">
              {total <= PAGE_SIZE
                ? `${total} request${total === 1 ? "" : "s"}`
                : `${page * PAGE_SIZE + 1}–${Math.min((page + 1) * PAGE_SIZE, total)} of ${total.toLocaleString()}`}
            </span>
            {total > PAGE_SIZE && (
              <span className="pager__nav">
                <DS.Button variant="ghost" icon="chevron-left" disabled={page === 0}
                  onClick={() => setPage((p) => Math.max(0, p - 1))}>Previous</DS.Button>
                <DS.Button variant="ghost" disabled={(page + 1) * PAGE_SIZE >= total}
                  onClick={() => setPage((p) => p + 1)}>Next</DS.Button>
              </span>
            )}
          </div>
        )}
      </div>
    );
  }

  window.LedgerOpsQueue = OpsQueue;
})();
