/* SortKit — one sorting affordance for every list in Ledger. Assigns window.LedgerSortKit.

   WHY THIS EXISTS. Before 2026-09-11 no list view in the app rendered a sortable
   header. Every order was hard-coded server-side — matters by `updated_at desc`,
   the request queue by `due_date asc`, agencies by name — which was invisible at
   the ~30 rows this app was designed against and is the whole experience at
   11,738 matters and 11,007 requests: the fixed order is the only order anyone
   can ever see. Operator, 2026-09-11.

   ONE COMPONENT, TWO BACKENDS, ON PURPOSE. A list that holds its whole result set
   in the browser (Agencies) sorts with `sortRows`; a list that pages server-side
   (Matters, Asset Requests) hands `sort` to its API call and Postgres does it.
   The HEADER is the same either way, so the two can't drift into looking different
   or behaving differently, and a list can move from one to the other without its
   header changing. What must never happen is the paged case sorting in the
   browser: that orders the page you already have, which is a list confidently
   claiming to be sorted while describing an arbitrary 50 of 11,738 rows — the
   same defect 0092 removed from the Ops queue's filters.

   A COLUMN WITH NO `k` IS NOT SORTABLE AND DOES NOT PRETEND TO BE. Several columns
   on the Asset Requests desk (matter title, agency, assignee) are joined in the
   browser after the page is fetched, so Postgres cannot order by them at all.
   Those render as plain headers rather than as buttons that quietly sort 50 rows
   out of 11,007. A control that looks clickable and isn't is worse than no
   control, because it is the one a user stops trusting the rest of the page over. */
(function () {
  // useSort(defaultKey, defaultDir) → { sort: {key, dir}, toggle(key, firstDir) }
  // `firstDir` is the direction a column takes on its FIRST click. It is per column
  // because the useful first answer differs: a date wants newest first, a name wants
  // A–Z, a count wants largest first. Clicking again always flips.
  function useSort(defaultKey, defaultDir) {
    const [sort, setSort] = React.useState({ key: defaultKey, dir: defaultDir || "desc" });
    const toggle = React.useCallback((k, firstDir) => {
      setSort((s) => (s.key === k
        ? { key: k, dir: s.dir === "asc" ? "desc" : "asc" }
        : { key: k, dir: firstDir || "asc" }));
    }, []);
    return { sort, setSort, toggle };
  }

  // 🐛 THE CHEVRON IS INLINE SVG, NOT A LUCIDE `<i data-lucide>`, AND THAT IS NOT A
  // STYLE CHOICE. lucide's createIcons() REPLACES the `<i>` node with an `<svg>`, so
  // React is left holding a reference to a node that is no longer in the document —
  // and every later re-render patches the detached one. A static icon never notices.
  // This icon changes on every click, so it silently never changed: verified in a
  // browser, the active column kept the neutral `chevrons-up-down` glyph while its
  // aria-sort and its data were both correct. Any icon in this app that has to
  // change with state needs the same treatment.
  const CHEV = {
    up: ["m18 15-6-6-6 6"],
    down: ["m6 9 6 6 6-6"],
    both: ["m7 15 5 5 5-5", "m7 9 5-5 5 5"],
  };
  function Chevron({ dir }) {
    return (
      <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor"
           strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        {CHEV[dir].map((d, i) => <path key={i} d={d} />)}
      </svg>
    );
  }

  // A table header cell. With `k` it is a sort button; without, a plain label.
  function Th({ k, sort, onSort, firstDir, width, align, title, children }) {
    const on = !!k && sort && sort.key === k;
    const style = {};
    if (width) style.width = width;
    if (align) style.textAlign = align;
    if (!k) return <th style={style}>{children}</th>;
    return (
      <th style={style} className={"th-sort" + (on ? " th-sort--on" : "")}
          aria-sort={on ? (sort.dir === "asc" ? "ascending" : "descending") : "none"}>
        <button type="button" onClick={() => onSort(k, firstDir)}
                title={title || `Sort by ${String(children)}`}>
          <span>{children}</span>
          {/* The neutral chevron pair is drawn on every sortable column, not only on
              hover: the affordance has to be discoverable on a first visit, and hover
              is not an affordance on a touch screen at all. */}
          <Chevron dir={on ? (sort.dir === "asc" ? "up" : "down") : "both"} />
        </button>
      </th>
    );
  }

  // Client-side sort, for lists the browser already holds whole.
  // `accessors` maps a sort key to a value getter; a key with no entry reads the
  // property of the same name. NULL/blank always sorts LAST regardless of
  // direction — "no value" is not smaller than every value, it is absent, and
  // burying 8,000 empty cells at the top of a descending sort helps nobody.
  function sortRows(rows, key, dir, accessors) {
    if (!key) return rows;
    const get = (r) => {
      const f = accessors && accessors[key];
      return typeof f === "function" ? f(r) : r[key];
    };
    const blank = (v) => v === null || v === undefined || v === "";
    const sign = dir === "asc" ? 1 : -1;
    return rows.slice().sort((a, b) => {
      const va = get(a); const vb = get(b);
      if (blank(va) && blank(vb)) return 0;
      if (blank(va)) return 1;
      if (blank(vb)) return -1;
      if (typeof va === "number" && typeof vb === "number") return (va - vb) * sign;
      return String(va).localeCompare(String(vb), undefined, { numeric: true, sensitivity: "base" }) * sign;
    });
  }

  window.LedgerSortKit = { useSort, Th, sortRows };
})();
