"use client";

import * as React from "react";
import Link from "next/link";
import {
  ChevronsDownUp,
  ChevronsUpDown,
  Coins,
  LayoutGrid,
  RefreshCw,
  RotateCcw,
} from "lucide-react";
import { saveDashboardCardPrefsClient } from "@/app/customer/[tenant]/dashboard/_lib/dashboard-card-prefs-api";
import type { DashboardAssetCard, DashboardStatisticsData } from "@/app/customer/[tenant]/dashboard/_lib/dashboard-server-api";
import {
  isCashLeverageCardKey,
  normalizeDashboardAumMode,
  resolveCashLeverageCardDisplay,
  resolveDashboardTotalAmount,
  type CashLeverageCardKey,
} from "@/app/customer/_lib/dashboard-card-aum-display";
import { AUM_MODE_CHANGED_EVENT } from "@/app/customer/_lib/aum-mode-events";
import type { AumCalcMode } from "@/app/customer/[tenant]/settings/_lib/aum-mode-api";
import { DashboardLayoutShell } from "@/app/customer/_components/dashboard-layout-shell";
import { ConsolidatedHoldingsWidget } from "@/app/customer/_components/consolidated-holdings-widget";
import { DiyConsolidatedHoldingsWidget } from "@/app/customer/_components/diy-consolidated-holdings-widget";
import { AssetAllocationWidget } from "@/app/customer/_components/asset-allocation-widget";
import { CashflowCalendarWidget } from "@/app/customer/_components/cashflow-calendar-widget";
import { DepositWidget } from "@/app/customer/_components/deposit-widget";
import { TopGainersLosersWidget } from "@/app/customer/_components/top-gainers-losers-widget";
import { useHoldingsWidgetCatalog } from "@/app/customer/_components/holdings-widgets-grid";
import { DashboardWidgetsProvider } from "@/app/customer/_lib/dashboard-widgets-context";
import { DashboardWidgetConfigureProvider } from "@/app/customer/_components/dashboard-widget-configure-provider";
import {
  buildStaticWidgetCatalogItems,
  collectVisibleStaticWidgetIds,
  filterEnabledStaticWidgets,
  renderStaticDashboardWidget,
} from "@/app/customer/_lib/dashboard-widget-registry-components";
import {
  depositInstanceLayoutId,
  topGainersInstanceLayoutId,
} from "@/app/customer/_lib/dashboard-widget-registry-definitions";
import {
  CustomerFieldsButton,
  CustomerFieldsPanel,
} from "@/app/customer/_components/customer-fields-panel";
import { useDashboardCardPrefs } from "@/app/customer/_lib/use-dashboard-card-prefs";
import type { CustomerGridColumn } from "@/app/customer/_lib/customer-grid-columns";
import { useEnabledDashboardWidgets } from "@/app/customer/_lib/use-enabled-dashboard-widgets";
import { customerUrl } from "@/lib/tenant";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
  clearDashboardLayouts,
  layoutsFingerprint,
  loadDashboardLayouts,
  mergeLayoutsForIds,
  normalizeSectionHeights,
  parseDashboardLayouts,
  saveDashboardLayouts,
  cardLayoutId,
  sectionLayoutId,
  sectionIdFromLayoutId,
  sectionTileHeight,
  type DashboardLayouts,
  type MetricLayoutSpec,
} from "@/app/customer/_lib/dashboard-layout";
import {
  buildTopLevelMetricItems,
  sanitizeCardGroups,
} from "@/app/customer/_lib/dashboard-card-groups";
import {
  DashboardCardSection,
} from "@/app/customer/_components/dashboard-card-section";
import {
  resetDashboardLayoutPrefsClient,
  saveDashboardLayoutPrefsClient,
} from "@/app/customer/[tenant]/dashboard/_lib/dashboard-layout-prefs-api";
import {
  ASSET_ICONS,
  ASSET_ORDER,
  AssetCardView,
  CORPORATE_COUNT_CARD_KEYS,
  CountMetricCard,
  DashboardPageHeader,
  DashboardSkeleton,
  DEFAULT_CORPORATE_CATALOG,
  DASHBOARD_FIELD_GROUPS,
  isStaticWidgetKey,
} from "./dashboard-overview-layout";

type CorporateCountCardKey =
  | "users"
  | "customers"
  | "userGroups"
  | "accumulator"
  | "options";

type CorporateCardItem =
  | { kind: "asset"; key: string; card: DashboardAssetCard }
  | { kind: "count"; key: CorporateCountCardKey; title: string; value: number };

