"use client";

import * as React from "react";
import Link from "next/link";
import {
  ChevronsDownUp,
  ChevronsUpDown,
  Coins,
  Eye,
  EyeOff,
  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,
} from "@/app/customer/_lib/dashboard-card-aum-display";
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 { selectVisibleHoldingsWidgets } from "@/app/customer/_lib/select-visible-holdings-widgets";
import { useCardAumMode } from "@/app/customer/_lib/use-card-aum-mode";
import { useHiddenFigures } from "@/app/customer/_lib/use-hidden-figures";
import {
  clearDashboardLayouts,
  layoutsFingerprint,
  loadDashboardLayoutState,
  mergeLayoutsForIds,
  normalizeSectionHeights,
  parseDashboardLayouts,
  saveDashboardLayouts,
  storedCardsSitBelowWidgets,
  storedMetricsMatchCurrent,
  cardLayoutId,
  sectionLayoutId,
  sectionIdFromLayoutId,
  COLLAPSED_SECTION_HEIGHT,
  equalizeWidgetRowHeights,
  layoutAfterGroupToggle,
  sectionSpecHeight,
  DASHBOARD_COLS,
  type DashboardLayouts,
  type MetricLayoutSpec,
} from "@/app/customer/_lib/dashboard-layout";
import {
  buildTopLevelMetricItems,
} from "@/app/customer/_lib/dashboard-card-groups";
import {
  activeGroupsForLayout,
  minMembersForCardGroup,
  withMacroUnassignedGroup,
  type MacroDashboardAssetType,
  type MacroDashboardCardGroupDef,
} from "@/app/customer/_lib/dashboard-layout-card-groups";
import { useDefaultLayout } from "@/app/customer/_lib/use-default-layout";
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,
  FiguresMaskedContext,
  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 };

/** Base card keys that only appear via type-refined Macro tiles (`structure__mac_12`). */
function typedMacroBaseKeys(assets: Record<string, unknown> | null | undefined): Set<string> {
  const keys = new Set<string>();
  for (const key of Object.keys(assets ?? {})) {
    const idx = key.indexOf("__mac_");
    if (idx > 0) keys.add(key.slice(0, idx));
  }
  return keys;
}

/**
 * Layout spec of one group. A Macro group with asset types is as long as it has types (compact
 * rows) and opens in place; any other group is as long as it has cards (nested metric tiles).
 */
function sectionSpecFor(
  group: { id: string; collapsed?: boolean },
  cardCount: number,
  assetTypes: MacroDashboardAssetType[] | undefined,
  inPlace: boolean,
): Extract<MetricLayoutSpec, { kind: "section" }> {
  const typeRows = Boolean(assetTypes && assetTypes.length > 0);
  return {
    kind: "section",
    id: group.id,
    memberCount: typeRows ? assetTypes!.length : cardCount,
    collapsed: group.collapsed,
    typeRows,
    inPlace,
  };
}

