"use client"

import { errorMessageOr } from "@/lib/toast-api-error";

import * as React from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
  type ColumnDef,
  type ColumnFiltersState,
  type PaginationState,
  type RowSelectionState,
  type SortingState,
} from "@tanstack/react-table"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { XmlApiTablePagination } from "@/components/xml-apis/shared/xml-api-table-pagination"
import { SETTINGS_INPUT_CLASS } from "@/components/settings/constants"
import { dashboardCsrfHeader } from "@/lib/csrf.client"
import { cn } from "@/lib/utils"

import { DeleteConfirmDialog } from "@/components/shared/delete-confirm-dialog"

import { LIST_HREF, type IsinAssetMasterRow } from "./schema"
import { IsinAssetMasterPibSelect } from "./isin-asset-master-pib-select"
import type {
  AcModuleOption,
  ATypeOptions,
} from "../_lib/isin-asset-master-options"
import {
  formatPibOption,
  type PibClassOption,
} from "../_lib/pib-class-options"
import { formatPlainAmount } from "@/lib/format/numbers"

type Props = {
  data: IsinAssetMasterRow[]
  totalCount: number
  pibClassList: PibClassOption[]
  moduleOptions: AcModuleOption[]
  aTypeOptions: ATypeOptions
  acAtEditExcludedCodes: string[]
}

const filterInputClass = cn(SETTINGS_INPUT_CLASS, "h-8")
const BULK_PIB_HREF = `${LIST_HREF}/bulk-update-pib`
const BULK_AC_AT_HREF = `${LIST_HREF}/bulk-update-ac-at`
const BULK_LOCK_HREF = `${LIST_HREF}/bulk-lock`
const BULK_PROPAGATE_HREF = `${LIST_HREF}/bulk-propagate-transactions`
const PROPAGATE_ALL_LOCKED_HREF = `${LIST_HREF}/propagate-all-locked`
const APPLY_MAPPING_HREF = `${LIST_HREF}/apply-ac-at-mapping`
const ALL_AC = "__all__"
const PROPAGATE_CHUNK = 500

type LockTab = "locked" | "unlocked" | "all"
type PibFilter = "all" | "unassigned" | "assigned"
type FlagFilter = "all" | "flagged" | "clear"

type AssetClassGroup = {
  code: string
  label: string
  count: number
}

