/* Ported from apps/web/src/features/home/accounts/AccountsTab.tsx
 * + AddAccountsDialog.tsx, candidate-key.ts, search-query.ts, and the
 * tab-owned pieces of accounts-query.ts (homeAccountsVars,
 * HOME_ACCOUNTS_PAGE_SIZE, HOME_ACCOUNTS_DEFAULT_SORT). */
(() => {
  const T = window.T;
  const { useCallback, useEffect, useMemo, useRef, useState } = React;
  const {
    AccountImportDialog,
    Button,
    Dialog,
    DialogContent,
    DialogDescription,
    DialogHeader,
    DialogTitle,
    LogoAvatar,
    TableScrollRegion,
    TypeChip,
    WORKFLOW_JOB_STARTED_EVENT,
    getInitials,
  } = T.UI;
  const { Check, ChevronDown, Loader2, Plus } = T.Icons;

  // 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 };

  /* ================= search-query.ts ================= */

  /** Bare host of a URL/domain-ish string: `https://www.acme.com/x` → `acme.com`. */
  const domainOf = (website) => {
    if (!website) return null;
    return (
      website
        .replace(/^https?:\/\//, '')
        .replace(/^www\./, '')
        .split('/')[0] || null
    );
  };

  /**
   * Company (or showcase) slug out of a pasted LinkedIn URL, lowercased.
   * `https://www.linkedin.com/company/riverside-fm/` → `riverside-fm`.
   * Returns null for anything that isn't a recognised company URL — including
   * person `/in/` URLs and bare slugs, which must NOT hijack the name search.
   */
  const linkedinHandleOf = (q) => {
    const match = q
      .trim()
      .match(/(?:^|\/\/|\.)linkedin\.com\/(?:company|showcase)\/([^/?\s#]+)/i);
    return match ? match[1].toLowerCase() : null;
  };

  /** `linkedin.com` or any of its country subdomains (`il.linkedin.com`). */
  const isLinkedinHost = (host) =>
    !!host && /(^|\.)linkedin\.com$/i.test(host);

  /**
   * A pasted website/domain resolves to a direct "add this domain" row rather
   * than a CoreSignal name search. TRA-1444: NO LinkedIn URL carrying a path
   * is a domain query — left to `domainOf` they all collapse to `linkedin.com`.
   */
  const isDomainQuery = (q) => {
    const trimmed = q.trim();
    if (linkedinHandleOf(trimmed) !== null) return false;
    if (isLinkedinHost(domainOf(trimmed)) && /linkedin\.com\/\S/i.test(trimmed)) {
      return false;
    }
    return /\.[a-z]{2,}$/.test(domainOf(trimmed) ?? '') && !trimmed.includes(' ');
  };

  /** Public suffixes whose registrable domain is the last THREE labels. */
  const MULTIPART_TLDS = new Set([
    'co.uk',
    'org.uk',
    'gov.uk',
    'ac.uk',
    'co.jp',
    'com.au',
    'net.au',
    'org.au',
    'co.nz',
    'com.br',
    'com.mx',
    'com.sg',
    'co.za',
    'com.cn',
    'co.in',
    'com.tr',
  ]);

  /**
   * Name to fall back to when a pasted domain resolves to no company: the
   * registrable label, not the raw query and not the subdomain.
   * `riverside.fm` → `riverside`; `careers.riverside.fm` → `riverside`.
   */
  const domainLabel = (domain) => {
    const parts = domain
      .trim()
      .toLowerCase()
      .split('.')
      .filter((p) => p.length > 0);
    // A single label is a bare suffix (".com") or junk — there is no
    // registrable name to search.
    if (parts.length < 2) return null;
    const suffixLen = MULTIPART_TLDS.has(parts.slice(-2).join('.')) ? 2 : 1;
    return parts[parts.length - suffixLen - 1] ?? null;
  };

  /* ================= candidate-key.ts ================= */

  // Stable key for a "Beyond your accounts" candidate, used to mark its row as
  // "adding" the instant it's clicked (TRA-1359 step 4). coresignalId is the
  // canonical identity; website/name are fallbacks for the odd null-id row.
  const candidateKey = (c) => String(c.coresignalId ?? c.website ?? c.name ?? '');

  /* ================= accounts-query.ts (tab-owned pieces) ================= */

  /** First-page size for the Home Accounts tab. */
  const HOME_ACCOUNTS_PAGE_SIZE = 50;

  /** The tab's default sort, also used by the import hand-off's prefetch. */
  const HOME_ACCOUNTS_DEFAULT_SORT = { sortBy: 'timingScore', sortDir: 'desc' };

  /**
   * Variables for the Home Accounts tab's first page. Shared by the tab and by
   * the import hand-off's prefetch — they must match EXACTLY or the prefetch
   * writes a different cache entry than the tab reads.
   */
  function homeAccountsVars(opts) {
    const sortBy = opts.sortBy ?? HOME_ACCOUNTS_DEFAULT_SORT.sortBy;
    const sortDir = opts.sortDir ?? HOME_ACCOUNTS_DEFAULT_SORT.sortDir;
    return {
      ids: opts.ids ?? undefined,
      ...(opts.withUserId ? { userId: opts.userId } : {}),
      search: opts.search || undefined,
      sortBy,
      sortDir,
      skip: 0,
      take: HOME_ACCOUNTS_PAGE_SIZE,
    };
  }

  /** apolloClient.query({ query: GET_HOME_ACCOUNTS, variables }) stand-ins:
   *  synchronous mock reads wrapped in promises so loadMore / bulk selection
   *  keep their async shape (fetchPolicy 'network-only' /
   *  HOME_TABLE_BULK_FETCH_POLICY are meaningless without a cache). */
  async function queryHomeAccounts(variables) {
    const rows = T.Data.raw.filterAccounts(variables);
    const skip = variables.skip || 0;
    const take = variables.take != null ? variables.take : rows.length;
    return { data: { accounts: rows.slice(skip, skip + take) } };
  }
  async function queryHomeMyAccounts(variables) {
    const rows = T.Data.raw.filterAccounts(variables);
    const skip = variables.skip || 0;
    const take = variables.take != null ? variables.take : rows.length;
    return { data: { myAccounts: rows.slice(skip, skip + take) } };
  }
  /** apolloClient.query({ query: GET_HOME_ACCOUNT_COUNTS }) stand-in. */
  async function queryAccountCounts(ids) {
    const { eventsForAccount, peopleForAccount, lastOutreachForAccount } =
      T.Data.raw;
    return {
      data: {
        accountCounts: (ids || []).map((id) => ({
          accountId: id,
          eventCount: eventsForAccount(id).length,
          peopleCount: peopleForAccount(id).length,
          // The mock keeps per-account discovery flags on the row itself so the
          // simulated research run can drive the spinners (see below).
          contactsDiscovering: !!discoveringIds.has(id),
          signalsDiscovering: false,
          lastOutreachAt: lastOutreachForAccount(id),
        })),
      },
    };
  }

  /* ============ lib/formatters.ts (formatRevenue slice — private) ============ */

  function formatUSD(value) {
    if (value >= 1000000000) return `$${(value / 1000000000).toFixed(1)}B`;
    if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
    if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
    return `$${value.toLocaleString()}`;
  }

  function formatRevenue(revenue) {
    if (!revenue || revenue.from === undefined || revenue.from === null) {
      return null;
    }
    if (
      revenue.to === undefined ||
      revenue.to === null ||
      revenue.from === revenue.to
    ) {
      return formatUSD(revenue.from);
    }
    return `${formatUSD(revenue.from)} - ${formatUSD(revenue.to)}`;
  }

  /* ============ simulated initial-discovery run (CONVENTIONS rule 7) ============
   * The app's create triggers CoreSignal resolution + paid initial discovery
   * server-side; contacts land minutes later via polling. The port simulates
   * the run with a setTimeout that creates a small fictional roster for the
   * new account — inside AudienceProvider's RESEARCH_GRACE_MS window, so the
   * fresh (researching) entry settles into real picks and the "<Account>
   * added" note raises exactly as it does when a live roster lands. */

  const discoveringIds = new Set();

  const ROSTER_POOL = [
    ['Maren Holloway', 'VP Sales'],
    ['Dmitri Falk', 'Chief Technology Officer'],
    ['Solene Barreto', 'Head of Marketing'],
    ['Casper Nyland', 'Director of Operations'],
    ['Imani Oyelaran', 'VP Engineering'],
    ['Rhea Puddicombe', 'Head of Partnerships'],
    ['Anders Vale', 'Chief Revenue Officer'],
    ['Noor El-Amin', 'Director of Product'],
  ];
  let rosterSeq = 0;

  function simulateInitialDiscovery(accountId) {
    discoveringIds.add(accountId);
    T.Data.raw.bump();
    // Contacts "land" well inside the audience provider's 6s research window.
    setTimeout(() => {
      const still = T.Data.raw.acctById(accountId);
      discoveringIds.delete(accountId);
      if (!still) {
        T.Data.raw.bump();
        return;
      }
      const n = 3 + (rosterSeq % 2);
      const picks = [];
      for (let i = 0; i < n; i++) {
        picks.push(ROSTER_POOL[rosterSeq++ % ROSTER_POOL.length]);
      }
      // createPerson pushes into the mock dataset + bumps, so every reactive
      // hook (the audience roster poll included) sees the roster at once.
      for (const [fullName, title] of picks) {
        void T.Data.createPerson({ accountId, fullName, title });
      }
    }, 2800);
  }

  /* ================= AccountsTab.tsx ================= */

  /** Text columns read best A→Z; counts, scores and revenue open biggest-first. */
  const ACCOUNTS_ASC_FIRST = ['name', 'hq'];

  /** Per-row batch counts + TRA-1359 discovery-stage flags. */
  function toRowCounts(c) {
    return {
      events: c.eventCount ?? 0,
      people: c.peopleCount ?? 0,
      contactsDiscovering: c.contactsDiscovering ?? false,
      signalsDiscovering: c.signalsDiscovering ?? false,
      lastOutreachAt: c.lastOutreachAt ?? null,
    };
  }

  function BeyondRow({
    name,
    website,
    employeeCount,
    annualRevenue,
    ownership,
    hqLocation,
    // TRA-1359 step 4: the click already fired an add and we're awaiting the
    // server create (blocks on the inline CoreSignal resolve, a few hundred ms).
    // Paint the row's "in-audience" background NOW so the click registers on the
    // first frame instead of after the round-trip lands the real account.
    pending,
    onAdd,
  }) {
    const { HomeSelectCell, cn } = T;
    return (
      // Clicking anywhere in the row used to create the account. That is far too
      // easy to trigger by accident for something that writes a record and starts
      // paid discovery, so adding is an explicit act via the checkbox or the
      // button. `.htbl-ext` also drops the pointer cursor `.htbl-row` sets for
      // owned rows (which DO open a drawer), so the row never advertises a click
      // it will not honour.
      <tr
        data-testid="home-accounts-beyond-row"
        className={cn('htbl-row htbl-ext', pending && 'is-sel')}
      >
        {/* The SAME cell component as an owned row: one click, one gesture, and
            one place that owns the click semantics. `busy` swaps a spinner into
            the checkbox's own slot. */}
        <HomeSelectCell
          checked={false}
          busy={!!pending}
          ariaLabel={`Add ${name} to your workspace`}
          onToggle={onAdd}
        />
        <td>
          <span className="htbl-acct">
            <LogoAvatar
              domain={domainOf(website) ?? undefined}
              alt={name}
              fallbackText={getInitials(name)}
              size="sm"
              className="size-[26px] rounded-[7px] shrink-0"
            />
            <span className="htbl-acct-t">
              <span className="htbl-acct-name">{name}</span>
            </span>
          </span>
        </td>
        {/* Firmographics from CoreSignal so the row reads like a real account,
            not an empty stub. Signals/Contacts/Score stay blank until the
            account is added and discovery runs. */}
        <td>
          <span className="htbl-num">
            {employeeCount != null ? employeeCount.toLocaleString() : ''}
          </span>
        </td>
        <td>
          <span className="htbl-num">
            {formatRevenue(
              annualRevenue != null ? { from: annualRevenue, to: null } : null
            ) || ''}
          </span>
        </td>
        {/* `flex` (block) overrides TypeChip's inline-flex so the td's
            vertical-align:middle centers it like the sibling text cells. */}
        <td>
          {ownership ? <TypeChip type={ownership} className="flex" /> : null}
        </td>
        <td>
          <span className="htbl-muted" title={hqLocation ?? undefined}>
            {hqLocation || ''}
          </span>
        </td>
        <td colSpan={4} className="text-right">
          {/* The labelled path to the same create-and-select as the checkbox.
              stopPropagation is retained because the row may regain a click
              handler (opening the drawer) later. */}
          <span className="inline-flex items-center justify-end">
            <Button
              variant="secondary"
              size="xs"
              // Pinned width: "Add" and "Adding…" are different lengths and the
              // cell is right-aligned, so without it the button jumps at the row
              // edge on every click.
              className="min-w-[5.25rem] justify-center"
              loading={pending}
              onClick={(e) => {
                e.stopPropagation();
                if (!pending) onAdd();
              }}
              title="Create this account and add to your list"
            >
              {!pending && <Plus />}
              {pending ? 'Adding…' : 'Add'}
            </Button>
          </span>
        </td>
      </tr>
    );
  }

  function AccountsTab({
    scopeUserId,
    canFilterByUser,
    scopeAccountIds,
    onOpenAccount,
    openAccountId,
    onCountChange,
    pollMs = 0,
    onImportReady,
    toolbarExtra,
    bulkSelection,
    onBulkSelectionChange,
    onClearSelection,
    selectionResetVersion = 0,
  }) {
    const {
      ACCOUNTS_SEARCH_LIMIT,
      ExternalGroupBand,
      HomeSearchSplitCount,
      useExternalBandOffset,
      accountsToAudienceBatch,
      countSelectedMatchingIds,
      fetchAllMatchingRows,
      hasAudienceSelection,
      shouldShowHomeTableSelectionScope,
      HomeSelectCell,
      HomeTableResultCount,
      HomeTableSearch,
      HomeTableSelectAllCheckbox,
      HomeTableSelectionScopeRow,
      HomeTableSortTh,
      nextSortDir,
      SearchInFindLink,
      useWorkingSetPin,
      leverPer,
      cn,
      homeAnalytics,
      toast,
    } = T;
    const pb = T.useAudience();
    // usePermissions → fixture user holds every permission (auth not ported).
    const has = () => true;
    // GET_TENANT_SUBSCRIPTION (cache-first) → mock tenant subscription.
    const { data: subData } = T.Data.useTenantSubscription();
    // TRA-1359: the redesign routes Accounts to this Home tab, so the CSV
    // account import entry point lives here. The per-teammate assignment flow
    // (userEmail column, "reassign to owner") is an ENTERPRISE capability —
    // only enterprise admins get it.
    const isEnterprise = subData?.myTenant?.plan === 'ENTERPRISE';
    const importIsAdmin = isEnterprise && has('users:manage');
    const [importOpen, setImportOpen] = useState(false);
    // Seeded from `?q=` so one table can hand a search to another.
    const [query, setQuery] = T.useSeededSearch();
    // Server queries (and the paid CoreSignal name search) see only the settled
    // term, not every keystroke.
    const debouncedQuery = T.useDebouncedValue(query, 250);
    const [sortField, setSortField] = useState('timingScore');
    const [sortDir, setSortDir] = useState('desc');

    const toggleSort = (field) => {
      const nextDir = nextSortDir({
        field,
        sortField,
        sortDir,
        ascFirst: ACCOUNTS_ASC_FIRST,
      });
      homeAnalytics.sortChanged({
        table: 'accounts',
        column: field,
        direction: nextDir,
      });
      setSortField(field);
      setSortDir(nextDir);
    };

    // Built by the SAME helper the import hand-off prefetches with, so the two
    // hit the same cache entry (a mismatch would reintroduce the stale-rows flash).
    const adminVars = homeAccountsVars({
      ids: scopeAccountIds,
      userId: scopeUserId,
      search: debouncedQuery,
      sortBy: sortField,
      sortDir,
      withUserId: true,
    });
    const memberVars = homeAccountsVars({
      ids: scopeAccountIds,
      search: debouncedQuery,
      sortBy: sortField,
      sortDir,
      withUserId: false,
    });
    const PAGE_SIZE = HOME_ACCOUNTS_PAGE_SIZE;
    // App: useQuery(GET_HOME_ACCOUNTS / GET_HOME_MY_ACCOUNTS,
    // { fetchPolicy: 'cache-and-network', pollInterval: pollMs }). Mock hooks
    // recompute reactively on data mutations, so the host's poll cadence is
    // unnecessary here; both run (skip is meaningless without a network).
    const adminQ = T.Data.useHomeAccounts(adminVars);
    const memberQ = T.Data.useHomeMyAccounts(memberVars);
    const loading = canFilterByUser ? adminQ.loading : memberQ.loading;

    // Soft "N of M" chip inside search — only when search narrows below the list/tenant baseline.
    const filtersActive = Boolean(debouncedQuery.trim());
    const accountCountVars = useMemo(
      () => ({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        ...(scopeAccountIds?.length ? { ids: scopeAccountIds } : {}),
        ...(debouncedQuery ? { search: debouncedQuery } : {}),
      }),
      [scopeUserId, scopeAccountIds, debouncedQuery]
    );
    const accountBaselineVars = useMemo(
      () => ({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        ...(scopeAccountIds?.length ? { ids: scopeAccountIds } : {}),
      }),
      [scopeUserId, scopeAccountIds]
    );
    const { data: accountCountData, previousData: accountCountPrev } =
      T.Data.useHomeAccountsTotal(accountCountVars);
    // App passes `skip: !filtersActive`; the mock hook is synchronous and
    // cheap, so it always runs and the derived reads below stay identical.
    const { data: accountBaselineData, previousData: accountBaselinePrev } =
      T.Data.useHomeAccountsTotal(accountBaselineVars);
    const accountCountResolved = accountCountData ?? accountCountPrev;
    const accountMatchCount = accountCountResolved?.accountsCount ?? 0;
    const resultBaseline = filtersActive
      ? (accountBaselineData ?? accountBaselinePrev)?.accountsCount
      : undefined;
    const resultCount =
      filtersActive && accountCountResolved !== undefined
        ? accountMatchCount
        : undefined;
    const showResultCount =
      resultCount !== undefined &&
      resultBaseline !== undefined &&
      resultBaseline > resultCount;

    // previousData keeps rows painted while a keystroke's refetch is in flight —
    // scoped to the SAME list: reusing it across a scope change paints the
    // previous list's accounts under the new list's filter.
    const listScopeKey = useMemo(
      () => JSON.stringify(scopeAccountIds ?? null),
      [scopeAccountIds]
    );
    const lastPaintedScope = useRef(listScopeKey);
    if (adminQ.data || memberQ.data) lastPaintedScope.current = listScopeKey;
    const sameScope = lastPaintedScope.current === listScopeKey;
    const adminData =
      adminQ.data ?? (sameScope ? adminQ.previousData : undefined);
    const memberData =
      memberQ.data ?? (sameScope ? memberQ.previousData : undefined);
    const firstPage = useMemo(
      () =>
        (canFilterByUser ? adminData?.accounts : memberData?.myAccounts) ?? [],
      [canFilterByUser, adminData, memberData]
    );

    // Appended pages live outside the reactive first page; reset on scope change.
    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(canFilterByUser ? adminVars : memberVars);
    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);
      // The callbacks are intentionally excluded: the page creates them inline,
      // while scopeKey is the complete server selection identity.
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [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]);

    // ── Working set pinned on top (the audience bar is the source of truth). The
    // bar's account groups drive which accounts pin. useWorkingSetPin snapshots
    // them per view-entry (incl. modal close via bumpPin) + streams live
    // arrivals, so checkboxes never reorder rows yet a just-added /
    // just-discovered account still surfaces. Fetched by id (unscoped) and
    // sorted newest-first.
    const audienceAccountIds = pb.res.groups.map((g) => g.account.id);
    const loadedIds = useMemo(
      () => new Set([...firstPage, ...extra].map((r) => r.id)),
      [firstPage, extra]
    );
    const nonPinningIds = useMemo(
      () => [
        ...new Set([
          ...(bulkSelection?.ids ?? []),
          ...(loadedSelection?.ids ?? []),
        ]),
      ],
      [bulkSelection?.ids, loadedSelection?.ids]
    );
    const { pinnedIds, bump: bumpPin } = useWorkingSetPin({
      audienceIds: audienceAccountIds,
      loadedIds,
      viewKey: scopeKey,
      resetKey: selectionResetVersion,
      nonPinningIds,
    });
    // Unscoped by LIST focus (added accounts show even outside the focused list),
    // but the active text search still applies — otherwise a search would leave
    // non-matching selected rows pinned at the top. App passes
    // `skip: pinnedIds.length === 0`; the pinnedRows memo guards it identically.
    const pinAdminQ = T.Data.useHomeAccounts({
      ids: pinnedIds,
      search: debouncedQuery || undefined,
      skip: 0,
      take: 500,
    });
    const pinMemberQ = T.Data.useHomeMyAccounts({
      ids: pinnedIds,
      search: debouncedQuery || undefined,
      skip: 0,
      take: 500,
    });
    const pinnedRows = useMemo(() => {
      if (pinnedIds.length === 0) return [];
      const d = canFilterByUser
        ? (pinAdminQ.data ?? pinAdminQ.previousData)
        : (pinMemberQ.data ?? pinMemberQ.previousData);
      const rows =
        (canFilterByUser ? d?.accounts : d?.myAccounts) ?? [];
      // Newest-created first → a just-added account sits atop the selected group.
      return [...rows].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    }, [
      canFilterByUser,
      pinnedIds.length,
      pinAdminQ.data,
      pinAdminQ.previousData,
      pinMemberQ.data,
      pinMemberQ.previousData,
    ]);
    const pinnedIdSet = useMemo(
      () => new Set(pinnedRows.map((r) => r.id)),
      [pinnedRows]
    );

    // Scope is applied server-side (ids arg); rows ARE the scoped population.
    const accounts = useMemo(() => {
      const seen = new Set();
      const out = [];
      // Pinned working set first (newest-first), then the paged/scoped rows.
      for (const row of [...pinnedRows, ...firstPage, ...extra]) {
        if (seen.has(row.id)) continue;
        seen.add(row.id);
        out.push(row);
      }
      // Domain query: the server matches `url CONTAINS "x.com"`, which pollutes
      // the list with every account whose domain merely ENDS in the query. For
      // a pasted domain the user wants the company AT that domain — keep only
      // exact-domain matches. The company itself still surfaces under "Beyond
      // your accounts" if not already added.
      if (!scopeAccountIds && isDomainQuery(query)) {
        const dom = domainOf(query);
        return out.filter((a) => domainOf(a.url) === dom);
      }
      return out;
    }, [pinnedRows, firstPage, extra, scopeAccountIds, query]);
    const accountLoadedCount = Math.min(
      firstPage.length + extra.length,
      accountMatchCount
    );
    const loadMore = useCallback(async () => {
      if (loadingMore || !hasMore || loading) return;
      setLoadingMore(true);
      try {
        const skipN = firstPage.length + extra.length;
        const { data: page } = canFilterByUser
          ? await queryHomeAccounts({ ...adminVars, skip: skipN, take: PAGE_SIZE })
          : await queryHomeMyAccounts({
              ...memberVars,
              skip: skipN,
              take: PAGE_SIZE,
            });
        const rows =
          (canFilterByUser ? page.accounts : page.myAccounts) ?? [];
        homeAnalytics.loadedMore({
          table: 'accounts',
          loaded: rows.length,
          total_loaded: skipN + rows.length,
        });
        setHasMore(rows.length === PAGE_SIZE);
        setExtra((prev) => [...prev, ...rows]);
      } finally {
        setLoadingMore(false);
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps -- vars objects are rebuilt per render; scopeKey covers their content
    }, [
      scopeKey,
      canFilterByUser,
      extra.length,
      firstPage.length,
      hasMore,
      loading,
      loadingMore,
    ]);
    const scrollerRootRef = useRef(null);
    const setScrollerRoot = useCallback((node) => {
      scrollerRootRef.current = node;
    }, []);
    const sentinelRef = useRef(null);
    useEffect(() => {
      if (!hasMore || loading) return;
      const el = sentinelRef.current;
      if (!el || !scrollerRootRef.current) return;
      const io = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting) void loadMore();
        },
        { root: scrollerRootRef.current, rootMargin: '200px' }
      );
      io.observe(el);
      return () => io.disconnect();
    }, [hasMore, loading, loadMore]);

    // eventCount / peopleCount aren't materialized on list rows — batch them in
    // chunks (the server caps accountCounts at 200 ids per call) and accumulate,
    // so paging deep never drops counts.
    const [counts, setCounts] = useState(() => new Map());
    const requestedIdsRef = useRef(new Set());
    // One shared chunked fetch for the initial, polling, and final-refetch
    // paths (TRA-1359 review). Merges results into `counts`; a failed chunk is
    // reported so the caller can decide whether to retry.
    const fetchCountsInto = useCallback(async (ids, onChunkError) => {
      for (let i = 0; i < ids.length; i += 200) {
        const chunk = ids.slice(i, i + 200);
        try {
          const { data: page } = await queryAccountCounts(chunk);
          setCounts((prev) => {
            const next = new Map(prev);
            for (const c of page?.accountCounts ?? []) {
              next.set(c.accountId, toRowCounts(c));
            }
            return next;
          });
        } catch {
          onChunkError?.(chunk);
        }
      }
    }, []);
    useEffect(() => {
      const missing = accounts
        .map((a) => a.id)
        .filter((id) => !requestedIdsRef.current.has(id));
      if (missing.length === 0) return;
      missing.forEach((id) => requestedIdsRef.current.add(id));
      // No cancellation: counts are absolute per id, so a result arriving after
      // a newer page kicked off its own loop is still correct.
      // Failed chunk: un-mark so a later run retries these ids.
      void fetchCountsInto(missing, (chunk) =>
        chunk.forEach((id) => requestedIdsRef.current.delete(id))
      );
    }, [accounts, fetchCountsInto]);
    // Mock-only liveness: the app's counts go stale only until the next poll
    // tick; the mock dataset mutates in place (the simulated research run lands
    // a roster, deletes cascade), so re-read counts for every requested id
    // whenever the dataset version bumps. Cheap: synchronous reads.
    useEffect(
      () =>
        T.Data.raw.subscribe(() => {
          const ids = [...requestedIdsRef.current];
          if (ids.length > 0) void fetchCountsInto(ids);
        }),
      [fetchCountsInto]
    );

    // TRA-1359: while the page is polling, re-fetch counts for just the rows
    // still flagged discovering so spinners clear and real numbers replace
    // "finding…" / "discovering…" without a reload. The busy set is read from a
    // ref so the interval survives count updates.
    const busyIdsRef = useRef([]);
    useEffect(() => {
      busyIdsRef.current = [...counts.entries()]
        .filter(
          ([, c]) =>
            c.contactsDiscovering ||
            c.signalsDiscovering ||
            // TRA-1359: a just-imported account that has its contacts but no
            // signals yet is in the post-import grace — keep refetching it.
            (c.people > 0 && c.events === 0)
        )
        .map(([id]) => id);
    }, [counts]);
    useEffect(() => {
      if (!pollMs) return;
      const t = setInterval(() => {
        const busy = busyIdsRef.current;
        if (busy.length === 0) return;
        // Transient chunk failure: next tick retries.
        void fetchCountsInto(busy);
      }, pollMs);
      return () => clearInterval(t);
    }, [pollMs, fetchCountsInto]);

    // Falling edge (TRA-1359 review): when polling stops (pollMs -> 0) with
    // rows still flagged discovering, run ONE final refetch of just those ids.
    const prevPollMsRef = useRef(pollMs);
    useEffect(() => {
      const prev = prevPollMsRef.current;
      prevPollMsRef.current = pollMs;
      if (prev > 0 && pollMs === 0 && busyIdsRef.current.length > 0) {
        void fetchCountsInto(busyIdsRef.current);
      }
    }, [pollMs, fetchCountsInto]);

    // Row order comes entirely from the pin (pinned working set first, then paged
    // rows). Section membership is the pinned set — NOT live `pb.hasAccount` — so
    // a checkbox toggle never re-buckets or reorders a row.
    const sortedRows = accounts;

    // Stale-while-refetch: rows stay painted; this flags the in-flight fetch so
    // the UI can still signal that results are updating. Only for an actual
    // SEARCH (networkStatus.setVariables), NOT the 2.5s background polls. The
    // mock always reports networkStatus 7, so this stays false.
    const searchNetworkStatus = canFilterByUser
      ? adminQ.networkStatus
      : memberQ.networkStatus;
    const refetching =
      searchNetworkStatus === NetworkStatus.setVariables && accounts.length > 0;

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

    // See SignalsTab: the settled query, not the keystroke; length only, because
    // an account search is often a company the rep is targeting; and the FRESH
    // count so the number belongs to this search rather than the one before it.
    const searchReportedFor = useRef(null);
    const freshAccountCount = accountCountData?.accountsCount;
    useEffect(() => {
      if (!debouncedQuery || freshAccountCount === undefined) return;
      if (searchReportedFor.current === debouncedQuery) return;
      searchReportedFor.current = debouncedQuery;
      homeAnalytics.searched({
        table: 'accounts',
        query_length: debouncedQuery.length,
        result_count: freshAccountCount,
      });
    }, [debouncedQuery, freshAccountCount]);

    const accountsEmpty = !loading && accounts.length === 0;
    // Deduped on (empty, reason) — see the app for the polling-flicker history.
    const emptyReportedRef = useRef(null);
    useEffect(() => {
      // Re-arm ONLY when the table actually has rows. A mid-fetch empty is not
      // a dead end the user reached; it is a table waiting.
      if (accounts.length > 0) {
        emptyReportedRef.current = null;
        return;
      }
      if (!accountsEmpty) return;
      const reason = filtersActive ? 'filtered' : 'no_data';
      if (emptyReportedRef.current === reason) return;
      emptyReportedRef.current = reason;
      homeAnalytics.emptyStateViewed({ table: 'accounts', reason });
    }, [accountsEmpty, accounts.length, filtersActive]);

    // "Beyond your accounts" — a pasted LinkedIn company URL or domain resolves
    // to one company; otherwise CoreSignal name candidates. Suppressed while
    // scoped to a list. Rows already in the tenant are filtered out.
    const beyondActive = !scopeAccountIds && debouncedQuery.trim().length >= 3;
    // TRA-1444: LinkedIn is checked FIRST. `linkedin.com/company/<slug>` is a
    // domain-shaped string, so left to isDomainQuery it resolved LinkedIn
    // Corporation and skipped the name search.
    // Both classifications read the DEBOUNCED query.
    const pastedLinkedin = beyondActive
      ? linkedinHandleOf(debouncedQuery)
      : null;
    const pastedDomain =
      beyondActive && !pastedLinkedin && isDomainQuery(debouncedQuery)
        ? domainOf(debouncedQuery)
        : null;
    // App: useQuery(RESOLVE_ACCOUNT_BY_LINKEDIN / _BY_DOMAIN, { skip }). The
    // mock hooks always run; empty variables resolve to null, matching skip.
    const { data: linkedinData, loading: linkedinLoading } =
      T.Data.useResolveAccountByLinkedin({ handle: pastedLinkedin ?? '' });
    const linkedinHit = pastedLinkedin
      ? (linkedinData?.resolveAccountByLinkedin ?? null)
      : null;
    const { data: domainData, loading: domainLoading } =
      T.Data.useResolveAccountByDomain({ domain: pastedDomain ?? '' });
    const domainHit = pastedDomain
      ? (domainData?.resolveAccountByDomain ?? null)
      : null;
    // The single-company lookups and the name search are no longer mutually
    // exclusive (TRA-1444): when a pasted domain resolves to nothing the search
    // falls through to a name search on the domain's own LABEL — the raw
    // `riverside.fm` fails the server's name guard, `riverside` matches.
    const domainFallbackName = pastedDomain ? domainLabel(pastedDomain) : null;
    const domainExhausted = !!pastedDomain && !domainLoading && !domainHit;
    const nameQuery = domainExhausted
      ? (domainFallbackName ?? debouncedQuery.trim())
      : debouncedQuery.trim();
    // A pasted LinkedIn URL has no useful name search (the raw URL matches
    // nothing), so that branch does not fall through. The app expresses this as
    // `skip`; the mock hook always runs, so the gate is applied to the result.
    const beyondSkipped =
      !beyondActive || !!pastedLinkedin || (!!pastedDomain && !domainExhausted);
    const {
      data: beyondDataRaw,
      loading: beyondLoading,
      error: beyondError,
    } = T.Data.useSearchCoresignal({
      query: nameQuery,
      limit: ACCOUNTS_SEARCH_LIMIT,
    });
    const beyondData = beyondSkipped ? undefined : beyondDataRaw;
    // Candidates already created this session. `existingDomains` cannot cover
    // this: it is keyed on DOMAIN, so a candidate with no website never matches,
    // and the row stayed in "available to add" — unchecked and clickable — after
    // a successful create. A second click then made a second account and a
    // second paid discovery run.
    const [createdKeys, setCreatedKeys] = useState(() => new Set());
    const existingDomains = useMemo(
      () => new Set(accounts.map((a) => domainOf(a.url)).filter(Boolean)),
      [accounts]
    );
    // All three lookups yield the same row shape, so normalize to ONE list and
    // keep the render a single branch (TRA-1444).
    const directHit = pastedLinkedin
      ? linkedinHit
      : pastedDomain
        ? domainHit
        : null;
    const directLoading = pastedLinkedin
      ? linkedinLoading
      : pastedDomain
        ? domainLoading
        : false;

    // Ownership for a single hit CANNOT come from `existingDomains`: that set is
    // built from `accounts`, which the server filtered by the user's raw query,
    // and neither branch's query matches any account field. Ask by the HIT's own
    // domain instead.
    const directDomain = directHit ? domainOf(directHit.website) : null;
    const { data: ownedData, loading: ownedLoading } =
      T.Data.useHomeAccountsTotal({
        ...(scopeUserId ? { userId: scopeUserId } : {}),
        search: directDomain ?? '',
      });
    const directHitOwned =
      !!directDomain && (ownedData?.accountsCount ?? 0) > 0;

    // Name-guarding and same-domain dedup happen server-side. Here we only drop
    // rows already in the tenant, which the server can't know about.
    const beyond = useMemo(() => {
      if (directHit && directHitOwned) return [];
      const rows = directHit
        ? [
            {
              ...directHit,
              name: directHit.name ?? pastedDomain ?? null,
              website: directHit.website ?? pastedDomain ?? null,
            },
          ]
        : (beyondData?.searchCoresignalByName ?? []);
      return rows.filter(
        (c) =>
          !existingDomains.has(domainOf(c.website)) &&
          // Domain-keyed dedup misses a candidate with no website, and misses
          // the window before the accounts list refreshes. Keyed on the same
          // candidate identity the add uses.
          !createdKeys.has(
            candidateKey({
              coresignalId: c.coresignalId,
              website: c.website ?? null,
              name: c.name ?? 'New account',
            })
          )
      );
    }, [
      directHit,
      directHitOwned,
      pastedDomain,
      beyondData,
      existingDomains,
      createdKeys,
    ]);
    /** Directory rows actually on screen, for the band and the split count. */
    const externalCount = beyond.length;

    // The fall-through hands off between two queries; the third clause guards
    // the frame between un-skipping the name search and it starting.
    const beyondLoadingAny =
      directLoading ||
      // Don't paint an "Add to Trayo" row before we know whether it's already ours.
      ownedLoading ||
      beyondLoading ||
      (domainExhausted && !beyondData && !beyondError);
    // A single hit that the tenant already owns is not "nothing found" — it's
    // already in the account list, so render neither a row nor a no-results line.
    const directHitAlreadyOwned = !!directHit && beyond.length === 0;

    // "Beyond" resolved to nothing addable. In that case the no-results message
    // and the "Search in Find" escape hatch collapse onto ONE line, and the
    // standalone trailing link below is suppressed to avoid showing it twice.
    const beyondNoResults =
      beyondActive &&
      !beyondLoadingAny &&
      beyond.length === 0 &&
      !directHitAlreadyOwned;

    const beyondEmptyPrefix = pastedLinkedin
      ? 'No account found for that LinkedIn company.'
      : pastedDomain
        ? `No account found for ${pastedDomain}.`
        : 'No new accounts found.';

    // Candidates whose add-click has fired but whose server create hasn't landed
    // yet — their BeyondRow paints the "in-audience" background immediately so
    // the click feels instant (TRA-1359 step 4). Cleared when the create settles.
    const [pendingAdds, setPendingAdds] = useState(() => new Set());
    /** Same set, synchronously — see the guard in createFromCoresignal. */
    const inFlightAdds = useRef(new Set());

    const createFromCoresignal = async (candidate) => {
      // Seed the firmographics we already resolved for the "Beyond" row into
      // research_info (the snake_case shape the accounts list reads), so the new
      // row shows Employees/Revenue/Type/HQ immediately instead of blank until
      // the async CoreSignal resolution lands after the create refetch.
      const company = {};
      if (candidate.coresignalId != null)
        company.coresignal_id = String(candidate.coresignalId);
      if (candidate.employeeCount != null)
        company.employee_count = candidate.employeeCount;
      if (candidate.ownership) company.ownership = candidate.ownership;
      if (candidate.hqLocation) company.hq_location = candidate.hqLocation;
      // Nested under company (where the CoreSignal resolve writes it and the
      // AccountDrawer's researchDescription reads it), so the About paragraph
      // shows immediately rather than waiting on the async resolve.
      if (candidate.description) company.description = candidate.description;
      const researchInfo =
        Object.keys(company).length > 0 || candidate.annualRevenue != null
          ? {
              company,
              ...(candidate.annualRevenue != null
                ? {
                    financials: {
                      annual_revenue: {
                        from: candidate.annualRevenue,
                        to: candidate.annualRevenue,
                      },
                    },
                  }
                : {}),
            }
          : undefined;
      // Paint the clicked row's "in-audience" background on the first frame —
      // the create below blocks on the inline CoreSignal resolve (a few hundred
      // ms), so without this the click reads as dead air.
      const key = candidateKey(candidate);
      // Synchronous re-entrancy guard: `pendingAdds` is state, so two calls in
      // one tick both read the pre-update value and both create. The failure
      // mode is a duplicate ACCOUNT plus a second paid discovery run.
      if (inFlightAdds.current.has(key)) return;
      inFlightAdds.current.add(key);
      setPendingAdds((s) => new Set(s).add(key));
      try {
        // App: useMutation(CREATE_ACCOUNT) with { triggerCoresignal: true,
        // triggerInitialDiscovery: true, researchInfo }. The mock create takes
        // the flattened firmographics, so the seeded row carries them directly.
        const res = await T.Data.createAccount({
          name: candidate.name,
          url: candidate.website ?? undefined,
          oneLiner: candidate.description ?? null,
          employeeCount: candidate.employeeCount ?? null,
          annualRevenueFrom: candidate.annualRevenue ?? null,
          annualRevenueTo: candidate.annualRevenue ?? null,
          ownership: candidate.ownership ?? null,
          hqLocation: candidate.hqLocation ?? null,
        });
        const account = res?.account;
        // Bringing a brand-new company into the tenant triggers CoreSignal
        // resolution and initial discovery — real spend, and previously the only
        // trace was an `audience_account_added`.
        homeAnalytics.entityCreated({
          entity: 'account',
          via: 'search',
          ok: !!account,
        });
        if (!account) {
          // Previously silent: the spinner stopped, the checkbox went back to
          // unchecked and nothing said why, which invites the very re-click
          // that duplicates the record.
          toast.error(`Could not add ${candidate.name}.`);
          return;
        }
        // Seed the research_info snake_case blob onto the created row so the
        // drawer's About paragraph shows immediately (the app sends it through
        // the mutation input; the mock create doesn't take it).
        if (researchInfo) {
          account.researchInfo = researchInfo;
          T.Data.raw.bump();
        }
        setCreatedKeys((prev) => new Set(prev).add(key));
        {
          // Fresh entry: shows a researching group in the bar/audience drawer
          // and resolves to real picks once the roster lands.
          pb.addFreshAccount(account.id, candidate.name, candidate.website);
          toast(`${candidate.name} added and now researching.`);
          // TRA-1359: nudge the page-level discovering check (banner + tab
          // polling) and the run-monitor — parity with the Find add path.
          window.dispatchEvent(new CustomEvent(WORKFLOW_JOB_STARTED_EVENT));
          // Simulated initial discovery (CONVENTIONS rule 7): a setTimeout
          // lands a small fictional roster for the account, inside the
          // audience provider's research window, so the fresh group settles
          // into real picks like a live discovery run.
          simulateInitialDiscovery(account.id);
          // App: prepend the created account into the active accounts query's
          // cache (readQuery/writeQuery) instead of a network refetch. The mock
          // create pushed the row into the reactive dataset and bumped, so
          // every watching hook already repainted with it — no cache write.
        }
      } catch (error) {
        // The mutation rejected outright, so the `ok: false` above never ran.
        homeAnalytics.entityCreated({
          entity: 'account',
          via: 'search',
          ok: false,
        });
        // A duplicate is not a failure retrying can fix — say what happened.
        // Apollo Client v4 exposes CombinedGraphQLErrors as `error.errors`.
        const extensions = error?.errors?.[0]?.extensions;
        const code = (extensions?.originalError ?? extensions)?.code;
        toast.error(
          code === 'ACCOUNT_ALREADY_EXISTS'
            ? `${candidate.name} is already in your accounts.`
            : `Could not add ${candidate.name}.`
        );
      } finally {
        inFlightAdds.current.delete(key);
        setPendingAdds((s) => {
          const next = new Set(s);
          next.delete(key);
          return next;
        });
      }
    };

    // Bucket separators precomputed (reassigning a running var during render is
    // disallowed by the react-compiler lint).
    const bucketLabels = useMemo(() => {
      const m = new Map();
      let last = null;
      for (const a of sortedRows) {
        const bucket = pinnedIdSet.has(a.id) ? 'in' : 'all';
        m.set(
          a.id,
          bucket !== last && pinnedIdSet.size > 0
            ? bucket === 'in'
              ? 'In this list'
              : 'All accounts'
            : null
        );
        last = bucket;
      }
      return m;
    }, [sortedRows, pinnedIdSet]);

    const selectedLoadedCount = useMemo(
      () =>
        sortedRows.filter(
          (account) =>
            pb.hasAccount(account.id) || pb.pickedCount(account.id) > 0
        ).length,
      [sortedRows, pb]
    );
    const loadedAccountIds = useMemo(
      () => sortedRows.map((account) => account.id),
      [sortedRows]
    );
    const selectedBulkMatchingCount = countSelectedMatchingIds(
      activeBulkSelection?.ids ?? null,
      (id) => pb.hasAccount(id) || pb.pickedCount(id) > 0
    );
    const selectedLoadedSelectionCount = countSelectedMatchingIds(
      loadedSelectionActive ? loadedSelection.ids : null,
      (id) => pb.hasAccount(id) || pb.pickedCount(id) > 0
    );
    const selectionScopeCount =
      selectedBulkMatchingCount ??
      selectedLoadedSelectionCount ??
      selectedLoadedCount;
    const allLoadedSelected =
      sortedRows.length > 0 && selectedLoadedCount === sortedRows.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(
        accountsToAudienceBatch(sortedRows, leverPer(pb.lever)),
        'accounts-table'
      );
      homeAnalytics.bulkAdded({
        table: 'accounts',
        via: 'accounts-table',
        matched: sortedRows.length,
        added,
        capped: false,
        audience_people_before: pb.res.counts.people,
        audience_accounts_before: pb.res.counts.accounts,
      });
      setLoadedSelection({ ids: loadedAccountIds, scopeKey });
    }, [
      anySelection,
      clearSelection,
      loadedAccountIds,
      pb,
      scopeKey,
      sortedRows,
    ]);

    const selectAllMatching = useCallback(async () => {
      if (accountMatchCount <= 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 : loadedAccountIds);
        const progressBase = selectionScopeCount;
        const result = await fetchAllMatchingRows({
          totalCount: accountMatchCount,
          startAt: activeBulkSelection?.nextOffset ?? 0,
          excludeIds: selectionIds,
          getRowId: (row) => row.id,
          signal: controller.signal,
          onProgress: (count) => setBulkSelectionProgress(progressBase + count),
          fetchPage: async (skip, take) => {
            if (canFilterByUser) {
              const { data: page } = await queryHomeAccounts({
                ...adminVars,
                skip,
                take,
              });
              return page.accounts ?? [];
            }
            const { data: page } = await queryHomeMyAccounts({
              ...memberVars,
              skip,
              take,
            });
            return page.myAccounts ?? [];
          },
        });
        if (
          controller.signal.aborted ||
          bulkRequest.current?.id !== requestId
        ) {
          return;
        }
        const added = pb.mergeAudience(
          accountsToAudienceBatch(result.rows, leverPer(pb.lever)),
          'accounts-table'
        );
        homeAnalytics.bulkAdded({
          table: 'accounts',
          via: 'accounts-table',
          matched: accountMatchCount,
          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)]),
        ];
        onBulkSelectionChange({
          scopeKey,
          ids,
          nextOffset: result.nextOffset,
          totalCount: accountMatchCount,
          outcome: result.outcome,
        });
        setLoadedSelection(null);
        toast.dismiss('home-accounts-select-all');
      } catch (error) {
        if (
          controller.signal.aborted ||
          (error instanceof Error && error.name === 'AbortError')
        ) {
          return;
        }
        toast.error('Couldn’t select all matching accounts. Try again.', {
          id: 'home-accounts-select-all',
        });
      } finally {
        if (bulkRequest.current?.id === requestId) {
          bulkRequest.current = null;
          setBulkSelectionBusy(false);
          setBulkSelectionProgress(0);
        }
      }
    }, [
      accountMatchCount,
      activeBulkSelection,
      adminVars,
      bulkSelectionBusy,
      canFilterByUser,
      loadedAccountIds,
      loadedSelection,
      loadedSelectionActive,
      memberVars,
      onBulkSelectionChange,
      pb,
      scopeKey,
      selectionScopeCount,
    ]);

    const exactDomainSelection = !scopeAccountIds && isDomainQuery(query);
    const selectionTotalCount = exactDomainSelection
      ? sortedRows.length
      : accountMatchCount;
    const showSelectionScope = shouldShowHomeTableSelectionScope({
      selectedBulkMatchingCount,
      selectedLoadedSelectionCount,
      allLoadedSelected,
      totalCount: selectionTotalCount,
      loadedCount: selectedLoadedCount,
    });

    /** The table element, so the sticky band's offset can be measured rather
     *  than declared. */
    const accountsTableRef = useRef(null);
    /** Whether one of main's sticky selection rows is showing above the data, so
     *  the "available to add" band parks below it rather than under it. */
    const selectionRowShowing = showSelectionScope;
    useExternalBandOffset(accountsTableRef, selectionRowShowing);

    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 accounts, or find new ones by name"
            searching={refetching}
            trailing={
              // While a directory search runs the count describes TWO
              // populations, so `N of M` gives way to `1 yours · 12 to add`.
              beyondActive ? (
                <HomeSearchSplitCount
                  owned={accountMatchCount}
                  external={externalCount}
                  limit={ACCOUNTS_SEARCH_LIMIT}
                />
              ) : showResultCount ? (
                <HomeTableResultCount
                  count={resultCount}
                  baseline={resultBaseline}
                />
              ) : undefined
            }
          />
          {toolbarExtra}
          <Button
            variant="default"
            size="xs"
            className="ml-auto"
            onClick={() => setImportOpen(true)}
          >
            <Plus />
            Import CSV
          </Button>
        </div>

        <AddAccountsDialog
          open={importOpen}
          onOpenChange={(o) => {
            setImportOpen(o);
            // Closing the add surface is a view-entry event: re-snapshot so what
            // was added folds into the pinned set at the top.
            if (!o) bumpPin();
          }}
          isAdmin={importIsAdmin}
          // The modal blocks until every row is applied, so by the time this
          // fires the list is complete: the host focuses it and the user lands
          // on a full table with the import selected in the audience bar.
          onImportReady={(info) => onImportReady?.(info)}
        />

        {/* TRA-1428: see PeopleTab — one scroller for both axes (sticky header),
            affordance on the non-scrolling anchor. */}
        <TableScrollRegion
          onScrollerChange={setScrollerRoot}
          // See PeopleTab: the refetch dim goes on the ANCHOR so the edge fades
          // and the `N more` badge dim with the table instead of staying bright
          // over it (TRA-1428).
          rootClassName={cn(
            'flex min-h-0 flex-1 flex-col transition-opacity duration-150',
            // Don't dim while rows stream in — content appearing is the point;
            // a dim reads as a flicker (TRA-1359).
            refetching && !pollMs && 'opacity-60'
          )}
          className="min-h-0 flex-1 overflow-y-auto"
        >
          {loading && accounts.length === 0 ? (
            <div className="flex items-center justify-center py-16 text-text-muted">
              <Loader2 className="mr-2 size-4 animate-spin" />
              Loading accounts
            </div>
          ) : (
            <table
              className="htbl htbl-w-accounts"
              // `useExternalBandOffset` MEASURES the sticky stack above the band
              // (header + selection row) and writes `--htbl-band-top` here.
              ref={accountsTableRef}
            >
              <thead>
                <tr>
                  <th className="htbl-c-check">
                    <HomeTableSelectAllCheckbox
                      checked={headerChecked}
                      busy={bulkSelectionBusy}
                      disabled={sortedRows.length === 0}
                      entityName="account"
                      loadedCount={sortedRows.length}
                      onToggle={toggleLoadedSelection}
                    />
                  </th>
                  <HomeTableSortTh
                    label="Account"
                    field="name"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-acctmain"
                  />
                  <HomeTableSortTh
                    label="Employees"
                    field="employees"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-num"
                  />
                  <HomeTableSortTh
                    label="Revenue"
                    field="revenue"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-num"
                  />
                  <HomeTableSortTh
                    label="Type"
                    field="type"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-type"
                  />
                  <HomeTableSortTh
                    label="HQ"
                    field="hq"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                  />
                  <HomeTableSortTh
                    label="Signals"
                    field="events"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-num-sm htbl-th-center"
                  />
                  <HomeTableSortTh
                    label="Contacts"
                    field="stakeholders"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-cts htbl-th-center"
                  />
                  <HomeTableSortTh
                    label="Score"
                    field="timingScore"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-score htbl-th-center"
                  />
                  <HomeTableSortTh
                    label="Last outreach"
                    field="lastOutreach"
                    sortField={sortField}
                    sortDir={sortDir}
                    onSort={toggleSort}
                    className="htbl-c-last"
                  />
                  <th className="htbl-c-tw" />
                </tr>
              </thead>
              <tbody>
                {showSelectionScope ? (
                  <HomeTableSelectionScopeRow
                    colSpan={11}
                    selectedCount={selectionScopeCount}
                    totalCount={selectionTotalCount}
                    remainingCount={
                      activeBulkSelection
                        ? Math.max(
                            0,
                            selectionTotalCount - activeBulkSelection.nextOffset
                          )
                        : Math.max(0, selectionTotalCount - selectionScopeCount)
                    }
                    entityName="account"
                    bulkOutcome={activeBulkSelection?.outcome ?? null}
                    bulkSelectionComplete={bulkSelectionComplete}
                    loadedSelectionComplete={loadedSelectionComplete}
                    busy={bulkSelectionBusy}
                    progressCount={bulkSelectionProgress}
                    allowServerSelection={!exactDomainSelection}
                    onSelectAll={() => void selectAllMatching()}
                    onClear={clearSelection}
                  />
                ) : null}
                {sortedRows.map((a) => (
                  <AccountRowView
                    key={a.id}
                    account={a}
                    bucketLabel={bucketLabels.get(a.id) ?? null}
                    counts={counts.get(a.id)}
                    open={openAccountId === a.id}
                    onOpen={() => onOpenAccount(a)}
                  />
                ))}

                {/* Owned-side empty state, rendered even though the directory
                    group below has rows — otherwise a screen of entirely unowned
                    results reads as ordinary search results. */}
                {beyondActive && accounts.length === 0 && !loading && (
                  <tr className="htbl-ext-none">
                    <td colSpan={11} data-testid="home-owned-empty">
                      No accounts in your workspace match “{query.trim()}”.
                    </td>
                  </tr>
                )}

                {beyondActive && (
                  <>
                    <ExternalGroupBand
                      colSpan={11}
                      count={externalCount}
                      noun="accounts"
                      limit={ACCOUNTS_SEARCH_LIMIT}
                    />
                    {beyondLoadingAny ? (
                      <tr>
                        <td
                          colSpan={11}
                          className="px-4 py-4 text-meta text-text-muted"
                        >
                          <span className="inline-flex items-center gap-2">
                            <Loader2 className="size-4 animate-spin" />
                            {pastedDomain && !domainExhausted
                              ? `Looking for an account at ${pastedDomain}`
                              : `Looking for accounts matching “${query.trim()}”`}
                          </span>
                        </td>
                      </tr>
                    ) : directHitAlreadyOwned ? null : beyond.length === 0 ? (
                      <tr>
                        <td colSpan={11} className="px-4 py-3">
                          <SearchInFindLink
                            query={query}
                            prefix={beyondEmptyPrefix}
                          />
                        </td>
                      </tr>
                    ) : (
                      beyond.map((c) => (
                        <BeyondRow
                          key={c.coresignalId}
                          name={c.name ?? 'New account'}
                          website={c.website}
                          employeeCount={c.employeeCount}
                          annualRevenue={c.annualRevenue}
                          ownership={c.ownership}
                          hqLocation={c.hqLocation}
                          pending={pendingAdds.has(
                            candidateKey({
                              coresignalId: c.coresignalId,
                              website: c.website ?? null,
                              name: c.name ?? 'New account',
                            })
                          )}
                          onAdd={() =>
                            void createFromCoresignal({
                              name: c.name ?? 'New account',
                              website: c.website ?? null,
                              coresignalId: c.coresignalId,
                              employeeCount: c.employeeCount,
                              annualRevenue: c.annualRevenue,
                              ownership: c.ownership,
                              hqLocation: c.hqLocation,
                              description: c.description,
                            })
                          }
                        />
                      ))
                    )}
                  </>
                )}

                {!loading && accounts.length === 0 && !beyondActive && (
                  <tr className="htbl-empty">
                    <td colSpan={11}>
                      No accounts match. Search an account name to find new ones.
                    </td>
                  </tr>
                )}

                {query.trim().length >= 3 &&
                  !loading &&
                  !beyondLoadingAny &&
                  !beyondNoResults && (
                    <tr>
                      <td colSpan={11} className="px-4 py-3">
                        <SearchInFindLink query={query} />
                      </td>
                    </tr>
                  )}
              </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 footer takes
              `sticky left-0` instead. */}
          {hasMore && accounts.length > 0 && (
            <>
              <div
                ref={sentinelRef}
                className={cn(
                  !(loading && accounts.length === 0) && 'htbl-w-accounts'
                )}
              />
              <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
                  </>
                ) : (
                  `Showing ${accountLoadedCount.toLocaleString()} of ${accountMatchCount.toLocaleString()} accounts`
                )}
              </div>
            </>
          )}
        </TableScrollRegion>
      </div>
    );
  }

  function AccountRowView({ account, bucketLabel, counts, open, onOpen }) {
    const { HomeSelectCell, formatOutreach, homeAnalytics } = T;
    const pb = T.useAudience();
    const inAudience = pb.hasAccount(account.id);
    const pickedN = pb.pickedCount(account.id);
    const someSelected = pickedN > 0;
    // TRA-1359: a just-added ("beyond your accounts") row is still settling —
    // its roster and auto-picks resolve on separate async ticks. `fresh` is the
    // single "still researching" signal; hold ONE stable spinner while it's set
    // instead of flashing "0 of 0" → "0 of 4".
    const isFresh = !!pb.audience.fresh[account.id];
    // Coalesce the two disagreeing roster totals (server peopleCount vs the
    // audience roster): max = the most-complete known total, so the single
    // post-research flip lands on the final value.
    const rosterCount = pb.accountPeopleTotal(account.id);
    const totalPeople = Math.max(counts?.people ?? 0, rosterCount);
    const allIn = totalPeople > 0 && pickedN >= totalPeople;
    // inAudience with zero resolved picks = whole-account add whose roster is
    // still resolving (or fresh): render checked so the box never disagrees
    // with the click handler (which removes when inAudience).
    const checkedState = allIn
      ? true
      : someSelected
        ? 'indeterminate'
        : inAudience
          ? true
          : false;

    const rowSel = inAudience || someSelected;
    // Server counts report this account HAS contacts, but the audience roster
    // hasn't loaded them yet (so no pick has resolved) — the transient behind
    // the "0 of x" → "1 of x" flash. Treat it as still-settling so the spinner
    // holds until the roster lands the people and the pick appears (TRA-1359).
    const contactsSettling =
      rowSel && (counts?.people ?? 0) > 0 && rosterCount === 0;
    return (
      <>
        {bucketLabel && (
          <tr className="htbl-day">
            <td colSpan={11}>{bucketLabel}</td>
          </tr>
        )}
        <tr
          onClick={onOpen}
          className={`htbl-row${open ? ' is-open' : ''}${rowSel ? ' is-sel' : ''}`}
        >
          <HomeSelectCell
            checked={checkedState}
            ariaLabel={
              inAudience
                ? `Remove ${account.name} from list`
                : `Add ${account.name} to list`
            }
            onToggle={() => {
              const removing = inAudience || someSelected;
              // The audience add/remove itself is captured in the provider;
              // this records the TABLE gesture, so "ticked a row" can be told
              // apart from the same member arriving via a drawer or bulk add.
              homeAnalytics.rowSelected({
                table: 'accounts',
                selected: !removing,
              });
              return removing
                ? pb.removeGroup(account.id)
                : pb.addAccount(account.id, 'accounts-table', {
                    name: account.name,
                    url: account.url ?? null,
                    logoUrl: account.logoUrl ?? null,
                  });
            }}
          />
          <td>
            <span className="htbl-acct">
              <LogoAvatar
                src={account.logoUrl}
                domain={account.url}
                alt={account.name}
                fallbackText={getInitials(account.name)}
                size="sm"
                className="size-[26px] rounded-[7px] shrink-0"
              />
              <span className="htbl-acct-t">
                <span className="htbl-acct-name">{account.name}</span>
              </span>
              {/* Name spinner tracks CONTACTS resolution only (per-account, and
                  actually detectable). It intentionally does NOT key off
                  signalsDiscovering: signals just appear as numbers when found. */}
              {(isFresh ||
                (counts?.contactsDiscovering && !counts.people) ||
                contactsSettling) && (
                <Loader2
                  className="size-3 shrink-0 animate-spin text-text-muted"
                  aria-label="Finding contacts"
                  data-testid="account-discovering-spinner"
                />
              )}
            </span>
          </td>
          <td>
            <span className="htbl-num">
              {account.employeeCount != null
                ? account.employeeCount.toLocaleString()
                : ''}
            </span>
          </td>
          <td>
            <span className="htbl-num">
              {formatRevenue(
                account.annualRevenueFrom != null
                  ? {
                      from: account.annualRevenueFrom,
                      to: account.annualRevenueTo ?? null,
                    }
                  : null
              ) || ''}
            </span>
          </td>
          <td>
            {account.ownership ? (
              // `flex` (block) centers the chip in the row like the sibling
              // text cells — see the BeyondRow note above.
              <TypeChip type={account.ownership} className="flex" />
            ) : null}
          </td>
          <td>
            <span className="htbl-muted" title={account.hqLocation ?? undefined}>
              {account.hqLocation || ''}
            </span>
          </td>
          <td className="htbl-cell-center">
            {/* Signals spinner shows ONLY while the import job is genuinely
                RUNNING — a real, bounded window that ends on job completion,
                never a blunt timeout. */}
            {counts?.signalsDiscovering && !counts.events ? (
              <Loader2
                className="mx-auto size-3.5 animate-spin text-text-muted"
                aria-label="Discovering signals"
                data-testid="account-signals-discovering-spinner"
              />
            ) : (
              <span className="htbl-num">{counts?.events || ''}</span>
            )}
          </td>
          <td className="htbl-cell-center">
            {isFresh ||
            (counts?.contactsDiscovering && !counts.people) ||
            contactsSettling ? (
              // Still finding contacts, OR contacts exist server-side but the
              // roster hasn't loaded them / picked one yet: ONE stable spinner.
              // Held BEFORE the in-audience "N of M" branch so a SELECTED
              // discovering row shows the spinner instead of a premature
              // "0 of 0"/"0 of x" that flips to a count (TRA-1359).
              <Loader2
                className="mx-auto size-3.5 animate-spin text-text-muted"
                aria-label="Finding contacts"
                data-testid="account-contacts-discovering-spinner"
              />
            ) : rowSel ? (
              Math.max(totalPeople, pickedN) > 0 ? (
                <span className="htbl-picked">
                  <Check />
                  {pickedN} of {Math.max(totalPeople, pickedN)}
                </span>
              ) : (
                // In the audience but no contacts found — a muted dash reads
                // cleaner than "0 of 0" (Ohad).
                <span className="htbl-num text-text-muted">—</span>
              )
            ) : (
              <span className="htbl-num">{counts?.people || ''}</span>
            )}
          </td>
          <td className="htbl-cell-center">
            {account.timingScore != null && account.timingScore > 8 ? (
              <span
                className={`htbl-score${account.timingScore >= 80 ? ' is-hot' : ''}`}
              >
                {account.timingScore}
              </span>
            ) : null}
          </td>
          <td>
            <span className="htbl-when">
              {formatOutreach(counts?.lastOutreachAt ?? null) || '-'}
            </span>
          </td>
          <td>
            <span className="htbl-tw">
              <ChevronDown />
            </span>
          </td>
        </tr>
      </>
    );
  }

  /* ================= AddAccountsDialog.tsx ================= */

  /* PostHog is not ported — keep the call sites (lib/analytics captureEvent). */
  const captureEvent = () => {};

  /**
   * The "Import CSV" surface for accounts: a dialog shell around the shared CSV
   * account importer. Company search/one-click-add used to live here behind a
   * segmented control; that path was removed, so CSV is the only way to bring
   * accounts in from this surface (search lives on Find). The twin of
   * AddContactsDialog on the People tab.
   */
  function AddAccountsDialog({
    open,
    onOpenChange,
    isAdmin,
    onImportSubmitted,
    onImportReady,
  }) {
    const { homeAnalytics } = T;
    // The importer tells us when it is mid-import; the shell then refuses to be
    // dismissed (no X, no Escape, no click-outside) until the rows are in.
    const [busy, setBusy] = useState(false);
    // Import funnel timing + size, assembled from the importer's own hooks:
    // `onEvent` for its events, `onSubmitted` for the queue moment, `onReady`
    // for the hand-off. Row count is stashed on the way past.
    const startedAt = useRef(0);
    const rowsRef = useRef(0);
    const readyRef = useRef(false);

    useEffect(() => {
      if (!open) return;
      startedAt.current = Date.now();
      rowsRef.current = 0;
      readyRef.current = false;
      homeAnalytics.importOpened({ kind: 'accounts' });
    }, [open]);

    return (
      <Dialog open={open} onOpenChange={(o) => !busy && onOpenChange(o)}>
        {/* Fixed height + flex column so the modal never resizes between CSV
            steps. `!flex` beats DialogContent's default `grid`. */}
        <DialogContent
          size="medium"
          className="!flex h-[34rem] max-h-[85vh] flex-col overflow-hidden"
          data-testid="add-accounts-dialog"
          showCloseButton={!busy}
          onEscapeKeyDown={(e) => busy && e.preventDefault()}
          onPointerDownOutside={(e) => busy && e.preventDefault()}
          onInteractOutside={(e) => busy && e.preventDefault()}
        >
          <DialogHeader>
            <DialogTitle>Import CSV</DialogTitle>
            <DialogDescription>
              Add accounts from a CSV with name and website columns.
            </DialogDescription>
          </DialogHeader>

          <AccountImportDialog
            embedded
            open={open}
            // The importer's OWN dismissals land here, and this is the only
            // place they can be seen — "Continue in background" routes straight
            // through here.
            onOpenChange={(o) => {
              if (!o && busy && !readyRef.current) {
                homeAnalytics.importAbandoned({
                  kind: 'accounts',
                  elapsed_ms: Date.now() - startedAt.current,
                });
              }
              onOpenChange(o);
            }}
            isAdmin={isAdmin}
            onSubmitted={(info) => {
              homeAnalytics.importSubmitted({
                kind: 'accounts',
                rows: rowsRef.current,
                named_list: Boolean(info.listId),
              });
              onImportSubmitted?.(info);
            }}
            onReady={(info) => {
              readyRef.current = true;
              homeAnalytics.importRowsReady({
                kind: 'accounts',
                rows: rowsRef.current,
                duration_ms: Date.now() - startedAt.current,
              });
              return onImportReady?.(info);
            }}
            onBusyChange={setBusy}
            // App: forwards the importer's TRA-880 events (eventName,
            // eventProps) into PostHog, stashing rows_submitted. The shim
            // importer emits a single `{ kind, rows }` object — read both
            // shapes so the row count still lands.
            onEvent={(eventName, eventProps) => {
              const rows =
                (eventProps && eventProps['rows_submitted']) ??
                (eventName && typeof eventName === 'object'
                  ? eventName.rows
                  : undefined);
              if (typeof rows === 'number') rowsRef.current = rows;
              captureEvent(eventName, {
                ...eventProps,
                surface: 'home_accounts',
              });
            }}
          />
        </DialogContent>
      </Dialog>
    );
  }

  Object.assign(T, {
    AccountsTab,
    AddAccountsDialog,
    // candidate-key.ts
    candidateKey,
    // search-query.ts
    domainOf,
    linkedinHandleOf,
    isDomainQuery,
    domainLabel,
    // accounts-query.ts (tab-owned pieces)
    homeAccountsVars,
    HOME_ACCOUNTS_PAGE_SIZE,
    HOME_ACCOUNTS_DEFAULT_SORT,
  });
})();
