/* Ported from apps/web/src/features/home/signals/SignalsTab.tsx */
(() => {
  const T = window.T;
  const { useCallback, useEffect, useMemo, useRef, useState } = React;
  const { TableScrollRegion, SignalFilter, toSignalType } = T.UI;
  const { ChevronDown, Loader2 } = T.Icons;

  const PAGE_SIZE = 20;

  // Apollo's NetworkStatus.setVariables. The mock envelope always reports
  // networkStatus 7 (ready), so `refetching` stays false — the mock resolves
  // synchronously and there is no in-flight refetch window to indicate.
  const NetworkStatus = { setVariables: 2 };

  /** apolloClient.query({ query: GET_HOME_SIGNALS, variables }) stand-in:
   *  a synchronous mock read wrapped in a promise so loadMore / bulk selection
   *  keep their async shape (fetchPolicy 'network-only' /
   *  HOME_TABLE_BULK_FETCH_POLICY are meaningless without a cache). */
  async function queryHomeSignals(variables) {
    const rows = T.Data.raw.filterEvents(variables);
    const skip = variables.skip || 0;
    const take = variables.take != null ? variables.take : rows.length;
    return { data: { events: rows.slice(skip, skip + take) } };
  }

  /** Execute a row's add against the audience: the ONE code path shared by the
   *  per-row checkbox and the server-wide bulk selection. */
  function applySignalRowAdd(pb, event, add) {
    if (add.kind === 'account') {
      if (pb.audience.accounts[add.accountId]) return;
      pb.addAccount(
        add.accountId,
        'signal-table',
        event.account
          ? {
              name: event.account.name,
              url: event.account.url ?? null,
              logoUrl: event.account.logoUrl ?? null,
            }
          : undefined,
        event.id
      );
    } else if (add.kind === 'people') {
      add.personIds.forEach(id => {
        // Row selection is independent from membership. Preserve the source
        // that originally added a shared/manual person so a later signal cannot
        // accidentally remove that person when it is unchecked.
        if (pb.audience.people[id]) return;
        // The event already carries the person's display data — pass it so the
        // bar paints the real face/name on the first frame instead of the raw
        // id + placeholder until the roster query hydrates it (TRA-1359).
        const p = event.people.find(ep => ep.person?.id === id)?.person;
        pb.addPerson(
          add.accountId,
          id,
          'signal',
          event.id,
          event.account
            ? {
                name: event.account.name,
                url: event.account.url ?? null,
                logoUrl: event.account.logoUrl ?? null,
              }
            : undefined,
          p
            ? {
                fullName: p.fullName,
                title: p.title ?? null,
                profileImageUrl: p.profileImageUrl ?? null,
              }
            : undefined
        );
      });
    }
  }

  function SignalsTab({
    scopeUserId,
    scopeAccountIds,
    onOpenSignal,
    openEventId,
    onCountChange,
    pollMs = 0,
    onLiveStats,
    toolbarExtra,
    bulkSelection,
    onBulkSelectionChange,
    onClearSelection,
    selectionResetVersion = 0,
  }) {
    const {
      countSelectedMatchingIds,
      fetchAllMatchingRows,
      hasAudienceSelection,
      shouldShowHomeTableSelectionScope,
      HomeSelectCell,
      HomeTableResultCount,
      HomeTableSearch,
      HomeTableSelectAllCheckbox,
      HomeTableSelectionScopeRow,
      SearchInAccountsLink,
      eventsToAudienceBatch,
      signalRowFacts,
      signalRowCheckState,
      signalIdentityForKey,
      dayBucket,
      SignalNamePill,
      homeAnalytics,
      toast,
    } = T;
    const pb = T.useAudience();
    // Seeded from `?q=` so one table can hand a search to another.
    const [query, setQuery] = T.useSeededSearch();
    const debouncedQuery = T.useDebouncedValue(query, 250);
    // Selected signal KEYS (event_signal.signal_key) - the selection filters the
    // events query server-side, so it applies to everything, not just the pages
    // the infinite scroll happens to have loaded.
    const [selectedKeys, setSelectedKeys] = useState([]);

    // Joined key so the memo/effect re-run on scope change without depending on a
    // fresh array identity each render.
    const scopeAccountsKey = (scopeAccountIds ?? []).join(',');
    const baseVars = useMemo(
      () => ({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        ...(scopeAccountsKey ? { accountIds: scopeAccountsKey.split(',') } : {}),
        ...(selectedKeys.length ? { signalIds: selectedKeys } : {}),
        ...(debouncedQuery ? { search: debouncedQuery } : {}),
        skip: 0,
        take: PAGE_SIZE,
      }),
      [scopeUserId, scopeAccountsKey, selectedKeys, debouncedQuery]
    );

    // App: useQuery(GET_HOME_SIGNALS, { fetchPolicy: 'cache-and-network',
    // pollInterval: pollMs }). Mock hooks recompute reactively on data
    // mutations, so polling is unnecessary here.
    const { data, loading, networkStatus } = T.Data.useHomeSignals(baseVars);

    const filtersActive = Boolean(debouncedQuery || selectedKeys.length > 0);
    const scopeVars = useMemo(
      () => ({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        ...(scopeAccountsKey ? { accountIds: scopeAccountsKey.split(',') } : {}),
        ...(selectedKeys.length ? { signalIds: selectedKeys } : {}),
        ...(debouncedQuery ? { search: debouncedQuery } : {}),
      }),
      [scopeUserId, scopeAccountsKey, selectedKeys, debouncedQuery]
    );
    const baselineVars = useMemo(
      () => ({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        ...(scopeAccountsKey ? { accountIds: scopeAccountsKey.split(',') } : {}),
      }),
      [scopeUserId, scopeAccountsKey]
    );
    const { data: signalCountData, previousData: signalCountPrev } =
      T.Data.useHomeSignalsCount(scopeVars);
    // App passes `skip: !filtersActive`; the mock hook is synchronous and
    // cheap, so it always runs and the derived reads below stay identical.
    const { data: baselineCountData, previousData: baselineCountPrev } =
      T.Data.useHomeSignalsCount(baselineVars);
    const signalCountResolved = signalCountData ?? signalCountPrev;
    const signalMatchCount = signalCountResolved?.eventsCount ?? 0;
    const resultBaseline = filtersActive
      ? (baselineCountData ?? baselineCountPrev)?.eventsCount
      : undefined;
    const resultCount =
      filtersActive && signalCountResolved !== undefined
        ? signalMatchCount
        : undefined;
    const showResultCount =
      resultCount !== undefined &&
      resultBaseline !== undefined &&
      resultBaseline > resultCount;
    // Appended pages live outside the reactive first page; reset when the scope
    // (the first-page variables) changes.
    const [extra, setExtra] = useState([]);
    const [hasMore, setHasMore] = useState(true);
    const [loadingMore, setLoadingMore] = useState(false);
    const [bulkSelectionBusy, setBulkSelectionBusy] = useState(false);
    const [bulkSelectionProgress, setBulkSelectionProgress] = useState(0);
    const [loadedSelection, setLoadedSelection] = useState(null);
    const bulkRequest = useRef(null);
    const bulkRequestId = useRef(0);
    const scopeKey = JSON.stringify(baseVars);
    const activeBulkSelection =
      bulkSelection?.scopeKey === scopeKey ? bulkSelection : null;
    const loadedSelectionActive = loadedSelection?.scopeKey === scopeKey;
    useEffect(() => {
      bulkRequest.current?.controller.abort();
      bulkRequestId.current += 1;
      bulkRequest.current = null;
      setBulkSelectionBusy(false);
      setBulkSelectionProgress(0);
      setLoadedSelection(null);
      setExtra([]);
      setHasMore(true);
      if (bulkSelection?.scopeKey !== scopeKey) onBulkSelectionChange(null);
      // eslint-disable-next-line react-hooks/exhaustive-deps -- scopeKey is the complete server selection identity
    }, [scopeKey]);
    useEffect(
      () => () => {
        bulkRequest.current?.controller.abort();
        bulkRequestId.current += 1;
      },
      []
    );
    useEffect(() => {
      bulkRequest.current?.controller.abort();
      bulkRequestId.current += 1;
      bulkRequest.current = null;
      setBulkSelectionBusy(false);
      setBulkSelectionProgress(0);
      setLoadedSelection(null);
    }, [selectionResetVersion]);
    const invalidateBulkSelection = useCallback(() => {
      bulkRequest.current?.controller.abort();
      bulkRequestId.current += 1;
      bulkRequest.current = null;
      setBulkSelectionBusy(false);
      setBulkSelectionProgress(0);
      setLoadedSelection(null);
      if (activeBulkSelection) onBulkSelectionChange(null);
    }, [activeBulkSelection, onBulkSelectionChange]);

    const firstPage = useMemo(() => data?.events ?? [], [data]);
    const allEvents = useMemo(() => {
      // Dedupe by id only (pagination overlap). Content-identical duplicates are
      // an ingestion bug (TRA-1316) fixed server-side; hiding them here would
      // mask it.
      const seen = new Set();
      const out = [];
      for (const e of [...firstPage, ...extra]) {
        if (seen.has(e.id)) continue;
        seen.add(e.id);
        out.push(e);
      }
      return out;
    }, [firstPage, extra]);

    // Facts for every loaded row: unchecking a signal needs to know which OTHER
    // still-selected rows claim a shared contact before evicting anyone.
    const allRowFacts = useMemo(() => {
      const facts = new Map();
      activeBulkSelection?.signalFacts?.forEach(row =>
        facts.set(row.eventId, row)
      );
      allEvents.map(signalRowFacts).forEach(row => facts.set(row.eventId, row));
      return [...facts.values()];
    }, [activeBulkSelection?.signalFacts, allEvents]);
    const selectableEvents = useMemo(
      () => allEvents.filter(event => !!signalRowFacts(event).accountId),
      [allEvents]
    );
    const selectedLoadedCount = useMemo(
      () =>
        selectableEvents.filter(event =>
          signalRowCheckState(signalRowFacts(event), pb.audience)
        ).length,
      [pb.audience, selectableEvents]
    );
    const loadedSignalIds = useMemo(
      () => selectableEvents.map(event => event.id),
      [selectableEvents]
    );
    const selectedBulkMatchingCount = countSelectedMatchingIds(
      activeBulkSelection?.ids ?? null,
      id => !!pb.audience.selectedEventIds?.[id]
    );
    const selectedLoadedSelectionCount = countSelectedMatchingIds(
      loadedSelectionActive ? loadedSelection.ids : null,
      id => !!pb.audience.selectedEventIds?.[id]
    );
    const selectionScopeCount =
      selectedBulkMatchingCount ??
      selectedLoadedSelectionCount ??
      selectedLoadedCount;
    const allLoadedSelected =
      selectableEvents.length > 0 &&
      selectedLoadedCount === selectableEvents.length;
    const anySelection = hasAudienceSelection(pb.audience);
    const headerChecked = allLoadedSelected
      ? true
      : anySelection
        ? 'indeterminate'
        : false;

    const bulkSelectionComplete =
      activeBulkSelection !== null &&
      selectedBulkMatchingCount === activeBulkSelection.ids.length;
    const loadedSelectionComplete =
      loadedSelectionActive &&
      selectedLoadedSelectionCount === loadedSelection.ids.length;

    const clearSelection = useCallback(() => {
      bulkRequest.current?.controller.abort();
      setLoadedSelection(null);
      onClearSelection();
    }, [onClearSelection]);

    const toggleLoadedSelection = useCallback(() => {
      if (anySelection) {
        clearSelection();
        return;
      }
      const added = pb.mergeAudience(
        eventsToAudienceBatch(selectableEvents, pb.audience),
        'signal-table'
      );
      homeAnalytics.bulkAdded({
        table: 'signals',
        via: 'signal-table',
        matched: selectableEvents.length,
        added,
        capped: false,
        audience_people_before: pb.res.counts.people,
        audience_accounts_before: pb.res.counts.accounts,
      });
      setLoadedSelection({ ids: loadedSignalIds, scopeKey });
    }, [
      anySelection,
      clearSelection,
      loadedSignalIds,
      pb,
      scopeKey,
      selectableEvents,
    ]);

    const selectAllMatching = useCallback(async () => {
      if (signalMatchCount <= 0 || bulkSelectionBusy) return;
      bulkRequest.current?.controller.abort();
      const controller = new AbortController();
      const requestId = ++bulkRequestId.current;
      bulkRequest.current = { id: requestId, controller };
      setBulkSelectionBusy(true);
      setBulkSelectionProgress(0);
      try {
        const selectionIds =
          activeBulkSelection?.ids ??
          (loadedSelectionActive ? loadedSelection.ids : loadedSignalIds);
        const progressBase = selectionScopeCount;
        const result = await fetchAllMatchingRows({
          totalCount: signalMatchCount,
          startAt: activeBulkSelection?.nextOffset ?? 0,
          excludeIds: selectionIds,
          getRowId: row => row.id,
          pageSize: 100,
          isSelectable: row => !!signalRowFacts(row).accountId,
          signal: controller.signal,
          onProgress: count => setBulkSelectionProgress(progressBase + count),
          fetchPage: async (skip, take, signal) => {
            const { data: page } = await queryHomeSignals({
              ...scopeVars,
              skip,
              take,
            });
            return page.events ?? [];
          },
        });
        if (controller.signal.aborted || bulkRequest.current?.id !== requestId) {
          return;
        }
        const added = pb.mergeAudience(
          eventsToAudienceBatch(result.rows, pb.audience),
          'signal-table'
        );
        homeAnalytics.bulkAdded({
          table: 'signals',
          via: 'signal-table',
          matched: signalMatchCount,
          added,
          capped: result.outcome === 'capped',
          audience_people_before: pb.res.counts.people,
          audience_accounts_before: pb.res.counts.accounts,
        });
        const ids = [
          ...new Set([...selectionIds, ...result.rows.map(row => row.id)]),
        ];
        const signalFacts = [
          ...new Map(
            [
              ...(activeBulkSelection?.signalFacts ??
                selectableEvents.map(signalRowFacts)),
              ...result.rows.map(signalRowFacts),
            ].map(fact => [fact.eventId, fact])
          ).values(),
        ];
        onBulkSelectionChange({
          scopeKey,
          ids,
          nextOffset: result.nextOffset,
          totalCount: signalMatchCount,
          outcome: result.outcome,
          signalFacts,
        });
        setLoadedSelection(null);
        toast.dismiss('home-signals-select-all');
      } catch (error) {
        if (
          controller.signal.aborted ||
          (error instanceof Error && error.name === 'AbortError')
        ) {
          return;
        }
        toast.error('Couldn’t select all matching signals. Try again.', {
          id: 'home-signals-select-all',
        });
      } finally {
        if (bulkRequest.current?.id === requestId) {
          bulkRequest.current = null;
          setBulkSelectionBusy(false);
          setBulkSelectionProgress(0);
        }
      }
    }, [
      activeBulkSelection,
      bulkSelectionBusy,
      loadedSignalIds,
      loadedSelection,
      loadedSelectionActive,
      onBulkSelectionChange,
      pb,
      scopeKey,
      scopeVars,
      selectionScopeCount,
      signalMatchCount,
      selectableEvents,
    ]);

    const showSelectionScope = shouldShowHomeTableSelectionScope({
      selectedBulkMatchingCount,
      selectedLoadedSelectionCount,
      allLoadedSelected,
      totalCount: signalMatchCount,
      loadedCount: selectedLoadedCount,
    });

    const refetching =
      networkStatus === NetworkStatus.setVariables && allEvents.length > 0;

    const loadMore = useCallback(async () => {
      if (loadingMore || !hasMore || loading) return;
      setLoadingMore(true);
      try {
        const { data: page } = await queryHomeSignals({
          ...baseVars,
          skip: firstPage.length + extra.length,
        });
        const rows = page?.events ?? [];
        homeAnalytics.loadedMore({
          table: 'signals',
          loaded: rows.length,
          total_loaded: firstPage.length + extra.length + rows.length,
        });
        setHasMore(rows.length === PAGE_SIZE);
        setExtra(prev => [...prev, ...rows]);
      } finally {
        setLoadingMore(false);
      }
    }, [baseVars, extra.length, firstPage.length, hasMore, loading, loadingMore]);

    // Signal facet from the server: ALL the tenant's signals with counts, not
    // just those visible in the loaded pages. Same userId/list scope as the
    // table. Names/icons derive from the signal key exactly like the row pills.
    const { data: facetData } = T.Data.useHomeSignalFacet({
      userId: scopeUserId,
      accountIds: scopeAccountsKey ? scopeAccountsKey.split(',') : undefined,
    });
    const { signalOptions, keysByName } = useMemo(() => {
      const rows = facetData?.newsfeedFilterStats.signalCounts ?? [];
      // Group by display name: distinct keys can humanize to the same label, and
      // duplicate names would collide in the dropdown. Selecting a name selects
      // all of its keys.
      const byName = new Map();
      for (const r of rows) {
        const sig = signalIdentityForKey(r.signalId, r.signalType);
        const cur = byName.get(sig.name);
        if (cur) {
          cur.count += r.count;
          cur.keys.push(r.signalId);
        } else {
          byName.set(sig.name, { count: r.count, keys: [r.signalId], sig });
        }
      }
      const options = [...byName.entries()]
        .map(([name, v]) => ({
          name,
          count: v.count,
          // Reuse the signal type so the collapsed trigger shows the same
          // category icon and hue as this signal's pill in the dropdown.
          type: v.sig.type,
          labelNode: (
            <span className="flex min-w-0 items-center">
              <SignalNamePill sig={v.sig} />
            </span>
          ),
        }))
        .sort((a, b) => b.count - a.count);
      const keysByName = new Map(
        [...byName.entries()].map(([name, v]) => [name, v.keys])
      );
      return { signalOptions: options, keysByName };
    }, [facetData]);
    const selectedNames = useMemo(
      () =>
        [...keysByName.entries()]
          .filter(([, keys]) => keys.some(k => selectedKeys.includes(k)))
          .map(([name]) => name),
      [keysByName, selectedKeys]
    );
    const toggleName = (name) => {
      invalidateBulkSelection();
      const keys = keysByName.get(name) ?? [];
      const turningOff = keys.every((k) => selectedKeys.includes(k));
      homeAnalytics.filterChanged({
        table: 'signals',
        filter: 'signal_key',
        active: !turningOff,
        selected_count: turningOff
          ? selectedNames.length - 1
          : selectedNames.length + 1,
        result_count: signalCountResolved?.eventsCount,
      });
      setSelectedKeys((prev) => {
        const allIn = keys.every((k) => prev.includes(k));
        return allIn
          ? prev.filter(k => !keys.includes(k))
          : [...new Set([...prev, ...keys])];
      });
    };

    useEffect(() => {
      onCountChange?.(allEvents.length);
    }, [allEvents.length, onCountChange]);

    // Fired on the SETTLED query, not per keystroke: `debouncedQuery` is what
    // actually hit the server, so the count reads as "searches run". The term
    // itself never leaves the browser — it names real companies and people — so
    // only its length rides along.
    //
    // It waits for THIS query's count. `signalCountResolved` falls back to
    // `previousData` so rows stay painted during a refetch, so emitting as soon
    // as the query changed labelled every search with its predecessor's total:
    // search "acme" (3 hits) then "vercel" (41) reported `vercel → 3`. Reading
    // the fresh `signalCountData` alone, and deduping on the query, ties the
    // number to the search it belongs to.
    const searchReportedFor = useRef(null);
    const freshSignalCount = signalCountData?.eventsCount;
    useEffect(() => {
      if (!debouncedQuery || freshSignalCount === undefined) return;
      if (searchReportedFor.current === debouncedQuery) return;
      searchReportedFor.current = debouncedQuery;
      homeAnalytics.searched({
        table: 'signals',
        query_length: debouncedQuery.length,
        result_count: freshSignalCount,
      });
    }, [debouncedQuery, freshSignalCount]);

    // An empty table is a dead end worth counting, and the two kinds are
    // different problems: `filtered` means the user over-narrowed and can back
    // out, `no_data` means we have nothing to show them at all.
    const signalsEmpty = !loading && allEvents.length === 0;
    // Deduped on (empty, reason). `signalsEmpty` derives from `loading`, which flips on
    // every refetch AND every poll tick, so the raw effect re-fired constantly:
    // an empty tenant in live-onboarding mode (2.5s polling for up to 6 minutes)
    // logged well over a hundred events for one dead end. The ref re-arms when
    // the table stops being empty, so a genuine second dead end still counts.
    const emptyReportedRef = useRef(null);
    useEffect(() => {
      // Re-arm ONLY when the table actually has rows. Keying the reset on
      // `!signalsEmpty` re-armed on `loading` too, which flips on every refetch and
      // every poll tick — so an empty tenant polling at 2.5s during onboarding
      // logged a fresh dead end every couple of seconds. A mid-fetch empty is
      // not a dead end the user reached; it is a table waiting.
      if (allEvents.length > 0) {
        emptyReportedRef.current = null;
        return;
      }
      if (!signalsEmpty) return;
      const reason = filtersActive ? 'filtered' : 'no_data';
      if (emptyReportedRef.current === reason) return;
      emptyReportedRef.current = reason;
      homeAnalytics.emptyStateViewed({ table: 'signals', reason });
    }, [signalsEmpty, allEvents.length, filtersActive]);

    // Live-onboarding: report how complete the loaded rows are, so the page can
    // keep polling until the enrichment backfill (matched signal, why, faces)
    // has caught up - not just until event counts stop growing.
    useEffect(() => {
      if (!onLiveStats) return;
      const d = (e) => e.data;
      onLiveStats({
        events: allEvents.length,
        enriched: allEvents.filter(
          e => Array.isArray(d(e)?.matched_signals) || d(e)?.why_it_matters
        ).length,
        faces: allEvents.reduce((n, e) => n + e.people.length, 0),
      });
    }, [allEvents, onLiveStats]);

    // Infinite scroll sentinel.
    const sentinelRef = useRef(null);
    useEffect(() => {
      if (!hasMore || loading) return;
      const el = sentinelRef.current;
      if (!el) return;
      const io = new IntersectionObserver(
        entries => {
          if (entries[0].isIntersecting) void loadMore();
        },
        { rootMargin: '200px' }
      );
      io.observe(el);
      return () => io.disconnect();
    }, [hasMore, loading, loadMore]);

    return (
      <div className="flex min-h-0 flex-1 flex-col">
        <div className="htbl-tools">
          <HomeTableSearch
            value={query}
            onChange={value => {
              invalidateBulkSelection();
              setQuery(value);
            }}
            placeholder="Search signals by account, headline, why, or signal"
            searching={refetching}
            trailing={
              showResultCount ? (
                <HomeTableResultCount
                  count={resultCount}
                  baseline={resultBaseline}
                />
              ) : undefined
            }
          />
          {toolbarExtra}
          <SignalFilter
            signals={signalOptions}
            selected={selectedNames}
            onToggle={toggleName}
            onClear={() => {
              invalidateBulkSelection();
              setSelectedKeys([]);
            }}
          />
        </div>

        {/* TRA-1428: see PeopleTab — one scroller for both axes (sticky header),
            affordance on the non-scrolling anchor. */}
        <TableScrollRegion
          rootClassName="flex min-h-0 flex-1 flex-col"
          className="min-h-0 flex-1 overflow-y-auto"
        >
          {loading && allEvents.length === 0 ? (
            <div className="flex items-center justify-center py-16 text-text-muted">
              <Loader2 className="mr-2 size-4 animate-spin" />
              Loading signals
            </div>
          ) : allEvents.length === 0 ? (
            <div className="flex flex-col items-center gap-2 py-16 text-center">
              <span className="text-body text-text-muted">
                {query.trim()
                  ? `No signals match “${query.trim()}”.`
                  : 'No signals match. Clear the filters, or try the Accounts and People tabs.'}
              </span>
              {/* A signal search term is nearly always a company, and Accounts is
                  the tab that can actually resolve one (owned rows → CoreSignal
                  candidates → Find). Hand the query over instead of answering it
                  here in a second place. */}
              <SearchInAccountsLink query={query} />
            </div>
          ) : (
            <table className="htbl htbl-w-signals">
              <thead>
                <tr>
                  <th className="htbl-c-check">
                    <HomeTableSelectAllCheckbox
                      checked={headerChecked}
                      busy={bulkSelectionBusy}
                      disabled={selectableEvents.length === 0}
                      entityName="signal"
                      loadedCount={selectableEvents.length}
                      onToggle={toggleLoadedSelection}
                    />
                  </th>
                  <th className="htbl-c-sig">Signal</th>
                  <th className="htbl-c-when">When</th>
                  <th>Details</th>
                  {/* People fold into the Details cell as name pills; the right
                      column shows "Where it fits" (the account's offering). */}
                  <th className="htbl-c-fit">Where it fits</th>
                  <th className="htbl-c-tw" />
                </tr>
              </thead>
              <tbody>
                {showSelectionScope ? (
                  <HomeTableSelectionScopeRow
                    colSpan={6}
                    selectedCount={selectionScopeCount}
                    totalCount={signalMatchCount}
                    remainingCount={
                      activeBulkSelection
                        ? Math.max(
                            0,
                            signalMatchCount - activeBulkSelection.nextOffset
                          )
                        : Math.max(0, signalMatchCount - selectionScopeCount)
                    }
                    entityName="signal"
                    bulkOutcome={activeBulkSelection?.outcome ?? null}
                    bulkSelectionComplete={bulkSelectionComplete}
                    loadedSelectionComplete={loadedSelectionComplete}
                    busy={bulkSelectionBusy}
                    progressCount={bulkSelectionProgress}
                    onSelectAll={() => void selectAllMatching()}
                    onClear={clearSelection}
                  />
                ) : null}
                {(() => {
                  let lastBucket = null;
                  return allEvents.map(event => {
                    const bucket = dayBucket(event.eventDate);
                    const showBucket = bucket !== lastBucket;
                    lastBucket = bucket;
                    return (
                      <SignalRow
                        key={event.id}
                        event={event}
                        allRowFacts={allRowFacts}
                        bucket={showBucket ? bucket : null}
                        open={openEventId === event.id}
                        onOpen={() => onOpenSignal(event)}
                      />
                    );
                  });
                })()}
              </tbody>
            </table>
          )}
          {/* TRA-1428: see PeopleTab — the zero-height sentinel takes the
              min-width floor so it stays inside the IntersectionObserver's reach
              once the table is scrolled right, while the VISIBLE status line
              takes `sticky left-0` instead: a floor would center it on 900px
              rather than on the scrollport. They are separate elements for
              exactly that reason. */}
          {hasMore && allEvents.length > 0 && (
            <>
              <div ref={sentinelRef} className="htbl-w-signals" />
              <div className="sticky left-0 flex items-center justify-center py-6 text-meta text-text-muted">
                {loadingMore ? (
                  <>
                    <Loader2 className="mr-2 size-4 animate-spin" />
                    Loading more
                  </>
                ) : resultCount !== undefined &&
                  allEvents.length < resultCount ? (
                  `Showing ${allEvents.length.toLocaleString()} of ${resultCount.toLocaleString()} signals`
                ) : (
                  'Scroll for more'
                )}
              </div>
            </>
          )}
        </TableScrollRegion>
      </div>
    );
  }

  function SignalRow({
    event,
    /** Facts for every loaded row, so unchecking can tell whether another
     *  still-selected signal also wants a shared contact. */
    allRowFacts,
    bucket,
    open,
    onOpen,
  }) {
    const {
      HomeSelectCell,
      signalRowFacts,
      signalRowCheckState,
      signalRowSelected,
      signalRowAdd,
      signalRowPersonOwnershipReleases,
      signalIdentity,
      signalHeadline,
      relativeWhen,
      signalRichFacts,
      SignalCell,
      SignalDetailsCell,
      SignalFitCell,
      homeAnalytics,
    } = T;
    const pb = T.useAudience();
    const facts = signalRowFacts(event);
    const accountId = facts.accountId;

    // Per-SIGNAL selection: checking the box adds THIS signal's suggested
    // contacts (stamped with this eventId). Checked / lavender state follows
    // provenance — Moyshe being in the audience does not select every signal
    // that lists him; only the signal that was picked. Uncheck removes only
    // contacts this signal owns (shared ones kept if another signal owns them).
    // Signals with no suggested people stamp eventId on the account entry the
    // same way.
    const suggestedIds = facts.suggestedPersonIds;
    const checkedState = signalRowCheckState(facts, pb.audience);
    const toggle = () => {
      homeAnalytics.rowSelected({
        table: 'signals',
        selected: !checkedState,
        people_count: suggestedIds.length,
      });
      if (checkedState) {
        // Unchecking drops the row's selection and takes with it only the people
        // no OTHER selected row still wants — a contact two signals share stays
        // for the one that is still checked.
        pb.setSignalSelected(event.id, false);
        if (suggestedIds.length === 0) {
          const replacement = allRowFacts.find(
            row =>
              row.eventId !== event.id &&
              row.accountId === accountId &&
              row.suggestedPersonIds.length === 0 &&
              signalRowCheckState(row, pb.audience)
          );
          pb.releaseSignalAccount(accountId, event.id, replacement?.eventId);
          return;
        }
        signalRowPersonOwnershipReleases(facts, pb.audience, allRowFacts).forEach(
          ({ personId, replacementEventId }) =>
            pb.releaseSignalPerson(personId, event.id, replacementEventId)
        );
        return;
      }
      applySignalRowAdd(pb, event, signalRowAdd(facts));
      pb.setSignalSelected(event.id, true);
      if (suggestedIds.length > 0) pb.noteContactsAdded(accountId, suggestedIds);
    };
    const rowSel = signalRowSelected(facts, pb.audience);

    // Row identity is the SIGNAL, not the account (early-user feedback: leading
    // with a 26px logo + bold name read as an accounts list). The signal cell
    // leads with the signal type's icon + the signal's name in the pill;
    // the account is demoted to a quiet later column.
    const sig = signalIdentity(event.data, event.signalType);
    // Jobs: the Signal cell already names the company under the pill, so
    // the row reads "Hiring <role>" without restating it.
    const isJob = toSignalType(event.signalType) === 'job_listing';
    const headline = isJob
      ? `Hiring ${event.title}`
      : signalHeadline(event.title, event.signalType);

    // Details shows event_summary - the one-line "what happened" (real field
    // from event.data, no invented copy) - with the headline as fallback.
    const summary = (event.data?.event_summary ?? '').trim();
    const details = summary || headline;
    // The richer second/third lines each variant draws (why-it-matters, matched
    // signal evidence + confidence, source, contacts) come from one shared facts
    // read so the variants can't drift on how they interpret the event JSON.
    const richFacts = signalRichFacts(event);

    return (
      <>
        {bucket && (
          <tr className="htbl-day">
            <td colSpan={6}>{bucket}</td>
          </tr>
        )}
        <tr
          onClick={onOpen}
          className={`htbl-row htbl-v-storyx is-tall${open ? ' is-open' : ''}${rowSel ? ' is-sel' : ''}`}
        >
          <HomeSelectCell
            checked={checkedState}
            disabled={!accountId}
            ariaLabel={
              rowSel
                ? `Remove this signal's contacts from the list`
                : `Add this signal's contacts to the list`
            }
            onToggle={toggle}
          />
          {/* Signal identity: the signal AND the account it belongs to, so the
              row says WHAT and WHO without spending a column on the account. */}
          <td>
            <SignalCell sig={sig} account={event.account} />
          </td>
          <td>
            <span className="htbl-when">{relativeWhen(event.eventDate)}</span>
          </td>
          <td>
            <SignalDetailsCell
              event={event}
              details={details}
              facts={richFacts}
            />
          </td>
          {/* People live inline in the Details cell as name pills; this column
              shows "Where it fits" (the account's offering for this signal). */}
          <td>
            <SignalFitCell event={event} fit={richFacts.fit} />
          </td>
          <td>
            <span className="htbl-tw">
              <ChevronDown />
            </span>
          </td>
        </tr>
      </>
    );
  }

  Object.assign(window.T, { SignalsTab });
})();