export function IsinAssetMasterTable({
  data,
  totalCount,
  pibClassList,
  moduleOptions,
  aTypeOptions,
  acAtEditExcludedCodes,
}: Props) {
  const router = useRouter()
  const [lockTab, setLockTab] = React.useState<LockTab>("all")
  const [pibFilter, setPibFilter] = React.useState<PibFilter>("all")
  const [flagFilter, setFlagFilter] = React.useState<FlagFilter>("all")
  const [aClassFilter, setAClassFilter] = React.useState<string>(ALL_AC)
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
  const [sorting, setSorting] = React.useState<SortingState>([
    { id: "isin", desc: false },
  ])
  const [pagination, setPagination] = React.useState<PaginationState>({
    pageIndex: 0,
    pageSize: 20,
  })
  const [bulkPib, setBulkPib] = React.useState("")
  const [bulkAc, setBulkAc] = React.useState("")
  const [bulkAt, setBulkAt] = React.useState("")
  const [bulkBusy, setBulkBusy] = React.useState(false)
  const [mappingBusy, setMappingBusy] = React.useState(false)
  const [bulkError, setBulkError] = React.useState<string | null>(null)
  const [bulkMessage, setBulkMessage] = React.useState<string | null>(null)
  const [confirmDialog, setConfirmDialog] = React.useState<{
    title: string
    description: React.ReactNode
    confirmLabel: string
    confirmingLabel?: string
    run: () => Promise<void>
  } | null>(null)
  const [propagateProgress, setPropagateProgress] = React.useState<{
    phase: string
    percent: number
    isins: number
    schemas_total: number
    schemas_done: number
    current_schema: string | null
    ledger_rows: number
    os_rows: number
    temp_rows: number
    line: string | null
  } | null>(null)
  const propagateWatching = propagateProgress?.phase === "running"

  React.useEffect(() => {
    if (propagateProgress?.phase !== "running") {
      return
    }
    let cancelled = false
    const tick = async () => {
      try {
        const res = await fetch(PROPAGATE_ALL_LOCKED_HREF, {
          method: "GET",
          cache: "no-store",
          headers: {
            Accept: "application/json",
            ...dashboardCsrfHeader(),
          },
        })
        const json = await res.json().catch(() => ({}))
        const data = json?.data
        if (cancelled || !data || typeof data !== "object") {
          return
        }
        const next = {
          phase: String(data.phase ?? "running"),
          percent: Number(data.percent ?? 0),
          isins: Number(data.isins ?? 0),
          schemas_total: Number(data.schemas_total ?? 0),
          schemas_done: Number(data.schemas_done ?? 0),
          current_schema:
            typeof data.current_schema === "string" ? data.current_schema : null,
          ledger_rows: Number(data.ledger_rows ?? 0),
          os_rows: Number(data.os_rows ?? 0),
          temp_rows: Number(data.temp_rows ?? 0),
          line: typeof data.line === "string" ? data.line : null,
        }
        setPropagateProgress(next)
        if (next.phase === "done" || next.phase === "error") {
          setBulkBusy(false)
          if (next.phase === "error") {
            setBulkError(next.line)
          } else {
            setBulkMessage(next.line || "Propagate finished.")
          }
          router.refresh()
        }
      } catch {
        /* keep polling */
      }
    }
    void tick()
    const id = window.setInterval(() => void tick(), 1000)
    return () => {
      cancelled = true
      window.clearInterval(id)
    }
  }, [propagateProgress?.phase, router])

  const mappingFileRef = React.useRef<HTMLInputElement | null>(null)
  const directMappingFileRef = React.useRef<HTMLInputElement | null>(null)

  const excludedAc = React.useMemo(
    () => new Set(acAtEditExcludedCodes),
    [acAtEditExcludedCodes]
  )

  const editableModuleOptions = React.useMemo(
    () => moduleOptions.filter((opt) => !excludedAc.has(opt.code)),
    [moduleOptions, excludedAc]
  )

  const bulkAtOptions = React.useMemo(() => {
    const scoped =
      bulkAc && aTypeOptions.byAc[bulkAc]?.length
        ? aTypeOptions.byAc[bulkAc]
        : aTypeOptions.all
    return [...scoped].sort((a, b) =>
      a.localeCompare(b, undefined, { sensitivity: "base" })
    )
  }, [bulkAc, aTypeOptions])

  const lockedCount = React.useMemo(
    () => data.filter((row) => row.status === "locked").length,
    [data]
  )
  const unlockedCount = React.useMemo(
    () => data.filter((row) => row.status !== "locked").length,
    [data]
  )

  const lockRows = React.useMemo(() => {
    if (lockTab === "locked") {
      return data.filter((row) => row.status === "locked")
    }
    if (lockTab === "unlocked") {
      return data.filter((row) => row.status !== "locked")
    }
    return data
  }, [data, lockTab])

  const unassignedCount = React.useMemo(
    () => lockRows.filter((row) => !row.pibClass).length,
    [lockRows]
  )
  const assignedCount = React.useMemo(
    () => lockRows.filter((row) => Boolean(row.pibClass)).length,
    [lockRows]
  )

  const tabRows = React.useMemo(() => {
    if (pibFilter === "unassigned") {
      return lockRows.filter((row) => !row.pibClass)
    }
    if (pibFilter === "assigned") {
      return lockRows.filter((row) => Boolean(row.pibClass))
    }
    return lockRows
  }, [lockRows, pibFilter])

  const showUnlockedTools = lockTab === "unlocked" || lockTab === "all"
  const showLockedTools = lockTab === "locked" || lockTab === "all"
  const lockedEligibleCount = React.useMemo(
    () =>
      lockRows.filter(
        (row) =>
          row.status === "locked" &&
          row.propagatable &&
          !row.skipped &&
          !excludedAc.has(row.aClass)
      ).length,
    [lockRows, excludedAc]
  )

  const assetClassGroups = React.useMemo((): AssetClassGroup[] => {
    const map = new Map<string, AssetClassGroup>()
    for (const row of tabRows) {
      const code = row.aClass || "(empty)"
      const existing = map.get(code)
      if (existing) {
        existing.count += 1
        continue
      }
      map.set(code, {
        code,
        label: row.aClassLabel || code,
        count: 1,
      })
    }
    return Array.from(map.values()).sort((a, b) => {
      if (b.count !== a.count) return b.count - a.count
      return a.label.localeCompare(b.label)
    })
  }, [tabRows])

  const columns = React.useMemo<ColumnDef<IsinAssetMasterRow>[]>(
    () => [
      {
        id: "select",
        enableSorting: false,
        enableHiding: false,
        size: 40,
        header: ({ table }) => (
          <Checkbox
            checked={
              table.getIsAllPageRowsSelected()
                ? true
                : table.getIsSomePageRowsSelected()
                  ? "indeterminate"
                  : false
            }
            onCheckedChange={(value) => table.toggleAllPageRowsSelected(value === true)}
            aria-label="Select all on page"
          />
        ),
        cell: ({ row }) => (
          <Checkbox
            checked={row.getIsSelected()}
            onCheckedChange={(value) => row.toggleSelected(value === true)}
            aria-label={`Select ${row.original.isin}`}
            className="mt-0.5"
          />
        ),
      },
      {
        accessorKey: "isin",
        header: "ISIN",
        filterFn: (row, _columnId, filterValue) => {
          const q = String(filterValue ?? "")
            .trim()
            .toLowerCase()
          if (q === "") return true
          if (row.original.isin.toLowerCase().includes(q)) return true
          const names = [
            row.original.isinName,
            ...(row.original.isinNames ?? []),
          ].filter(Boolean) as string[]
          return names.some((name) => name.toLowerCase().includes(q))
        },
        cell: ({ row }) => {
          const names = [
            row.original.isinName,
            ...row.original.isinNames,
          ].filter((n, i, arr) => n && arr.indexOf(n) === i)

          return (
            <div className="flex min-w-0 max-w-full flex-col items-start gap-1.5">
              <Link
                href={`${LIST_HREF}/${row.original.id}`}
                className="inline-flex items-center gap-1.5 font-mono text-sm text-sky-700 hover:underline"
              >
                <span>{row.original.isin}</span>
                <span className="rounded-md bg-muted px-1.5 py-0.5 font-sans text-[11px] font-medium text-muted-foreground no-underline">
                  {row.original.bankCount}
                </span>
              </Link>
              {names.length > 0 ? (
                <div className="flex w-full flex-col items-start gap-1">
                  {names.map((name) => (
                    <span
                      key={name}
                      className="inline-block max-w-full rounded-md bg-secondary px-2 py-0.5 text-left text-xs font-normal leading-snug text-secondary-foreground whitespace-normal break-words"
                      title={name}
                    >
                      {name}
                    </span>
                  ))}
                </div>
              ) : null}
            </div>
          )
        },
      },
      {
        accessorKey: "bankCount",
        header: "Banks",
        cell: ({ row }) => (
          <div className="flex min-w-0 max-w-full flex-col flex-wrap items-start gap-1">
            {row.original.bankNames.length > 0 ? (
              row.original.bankNames.map((name) => (
                <span
                  key={name}
                  className="inline-block max-w-full rounded-md border border-border bg-background px-2 py-0.5 text-left text-xs font-normal leading-snug whitespace-normal break-words"
                  title={name}
                >
                  {name}
                </span>
              ))
            ) : (
              <span className="tabular-nums text-sm text-muted-foreground">
                {row.original.bankCount}
              </span>
            )}
          </div>
        ),
      },
      {
        accessorKey: "macroAssetClass",
        header: "Macro AC",
        cell: ({ row }) => (
          <span className="text-sm">
            {row.original.macroAssetClass.trim() !== ""
              ? row.original.macroAssetClass
              : "—"}
          </span>
        ),
      },
      {
        accessorKey: "aClass",
        header: "Asset Class",
        filterFn: (row, _columnId, filterValue) => {
          if (!filterValue || filterValue === ALL_AC) return true
          const code = row.original.aClass || "(empty)"
          return code === filterValue
        },
        cell: ({ row }) => {
          const skipped = row.original.skipped || excludedAc.has(row.original.aClass)
          return (
            <div className="flex flex-col items-start gap-1">
              <span className="text-sm">
                {row.original.aClassLabel || (skipped ? "Structure & others" : "—")}
                {row.original.aClass ? (
                  <span className="ml-1 font-mono text-xs text-muted-foreground">
                    ({row.original.aClass})
                  </span>
                ) : null}
              </span>
              {skipped ? (
                <span
                  className="inline-flex rounded-md bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-700 ring-1 ring-slate-400/30"
                  title={
                    row.original.skipReasonLabel ||
                    "Structure & others — Skipped (assign PIB only; no Update transactions)"
                  }
                >
                  Skipped
                </span>
              ) : null}
            </div>
          )
        },
      },
      { accessorKey: "aType", header: "Asset Type" },
      {
        id: "detectFlagged",
        accessorKey: "detectFlagged",
        header: "Flagged",
        filterFn: (row, _columnId, filterValue) => {
          if (!filterValue || filterValue === "all") return true
          if (filterValue === "flagged") return row.original.detectFlagged === true
          if (filterValue === "clear") return row.original.detectFlagged !== true
          return true
        },
        cell: ({ row }) =>
          row.original.detectFlagged ? (
            <span
              className="inline-flex rounded-md bg-rose-100 px-2 py-0.5 text-xs font-medium text-rose-900 ring-1 ring-rose-600/25"
              title="AT Detect Flag IAM rule matched on common_auto"
            >
              Flagged
            </span>
          ) : (
            <span className="text-xs text-muted-foreground">—</span>
          ),
      },
      {
        accessorKey: "pibClass",
        header: "PIB",
        filterFn: (row, _columnId, filterValue) => {
          const hasPib = Boolean(row.original.pibClass)
          if (filterValue === "empty") return !hasPib
          if (filterValue === "assigned") return hasPib
          return true
        },
        cell: ({ row }) => (
          <IsinAssetMasterPibSelect
            id={row.original.id}
            value={row.original.pibClass}
            options={pibClassList}
            disabled={row.original.pibAssignable === false}
          />
        ),
      },
      {
        accessorKey: "status",
        header: "Lock",
        enableSorting: false,
        cell: ({ row }) => {
          const locked = row.original.status === "locked"
          const skipped = row.original.skipped || excludedAc.has(row.original.aClass)
          return (
            <div className="flex flex-col items-start gap-1">
              <span
                className={cn(
                  "inline-flex rounded-md px-2 py-0.5 text-xs font-medium",
                  locked
                    ? "bg-amber-100 text-amber-950 ring-1 ring-amber-600/30"
                    : "bg-muted text-muted-foreground"
                )}
                title={
                  skipped
                    ? "Skipped module — lock only to keep PIB; Update transactions is disabled"
                    : locked
                      ? "Locked — used as final AC/AT on XML Detect"
                      : "Unlocked — Detect uses Common → Normal"
                }
              >
                {locked ? "Locked" : "Unlocked"}
              </span>
            </div>
          )
        },
      },
    ],
    [pibClassList, excludedAc]
  )

  const table = useReactTable({
    data: tabRows,
    columns,
    state: { columnFilters, pagination, rowSelection, sorting },
    getRowId: (r) => r.id,
    enableRowSelection: true,
    onRowSelectionChange: setRowSelection,
    onColumnFiltersChange: setColumnFilters,
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  })

  const filteredCount = table.getFilteredRowModel().rows.length
  const selectedIds = Object.keys(rowSelection).filter((id) => rowSelection[id])
  const selectedCount = selectedIds.length
  const pageIndex = table.getState().pagination.pageIndex
  const pageSize = table.getState().pagination.pageSize
  const start = filteredCount === 0 ? 0 : pageIndex * pageSize + 1
  const end = Math.min((pageIndex + 1) * pageSize, filteredCount)

  const headers = table.getHeaderGroups()[0]?.headers ?? []
  const activeGroup = assetClassGroups.find((g) => g.code === aClassFilter) ?? null

  function onFlagFilterChange(value: FlagFilter) {
    setFlagFilter(value)
    setFilters(pibFilter, aClassFilter, value)
    table.setPageIndex(0)
  }

  function setFilters(nextPib: PibFilter, nextAClass: string, nextFlag: FlagFilter = flagFilter) {
    const next: ColumnFiltersState = []
    if (nextPib === "unassigned") {
      next.push({ id: "pibClass", value: "empty" })
    } else if (nextPib === "assigned") {
      next.push({ id: "pibClass", value: "assigned" })
    }
    if (nextAClass !== ALL_AC) {
      next.push({ id: "aClass", value: nextAClass })
    }
    if (nextFlag === "flagged" || nextFlag === "clear") {
      next.push({ id: "detectFlagged", value: nextFlag })
    }
    setColumnFilters(next)
  }

  const flaggedCount = React.useMemo(
    () => tabRows.filter((row) => row.detectFlagged).length,
    [tabRows],
  )

  function onLockTabChange(value: string) {
    const next: LockTab =
      value === "locked" ? "locked" : value === "all" ? "all" : "unlocked"
    setLockTab(next)
    setAClassFilter(ALL_AC)
    setRowSelection({})
    setBulkMessage(null)
    setBulkError(null)
    setFilters(pibFilter, ALL_AC)
    setPagination((prev) => ({ ...prev, pageIndex: 0 }))
  }

  function onPibFilterChange(value: string) {
    const next: PibFilter =
      value === "assigned" ? "assigned" : value === "unassigned" ? "unassigned" : "all"
    setPibFilter(next)
    setAClassFilter(ALL_AC)
    setRowSelection({})
    setBulkMessage(null)
    setBulkError(null)
    setFilters(next, ALL_AC)
    setPagination((prev) => ({ ...prev, pageIndex: 0 }))
  }

  function onAClassChange(code: string) {
    setAClassFilter(code)
    setRowSelection({})
    setBulkMessage(null)
    setBulkError(null)
    setFilters(pibFilter, code)
    setPagination((prev) => ({ ...prev, pageIndex: 0 }))
  }

  async function applyBulkPib() {
    if (selectedCount === 0 || bulkBusy) return
    if (selectedCount > 500) {
      setBulkError("Maximum 500 rows per bulk update. Narrow selection first.")
      return
    }
    setBulkBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      const res = await fetch(BULK_PIB_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({
          ids: selectedIds,
          pib_class: bulkPib,
        }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not mass-assign PIB.")
      }
      setBulkMessage(json?.message || `Updated ${selectedCount} row(s).`)
      setRowSelection({})
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not mass-assign PIB."))
    } finally {
      setBulkBusy(false)
    }
  }

  function onBulkAcChange(next: string) {
    setBulkAc(next)
    setBulkMessage(null)
    const scoped =
      next && aTypeOptions.byAc[next]?.length
        ? aTypeOptions.byAc[next]
        : aTypeOptions.all
    if (bulkAt && scoped.length > 0 && !scoped.includes(bulkAt)) {
      setBulkAt("")
    }
  }

  async function applyBulkAcAt() {
    if (selectedCount === 0 || bulkBusy) return
    if (bulkAc.trim() === "") {
      setBulkError("Select an asset class before applying.")
      return
    }
    if (selectedCount > 500) {
      setBulkError("Maximum 500 rows per bulk update. Narrow selection first.")
      return
    }
    setBulkBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      const res = await fetch(BULK_AC_AT_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({
          ids: selectedIds,
          a_class: bulkAc.trim(),
          a_type: bulkAt.trim(),
        }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not mass-assign asset class / type.")
      }
      setBulkMessage(json?.message || `Updated ${selectedCount} row(s).`)
      setRowSelection({})
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not mass-assign asset class / type."))
    } finally {
      setBulkBusy(false)
    }
  }

  async function applyBulkLock() {
    if (selectedCount === 0 || bulkBusy) return
    if (selectedCount > 500) {
      setBulkError("Maximum 500 rows per bulk update. Narrow selection first.")
      return
    }
    const count = selectedCount
    const ids = selectedIds
    setConfirmDialog({
      title: `Lock ${count} selected ISIN(s)?`,
      description: (
        <>
          Each row keeps its current asset class, asset type, and PIB. Locked
          rows are the final classification on XML Detect and for Update
          transactions.
        </>
      ),
      confirmLabel: "Lock",
      confirmingLabel: "Locking…",
      run: () => executeBulkLock(ids, count),
    })
  }

  async function executeBulkLock(ids: string[], count: number) {
    setBulkBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      const res = await fetch(BULK_LOCK_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({ ids }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not bulk lock masters.")
      }
      setBulkMessage(json?.message || `Locked ${count} row(s).`)
      setRowSelection({})
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not bulk lock masters."))
      throw e
    } finally {
      setBulkBusy(false)
    }
  }

  function lockedPropagatableRows(source: IsinAssetMasterRow[]): IsinAssetMasterRow[] {
    return source.filter(
      (row) =>
        row.status === "locked" &&
        row.propagatable &&
        !row.skipped &&
        !excludedAc.has(row.aClass)
    )
  }

  async function postPropagateChunks(ids: string[]): Promise<string> {
    const messages: string[] = []
    for (let i = 0; i < ids.length; i += PROPAGATE_CHUNK) {
      const chunk = ids.slice(i, i + PROPAGATE_CHUNK)
      const batch = Math.floor(i / PROPAGATE_CHUNK) + 1
      const batches = Math.ceil(ids.length / PROPAGATE_CHUNK)
      setBulkMessage(
        `Propagating locked ISINs… batch ${batch}/${batches} (${chunk.length} of ${ids.length})`
      )
      const res = await fetch(BULK_PROPAGATE_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({ ids: chunk }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not update transactions.")
      }
      if (typeof json?.message === "string" && json.message !== "") {
        messages.push(json.message)
      }
    }
    return messages[messages.length - 1] ?? `Updated transactions for ${ids.length} locked ISIN(s).`
  }

  async function applyBulkPropagate() {
    if (selectedCount === 0 || bulkBusy) return

    const selectedRows = data.filter((row) => rowSelection[row.id])
    const eligible = lockedPropagatableRows(selectedRows)
    const eligibleIds = eligible.map((row) => row.id)
    const skippedCount = selectedCount - eligibleIds.length

    if (eligibleIds.length === 0) {
      setBulkError(
        "No propagatable rows selected. Structure & others are Skipped — assign PIB only (Update transactions is disabled)."
      )
      return
    }

    const sample = eligible[0]
    const acLabel = `${sample.aClassLabel || sample.aClass || "—"} (${sample.aClass || "—"})`
    setConfirmDialog({
      title: `Update transactions for ${eligibleIds.length} locked ISIN(s)?`,
      description: (
        <>
          Locked classification is applied to ledger rows across all portfolios.
          Sample: {acLabel}, {sample.aType || "—"}, PIB {sample.pibClass || "—"}.
          {skippedCount > 0 ? (
            <> {skippedCount} selected Structure/Accu/Option/unlocked row(s) stay PIB-only.</>
          ) : (
            <> Structure and others stay skipped.</>
          )}
        </>
      ),
      confirmLabel: "Update transactions",
      confirmingLabel: "Updating…",
      run: () => executeBulkPropagate(eligibleIds, skippedCount),
    })
  }

  async function executeBulkPropagate(eligibleIds: string[], skippedCount: number) {
    setBulkBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      const last = await postPropagateChunks(eligibleIds)
      const skipNote =
        skippedCount > 0 ? ` Skipped ${skippedCount} Structure/others (PIB-only).` : ""
      setBulkMessage(last + skipNote)
      setRowSelection({})
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not update transactions."))
      throw e
    } finally {
      setBulkBusy(false)
    }
  }

  async function applyPropagateAllLocked() {
    if (bulkBusy) return
    const eligible = lockedPropagatableRows(lockRows)
    if (eligible.length === 0) {
      setBulkError(
        "No locked Base ISINs to propagate. Structure & others stay Skipped (PIB only)."
      )
      return
    }

    const count = eligible.length
    setConfirmDialog({
      title: `Propagate ${count} locked ISIN(s)?`,
      description:
        "Runs in the background. Progress updates live on this page. Structure, Accu, Option, and Cash stay skipped.",
      confirmLabel: "Propagate locked",
      confirmingLabel: "Starting…",
      run: () => executePropagateAllLocked(count),
    })
  }

  async function executePropagateAllLocked(count: number) {
    setBulkBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    setPropagateProgress({
      phase: "running",
      percent: 0,
      isins: count,
      schemas_total: 0,
      schemas_done: 0,
      current_schema: null,
      ledger_rows: 0,
      os_rows: 0,
      temp_rows: 0,
      line: "Starting CLI propagate-all-locked…",
    })
    try {
      const res = await fetch(PROPAGATE_ALL_LOCKED_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({}),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not start CLI propagate.")
      }
      const data = json?.data
      if (data && typeof data === "object") {
        const phase = String(data.phase ?? "running")
        setPropagateProgress({
          phase,
          percent: Number(data.percent ?? 0),
          isins: Number(data.isins ?? count),
          schemas_total: Number(data.schemas_total ?? 0),
          schemas_done: Number(data.schemas_done ?? 0),
          current_schema:
            typeof data.current_schema === "string" ? data.current_schema : null,
          ledger_rows: Number(data.ledger_rows ?? 0),
          os_rows: Number(data.os_rows ?? 0),
          temp_rows: Number(data.temp_rows ?? 0),
          line: typeof data.line === "string" ? data.line : String(json?.message || ""),
        })
        if (phase === "done" || phase === "error") {
          setBulkBusy(false)
          setBulkMessage(typeof data.line === "string" ? data.line : json?.message)
          router.refresh()
        }
      }
    } catch (e) {
      setPropagateProgress(null)
      setBulkBusy(false)
      setBulkError(errorMessageOr(e, "Could not start CLI propagate."))
      throw e
    }
  }



  async function applyBundledAcAtMapping(
    dryRun = false,
    opts: { direct?: boolean } = {}
  ) {
    if (mappingBusy || bulkBusy) return
    const direct = Boolean(opts.direct)
    if (dryRun) {
      await executeBundledAcAtMapping(true, direct)
      return
    }
    setConfirmDialog({
      title: direct ? "Direct lock from sheet?" : "Apply gated mapping?",
      description: direct
        ? "Sets asset class and type from the Direct Lock sheet and locks every ISIN on it. Does not update portfolios."
        : "Writes new asset class and type onto matching masters and locks them. Skips Interest/Dividend. Does not update portfolios — run Propagate locked after.",
      confirmLabel: direct ? "Map & lock" : "Apply mapping",
      confirmingLabel: "Applying…",
      run: () => executeBundledAcAtMapping(false, direct),
    })
  }

  async function executeBundledAcAtMapping(dryRun: boolean, direct: boolean) {
    setMappingBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      setBulkMessage(
        dryRun
          ? direct
            ? "Dry-running Direct Lock…"
            : "Dry-running mapping (Step 1)…"
          : direct
            ? "Applying Direct Lock (AC/AT + lock)…"
            : "Applying mapping (Step 1: AC/AT + lock)…"
      )
      const res = await fetch(APPLY_MAPPING_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          ...dashboardCsrfHeader(),
        },
        body: JSON.stringify({
          use_default: true,
          dry_run: dryRun,
          create_missing: true,
          propagate_transactions: false,
          direct,
          sheet: direct ? "Direct Lock" : undefined,
        }),
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not apply AC/AT mapping.")
      }
      let message = json?.message || "Mapping applied."
      if (!dryRun) {
        message += " Step 2: Locked tab → Propagate locked."
      }
      setBulkMessage(message)
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not apply AC/AT mapping."))
      throw e
    } finally {
      setMappingBusy(false)
    }
  }

  async function applyUploadedAcAtMapping(
    file: File,
    opts: { direct?: boolean } = {}
  ) {
    if (mappingBusy || bulkBusy) return
    const direct = Boolean(opts.direct)
    setConfirmDialog({
      title: direct ? `Direct lock from ${file.name}?` : `Apply mapping from ${file.name}?`,
      description: direct
        ? "Sets asset class and type and locks every ISIN on the sheet that has data. Does not update portfolios."
        : "Sets asset class and type and locks matching masters only. Does not update transactions.",
      confirmLabel: direct ? "Map & lock" : "Apply mapping",
      confirmingLabel: "Applying…",
      run: () => executeUploadedAcAtMapping(file, direct),
    })
  }

  async function executeUploadedAcAtMapping(file: File, direct: boolean) {
    setMappingBusy(true)
    setBulkError(null)
    setBulkMessage(null)
    try {
      setBulkMessage(
        direct
          ? "Applying Direct Lock (AC/AT + lock)…"
          : "Applying mapping (Step 1: AC/AT + lock)…"
      )
      const body = new FormData()
      body.append("file", file)
      body.append("create_missing", "1")
      body.append("propagate_transactions", "0")
      if (direct) {
        body.append("direct", "1")
      }
      const res = await fetch(APPLY_MAPPING_HREF, {
        method: "POST",
        headers: {
          Accept: "application/json",
          ...dashboardCsrfHeader(),
        },
        body,
      })
      const json = await res.json().catch(() => ({}))
      if (!res.ok || json?.status === "error") {
        throw new Error(json?.message || "Could not apply AC/AT mapping.")
      }
      setBulkMessage(
        (json?.message || "Mapping applied.") + " Step 2: Locked tab → Propagate locked."
      )
      router.refresh()
    } catch (e) {
      setBulkError(errorMessageOr(e, "Could not apply AC/AT mapping."))
      throw e
    } finally {
      setMappingBusy(false)
      if (mappingFileRef.current) mappingFileRef.current.value = ""
      if (directMappingFileRef.current) directMappingFileRef.current.value = ""
    }
  }

  const emptyMessage =
    data.length === 0
      ? "No master rows yet. Save Safra positions to seed votes."
      : lockTab === "locked" && lockedCount === 0
        ? "No locked masters."
        : lockTab === "unlocked" && unlockedCount === 0
          ? "No unlocked masters."
          : aClassFilter !== ALL_AC
            ? "No rows in this asset class for the current lock tab."
            : pibFilter === "unassigned"
              ? "No rows without PIB on this lock tab."
              : pibFilter === "assigned"
                ? "No rows with PIB on this lock tab."
                : "No rows for the current filters."

  const lockScopeLabel =
    lockTab === "locked" ? "locked" : lockTab === "unlocked" ? "unlocked" : "all"

  return (
    <div className="space-y-3">
      <div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
        <Tabs value={lockTab} onValueChange={onLockTabChange}>
          <TabsList>
            <TabsTrigger value="all">
              All ({formatPlainAmount(data.length)})
            </TabsTrigger>
            <TabsTrigger value="locked">
              Locked ({formatPlainAmount(lockedCount)})
            </TabsTrigger>
            <TabsTrigger value="unlocked">
              Unlocked ({formatPlainAmount(unlockedCount)})
            </TabsTrigger>
          </TabsList>
        </Tabs>
        <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 sm:justify-end">
          <Input
            className={cn(filterInputClass, "w-full max-w-sm")}
            placeholder="Search ISIN or name…"
            value={(table.getColumn("isin")?.getFilterValue() as string) ?? ""}
            onChange={(e) => {
              table.getColumn("isin")?.setFilterValue(e.target.value || undefined)
              table.setPageIndex(0)
            }}
          />
          <p className="text-xs text-muted-foreground whitespace-nowrap">
            {start}–{end} of {formatPlainAmount(filteredCount)}
            {activeGroup ? ` · ${activeGroup.label}` : ` · ${lockScopeLabel}`}
            {data.length < totalCount
              ? ` (loaded ${formatPlainAmount(data.length)}/${formatPlainAmount(totalCount)})`
              : ""}
          </p>
        </div>
      </div>

      <div className="flex flex-wrap items-center gap-1.5">
        {(
          [
            ["all", `PIB all (${formatPlainAmount(lockRows.length)})`],
            ["unassigned", `No PIB (${formatPlainAmount(unassignedCount)})`],
            ["assigned", `Has PIB (${formatPlainAmount(assignedCount)})`],
          ] as const
        ).map(([value, label]) => (
          <button
            key={value}
            type="button"
            onClick={() => onPibFilterChange(value)}
            className={cn(
              "rounded-full border px-2.5 py-0.5 text-[11px] transition-colors",
              pibFilter === value
                ? "border-primary bg-primary text-primary-foreground"
                : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
            )}
          >
            {label}
          </button>
        ))}
        <span className="mx-1 hidden h-3 w-px bg-border sm:inline-block" aria-hidden />
        {(
          [
            ["all", "Detect all"],
            ["flagged", `Flagged (${formatPlainAmount(flaggedCount)})`],
            ["clear", "Not flagged"],
          ] as const
        ).map(([value, label]) => (
          <button
            key={value}
            type="button"
            onClick={() => onFlagFilterChange(value)}
            className={cn(
              "rounded-full border px-2.5 py-0.5 text-[11px] transition-colors",
              flagFilter === value
                ? "border-rose-600 bg-rose-600 text-white"
                : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
            )}
          >
            {label}
          </button>
        ))}
        <span className="mx-1 hidden h-3 w-px bg-border sm:inline-block" aria-hidden />
        <button
          type="button"
          onClick={() => onAClassChange(ALL_AC)}
          className={cn(
            "rounded-full border px-2.5 py-0.5 text-[11px] transition-colors",
            aClassFilter === ALL_AC
              ? "border-primary bg-primary text-primary-foreground"
              : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
          )}
        >
          All asset classes ({formatPlainAmount(tabRows.length)})
        </button>
        {assetClassGroups.map((group) => (
          <button
            key={group.code}
            type="button"
            onClick={() => onAClassChange(group.code)}
            className={cn(
              "rounded-full border px-2.5 py-0.5 text-[11px] transition-colors",
              aClassFilter === group.code
                ? "border-primary bg-primary text-primary-foreground"
                : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
            )}
            title={
              group.code === "(empty)"
                ? "No asset class"
                : `${group.label} (${group.code})`
            }
          >
            {group.code === "(empty)"
              ? "Empty"
              : group.label || group.code}{" "}
            · {formatPlainAmount(group.count)}
          </button>
        ))}
      </div>

      <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/15 px-2.5 py-2">
        <span className="text-xs font-medium tabular-nums text-muted-foreground">
          {selectedCount > 0 ? `${selectedCount} selected` : "Select"}
        </span>
        <Button
          type="button"
          variant="outline"
          size="sm"
          className="h-7 text-xs"
          disabled={filteredCount === 0}
          onClick={() => {
            const next: RowSelectionState = {}
            for (const row of table.getFilteredRowModel().rows) {
              next[row.id] = true
            }
            setRowSelection(next)
          }}
        >
          Filtered ({filteredCount})
        </Button>
        <Button
          type="button"
          variant="ghost"
          size="sm"
          className="h-7 text-xs"
          disabled={selectedCount === 0}
          onClick={() => setRowSelection({})}
        >
          Clear
        </Button>

        <span className="mx-0.5 h-4 w-px bg-border" aria-hidden />

        {showUnlockedTools ? (
          <>
            <select
              className={cn(
                "h-7 max-w-[10rem] rounded border border-input bg-background px-1.5 text-xs",
                "disabled:cursor-not-allowed disabled:opacity-50"
              )}
              value={bulkAc}
              disabled={bulkBusy || selectedCount === 0}
              onChange={(e) => onBulkAcChange(e.target.value)}
              aria-label="Asset class"
            >
              <option value="">AC</option>
              {editableModuleOptions.map((opt) => (
                <option key={opt.code} value={opt.code}>
                  {opt.label} ({opt.code})
                </option>
              ))}
            </select>
            <select
              className={cn(
                "h-7 max-w-[11rem] rounded border border-input bg-background px-1.5 text-xs",
                "disabled:cursor-not-allowed disabled:opacity-50"
              )}
              value={bulkAt}
              disabled={bulkBusy || selectedCount === 0 || bulkAc === ""}
              onChange={(e) => {
                setBulkAt(e.target.value)
                setBulkMessage(null)
              }}
              aria-label="Asset type"
            >
              <option value="">AT</option>
              {bulkAtOptions.map((type) => (
                <option key={type} value={type}>
                  {type}
                </option>
              ))}
            </select>
            <Button
              type="button"
              size="sm"
              className="h-7 text-xs"
              disabled={bulkBusy || selectedCount === 0 || bulkAc.trim() === ""}
              onClick={() => void applyBulkAcAt()}
            >
              {bulkBusy ? "…" : "Apply AC/AT"}
            </Button>
            <Button
              type="button"
              variant="secondary"
              size="sm"
              className="h-7 text-xs"
              disabled={bulkBusy || selectedCount === 0}
              title="Lock selected with current AC/AT/PIB"
              onClick={() => void applyBulkLock()}
            >
              {bulkBusy ? "…" : "Lock"}
            </Button>
          </>
        ) : null}

        {showLockedTools ? (
          <>
            <Button
              type="button"
              size="sm"
              className="h-7 text-xs"
              disabled={bulkBusy || lockedEligibleCount === 0}
              title="Propagate every locked Base ISIN (no selection needed)"
              onClick={() => void applyPropagateAllLocked()}
            >
              {propagateWatching
                ? `${Math.max(0, Math.min(100, propagateProgress?.percent ?? 0))}%`
                : bulkBusy
                  ? "…"
                  : `Propagate locked (${lockedEligibleCount})`}
            </Button>
            <Button
              type="button"
              variant="secondary"
              size="sm"
              className="h-7 text-xs"
              disabled={bulkBusy || selectedCount === 0}
              title="Push selected locked master AC/AT to ledger + OS"
              onClick={() => void applyBulkPropagate()}
            >
              {bulkBusy ? "…" : "Update selected"}
            </Button>
          </>
        ) : null}

        <select
          className={cn(
            "h-7 max-w-[12rem] rounded border border-input bg-background px-1.5 text-xs",
            "disabled:cursor-not-allowed disabled:opacity-50"
          )}
          value={bulkPib}
          disabled={bulkBusy || selectedCount === 0}
          onChange={(e) => setBulkPib(e.target.value)}
          aria-label="PIB class"
        >
          <option value="">Clear PIB</option>
          {pibClassList.map((opt) => (
            <option key={opt.code} value={opt.code}>
              {formatPibOption(opt)}
            </option>
          ))}
        </select>
        <Button
          type="button"
          size="sm"
          variant="outline"
          className="h-7 text-xs"
          disabled={bulkBusy || selectedCount === 0}
          onClick={() => void applyBulkPib()}
        >
          {bulkBusy ? "…" : "Assign PIB"}
        </Button>
      </div>

      {showUnlockedTools ? (
        <Collapsible className="group">
          <CollapsibleTrigger className="flex w-full items-center gap-2 rounded-md border border-dashed px-2.5 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted/40 hover:text-foreground">
            <span className="font-medium">Sheet mapping</span>
            <span className="text-[11px] opacity-80">gated apply · direct lock · upload</span>
            <span className="ml-auto text-[11px] group-data-[state=open]:hidden">Show</span>
            <span className="ml-auto hidden text-[11px] group-data-[state=open]:inline">Hide</span>
          </CollapsibleTrigger>
          <CollapsibleContent className="mt-2 space-y-2">
            <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/10 px-2.5 py-2">
              <span className="text-xs font-medium">Gated mapping</span>
              <Button
                type="button"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                title="Apply sheet when a_class(DB) matches"
                onClick={() => void applyBundledAcAtMapping(false)}
              >
                {mappingBusy ? "…" : "Apply"}
              </Button>
              <Button
                type="button"
                variant="outline"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                onClick={() => void applyBundledAcAtMapping(true)}
              >
                Dry run
              </Button>
              <input
                ref={mappingFileRef}
                type="file"
                accept=".xlsx,.xls,.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                className="hidden"
                onChange={(e) => {
                  const file = e.target.files?.[0]
                  if (file) void applyUploadedAcAtMapping(file)
                }}
              />
              <Button
                type="button"
                variant="outline"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                onClick={() => mappingFileRef.current?.click()}
              >
                Upload…
              </Button>
            </div>
            <div className="flex flex-wrap items-center gap-2 rounded-md border border-amber-600/25 bg-amber-50/30 px-2.5 py-2">
              <span className="text-xs font-medium">Direct lock</span>
              <Button
                type="button"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                title="Set AC/AT + lock without a_class(DB) check"
                onClick={() => void applyBundledAcAtMapping(false, { direct: true })}
              >
                {mappingBusy ? "…" : "Map & lock"}
              </Button>
              <Button
                type="button"
                variant="outline"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                onClick={() => void applyBundledAcAtMapping(true, { direct: true })}
              >
                Dry run
              </Button>
              <input
                ref={directMappingFileRef}
                type="file"
                accept=".xlsx,.xls,.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                className="hidden"
                onChange={(e) => {
                  const file = e.target.files?.[0]
                  if (file) void applyUploadedAcAtMapping(file, { direct: true })
                }}
              />
              <Button
                type="button"
                variant="outline"
                size="sm"
                className="h-7 text-xs"
                disabled={mappingBusy || bulkBusy}
                onClick={() => directMappingFileRef.current?.click()}
              >
                Upload…
              </Button>
            </div>
          </CollapsibleContent>
        </Collapsible>
      ) : null}

      {propagateProgress && propagateProgress.phase !== "idle" ? (
        <div className="space-y-1.5 rounded-md border bg-muted/20 px-3 py-2">
          <div className="flex items-baseline justify-between gap-3">
            <p className="min-w-0 truncate text-sm text-foreground">
              {propagateProgress.phase === "running"
                ? "Propagating locked ISINs…"
                : propagateProgress.phase === "error"
                  ? "Propagate finished with errors."
                  : "Propagate finished."}
            </p>
            <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
              {Math.max(0, Math.min(100, propagateProgress.percent))}%
            </span>
          </div>
          <div className="h-1.5 overflow-hidden rounded-full bg-muted">
            <div
              className={cn(
                "h-full rounded-full transition-[width] duration-300",
                propagateProgress.phase === "error"
                  ? "bg-destructive"
                  : "bg-emerald-600"
              )}
              style={{
                width: `${Math.max(
                  propagateProgress.phase === "running" ? 2 : 0,
                  Math.min(100, propagateProgress.percent)
                )}%`,
              }}
            />
          </div>
          <p className="text-[11px] tabular-nums text-muted-foreground">
            {propagateProgress.schemas_total > 0
              ? `Portfolios ${propagateProgress.schemas_done}/${propagateProgress.schemas_total}`
              : "Portfolios —"}
            {` · ${formatPlainAmount(propagateProgress.isins)} ISIN(s)`}
          </p>
        </div>
      ) : bulkMessage ? (
        <p className="text-sm text-emerald-700">{bulkMessage}</p>
      ) : null}
      {bulkError ? (
        <p className="text-sm text-destructive">{bulkError}</p>
      ) : null}

      <div className="overflow-x-auto rounded-lg border bg-card">
        <Table>
          <TableHeader>
            <TableRow>
              {headers.map((header) => (
                <TableHead
                  key={header.id}
                  className={
                    header.column.id === "select"
                      ? "w-10 px-3"
                      : header.column.id === "isin" || header.column.id === "bankCount"
                        ? "whitespace-normal"
                        : undefined
                  }
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(header.column.columnDef.header, header.getContext())}
                </TableHead>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id} data-state={row.getIsSelected() ? "selected" : undefined}>
                  {row.getVisibleCells().map((cell) => (
                    <TableCell
                      key={cell.id}
                      className={
                        cell.column.id === "select"
                          ? "w-10 align-top px-3"
                          : cell.column.id === "isin" || cell.column.id === "bankCount"
                            ? "align-top whitespace-normal"
                            : "align-top"
                      }
                    >
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">
                  {emptyMessage}
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
      <XmlApiTablePagination table={table} pageSizeOptions={[10, 20, 50, 100, 500]} />
      <DeleteConfirmDialog
        open={Boolean(confirmDialog)}
        onOpenChange={(open) => {
          if (!open) setConfirmDialog(null)
        }}
        title={confirmDialog?.title ?? ""}
        description={confirmDialog?.description ?? ""}
        confirmLabel={confirmDialog?.confirmLabel}
        confirmingLabel={confirmDialog?.confirmingLabel}
        confirmVariant="default"
        onConfirm={async () => {
          if (confirmDialog) await confirmDialog.run()
        }}
      />
    </div>
  )
}
