/* Ported from apps/web/src/features/home/accounts/account-assign.ts and
 * apps/web/src/features/home/accounts/HomeAssignAccountsAction.tsx (TRA-1651:
 * bulk assign/unassign account owners from the bottom widget).
 *
 * Substitutions: useQuery(GET_HOME_ACCOUNT_OWNERSHIP) → T.Data.useHomeAccountOwnership,
 * useQuery(GET_ACCOUNT_FILTER_STATS) → T.Data.useAccountFilterStats,
 * useMutation(BULK_ADD/REMOVE_ACCOUNT_OWNERS) → T.Data.bulkAdd/RemoveAccountOwners,
 * useApolloClient().refetchQueries → T.Data.raw.bump() (the mock store is
 * one reactive dataset), useTeamAdminUi / useEntitlements → lib.jsx stubs.
 * `maxAccounts` is a PROTOTYPE-ONLY prop (default HOME_ASSIGN_MAX_ACCOUNTS)
 * so the /bar gallery can stage the over-cap state without 501 accounts. */
(() => {
  const T = window.T;
  const { useEffect, useMemo, useRef, useState } = React;
  const { Button, UserAvatar, AssignAccountsDialog } = T.UI;
  const { UserPen } = T.Icons;
  const toast = T.toast;
  const useAudience = T.useAudience;
  const NO_ACCOUNT_GROUP_ID = T.NO_ACCOUNT_GROUP_ID;

  /* ==================== account-assign.ts ==================== */

  /** Largest working list this surface can act on in one call. Mirrors
   *  BULK_ACCOUNT_OWNERSHIP_MAX_IDS on the server. */
  const HOME_ASSIGN_MAX_ACCOUNTS = 500;

  /**
   * Per-user coverage across the selection, plus the distinct current owners.
   * `coverageCounts` drives the dialog's tri-state rows ("owns 3 of 8
   * selected"), and `currentOwners` is its "Remove from" list.
   */
  function ownershipCoverage(rows) {
    const coverageCounts = new Map();
    const owners = new Map();
    let unassignedCount = 0;
    for (const row of rows) {
      const users = row.users ?? [];
      if (users.length === 0) unassignedCount += 1;
      for (const user of users) {
        coverageCounts.set(user.id, (coverageCounts.get(user.id) ?? 0) + 1);
        if (!owners.has(user.id)) owners.set(user.id, user);
      }
    }
    return { coverageCounts, currentOwners: [...owners.values()], unassignedCount };
  }

  /** The owner tab's sentence: "2 owners · 3 unassigned". */
  function ownerTabSummary(opts) {
    const parts = [];
    if (opts.ownerCount > 0) {
      parts.push(`${opts.ownerCount} owner${opts.ownerCount === 1 ? '' : 's'}`);
    }
    if (opts.unassignedCount > 0) {
      parts.push(`${opts.unassignedCount.toLocaleString()} unassigned`);
    }
    return parts.join(' · ');
  }

  function userDisplayName(user) {
    return user?.name || user?.email || 'user';
  }

  /** Comma-joined display names, in the order the ids were given. */
  function userNames(ids, pool) {
    return ids.map((id) => userDisplayName(pool.get(id))).join(', ');
  }

  function plural(n, noun) {
    return `${n.toLocaleString()} ${noun}${n === 1 ? '' : 's'}`;
  }

  /** Failure copy, one line per distinct reason rather than one per row. */
  const FAILURE_REASONS = {
    ACCOUNT_LIMIT_EXCEEDED: 'account limit reached',
    ADD_ACCOUNTS_DISABLED: 'adding accounts is not available on this plan',
    NOT_FOUND: 'account no longer available',
    FORBIDDEN: 'not permitted',
  };

  /** Collapse the batch's failures into ONE phrase, grouped by code. */
  function summarizeFailures(failures) {
    if (failures.length === 0) return null;
    const byCode = new Map();
    for (const failure of failures) {
      byCode.set(failure.code, (byCode.get(failure.code) ?? 0) + 1);
    }
    const parts = [...byCode.entries()].map(([code, count]) => {
      const reason =
        FAILURE_REASONS[code] ?? failures.find((f) => f.code === code).message;
      return `${count.toLocaleString()} skipped — ${reason}`;
    });
    return parts.join('; ');
  }

  /** The single toast for one apply. `applied` counts (account, user) PAIRS
   *  while `accountsChanged` counts accounts, and the sentence talks about
   *  accounts. */
  function assignResultMessage(result, opts) {
    const detail = summarizeFailures(result.failures);
    const { verb, names } = opts;

    if (result.accountsChanged === 0) {
      if (detail) return { tone: 'error', text: `No accounts changed. ${detail}` };
      return { tone: 'success', text: 'Already up to date' };
    }

    const subject = plural(result.accountsChanged, 'account');
    const text =
      verb === 'shared'
        ? `Shared ${subject} with ${names ?? 'the selected users'}`
        : verb === 'removed'
          ? `Removed ${names ?? 'the selected users'} from ${subject}`
          : `Unassigned ${subject}`;

    return detail ? { tone: 'warning', text: `${text}. ${detail}` } : { tone: 'success', text };
  }

  /** The single toast for a whole Apply, however many legs it had. */
  function composeAssignToast(legs, error) {
    const parts = legs.map((leg) =>
      assignResultMessage(leg.result, { verb: leg.verb, names: leg.names }),
    );

    // A no-op leg ("Already up to date") is noise next to a leg that did
    // something, so it only speaks when it is the whole story.
    const spoken = parts.filter((p) => p.text !== 'Already up to date');
    const sentences = spoken.length > 0 ? spoken : parts.slice(0, 1);

    const text = sentences
      .map((p, i) => (i === 0 ? p.text : lowerFirst(p.text)))
      .join('; ');

    if (error) {
      return {
        tone: 'error',
        text: sentences.length > 0 && spoken.length > 0 ? `${text}. ${error}` : error,
      };
    }
    const tone = parts.some((p) => p.tone === 'error')
      ? 'error'
      : parts.some((p) => p.tone === 'warning')
        ? 'warning'
        : 'success';
    return { tone, text };
  }

  function lowerFirst(text) {
    return text.charAt(0).toLowerCase() + text.slice(1);
  }

  /* ==================== HomeAssignAccountsAction.tsx ==================== */

  /** Owner faces before the strip collapses to a +N chip. */
  const OWNER_FACE_CAP = 3;

  function show(tone, text) {
    if (tone === 'error') toast.error(text);
    else if (tone === 'warning') (toast.warning || toast)(text);
    else toast.success(text);
  }

  /**
   * The bottom widget's owner control (TRA-1651). On Home the widget's
   * account strip IS the selection (ticking an Accounts row writes it into
   * the working list), so this acts on `res.groups` directly. Enterprise
   * team administration: hidden behind `useTeamAdminUi()`. The picker is the
   * shared `AssignAccountsDialog`: additive share, per-user removal, an
   * unassigned end-state when the last owner is unchecked, current owners
   * exempt from the per-user cap.
   */
  function HomeAssignAccountsAction({
    users,
    selfUserId,
    maxAccounts = HOME_ASSIGN_MAX_ACCOUNTS,
  }) {
    const { showTeamAdminUi } = T.useTeamAdminUi();
    const { res } = useAudience();
    const [open, setOpen] = useState(false);
    // The account set this dialog is acting on, frozen at open. `res.groups`
    // is LIVE: a focused list re-seeds the audience as its members stream in,
    // so the working list can grow while the dialog is up.
    const [frozenIds, setFrozenIds] = useState(null);
    const [working, setWorking] = useState(false);
    // Mirror of `working` for the handlers: `handleOpenChange` reads state
    // from its render closure, which is still the PREVIOUS value at the
    // moment the dialog asks to close.
    const workingRef = useRef(false);
    const setBusy = (busy) => {
      workingRef.current = busy;
      setWorking(busy);
    };
    // Set by a successful apply, consumed on close: one refetch, after both
    // legs.
    const appliedRef = useRef(false);

    // Group ids are ALMOST account ids: a working-list person whose account
    // is unknown lands in a synthetic "No account" group whose id is a
    // sentinel, not a UUID. `ranking` skips it the same way.
    const liveAccountIds = useMemo(
      () => res.groups.map((g) => g.account.id).filter((id) => id !== NO_ACCOUNT_GROUP_ID),
      [res.groups],
    );
    // Everything below — the dialog's count and the mutations — works off the
    // frozen set while the dialog is open.
    const accountIds = frozenIds ?? liveAccountIds;
    // The READ is debounced, because the tab reads on every list change and a
    // row tick is one keystroke-equivalent. The frozen set skips the debounce.
    const debouncedIds = T.useDebouncedValue(liveAccountIds);
    const queryIds = frozenIds ?? debouncedIds;

    // Owners of the working list. Read continuously, not just on dialog
    // open: the tab reports ownership at a glance. Over the cap the server
    // rejects the READ too, so the tab says why it is disabled instead.
    const { data: ownershipData, error: ownershipError } = T.Data.useHomeAccountOwnership({
      ids: queryIds,
      skip: !showTeamAdminUi || queryIds.length === 0 || queryIds.length > maxAccounts,
    });
    const { coverageCounts, currentOwners, unassignedCount } = useMemo(
      () => ownershipCoverage(ownershipData?.accountOwners ?? []),
      [ownershipData],
    );
    // Until the read lands there is no coverage, which is NOT the same as
    // zero coverage: undefined puts the dialog in its count-less mode.
    const coverageKnown = ownershipData !== undefined;

    // Tenant-wide per-user account counts + the cap: the at-limit gate is
    // about a user's TOTAL owned accounts, not this selection.
    const { data: statsData } = T.Data.useAccountFilterStats({ skip: !open });
    const userCounts = useMemo(() => {
      const counts = new Map();
      for (const row of statsData?.accountFilterStats?.userCounts ?? []) {
        counts.set(row.userId, row.count);
      }
      return counts;
    }, [statsData]);
    const { entitlements } = T.useEntitlements();

    // Re-adding an existing owner does not grow their count, so the cap must
    // not block it.
    const exemptFromLimitUserIds = useMemo(
      () => new Set(currentOwners.map((u) => u.id)),
      [currentOwners],
    );

    // Say so, once, when coverage could not be loaded. A ref, not state: this
    // must not re-render, and StrictMode double-invokes the effect.
    const warnedForRef = useRef(null);
    useEffect(() => {
      if (!ownershipError) {
        warnedForRef.current = null;
        return;
      }
      if (warnedForRef.current === ownershipError.message) return;
      warnedForRef.current = ownershipError.message;
      show('warning', 'Could not load current owners. You can still share these accounts.');
    }, [ownershipError]);

    const namePool = useMemo(() => {
      const byId = new Map();
      for (const user of [...users, ...currentOwners]) byId.set(user.id, user);
      return byId;
    }, [users, currentOwners]);

    // A current owner past the roster page — or a deactivated member still
    // holding owner rows — would otherwise be impossible to remove here.
    const pickerUsers = useMemo(() => [...namePool.values()], [namePool]);

    // Legs of the Apply in flight: the dialog can encode an add AND a remove
    // in one Apply, so each leg accumulates here and the whole thing speaks
    // once — see `composeAssignToast`.
    const legsRef = useRef([]);

    /** Emit the one toast for this Apply and reset the buffer. */
    const flush = (error) => {
      if (legsRef.current.length === 0 && !error) return;
      const { tone, text } = composeAssignToast(legsRef.current, error);
      legsRef.current = [];
      show(tone, text);
    };

    const handleOpenChange = (next) => {
      // Escape and overlay clicks reach here mid-Apply; dismissing then would
      // split the toast. The dialog's own post-Apply close arrives after the
      // last leg cleared this, so it is not blocked.
      if (!next && workingRef.current) return;
      setOpen(next);
      if (next) return;
      setFrozenIds(null);
      flush();
      if (!appliedRef.current) return;
      appliedRef.current = false;
      // Rows, the owner column, and the "N of M" counts all move together.
      T.Data.raw.bump();
    };

    /**
     * Run one leg. Re-thrown so the dialog stays open on a hard failure
     * rather than closing over an unapplied change. The toast is NOT emitted
     * here: a successful Apply flushes when the dialog closes.
     */
    const apply = async (verb, userIds, run) => {
      setBusy(true);
      try {
        const result = await run();
        if (!result) throw new Error('No response');
        legsRef.current.push({ verb, names: userNames(userIds, namePool), result });
        appliedRef.current = true;
        setBusy(false);
      } catch (error) {
        setBusy(false);
        flush(error instanceof Error ? error.message : 'Failed to update owners');
        throw error;
      }
    };

    // Hooks all run above the gate, so this early return is safe. Gated on
    // the LIVE list: the control appears and disappears with the working list.
    if (!showTeamAdminUi || liveAccountIds.length === 0) return null;

    // Past the server's ceiling every Apply would be rejected, so say that
    // here. Still rendered, not hidden: a vanishing control reads as a
    // permission problem, which this is not.
    const overCap = liveAccountIds.length > maxAccounts;
    const ownerCount = currentOwners.length;
    // Before the read lands there is nothing truthful to say, so the tab
    // shows its action alone rather than a confident "0 owners · 0 unassigned".
    const summary = coverageKnown ? ownerTabSummary({ ownerCount, unassignedCount }) : '';

    return (
      <>
        {/* A segment INSIDE the pill, not a chip above it. It carries the
            INFO — who owns the list's accounts, how many have nobody — with
            the action beside it. Kept in the pill so the bar stays one object. */}
        <span
          className="hdw-ab-rule hdw-ab-owners-rule h-5 w-px shrink-0 bg-border-subtle"
          aria-hidden
        />
        <span className="hdw-ab-owners" data-testid="home-bar-owner-tab">
          {ownerCount > 0 && (
            <span className="hdw-ab-owners-faces">
              {currentOwners.slice(0, OWNER_FACE_CAP).map((user) => (
                <UserAvatar
                  key={user.id}
                  name={userDisplayName(user)}
                  size="xs"
                  // Same card-colored ring the Owner column's UserAvatarStack
                  // gives its faces, so the two stacks read as one device.
                  className="ring-surface-card ring-2"
                />
              ))}
              {ownerCount > OWNER_FACE_CAP && (
                <span className="hdw-ab-owners-more">+{ownerCount - OWNER_FACE_CAP}</span>
              )}
            </span>
          )}
          {/* Same shape as the pill's own counts (bold number, muted word) so
              "2 owners" reads as a sibling of "9 people"; the sr-only sentence
              keeps the full summary for assistive tech. */}
          {summary && (
            <span className="hdw-ab-owners-t" aria-hidden>
              {ownerCount > 0 && (
                <>
                  <b>{ownerCount}</b>
                  <span className="hdw-ab-count-t">
                    {ownerCount === 1 ? 'owner' : 'owners'}
                  </span>
                </>
              )}
              {ownerCount > 0 && unassignedCount > 0 && (
                <span className="hdw-ab-owners-sep">·</span>
              )}
              {unassignedCount > 0 && (
                <>
                  <b>{unassignedCount.toLocaleString()}</b>
                  <span className="hdw-ab-count-t">unassigned</span>
                </>
              )}
            </span>
          )}
          {summary && <span className="sr-only">{summary}</span>}
          <Button
            variant="secondary"
            size="xs"
            className="hdw-ab-owners-cta shrink-0"
            data-testid="home-bar-assign-owners"
            disabled={overCap}
            title={
              overCap
                ? `Too many accounts to assign at once (${liveAccountIds.length.toLocaleString()}; the limit is ${maxAccounts.toLocaleString()}). Narrow the list first.`
                : 'Assign or unassign owners for the accounts in this list'
            }
            onClick={() => {
              setFrozenIds(liveAccountIds);
              setOpen(true);
            }}
          >
            <UserPen />
            <span aria-hidden className="hdw-ab-owners-cta-t">
              Assign
            </span>
            <span className="sr-only">Assign owners</span>
          </Button>
        </span>
        <AssignAccountsDialog
          open={open}
          onOpenChange={handleOpenChange}
          users={pickerUsers}
          count={accountIds.length}
          coverageCounts={coverageKnown ? coverageCounts : undefined}
          currentOwners={currentOwners}
          userCounts={userCounts}
          accountLimit={entitlements?.accountLimit ?? null}
          exemptFromLimitUserIds={exemptFromLimitUserIds}
          canAssignOthers
          selfUserId={selfUserId}
          sharingEnabled
          working={working}
          onAssign={(userIds) =>
            apply('shared', userIds, async () =>
              T.Data.bulkAddAccountOwners({ accountIds, userIds }),
            )
          }
          onRemove={(userIds) =>
            apply('removed', userIds, async () =>
              T.Data.bulkRemoveAccountOwners({ accountIds, userIds }),
            )
          }
        />
      </>
    );
  }

  Object.assign(T, {
    // account-assign.ts
    HOME_ASSIGN_MAX_ACCOUNTS,
    ownershipCoverage,
    ownerTabSummary,
    userDisplayName,
    userNames,
    summarizeFailures,
    assignResultMessage,
    composeAssignToast,
    // HomeAssignAccountsAction.tsx
    HomeAssignAccountsAction,
  });
})();