export function CorporateDashboardCards({
  data,
  tenant,
  loading,
  onRefresh,
  formattedDate,
}: {
  data: DashboardStatisticsData;
  tenant: string;
  loading: boolean;
  onRefresh: () => void;
  formattedDate: string;
}) {
  const [fieldsOpen, setFieldsOpen] = React.useState(false);
  const [editMode, setEditMode] = React.useState(false);
  const [layouts, setLayouts] = React.useState<DashboardLayouts | null>(null);
  const dashboardAumMode = normalizeDashboardAumMode(data.aumMode);
  const [pendingGlobalAumMode, setPendingGlobalAumMode] = React.useState<AumCalcMode | null>(
    null,
  );
  const effectiveGlobalAumMode = pendingGlobalAumMode ?? dashboardAumMode;
  const [cardAumOverrides, setCardAumOverrides] = React.useState<
    Partial<Record<CashLeverageCardKey, AumCalcMode>>
  >({});

  React.useEffect(() => {
    setPendingGlobalAumMode(null);
  }, [dashboardAumMode]);

  React.useEffect(() => {
    const onAumModeChanged = (event: Event) => {
      const detail = (event as CustomEvent<{ aumCalcMode?: AumCalcMode }>).detail;
      if (detail?.aumCalcMode === "gross" || detail?.aumCalcMode === "net") {
        setPendingGlobalAumMode(detail.aumCalcMode);
      }
      setCardAumOverrides({});
    };

    window.addEventListener(AUM_MODE_CHANGED_EVENT, onAumModeChanged);
    return () => window.removeEventListener(AUM_MODE_CHANGED_EVENT, onAumModeChanged);
  }, []);

  const handleCardAumModeChange = React.useCallback(
    (key: CashLeverageCardKey, mode: AumCalcMode) => {
      setCardAumOverrides((prev) => {
        if (mode === effectiveGlobalAumMode) {
          const next = { ...prev };
          delete next[key];
          return next;
        }
        return { ...prev, [key]: mode };
      });
    },
    [effectiveGlobalAumMode],
  );

  const { entries: holdingsEntries, loading: holdingsLoading } = useHoldingsWidgetCatalog(tenant);
  const { enabled: enabledWidgets, loading: settingsWidgetsLoading } =
    useEnabledDashboardWidgets(tenant);

  const gatedHoldingsEntries = React.useMemo(() => {
    if (!enabledWidgets.loaded) return [];
    return holdingsEntries.filter((entry) => {
      const id = entry.widget.widget_id;
      if (entry.kind === "ch") return enabledWidgets.consolidatedIds.has(id);
      if (entry.kind === "diy") return enabledWidgets.diyIds.has(id);
      return enabledWidgets.assetAllocationIds.has(id);
    });
  }, [enabledWidgets, holdingsEntries]);

  const perms = data.permissions;
  const counts = data.counts ?? {
    users: 0,
    customers: 0,
    userGroups: 0,
    accumulator: 0,
    options: 0,
  };

  const availableKeys = React.useMemo(() => {
    const keys = new Set<string>();
    for (const key of ASSET_ORDER) {
      const card = data.assets?.[key];
      if (!card) continue;
      if (perms.assets?.[key] === false) continue;
      keys.add(key);
    }
    if (perms.users) keys.add("users");
    if (perms.customers) keys.add("customers");
    if (perms.userGroups) keys.add("userGroups");
    if (perms.accumulatorCount) keys.add("accumulator");
    if (perms.optionsCount) keys.add("options");
    return keys;
  }, [data.assets, perms]);

  const catalog = React.useMemo(() => {
    const fromApi = (data.cardCatalog ?? []).filter(
      (item) => availableKeys.has(item.key) || isStaticWidgetKey(item.key),
    );
    const metricFromApi = fromApi.filter((item) => availableKeys.has(item.key));
    const metricSourceBase =
      metricFromApi.length > 0
        ? metricFromApi.map((item) => ({
            key: item.key,
            label: item.label,
            defaultVisible: item.defaultVisible !== false,
            group: "cards" as const,
          }))
        : DEFAULT_CORPORATE_CATALOG.filter((item) => availableKeys.has(item.key));

    // Yii catalogs may omit org count cards; keep them in customize like Accumulator/Options.
    const presentMetricKeys = new Set(metricSourceBase.map((item) => item.key));
    const missingFromDefaults = DEFAULT_CORPORATE_CATALOG.filter(
      (item) => availableKeys.has(item.key) && !presentMetricKeys.has(item.key),
    ).map((item) => ({
      key: item.key,
      label: item.label,
      defaultVisible: item.defaultVisible !== false,
      group: "cards" as const,
    }));
    const countCardKeySet = new Set<string>(CORPORATE_COUNT_CARD_KEYS);
    const missingCountCards = missingFromDefaults.filter((item) => countCardKeySet.has(item.key));
    const missingOtherMetrics = missingFromDefaults.filter((item) => !countCardKeySet.has(item.key));
    const metricSource = [...missingCountCards, ...metricSourceBase, ...missingOtherMetrics];

    const staticWidgets = buildStaticWidgetCatalogItems(enabledWidgets, fromApi);

    const holdingsWidgets: CustomerGridColumn[] = gatedHoldingsEntries.map((entry) => ({
      key: entry.layoutId,
      label:
        entry.kind === "ch"
          ? entry.widget.name || "Consolidated Holding"
          : entry.kind === "diy"
            ? entry.widget.name || "DIY Consolidated Holdings"
            : entry.widget.name || "Asset Allocation",
      defaultVisible: true,
      group: "widgets",
    }));

    const cashflowWidgets: CustomerGridColumn[] = Array.from(enabledWidgets.cashflowIds)
      .sort((a, b) => a - b)
      .map((id) => ({
        key: `cfw-${id}`,
        label: enabledWidgets.cashflowNames.get(id) || "Cashflow Calendar",
        defaultVisible: true,
        group: "widgets" as const,
      }));

    const topGainersCount = enabledWidgets.topGainersIds.size;
    const topGainersWidgets: CustomerGridColumn[] = Array.from(enabledWidgets.topGainersIds)
      .sort((a, b) => a - b)
      .map((id) => ({
        key: topGainersInstanceLayoutId(id, topGainersCount),
        label: enabledWidgets.topGainersNames.get(id) || "Top Gainers and Losers",
        defaultVisible: true,
        group: "widgets" as const,
      }));

    const depositCount = enabledWidgets.depositIds.size;
    const depositWidgets: CustomerGridColumn[] = Array.from(enabledWidgets.depositIds)
      .sort((a, b) => a - b)
      .map((id) => ({
        key: depositInstanceLayoutId(id, depositCount),
        label: enabledWidgets.depositNames.get(id) || "Deposit",
        defaultVisible: true,
        group: "widgets" as const,
      }));

    return [
      ...metricSource,
      ...staticWidgets,
      ...holdingsWidgets,
      ...cashflowWidgets,
      ...topGainersWidgets,
      ...depositWidgets,
    ];
  }, [availableKeys, data.cardCatalog, enabledWidgets, gatedHoldingsEntries]);

  const savePrefs = React.useCallback(
    async (prefs: {
      visibility: Record<string, boolean>;
      order: string[];
      groups?: Array<{
        id: string;
        title: string;
        memberKeys: string[];
        collapsed?: boolean;
      }>;
    }) => {
      await saveDashboardCardPrefsClient(prefs, "corporate", tenant);
    },
    [tenant],
  );

  const clubbableKeys = React.useMemo(
    () => [...availableKeys].filter((key) => key !== "total"),
    [availableKeys],
  );

  const {
    visibility,
    order,
    groups,
    visibleOrderedKeys,
    hideAll,
    hideGroup,
    setCardVisible,
    reorderShown,
    clubCards,
    ungroupSection,
    removeFromSection,
    renameSection,
    toggleSectionCollapsed,
    setSectionsCollapsed,
    reorderSectionMembers,
  } = useDashboardCardPrefs({
    catalog,
    initialPrefs: data.cardPrefs ?? null,
    lockedKeys: ["total"],
    clubbableKeys,
    savePrefs,
  });

  const cardByKey = React.useMemo(() => {
    const map = new Map<string, CorporateCardItem>();
    for (const key of ASSET_ORDER) {
      const card = data.assets?.[key];
      if (!card || perms.assets?.[key] === false) continue;
      map.set(key, { kind: "asset", key, card });
    }
    if (perms.users) {
      map.set("users", {
        kind: "count",
        key: "users",
        title: "Total Users",
        value: counts.users,
      });
    }
    if (perms.customers) {
      map.set("customers", {
        kind: "count",
        key: "customers",
        title: "Total Customers",
        value: counts.customers,
      });
    }
    if (perms.userGroups) {
      map.set("userGroups", {
        kind: "count",
        key: "userGroups",
        title: "Total User Groups",
        value: counts.userGroups,
      });
    }
    if (perms.accumulatorCount) {
      map.set("accumulator", {
        kind: "count",
        key: "accumulator",
        title: "Accumulator",
        value: counts.accumulator,
      });
    }
    if (perms.optionsCount) {
      map.set("options", {
        kind: "count",
        key: "options",
        title: "Options",
        value: counts.options,
      });
    }
    return map;
  }, [
    counts.accumulator,
    counts.customers,
    counts.options,
    counts.userGroups,
    counts.users,
    data.assets,
    perms,
  ]);

  const shownKeys = visibleOrderedKeys.filter((key) => cardByKey.has(key));
  const shownKeysKey = shownKeys.join("|");
  const activeGroups = React.useMemo(
    () =>
      sanitizeCardGroups(
        groups,
        clubbableKeys.filter((key) => shownKeys.includes(key)),
      ),
    // shownKeysKey tracks membership/order without depending on array identity
    // eslint-disable-next-line react-hooks/exhaustive-deps -- shownKeysKey
    [groups, clubbableKeys, shownKeysKey],
  );
  const topLevelMetrics = React.useMemo(
    () => buildTopLevelMetricItems(shownKeys, activeGroups),
    [shownKeys, activeGroups],
  );
  const metricSpecs = React.useMemo<MetricLayoutSpec[]>(
    () =>
      topLevelMetrics.map((item) =>
        item.type === "card"
          ? { kind: "card", key: item.key }
          : {
              kind: "section",
              id: item.group.id,
              memberCount: item.group.memberKeys.filter((key) => shownKeys.includes(key)).length,
              collapsed: item.group.collapsed,
            },
      ),
    [topLevelMetrics, shownKeys],
  );
  // Collapse is handled by patching section height in-place — do not include it here
  // or the merge effect will rewrite the whole grid on every open/close.
  const metricSpecsKey = metricSpecs
    .map((spec) =>
      spec.kind === "card" ? `c:${spec.key}` : `s:${spec.id}:${spec.memberCount}`,
    )
    .join("|");
  const collapsedSpecsKey = metricSpecs
    .filter((spec) => spec.kind === "section" && spec.collapsed)
    .map((spec) => (spec.kind === "section" ? spec.id : ""))
    .sort()
    .join("|");
  const prevMetricSpecsKeyRef = React.useRef<string | null>(null);
  const prevShownKeysKeyRef = React.useRef<string | null>(null);
  const prevCollapsedSpecsKeyRef = React.useRef<string | null>(null);

  const layoutWidgetIds = React.useMemo(() => {
    const ids = collectVisibleStaticWidgetIds(enabledWidgets, visibility);
    for (const entry of gatedHoldingsEntries) {
      if (visibility[entry.layoutId] !== false) {
        ids.push(entry.layoutId);
      }
    }
    for (const id of enabledWidgets.cashflowIds) {
      const layoutId = `cfw-${id}`;
      if (visibility[layoutId] !== false) {
        ids.push(layoutId);
      }
    }
    const topGainersCount = enabledWidgets.topGainersIds.size;
    for (const id of enabledWidgets.topGainersIds) {
      const layoutId = topGainersInstanceLayoutId(id, topGainersCount);
      if (visibility[layoutId] !== false) {
        ids.push(layoutId);
      }
    }
    const depositCount = enabledWidgets.depositIds.size;
    for (const id of enabledWidgets.depositIds) {
      const layoutId = depositInstanceLayoutId(id, depositCount);
      if (visibility[layoutId] !== false) {
        ids.push(layoutId);
      }
    }
    return ids;
  }, [enabledWidgets, gatedHoldingsEntries, visibility]);

  // Batch API only understands static + holdings ids. Cashflow/Top Gainers/Deposit instances
  // fetch individually via skipBatch — keep them out of the provider list.
  const batchWidgetIds = React.useMemo(() => {
    const ids = collectVisibleStaticWidgetIds(enabledWidgets, visibility);
    for (const entry of gatedHoldingsEntries) {
      if (visibility[entry.layoutId] !== false) {
        ids.push(entry.layoutId);
      }
    }
    return ids;
  }, [enabledWidgets, gatedHoldingsEntries, visibility]);

  const widgetIdsKey = layoutWidgetIds.join("|");
  const saveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const layoutsRef = React.useRef<DashboardLayouts | null>(null);
  const lastPersistedFingerprintRef = React.useRef<string | null>(null);

  const persistLayouts = React.useCallback(
    (next: DashboardLayouts) => {
      const fingerprint = layoutsFingerprint(next);
      if (fingerprint === lastPersistedFingerprintRef.current) return;

      saveDashboardLayouts(tenant, next);
      if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
      saveTimerRef.current = setTimeout(() => {
        void saveDashboardLayoutPrefsClient(next, tenant).then((saved) => {
          if (saved) lastPersistedFingerprintRef.current = fingerprint;
        });
      }, 500);
    },
    [tenant],
  );

  React.useEffect(() => {
    layoutsRef.current = layouts;
  }, [layouts]);

  React.useEffect(() => {
    if (holdingsLoading || settingsWidgetsLoading) return;
    const fromServer = parseDashboardLayouts(data.layoutPrefs ?? null);
    const fromLocal = loadDashboardLayouts(tenant);
    const prevShown = prevShownKeysKeyRef.current;
    const prevSpecs = prevMetricSpecsKeyRef.current;
    prevShownKeysKeyRef.current = shownKeysKey;
    prevMetricSpecsKeyRef.current = metricSpecsKey;

    const sameMembership =
      prevShown != null &&
      prevShown.split("|").filter(Boolean).sort().join("|") ===
        shownKeys.slice().sort().join("|");
    // Only rebuild metric positions when the Cards panel reorders tiles.
    // Clubbing / ungrouping merges in place so the grid does not jump.
    const orderOnlyChange = Boolean(
      prevShown && prevShown !== shownKeysKey && sameMembership,
    );
    const structureChanged = Boolean(prevSpecs && prevSpecs !== metricSpecsKey);

    // Prefer in-memory layouts when adding/removing widgets so server/local
    // snapshots that predate the full widget set don't drop newly visible items.
    const base = layoutsRef.current ?? fromServer ?? fromLocal;
    const merged = mergeLayoutsForIds(base, metricSpecs, layoutWidgetIds, {
      reflowCards: orderOnlyChange,
    });
    const next = normalizeSectionHeights(merged, metricSpecs);
    lastPersistedFingerprintRef.current = layoutsFingerprint(next);
    setLayouts(next);
    if (orderOnlyChange || structureChanged) {
      persistLayouts(next);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by stable string joins
  }, [
    tenant,
    metricSpecsKey,
    shownKeysKey,
    widgetIdsKey,
    holdingsLoading,
    settingsWidgetsLoading,
    data.layoutPrefs,
    persistLayouts,
  ]);

  // Normalize collapsed bars + equalize expanded section heights in each row.
  React.useEffect(() => {
    if (!layouts) return;
    if (prevCollapsedSpecsKeyRef.current === collapsedSpecsKey) return;
    prevCollapsedSpecsKeyRef.current = collapsedSpecsKey;
    const synced = normalizeSectionHeights(layouts, metricSpecs);
    if (synced === layouts) return;
    setLayouts(synced);
    persistLayouts(synced);
  }, [collapsedSpecsKey, layouts, metricSpecs, persistLayouts]);

  React.useEffect(() => {
    return () => {
      if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    };
  }, []);

  const handleLayoutsChange = React.useCallback(
    (next: DashboardLayouts) => {
      // Don't lock to content during drag/resize — only raise peers to row max.
      const normalized = normalizeSectionHeights(next, metricSpecs, {
        lockToContent: false,
      });
      setLayouts((prev) => {
        if (
          prev &&
          (Object.keys(normalized) as (keyof DashboardLayouts)[]).every((bp) => {
            const a = prev[bp];
            const b = normalized[bp];
            if (a.length !== b.length) return false;
            const byId = new Map(b.map((item) => [item.i, item]));
            return a.every((item) => {
              const other = byId.get(item.i);
              return (
                !!other &&
                item.x === other.x &&
                item.y === other.y &&
                item.w === other.w &&
                item.h === other.h
              );
            });
          })
        ) {
          return prev;
        }
        persistLayouts(normalized);
        return normalized;
      });
    },
    [metricSpecs, persistLayouts],
  );

  const handleResetLayout = React.useCallback(() => {
    clearDashboardLayouts(tenant);
    const next = normalizeSectionHeights(
      mergeLayoutsForIds(null, metricSpecs, layoutWidgetIds),
      metricSpecs,
    );
    lastPersistedFingerprintRef.current = null;
    setLayouts(next);
    void resetDashboardLayoutPrefsClient(tenant);
  }, [tenant, metricSpecs, layoutWidgetIds]);

  const openCardsPanel = React.useCallback(() => {
    setFieldsOpen(true);
  }, []);

  const startCustomizeLayout = React.useCallback(() => {
    setEditMode(true);
  }, []);

  const finishCustomizeLayout = React.useCallback(() => {
    setEditMode(false);
  }, []);

  const handleToggleSectionCollapsed = React.useCallback(
    (groupId: string, memberCount: number, nextCollapsed: boolean) => {
      toggleSectionCollapsed(groupId);
      setLayouts((prev) => {
        if (!prev) return prev;
        const layoutId = sectionLayoutId(groupId);
        const nextH = sectionTileHeight(memberCount, nextCollapsed);
        let changed = false;
        const patchedLayouts: DashboardLayouts = { ...prev };
        for (const bp of Object.keys(prev) as (keyof DashboardLayouts)[]) {
          patchedLayouts[bp] = prev[bp].map((item) => {
            if (item.i !== layoutId) return item;
            const patched = {
              ...item,
              h: nextH,
              minH: nextCollapsed ? 1 : nextH,
              maxH: nextCollapsed ? 1 : Math.max(item.maxH ?? nextH, nextH),
            };
            if (
              patched.h === item.h &&
              patched.minH === item.minH
            ) {
              return item;
            }
            changed = true;
            return patched;
          });
        }
        const specsForSync = metricSpecs.map((spec) =>
          spec.kind === "section" && spec.id === groupId
            ? { ...spec, collapsed: nextCollapsed, memberCount }
            : spec,
        );
        const next = normalizeSectionHeights(patchedLayouts, specsForSync);
        if (!changed && next === patchedLayouts) return prev;
        persistLayouts(next);
        return next;
      });
    },
    [metricSpecs, persistLayouts, toggleSectionCollapsed],
  );

  const anyGroupExpanded = activeGroups.some((group) => !group.collapsed);
  const handleToggleAllGroupsCollapsed = React.useCallback(() => {
    const nextCollapsed = anyGroupExpanded;
    setSectionsCollapsed(nextCollapsed);
    setLayouts((prev) => {
      if (!prev) return prev;
      const memberCountById = new Map(
        activeGroups.map((group) => [
          group.id,
          group.memberKeys.filter((key) => shownKeys.includes(key)).length,
        ]),
      );
      const patchedLayouts: DashboardLayouts = { ...prev };
      for (const bp of Object.keys(prev) as (keyof DashboardLayouts)[]) {
        patchedLayouts[bp] = prev[bp].map((item) => {
          const groupId = sectionIdFromLayoutId(item.i);
          if (!groupId || !memberCountById.has(groupId)) return item;
          const memberCount = memberCountById.get(groupId) ?? 1;
          const nextH = sectionTileHeight(memberCount, nextCollapsed);
          return {
            ...item,
            h: nextH,
            minH: nextCollapsed ? 1 : nextH,
            maxH: nextCollapsed ? 1 : Math.max(item.maxH ?? nextH, nextH),
          };
        });
      }
      const specsForSync = metricSpecs.map((spec) =>
        spec.kind === "section"
          ? { ...spec, collapsed: nextCollapsed }
          : spec,
      );
      const next = normalizeSectionHeights(patchedLayouts, specsForSync);
      persistLayouts(next);
      return next;
    });
  }, [
    activeGroups,
    anyGroupExpanded,
    metricSpecs,
    persistLayouts,
    setSectionsCollapsed,
    shownKeys,
  ]);

  const renderMetricContent = React.useCallback(
    (key: string, totalAmount: number | undefined, nested = false) => {
      const item = cardByKey.get(key);
      if (!item) return null;
      if (item.kind === "asset") {
        const assetProps: React.ComponentProps<typeof AssetCardView> = {
          card: item.card,
          hero: key === "total",
          totalAmount: key === "total" ? undefined : totalAmount,
          nested,
        };

        if (isCashLeverageCardKey(key)) {
          const displayMode = cardAumOverrides[key] ?? effectiveGlobalAumMode;
          const resolved = resolveCashLeverageCardDisplay(
            item.card,
            displayMode,
          );
          assetProps.displayAmount = resolved.amount;
          assetProps.displayFormatted = resolved.display;
          assetProps.showGrossDeduction = resolved.showGrossDeduction;
          assetProps.aumMode = displayMode;
          assetProps.onAumModeChange = (mode) => handleCardAumModeChange(key, mode);
          assetProps.totalAmount = resolveDashboardTotalAmount(
            data.assets?.total,
            displayMode,
          );
        }

        return <AssetCardView {...assetProps} />;
      }
      return (
        <CountMetricCard
          title={item.title}
          value={item.value}
          icon={ASSET_ICONS[item.key] ?? Coins}
        />
      );
    },
    [
      cardByKey,
      cardAumOverrides,
      data.assets?.total,
      effectiveGlobalAumMode,
      handleCardAumModeChange,
    ],
  );

  const gridItems = React.useMemo(() => {
    const items: {
      id: string;
      content: React.ReactNode;
      clubTargetKey?: string;
    }[] = [];
    const totalItem = cardByKey.get("total");
    const totalAmount = totalItem?.kind === "asset" ? totalItem.card.amount : undefined;
    const currencyPrefix =
      totalItem?.kind === "asset" && totalItem.card.display
        ? totalItem.card.display.replace(/[\d,.\s-]+$/, "").trim() || "USD"
        : "USD";

    for (const entry of topLevelMetrics) {
      if (entry.type === "card") {
        const content = renderMetricContent(entry.key, totalAmount);
        if (!content) continue;
        items.push({ id: cardLayoutId(entry.key), content });
        continue;
      }

      const members = entry.group.memberKeys.filter((key) => cardByKey.has(key));
      let sum = 0;
      for (const key of members) {
        const item = cardByKey.get(key);
        if (item?.kind === "asset") sum += item.card.amount;
      }
      const abs = Math.abs(sum).toLocaleString("en-US", {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      });
      const sumDisplay = `${currencyPrefix} ${sum < 0 ? `-${abs}` : abs}`;
      const percentLabel =
        totalAmount != null && Number.isFinite(totalAmount) && totalAmount !== 0
          ? `${((sum / totalAmount) * 100).toFixed(2)}%`
          : null;

      items.push({
        id: sectionLayoutId(entry.group.id),
        clubTargetKey: members[0],
        content: (
          <DashboardCardSection
            group={entry.group}
            memberKeys={members}
            editMode={editMode}
            sumDisplay={sumDisplay}
            percentLabel={percentLabel}
            onRename={(title) => renameSection(entry.group.id, title)}
            onToggleCollapsed={() =>
              handleToggleSectionCollapsed(
                entry.group.id,
                members.length,
                !entry.group.collapsed,
              )
            }
            onUngroup={() => ungroupSection(entry.group.id)}
            onReorderMembers={(activeKey, overKey) =>
              reorderSectionMembers(entry.group.id, activeKey, overKey)
            }
            onPullOutMember={removeFromSection}
          >
            {(key) => renderMetricContent(key, totalAmount, true)}
          </DashboardCardSection>
        ),
      });
    }

    for (const widget of filterEnabledStaticWidgets(enabledWidgets)) {
      if (visibility[widget.layoutId] === false) continue;
      items.push({
        id: widget.layoutId,
        content: renderStaticDashboardWidget(widget.kind),
      });
    }

    for (const entry of gatedHoldingsEntries) {
      if (visibility[entry.layoutId] === false) continue;
      if (entry.kind === "ch") {
        items.push({
          id: entry.layoutId,
          content: (
            <ConsolidatedHoldingsWidget
              widgetId={entry.widget.widget_id}
              layoutId={entry.layoutId}
              title={entry.widget.name || "Consolidated Holding"}
            />
          ),
        });
      } else if (entry.kind === "diy") {
        items.push({
          id: entry.layoutId,
          content: (
            <DiyConsolidatedHoldingsWidget
              widgetId={entry.widget.widget_id}
              layoutId={entry.layoutId}
              title={entry.widget.name || "DIY Consolidated Holdings"}
            />
          ),
        });
      } else {
        items.push({
          id: entry.layoutId,
          content: (
            <AssetAllocationWidget
              allocationId={entry.widget.widget_id}
              layoutId={entry.layoutId}
              title={entry.widget.name || "Asset Allocation"}
            />
          ),
        });
      }
    }

    for (const id of Array.from(enabledWidgets.cashflowIds).sort((a, b) => a - b)) {
      const layoutId = `cfw-${id}`;
      if (visibility[layoutId] === false) continue;
      items.push({
        id: layoutId,
        content: (
          <CashflowCalendarWidget
            widgetId={id}
            layoutId={layoutId}
            title={enabledWidgets.cashflowNames.get(id) || "Cashflow Calendar"}
          />
        ),
      });
    }

    const topGainersCount = enabledWidgets.topGainersIds.size;
    for (const id of Array.from(enabledWidgets.topGainersIds).sort((a, b) => a - b)) {
      const layoutId = topGainersInstanceLayoutId(id, topGainersCount);
      if (visibility[layoutId] === false) continue;
      items.push({
        id: layoutId,
        content: (
          <TopGainersLosersWidget
            widgetId={id}
            layoutId={layoutId}
            title={enabledWidgets.topGainersNames.get(id) || "Top Gainers and Losers"}
          />
        ),
      });
    }

    for (const id of Array.from(enabledWidgets.depositIds).sort((a, b) => a - b)) {
      const layoutId = depositInstanceLayoutId(id, enabledWidgets.depositIds.size);
      if (visibility[layoutId] === false) continue;
      items.push({
        id: layoutId,
        content: (
          <DepositWidget
            widgetId={id}
            layoutId={layoutId}
            title={enabledWidgets.depositNames.get(id) || "Deposit"}
          />
        ),
      });
    }

    return items;
  }, [
    topLevelMetrics,
    cardByKey,
    gatedHoldingsEntries,
    visibility,
    enabledWidgets,
    editMode,
    renderMetricContent,
    renameSection,
    ungroupSection,
    removeFromSection,
    reorderSectionMembers,
    handleToggleSectionCollapsed,
  ]);

  const settingsHref = customerUrl(tenant, "/settings?section=dashboard");

  return (
    <DashboardWidgetsProvider tenant={tenant} widgetIds={batchWidgetIds}>
      <DashboardWidgetConfigureProvider tenant={tenant}>
      <div className="flex flex-col gap-2">
        <DashboardPageHeader
          formattedDate={formattedDate}
          actions={
            <>
              <CustomerFieldsButton
                label="Cards"
                showLabel
                className="h-8 min-w-28 justify-center gap-1.5 px-2.5"
                onClick={openCardsPanel}
              />
              {activeGroups.length > 0 ? (
                <Button
                  size="sm"
                  variant="outline"
                  className="h-8 min-w-28 justify-center gap-1.5"
                  onClick={handleToggleAllGroupsCollapsed}
                  title={
                    anyGroupExpanded
                      ? "Collapse all clubbed groups"
                      : "Expand all clubbed groups"
                  }
                >
                  {anyGroupExpanded ? (
                    <ChevronsUpDown className="size-3.5" />
                  ) : (
                    <ChevronsDownUp className="size-3.5" />
                  )}
                  {anyGroupExpanded ? "Collapse groups" : "Expand groups"}
                </Button>
              ) : null}
              <Button
                size="sm"
                variant={editMode ? "default" : "outline"}
                className="h-8 min-w-28 justify-center gap-1.5"
                onClick={editMode ? finishCustomizeLayout : startCustomizeLayout}
              >
                <LayoutGrid className="size-3.5" />
                {editMode ? "Done" : "Customize"}
              </Button>
              {editMode ? (
                <Button
                  size="sm"
                  variant="outline"
                  className="h-8 min-w-28 justify-center gap-1.5"
                  onClick={handleResetLayout}
                >
                  <RotateCcw className="size-3.5" />
                  Reset layout
                </Button>
              ) : null}
              <Button
                size="sm"
                variant="outline"
                className="h-8 min-w-28 justify-center gap-1.5"
                onClick={onRefresh}
                disabled={loading}
              >
                <RefreshCw className={cn("size-3.5", loading && "animate-spin")} />
                Refresh
              </Button>
            </>
          }
        />

        <CustomerFieldsPanel
          open={fieldsOpen}
          onOpenChange={setFieldsOpen}
          catalog={catalog}
          visibility={visibility}
          order={order}
          lockedKeys={["total"]}
          groups={[...DASHBOARD_FIELD_GROUPS]}
          title="Cards & widgets"
          description="Show or hide metric cards and widgets already enabled in Settings. Layout and temporary hide/show stay on this dashboard."
          onToggle={setCardVisible}
          onReorder={reorderShown}
          onHideAll={hideAll}
          onHideGroup={hideGroup}
          footer={
            <p className="text-xs text-muted-foreground">
              Only widgets enabled in Settings appear here.{" "}
              <Link href={settingsHref} className="font-medium text-primary underline-offset-2 hover:underline">
                Manage dashboard widgets
              </Link>
            </p>
          }
        />

        {editMode ? (
          <p className="rounded-md border border-dashed border-primary/30 bg-primary/5 px-3 py-2 text-xs text-muted-foreground">
            Customize mode: drag the grip to move, pull the bottom-right corner to resize. To club
            statistic cards, hold{" "}
            <span className="font-medium text-foreground">Shift</span> and drop one onto another
            (widgets cannot be clubbed). Click{" "}
            <span className="font-medium text-foreground">Done</span> when finished.
          </p>
        ) : null}

        {layouts ? (
          <DashboardLayoutShell
            layouts={layouts}
            onLayoutsChange={handleLayoutsChange}
            onClubCards={clubCards}
            editMode={editMode}
            items={gridItems}
          />
        ) : (
          <DashboardSkeleton />
        )}
      </div>
      </DashboardWidgetConfigureProvider>
    </DashboardWidgetsProvider>
  );
}