export function CorporateDashboardCards({
  data,
  tenant,
  loading,
  onRefresh,
  formattedDate,
}: {
  data: DashboardStatisticsData;
  tenant: string;
  loading: boolean;
  onRefresh: () => void;
  formattedDate: string;
}) {
  const { figuresHidden, toggleFigures } = useHiddenFigures();
  const { layout: defaultLayout } = useDefaultLayout(tenant);
  const bridgeCards = React.useMemo(() => data.bridgeCards ?? {}, [data.bridgeCards]);
  const bridgeCardKeys = React.useMemo(
    () =>
      Object.values(bridgeCards)
        .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.label.localeCompare(b.label))
        .map((card) => card.key),
    [bridgeCards],
  );
  const useBridgeCards = defaultLayout === "bridge" && bridgeCardKeys.length > 0;
  const backendMacroGroups = React.useMemo<MacroDashboardCardGroupDef[]>(() => {
    const raw = data.macroCardGroups ?? [];
    return raw
      .filter(
        (group) =>
          typeof group?.id === "string" &&
          group.id.startsWith("layout_macro_") &&
          typeof group.title === "string" &&
          Array.isArray(group.memberKeys) &&
          group.memberKeys.length >= minMembersForCardGroup(group.id),
      )
      .map((group) => ({
        id: group.id,
        title: group.title,
        memberKeys: group.memberKeys.map(String),
        priority: group.priority,
        assetTypes: (group.assetTypes ?? [])
          .filter((row) => typeof row?.label === "string" && Number.isFinite(row.amount))
          .map((row) => ({ label: row.label, amount: row.amount, display: String(row.display ?? "") })),
      }));
  }, [data.macroCardGroups]);

  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 { effectiveGlobalAumMode, cardAumOverrides, handleCardAumModeChange } =
    useCardAumMode(dashboardAumMode);

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

  const gatedHoldingsEntries = React.useMemo(
    () => selectVisibleHoldingsWidgets(holdingsEntries, enabledWidgets),
    [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>();
    // Base keys only used via type-refined tiles (structure__mac_12) — hide bare card.
    const typedBaseKeys =
      defaultLayout === "macro" ? typedMacroBaseKeys(data.assets) : new Set<string>();
    const groupMemberKeys = new Set(
      backendMacroGroups.flatMap((group) => group.memberKeys),
    );
    if (useBridgeCards) {
      const totalCard = data.assets?.total;
      if (totalCard && perms.assets?.total !== false) keys.add("total");
      for (const key of bridgeCardKeys) {
        if (perms.assets?.[key] === false) continue;
        keys.add(key);
      }
    } else {
      for (const key of ASSET_ORDER) {
        const card = data.assets?.[key];
        if (!card) continue;
        if (perms.assets?.[key] === false) continue;
        if (typedBaseKeys.has(key) && !groupMemberKeys.has(key)) continue;
        keys.add(key);
      }
      // Macro type-refined tiles (structure__mac_12, …) — not in ASSET_ORDER.
      if (defaultLayout === "macro") {
        for (const key of Object.keys(data.assets ?? {})) {
          if (!key.includes("__mac_")) 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;
  }, [
    backendMacroGroups,
    bridgeCardKeys,
    data.assets,
    defaultLayout,
    perms,
    useBridgeCards,
  ]);

  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 bridgeCatalog: CustomerGridColumn[] = useBridgeCards
      ? [
          ...(availableKeys.has("total")
            ? [{ key: "total", label: "Total Value", defaultVisible: true, group: "cards" as const }]
            : []),
          ...bridgeCardKeys
            .filter((key) => availableKeys.has(key))
            .map((key) => ({
              key,
              label: bridgeCards[key]?.label ?? key,
              defaultVisible: true,
              group: "cards" as const,
            })),
        ]
      : [];
    const metricSourceBase =
      useBridgeCards && bridgeCatalog.length > 0
        ? bridgeCatalog
        : 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));

    // Type-refined Macro tiles (structure__mac_12) are not in Yii cardCatalog.
    const presentMetricKeysForTyped = new Set(metricSourceBase.map((item) => item.key));
    const typedMacroCatalog: CustomerGridColumn[] = [];
    if (defaultLayout === "macro") {
      for (const key of availableKeys) {
        if (!key.includes("__mac_") || presentMetricKeysForTyped.has(key)) continue;
        const card = data.assets?.[key];
        typedMacroCatalog.push({
          key,
          label: card?.label ?? key,
          defaultVisible: true,
          group: "cards",
        });
      }
    }
    const metricSourceWithTyped = [...metricSourceBase, ...typedMacroCatalog];

    // Yii catalogs may omit org count cards; keep them in customize like Accumulator/Options.
    const presentMetricKeys = new Set(metricSourceWithTyped.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, ...metricSourceWithTyped, ...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, bridgeCardKeys, bridgeCards, data.assets, data.cardCatalog, defaultLayout, enabledWidgets, gatedHoldingsEntries, useBridgeCards]);

  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 macroGroupDefs = React.useMemo(
    () =>
      defaultLayout === "macro"
        ? withMacroUnassignedGroup(backendMacroGroups, clubbableKeys)
        : backendMacroGroups,
    [backendMacroGroups, clubbableKeys, defaultLayout],
  );

  // Macro groups open onto their asset types (the backend's per-type totals), not nested cards.
  const backendAssetTypesByGroupId = React.useMemo(() => {
    const map = new Map<string, MacroDashboardAssetType[]>();
    if (defaultLayout !== "macro") return map;
    for (const group of macroGroupDefs) {
      if (group.assetTypes && group.assetTypes.length > 0) map.set(group.id, group.assetTypes);
    }
    return map;
  }, [defaultLayout, macroGroupDefs]);

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

  React.useEffect(() => {
    if (defaultLayout !== "macro") return;
    // Seed Macro sections into card prefs so collapse targets them.
    ensureGroupsPresent(
      macroGroupDefs.map((group) => ({
        id: group.id,
        title: group.title,
        memberKeys: [...group.memberKeys],
        collapsed: true,
        customTitle: true,
      })),
    );
  }, [macroGroupDefs, defaultLayout, ensureGroupsPresent]);

  const cardByKey = React.useMemo(() => {
    const map = new Map<string, CorporateCardItem>();
    const typedBaseKeys =
      defaultLayout === "macro" ? typedMacroBaseKeys(data.assets) : new Set<string>();
    const groupMemberKeys = new Set(
      backendMacroGroups.flatMap((group) => group.memberKeys),
    );
    if (useBridgeCards) {
      const totalCard = data.assets?.total;
      if (totalCard && perms.assets?.total !== false) {
        map.set("total", { kind: "asset", key: "total", card: totalCard });
      }
      for (const key of bridgeCardKeys) {
        const card = bridgeCards[key];
        if (!card || perms.assets?.[key] === false) continue;
        map.set(key, { kind: "asset", key, card });
      }
    } else {
      for (const key of ASSET_ORDER) {
        const card = data.assets?.[key];
        if (!card || perms.assets?.[key] === false) continue;
        if (typedBaseKeys.has(key) && !groupMemberKeys.has(key)) continue;
        map.set(key, { kind: "asset", key, card });
      }
      if (defaultLayout === "macro") {
        for (const [key, card] of Object.entries(data.assets ?? {})) {
          if (!key.includes("__mac_") || !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;
  }, [
    backendMacroGroups,
    bridgeCardKeys,
    bridgeCards,
    counts.accumulator,
    counts.customers,
    counts.options,
    counts.userGroups,
    counts.users,
    data.assets,
    defaultLayout,
    perms,
    useBridgeCards,
  ]);

  const shownKeys = visibleOrderedKeys.filter((key) => cardByKey.has(key));
  const shownKeysKey = shownKeys.join("|");

  // Every Macro tile lists compact rows, worded and styled alike. A Macro with backend asset
  // types lists those; a tile without (Unassigned, built from leftover cards) lists its cards.
  const assetTypesByGroupId = React.useMemo(() => {
    const map = new Map<string, MacroDashboardAssetType[]>();
    if (defaultLayout !== "macro") return map;
    for (const group of macroGroupDefs) {
      const types = backendAssetTypesByGroupId.get(group.id);
      if (types) {
        map.set(group.id, types);
        continue;
      }
      const rows: MacroDashboardAssetType[] = [];
      for (const key of group.memberKeys) {
        if (!shownKeys.includes(key)) continue;
        const item = cardByKey.get(key);
        if (item?.kind === "asset") {
          rows.push({ label: item.card.label, amount: item.card.amount, display: item.card.display });
        } else if (item?.kind === "count") {
          rows.push({ label: item.title, amount: 0, display: String(item.value), plain: true });
        }
      }
      if (rows.length > 0) map.set(group.id, rows);
    }
    return map;
    // eslint-disable-next-line react-hooks/exhaustive-deps -- shownKeysKey
  }, [defaultLayout, macroGroupDefs, backendAssetTypesByGroupId, cardByKey, shownKeysKey]);
  const activeGroups = React.useMemo(
    () =>
      activeGroupsForLayout(
        groups,
        defaultLayout,
        clubbableKeys.filter((key) => shownKeys.includes(key)),
        macroGroupDefs,
      ),
    // eslint-disable-next-line react-hooks/exhaustive-deps -- shownKeysKey
    [macroGroupDefs, defaultLayout, 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 }
          : sectionSpecFor(
              item.group,
              item.group.memberKeys.filter((key) => shownKeys.includes(key)).length,
              assetTypesByGroupId.get(item.group.id),
              defaultLayout === "macro",
            ),
      ),
    [topLevelMetrics, shownKeys, assetTypesByGroupId, defaultLayout],
  );
  // 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}:${spec.typeRows ? "t" : "c"}:${spec.inPlace ? "p" : "f"}`,
    )
    .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 prevDefaultLayoutRef = React.useRef(defaultLayout);

  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, options?: { userArranged?: boolean }) => {
      const fingerprint = layoutsFingerprint(next);
      if (
        fingerprint === lastPersistedFingerprintRef.current &&
        options?.userArranged == null
      ) {
        return;
      }

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

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

  React.useEffect(() => {
    if (holdingsLoading || settingsWidgetsLoading) return;
    const prevLayout = prevDefaultLayoutRef.current;
    const layoutModeChanged = prevLayout !== defaultLayout;
    if (layoutModeChanged && layoutsRef.current) {
      saveDashboardLayouts(tenant, layoutsRef.current, prevLayout);
    }
    prevDefaultLayoutRef.current = defaultLayout;

    const localState = loadDashboardLayoutState(tenant, defaultLayout);
    const fromLocal = localState?.layouts ?? null;
    let userArranged = localState?.userArranged === true;
    const fromServer =
      defaultLayout === "default"
        ? parseDashboardLayouts(data.layoutPrefs ?? null)
        : null;
    const modeSnapshot = fromLocal ?? fromServer;

    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("|");
    const orderOnlyChange = Boolean(
      prevShown && prevShown !== shownKeysKey && sameMembership,
    );
    const structureChanged = Boolean(prevSpecs && prevSpecs !== metricSpecsKey);

    const base = layoutModeChanged
      ? modeSnapshot
      : (layoutsRef.current ?? modeSnapshot);
    // Clubbing used to mark userArranged incorrectly — heal cards-below-widgets.
    if (userArranged && storedCardsSitBelowWidgets(base, metricSpecs, layoutWidgetIds)) {
      userArranged = false;
    }
    // Auto layout: cards/groups above widgets. Customize drag sets userArranged.
    const shouldReflowCards =
      !userArranged ||
      orderOnlyChange ||
      (!layoutModeChanged && structureChanged) ||
      !storedMetricsMatchCurrent(base, metricSpecs);
    const merged = mergeLayoutsForIds(base, metricSpecs, layoutWidgetIds, {
      reflowCards: shouldReflowCards,
      // Widgets only keep their stored size once the user has arranged the layout.
      reflowWidgets: !userArranged,
    });
    const next = equalizeWidgetRowHeights(normalizeSectionHeights(merged, metricSpecs));
    lastPersistedFingerprintRef.current = layoutsFingerprint(next);
    setLayouts(next);
    if (shouldReflowCards) {
      persistLayouts(next, { userArranged });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by stable string joins
  }, [
    tenant,
    defaultLayout,
    metricSpecsKey,
    shownKeysKey,
    widgetIdsKey,
    holdingsLoading,
    settingsWidgetsLoading,
    data.layoutPrefs,
    persistLayouts,
  ]);

  // Normalize collapsed bars + equalize expanded section heights in each row. Applied during render
  // (once per collapsed-state change); only the persist is a side effect.
  const [normalizedForKey, setNormalizedForKey] = React.useState<string | null>(null);
  const [normalizedToPersist, setNormalizedToPersist] = React.useState<DashboardLayouts | null>(null);
  if (layouts && normalizedForKey !== collapsedSpecsKey) {
    setNormalizedForKey(collapsedSpecsKey);
    const synced = normalizeSectionHeights(layouts, metricSpecs);
    if (synced !== layouts) {
      setLayouts(synced);
      setNormalizedToPersist(synced);
    }
  }

  React.useEffect(() => {
    if (normalizedToPersist) persistLayouts(normalizedToPersist);
  }, [normalizedToPersist, persistLayouts]);

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

  const handleLayoutsChange = React.useCallback(
    (next: DashboardLayouts, options?: { userArranged?: boolean }) => {
      // Don't lock to content during drag/resize — only raise peers to row max.
      const normalized = normalizeSectionHeights(next, metricSpecs, {
        lockToContent: false,
      });
      const markArranged = options?.userArranged !== 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;
        }
        // Customize drag/resize marks arranged; club undo must not.
        persistLayouts(normalized, { userArranged: markArranged });
        return normalized;
      });
    },
    [metricSpecs, persistLayouts],
  );

  const handleResetLayout = React.useCallback(() => {
    clearDashboardLayouts(tenant, defaultLayout);
    const next = normalizeSectionHeights(
      mergeLayoutsForIds(null, metricSpecs, layoutWidgetIds),
      metricSpecs,
    );
    lastPersistedFingerprintRef.current = null;
    setLayouts(next);
    persistLayouts(next, { userArranged: false });
    if (defaultLayout === "default") {
      void resetDashboardLayoutPrefsClient(tenant);
    }
  }, [tenant, defaultLayout, metricSpecs, layoutWidgetIds, persistLayouts]);

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

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

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

  // Tiles are always re-flowed when a group opens or closes (see layoutAfterGroupToggle); widget
  // placements are kept once the user has arranged the layout.
  const layoutAfterSectionToggle = React.useCallback(
    (patched: DashboardLayouts, specs: MetricLayoutSpec[]): DashboardLayouts =>
      layoutAfterGroupToggle(
        patched,
        specs,
        layoutWidgetIds,
        loadDashboardLayoutState(tenant, defaultLayout)?.userArranged === true,
      ),
    [tenant, defaultLayout, layoutWidgetIds],
  );

  const handleToggleSectionCollapsed = React.useCallback(
    (groupId: string, nextCollapsed: boolean) => {
      const fallback = activeGroups.find((group) => group.id === groupId);
      const sectionSpec = metricSpecs.find(
        (spec): spec is Extract<MetricLayoutSpec, { kind: "section" }> =>
          spec.kind === "section" && spec.id === groupId,
      );
      if (!sectionSpec) return;
      toggleSectionCollapsed(groupId, fallback);
      setLayouts((prev) => {
        if (!prev) return prev;
        const layoutId = sectionLayoutId(groupId);
        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 nextH = sectionSpecHeight(sectionSpec, item.w, DASHBOARD_COLS[bp], nextCollapsed);
            const patched = {
              ...item,
              h: nextH,
              minH: nextCollapsed ? COLLAPSED_SECTION_HEIGHT : nextH,
              maxH: nextCollapsed ? COLLAPSED_SECTION_HEIGHT : 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 }
            : spec,
        );
        const next = layoutAfterSectionToggle(patchedLayouts, specsForSync);
        if (!changed && next === patchedLayouts) return prev;
        persistLayouts(next);
        return next;
      });
    },
    [
      activeGroups,
      layoutAfterSectionToggle,
      metricSpecs,
      persistLayouts,
      toggleSectionCollapsed,
    ],
  );

  // A one-line group is always a tile (nothing to expand), so it must not decide the button.
  const toggleableGroups = activeGroups.filter((group) =>
    metricSpecs.some(
      (spec) => spec.kind === "section" && spec.id === group.id && spec.memberCount >= 2,
    ),
  );
  const anyGroupExpanded = toggleableGroups.some((group) => !group.collapsed);
  const handleToggleAllGroupsCollapsed = React.useCallback(() => {
    const nextCollapsed = anyGroupExpanded;
    setSectionsCollapsed(
      nextCollapsed,
      activeGroups.length > 0 ? activeGroups : undefined,
    );
    setLayouts((prev) => {
      if (!prev) return prev;
      const specById = new Map(
        metricSpecs.flatMap((spec) => (spec.kind === "section" ? [[spec.id, spec] as const] : [])),
      );
      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);
          const sectionSpec = groupId ? specById.get(groupId) : undefined;
          if (!sectionSpec) return item;
          const nextH = sectionSpecHeight(sectionSpec, item.w, DASHBOARD_COLS[bp], nextCollapsed);
          return {
            ...item,
            h: nextH,
            minH: nextCollapsed ? COLLAPSED_SECTION_HEIGHT : nextH,
            maxH: nextCollapsed ? COLLAPSED_SECTION_HEIGHT : Math.max(item.maxH ?? nextH, nextH),
          };
        });
      }
      const specsForSync = metricSpecs.map((spec) =>
        spec.kind === "section"
          ? { ...spec, collapsed: nextCollapsed }
          : spec,
      );
      const next = layoutAfterSectionToggle(patchedLayouts, specsForSync);
      persistLayouts(next);
      return next;
    });
  }, [
    activeGroups,
    anyGroupExpanded,
    layoutAfterSectionToggle,
    metricSpecs,
    persistLayouts,
    setSectionsCollapsed,
  ]);

  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}
          tone={item.key === "accumulator" || item.key === "options" ? "structure" : "neutral"}
        />
      );
    },
    [
      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));
      const assetTypes = assetTypesByGroupId.get(entry.group.id);
      // A group that opens onto asset types totals those (they add up to its cards by
      // construction); any other group totals its cards.
      let sum = 0;
      if (assetTypes) {
        for (const row of assetTypes) if (!row.plain) sum += row.amount;
      } else {
        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}
            assetTypes={assetTypes}
            editMode={editMode}
            sumDisplay={sumDisplay}
            percentLabel={percentLabel}
            onRename={(title) => renameSection(entry.group.id, title)}
            onToggleCollapsed={() =>
              handleToggleSectionCollapsed(entry.group.id, !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;
      const topHoldingsWidgetId =
        widget.kind === "top_holdings"
          ? Array.from(enabledWidgets.topHoldingsIds).sort((a, b) => a - b)[0]
          : undefined;
      items.push({
        id: widget.layoutId,
        content: renderStaticDashboardWidget(widget.kind, {
          widgetId: topHoldingsWidgetId,
        }),
      });
    }

    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,
    assetTypesByGroupId,
    gatedHoldingsEntries,
    visibility,
    enabledWidgets,
    editMode,
    renderMetricContent,
    renameSection,
    ungroupSection,
    removeFromSection,
    reorderSectionMembers,
    handleToggleSectionCollapsed,
  ]);

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

  return (
    <FiguresMaskedContext.Provider value={figuresHidden}>
    <DashboardWidgetsProvider tenant={tenant} widgetIds={batchWidgetIds}>
      <DashboardWidgetConfigureProvider tenant={tenant}>
      <div className="flex flex-col gap-2">
        <DashboardPageHeader
          formattedDate={formattedDate}
          actions={
            <>
              <Button
                variant="ghost"
                size="sm"
                className="h-8 gap-1.5 rounded-lg px-2.5"
                onClick={toggleFigures}
                aria-label={figuresHidden ? "Show figures" : "Hide figures"}
              >
                {figuresHidden ? (
                  <EyeOff className="size-3.5" />
                ) : (
                  <Eye className="size-3.5" />
                )}
              </Button>
              <CustomerFieldsButton
                label="Cards"
                showLabel
                className="h-8 justify-center gap-1.5 rounded-lg border-transparent bg-transparent px-2.5 shadow-none hover:bg-muted"
                onClick={openCardsPanel}
              />
              {toggleableGroups.length > 0 ? (
                <Button
                  size="sm"
                  variant="ghost"
                  className="h-8 justify-center gap-1.5 rounded-lg"
                  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" : "ghost"}
                className="h-8 justify-center gap-1.5 rounded-lg"
                onClick={editMode ? finishCustomizeLayout : startCustomizeLayout}
              >
                <LayoutGrid className="size-3.5" />
                {editMode ? "Done" : "Customize"}
              </Button>
              {editMode ? (
                <Button
                  size="sm"
                  variant="ghost"
                  className="h-8 justify-center gap-1.5 rounded-lg"
                  onClick={handleResetLayout}
                >
                  <RotateCcw className="size-3.5" />
                  Reset layout
                </Button>
              ) : null}
              <Button
                size="sm"
                variant="ghost"
                className="h-8 justify-center gap-1.5 rounded-lg"
                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>
    </FiguresMaskedContext.Provider>
  );
}
