/* Search — the lifecycle payoff, and the single home for "find or browse."

   TWO MODES, ONE PAGE, and the split is the whole design (rebuilt 2026-09-11).

   RELEVANCE mode — text, no tags. `global_search` over matters / individuals /
   entities / documents (matters lead — 0045; documents last — 0147), lane RANKING rather than filtering (0086), a
   relevance cap that the page states out loud. Ranked results are not a list you
   page through; if the answer is not near the top, the fix is a better query or a
   tag, not page 4.

   DOCUMENTS tab (A4, 0147; folded in 2026-10-12). Every document, browsed rather than
   ranked: attachment_browse() takes the same text plus record / type / date filters and
   pages with no relevance cap (Documents.jsx). Open it with or without text. The tab's
   count is that uncapped total, so it is right even when the ranked list ran out of room.

   BROWSE mode — any tag selected. Tags are a property of MATTERS, so the result
   set becomes matters and the query moves to `listMatters`, which filters, counts
   and pages in Postgres. That is what makes "show me everything tagged Body Cam
   and Pursuit" a real answer over all 11,738 rows instead of a filter applied to
   whatever the ranked top-N happened to contain.

   WHAT THIS REPLACED. The page used to render the tag taxonomy FOUR TIMES: the
   whole 49-tag grid as the zero-query state, then — once you typed — the matching
   tags again as a chip rail, again as a facet count, and again as a full grid under
   a "Tags" facet. Clicking any of them LEFT the page for a Matters list filtered to
   exactly one tag. Operator, 2026-09-11: "particularly awful user experience… it'd
   also be nice if they could select multiple tags." Now there is one collapsed tag
   control (Tags.jsx), selected tags show as chips beside it, and results stay here.

   Assigns window.LedgerSearch. */
(function () {
  const DS = window.LawCrimeDesignSystem_e18d68;
  const T = window.titleize;
  const TYPE_META = {
    matter: { icon: "folder-open", label: "Matter" },
    individual: { icon: "user", label: "Person" },
    entity: { icon: "building-2", label: "Entity" },
    // A4 (0147): documents open in Drive (`url`), not on a Ledger page. RLS keeps
    // restricted (freelancer) files to Content Ops, because global_search runs as you.
    document: { icon: "file-text", label: "Document" },
    // G4 (0151): courthouses. global_search runs as you, so only Content Ops ever get these.
    courthouse: { icon: "building", label: "Courthouse" },
  };
  // How many ranked results we ask `global_search` for. It is a relevance cap, not
  // a page size, and the page says so rather than implying there is a page 2.
  const RELEVANCE_CAP = 50;
  // Browse mode pages for real.
  const BROWSE_PAGE = 25;

  // Profile panel: a person/entity and every matter they touch.
  function ProfilePanel({ kind, id, onBack, onOpenMatter }) {
    const { Badge } = DS;
    const T2 = window.titleize;
    const [data, setData] = React.useState(null);
    const [err, setErr] = React.useState(null);
    React.useEffect(() => {
      setData(null); setErr(null);
      const p = kind === "individual" ? window.LedgerAPI.getIndividualProfile(id) : window.LedgerAPI.getEntityProfile(id);
      p.then(setData).catch((e) => setErr(e.message || String(e)));
    }, [kind, id]);
    React.useEffect(() => { setTimeout(() => window.lucide && window.lucide.createIcons(), 30); }, [data]);

    if (err) return <div><div className="crumb" onClick={onBack}><i data-lucide="arrow-left" /> Back to results</div><div className="login__err">{err}</div></div>;
    if (!data) return <div><div className="crumb" onClick={onBack}><i data-lucide="arrow-left" /> Back to results</div><div style={{ color: "var(--text-muted)", fontSize: 13 }}>Loading…</div></div>;

    const isPerson = kind === "individual";
    const subject = isPerson ? data.individual : data.entity;
    const name = isPerson ? [subject.first_name, subject.last_name].filter(Boolean).join(" ") : subject.entity_name;
    const sub = isPerson
      ? [subject.individual_type && T2(subject.individual_type), (subject.known_as && subject.known_as.length) ? "a.k.a. " + subject.known_as.join(", ") : null].filter(Boolean).join(" · ")
      : [subject.entity_type && T2(subject.entity_type), subject.city, window.agencyPlace(subject) || subject.state].filter(Boolean).join(" · ");
    const contacts = isPerson
      ? [["phone", subject.phone], ["mail", subject.email], ["map-pin", subject.address]]
      : [["user", subject.contact_name], ["phone", subject.phone], ["mail", subject.email], ["globe", subject.website]];
    const shownContacts = contacts.filter(([, v]) => v);

    return (
      <div>
        <div className="crumb" onClick={onBack}><i data-lucide="arrow-left" /> Back to results</div>
        <div className="page-head">
          <div>
            <div className="eyebrow">{isPerson ? "Person" : "Organization"}{sub ? " · " + sub : ""}</div>
            <h1 className="page-title">{name}<window.LedgerComment section="Search profile" /></h1>
          </div>
        </div>

        {shownContacts.length > 0 && (
          <DS.Card style={{ marginBottom: 16 }}><DS.Card.Body>
            <div className="ag-contact">
              {shownContacts.map(([ic, v], i) => (
                <span key={i}><i data-lucide={ic} /> {ic === "globe" ? <a href={v} target="_blank" rel="noreferrer">{String(v).replace(/^https?:\/\//, "")}</a> : v}</span>
              ))}
            </div>
            {subject.notes && <div className="ag-notes">{subject.notes}</div>}
          </DS.Card.Body></DS.Card>
        )}

        <div className="form-subhead">Matters ({data.matters.length})</div>
        {data.matters.length === 0
          ? <div style={{ fontSize: 12.5, color: "var(--text-muted)" }}>Not linked to any matter yet.</div>
          : (
            <DS.Card><DS.Card.Body style={{ padding: 0 }}>
              <ul className="linklist">
                {data.matters.map((m) => (
                  <li key={m.matter_id} style={{ cursor: "pointer" }} onClick={() => onOpenMatter(m.matter_id)}>
                    <div className="li-main">
                      <b>{m.display_id ? m.display_id + " · " : ""}{m.working_title}</b>
                      {m.matter_summary && <span className="li-lede">{m.matter_summary}</span>}
                      <span>
                        {window.vocabLabel("matter_type", m.matter_type)}{m.legal_status ? " · " + T2(m.legal_status) : ""}
                        {isPerson && m.role ? " · " + T2(m.role) + (m.is_primary ? " (primary)" : "") : ""}
                      </span>
                    </div>
                    {!isPerson && m.involvement && m.involvement.map((t, i) => <span key={i} className="chip chip--inv">{t}</span>)}
                    {isPerson && m.role && <span className="chip chip--role">{T2(m.role)}</span>}
                    <i data-lucide="chevron-right" style={{ width: 16, height: 16, color: "var(--lc-slate-300)" }} />
                  </li>
                ))}
              </ul>
            </DS.Card.Body></DS.Card>
          )}
      </div>
    );
  }

  // Lane badge on a search result. Muted when the result is in the lane you are
  // already scoped to (it tells you nothing), highlighted when it is NOT — that is
  // the case worth flagging, because ranking pushed it down and the user should see
  // why before assuming search missed something.
  function LaneBadge({ lane, scope }) {
    const meta = (window.LEDGER_LANES || []).find((l) => l.db === lane);
    if (!meta) return null;
    const outOfScope = scope && scope !== "all" && scope !== lane;
    return (
      <span className={"lanebadge" + (outOfScope ? " lanebadge--out" : "")}
            title={outOfScope ? `Outside your ${(window.LEDGER_LANES.find((l) => l.id === scope) || {}).label} scope` : meta.label}>
        <i data-lucide={meta.icon} />{meta.label}
      </span>
    );
  }

  function Search({ initialQuery, initialTags, onOpenMatter, lane }) {
    const { Input, Button, Tabs, Badge } = DS;
    const [q, setQ] = React.useState(initialQuery || "");
    const [tags, setTags] = React.useState(initialTags || []); // selected tag_keys
    const [tagMode, setTagMode] = React.useState("all"); // "all" | "any"
    const [facet, setFacet] = React.useState("all");
    const [rows, setRows] = React.useState(null);
    const [total, setTotal] = React.useState(0);         // browse mode only
    const [page, setPage] = React.useState(0);
    const [busy, setBusy] = React.useState(false);
    const [err, setErr] = React.useState(null);
    const [profile, setProfile] = React.useState(null);  // { kind, id } | null
    const [covDec, setCovDec] = React.useState({});      // matter_id -> coverage_status
    const [docTotal, setDocTotal] = React.useState(null); // uncapped document matches for the text
    const taxonomy = window.LedgerTagTaxonomy();

    const browsing = tags.length > 0;
    const term = q.trim();
    const asked = browsing || term.length >= 2;

    // Arriving from the topbar — a typed term, or a tag picked from the type-ahead.
    // These are effects rather than initial state because the page does not remount
    // between two searches: without them, searching a second time from the topbar
    // would leave the box showing the first query.
    React.useEffect(() => { if (initialQuery != null) setQ(initialQuery); }, [initialQuery]);
    React.useEffect(() => { if (initialTags && initialTags.length) setTags(initialTags); }, [initialTags]);

    // Any change to the question restarts at the first page. Page 3 of the old
    // answer is an arbitrary slice of the new one.
    React.useEffect(() => { setPage(0); }, [q, tags, tagMode, lane]);
    // Facets describe the three result types, which only relevance mode returns.
    React.useEffect(() => { if (browsing) setFacet("all"); }, [browsing]);

    const run = React.useCallback(async () => {
      setBusy(true); setErr(null);
      try {
        let out; let count = 0;
        if (browsing) {
          // BROWSE — tags are a matters property, so this is a matters query:
          // filtered, counted and paged by Postgres over the whole table.
          const res = await window.LedgerAPI.listMatters({
            q: term || undefined, tags, tagMode, lane,
            page, pageSize: BROWSE_PAGE,
          });
          count = res.total;
          out = res.rows.map((m) => ({
            result_type: "matter",
            result_id: m.matter_id,
            display_text: m.display_alias || m.working_title,
            secondary: [m.display_id, m.matter_summary].filter(Boolean).join(" · "),
            lane: m.lane,
          }));
        } else {
          // RELEVANCE — lane ranks, it does not filter (0086), and every matter row
          // carries its lane so the badge below can say which desk it belongs to.
          // The Documents tab counts every match, not just those that made the ranked cap.
          const [ranked, docs] = await Promise.all([
            window.LedgerAPI.search(term, RELEVANCE_CAP, lane),
            window.LedgerAPI.att.browse({ q: term, pageSize: 1 }).catch(() => null),
          ]);
          out = ranked; count = out.length;
          setDocTotal(docs ? docs.total : null);
        }
        setRows(out); setTotal(count);
        const matterIds = out.filter((r) => r.result_type === "matter").map((r) => r.result_id);
        window.LedgerAPI.coverageDecisionsFor(matterIds).then(setCovDec).catch(() => {});
      } catch (e) { setErr(e.message || String(e)); setRows([]); setTotal(0); }
      finally { setBusy(false); }
    }, [browsing, term, tags, tagMode, lane, page]);

    // Type-ahead: the answer follows the question, debounced. Selecting a tag or
    // flipping the combinator is a click rather than a keystroke, so it does not
    // need the delay — but one code path is worth more than 40ms.
    //
    // 🐛 EMPTYING THE QUESTION CLEARS THE ANSWER SYNCHRONOUSLY, and that is not the
    // same as letting the debounced run() notice. It used to be: clearing the tags
    // left the previous 25 results on screen for a beat with the "nothing asked yet"
    // state rendered ABOVE them — a page telling you it had no question and showing
    // you an answer at the same time. Caught by clicking Clear tags in a browser.
    React.useEffect(() => {
      if (!asked) { setRows(null); setTotal(0); setErr(null); setBusy(false); setDocTotal(null); return; }
      const t = setTimeout(run, 250);
      return () => clearTimeout(t);
    }, [asked, run]);

    React.useEffect(() => { setTimeout(() => window.lucide && window.lucide.createIcons(), 30); },
      [rows, facet, tags, busy]);

    if (profile) {
      return <ProfilePanel kind={profile.kind} id={profile.id} onOpenMatter={onOpenMatter}
        onBack={() => { setProfile(null); setTimeout(() => window.lucide && window.lucide.createIcons(), 30); }} />;
    }

    const all = rows || [];
    const shown = all.filter((r) => facet === "all" || r.result_type === facet);
    const countBy = (t2) => all.filter((r) => r.result_type === t2).length;
    const facets = [
      { id: "all", label: "All", count: all.length },
      { id: "matter", label: "Matters", count: countBy("matter") },
      { id: "individual", label: "People", count: countBy("individual") },
      { id: "entity", label: "Entities", count: countBy("entity") },
      // D12: the Courthouses tab is shown to Content Ops only.
      ...(window.LedgerCourthouses && window.LedgerCourthouses.canUse() ? [{ id: "courthouse", label: "Courthouses", count: countBy("courthouse") }] : []),
      { id: "document", label: "Documents", count: docTotal },
    ];
    // The Documents tab browses on its own (text optional), so the tabs stay up while it is open.
    const docsTab = !browsing && facet === "document";
    const showTabs = !browsing && ((asked && rows) || docsTab);
    const rankedDocs = countBy("document");
    const capped = !browsing && all.length >= RELEVANCE_CAP;

    return (
      <div>
        <div className="page-head">
          <div>
            <div className="eyebrow">Lifecycle · Search</div>
            <h1 className="page-title">Search<window.LedgerComment section="Search" /></h1>
            <div className="page-lede">Find a matter, person, agency or document by name — or filter by tag to browse
              every matter behind one. Tags combine.</div>
          </div>
        </div>

        <div className="srch-controls">
          <div className="srch-box">
            <Input label="Find matters, people, entities, documents" icon="search"
              placeholder="Name, title, crime, jurisdiction, case number…"
              value={q} onChange={(e) => setQ(e.target.value)} />
          </div>
          <window.LedgerTagFilter tags={taxonomy} selected={tags} onChange={setTags}
            mode={tagMode} onMode={setTagMode} />
        </div>

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

        {/* NOTHING ASKED YET. Not the 49-tag wall this page used to open with — a
            handful of the most-used tags as a way in, and the control above for the
            rest. The empty state should suggest a next move, not be a directory. */}
        {!asked && !docsTab && (
          <div className="srch-empty">
            <div className="form-subhead" style={{ marginBottom: 10 }}>Start with a common tag</div>
            <window.LedgerTagBrowse tags={taxonomy} selected={tags} limit={12}
              onToggle={(k) => setTags(tags.includes(k) ? tags.filter((x) => x !== k) : tags.concat(k))} />
            <p className="srch-hint">
              <i data-lucide="info" /> Or type a name above. Tags filter matters; text searches
              matters, people, agencies and documents.
            </p>
            <button type="button" className="link-btn srch-docs" onClick={() => setFacet("document")}>
              <span className="ic-wrap"><i data-lucide="file-text" /></span> Browse every document</button>
          </div>
        )}

        {/* Facets describe the four types relevance mode returns. Browsing by tag
            returns matters and only matters, so the tabs would be one real option
            and two permanent zeroes. */}
        {showTabs && (
          <div style={{ marginBottom: 16 }}><Tabs items={asked ? facets : facets.map((f) => ({ ...f, count: null }))}
            value={facet} onChange={setFacet} /></div>
        )}

        {docsTab && <window.LedgerDocumentsBrowse q={term} onOpenMatter={onOpenMatter} />}

        {/* The combinator is only named once there is more than one tag: with a
            single tag "all of" and "any of" both read as nonsense about one thing. */}
        {asked && browsing && rows && (
          <div className="srch-count">
            <b>{total.toLocaleString()}</b> matter{total === 1 ? "" : "s"}{" "}
            {tags.length === 1
              ? "with this tag"
              : `with ${tagMode === "all" ? "all" : "any"} of these ${tags.length} tags`}
            {term ? <> matching “{term}”</> : null}
          </div>
        )}

        {asked && !docsTab && rows && shown.length === 0 && !busy && (
          <div className="ledger-empty"><i data-lucide="search-x" /><b>No matches</b>
            <span>{browsing
              ? (tagMode === "all" && tags.length > 1
                  ? "No matter carries all of those tags. Try “Any of these” instead."
                  : "Nothing matches that combination. Remove a tag or clear the text.")
              : `No ${facet === "all" ? "results" : facets.find((f) => f.id === facet).label.toLowerCase()} for “${term}”.`}</span></div>
        )}

        {/* The ranked list leads with matters, so documents can run out of room in it. */}
        {asked && !browsing && !docsTab && facet === "all" && docTotal != null && docTotal > rankedDocs && (
          <div className="srch-docnote">
            <button type="button" className="link-btn" onClick={() => setFacet("document")}>
              {docTotal.toLocaleString()} document{docTotal === 1 ? "" : "s"} match “{term}”{rankedDocs ? `; ${rankedDocs} shown here` : ""}. See them all</button>
          </div>
        )}

        {asked && !docsTab && shown.length > 0 && (
          <DS.Card><DS.Card.Body>
            <ul className="linklist">
              {shown.map((r) => {
                const meta = TYPE_META[r.result_type] || { icon: "circle", label: r.result_type };
                const onClick =
                  r.result_type === "matter" ? (onOpenMatter && (() => onOpenMatter(r.result_id)))
                  : r.result_type === "individual" ? (() => setProfile({ kind: "individual", id: r.result_id }))
                  : r.result_type === "entity" ? (() => setProfile({ kind: "entity", id: r.result_id }))
                  : r.result_type === "courthouse" ? (() => window.LedgerOpenCourthouse && window.LedgerOpenCourthouse(r.result_id))
                  : null;
                // A document opens in Drive. The URL comes back with the result (0147) so the
                // new tab opens inside the click itself: one opened after an async lookup
                // would be eaten by the popup blocker.
                const docUrl = r.result_type === "document" ? r.url : null;
                return (
                  <li key={r.result_type + r.result_id} style={onClick || docUrl ? { cursor: "pointer" } : null}
                      onClick={onClick || (docUrl ? (() => window.open(docUrl, "_blank", "noopener")) : undefined)}>
                    <div className="thumb" style={{ width: 34, height: 34, borderRadius: 8, background: "var(--lc-paper)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none", border: "1px solid var(--border-hair)" }}>
                      <i data-lucide={meta.icon} style={{ width: 16, height: 16, color: "var(--lc-slate-500)" }} />
                    </div>
                    <div className="li-main"><b>{r.display_text}</b><span>{r.secondary || ""}</span></div>
                    {r.result_type === "matter" && r.lane && <LaneBadge lane={r.lane} scope={lane} />}
                    {r.result_type === "matter" && covDec[r.result_id] &&
                      <window.LedgerCoveragePill status={covDec[r.result_id]} small />}
                    <Badge variant="outline">{meta.label}</Badge>
                    {onClick && <span className="ic-wrap"><i data-lucide="chevron-right" style={{ width: 16, height: 16, color: "var(--lc-slate-300)" }} /></span>}
                    {docUrl && <span className="ic-wrap"><i data-lucide="external-link" style={{ width: 15, height: 15, color: "var(--lc-slate-300)" }} /></span>}
                  </li>
                );
              })}
            </ul>
          </DS.Card.Body></DS.Card>
        )}

        {/* Browse pages for real. Relevance does not, and says why instead of
            offering a Next that would hand back progressively worse matches. */}
        {asked && browsing && total > BROWSE_PAGE && (
          <div className="pager">
            <span className="pager__count">
              {page * BROWSE_PAGE + 1}–{Math.min((page + 1) * BROWSE_PAGE, total)} of {total.toLocaleString()}
            </span>
            <span className="pager__nav">
              <Button variant="ghost" icon="chevron-left" disabled={page === 0 || busy}
                onClick={() => setPage((p) => Math.max(0, p - 1))}>Previous</Button>
              <Button variant="ghost" disabled={(page + 1) * BROWSE_PAGE >= total || busy}
                onClick={() => setPage((p) => p + 1)}>Next</Button>
            </span>
          </div>
        )}
        {asked && capped && !docsTab && (
          <div className="pager pager--note">
            <span className="pager__count">
              <i data-lucide="info" /> Showing the {RELEVANCE_CAP} best matches, ranked. Narrow the
              text or add a tag rather than looking for a next page.
            </span>
          </div>
        )}
      </div>
    );
  }

  window.LedgerSearch = Search;
})();
