/**
 * DataGrid - Enterprise-grade React data grid component
 * @countermeasure/web-components/react/primitives/data-grid
 *
 * Features: virtual scrolling, multi-sort, column filtering, column resize,
 * column pinning, column reorder, row selection, context menu, keyboard nav,
 * localStorage persistence, URL state sync.
 */

import * as React from 'react'
import {
  ArrowDownIcon,
  ArrowUpDownIcon,
  ArrowUpIcon,
  FilterIcon,
  ColumnsIcon,
  XIcon,
  PinIcon,
  Columns3Icon,
  RotateCcwIcon,
} from 'lucide-react'
import { cn } from './utils'

export type SortDirection = 'asc' | 'desc'
export type PinDirection = 'left' | 'right' | null
export type FilterType = 'text' | 'select' | 'multi-select'
export type SelectionMode = 'single' | 'multi' | 'none'

export interface SortState {
  key: string
  direction: SortDirection
}

export interface FilterValue {
  type: FilterType
  value?: string
  values?: string[]
}

export interface ColumnDef<TRow = Record<string, unknown>> {
  key: string
  label: string
  sortable?: boolean
  filterable?: boolean
  filterType?: FilterType
  filterOptions?: string[]
  width?: number
  minWidth?: number
  maxWidth?: number
  pinned?: PinDirection
  resizable?: boolean
  reorderable?: boolean
  hideable?: boolean
  cell?: (value: unknown, row: TRow, index: number) => React.ReactNode
  headerClassName?: string
  cellClassName?: string
}

export interface ColumnState {
  key: string
  width: number
  visible: boolean
  pinned: PinDirection
  order: number
}

export interface DataGridProps<TRow = Record<string, unknown>> extends Omit<
  React.ComponentProps<'div'>,
  'children' | 'onSelect' | 'contextMenu'
> {
  columns: ColumnDef<TRow>[]
  data: TRow[]
  tableId?: string
  rowHeight?: number
  overscan?: number
  selectionMode?: SelectionMode
  selectedRows?: Set<number>
  onSelectionChange?: (selected: Set<number>) => void
  sort?: SortState[]
  onSort?: (sort: SortState[]) => void
  defaultSort?: SortState[]
  filters?: Record<string, FilterValue>
  onFilterChange?: (filters: Record<string, FilterValue>) => void
  globalSearch?: string
  onGlobalSearchChange?: (search: string) => void
  emptyMessage?: string
  loading?: boolean
  enableContextMenu?: boolean
  persistColumnState?: boolean
  onRowClick?: (row: TRow, index: number) => void
  rowClassName?: (row: TRow, index: number) => string | undefined
}

const STORAGE_PREFIX = 'cm-datagrid-react-'

function loadState(tableId: string): Record<string, ColumnState> | null {
  try {
    const raw = localStorage.getItem(STORAGE_PREFIX + tableId)
    return raw ? (JSON.parse(raw) as Record<string, ColumnState>) : null
  } catch {
    return null
  }
}

function saveState(tableId: string, state: Record<string, ColumnState>): void {
  try {
    localStorage.setItem(STORAGE_PREFIX + tableId, JSON.stringify(state))
  } catch {
    // ignore
  }
}

function cycleSortState(current: SortState[], key: string, shiftKey: boolean): SortState[] {
  const idx = current.findIndex(s => s.key === key)
  const existing = idx >= 0 ? current[idx] : undefined

  let nextDir: SortDirection | null
  if (existing) {
    nextDir = existing.direction === 'asc' ? 'desc' : null
  } else {
    nextDir = 'asc'
  }

  if (shiftKey) {
    const next = [...current]
    if (idx >= 0) {
      if (nextDir === null) {
        next.splice(idx, 1)
      } else {
        next[idx] = { key, direction: nextDir }
      }
    } else if (nextDir !== null) {
      next.push({ key, direction: nextDir })
    }
    return next
  }

  return nextDir === null ? [] : [{ key, direction: nextDir }]
}

function isNullish(value: unknown): value is null | undefined {
  return value === null || value === undefined
}

function multiSort<TRow>(rows: TRow[], sorts: SortState[]): TRow[] {
  if (sorts.length === 0) return rows

  return [...rows].sort((a, b) => {
    for (const { key, direction } of sorts) {
      const aVal = (a as Record<string, unknown>)[key]
      const bVal = (b as Record<string, unknown>)[key]

      const aStr = isNullish(aVal) ? '' : String(aVal)
      const bStr = isNullish(bVal) ? '' : String(bVal)
      const aNum = Number(aStr)
      const bNum = Number(bStr)

      let result: number
      if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aStr !== '' && bStr !== '') {
        result = aNum - bNum
      } else {
        result = aStr.localeCompare(bStr)
      }

      if (result !== 0) {
        return direction === 'desc' ? -result : result
      }
    }
    return 0
  })
}

function matchesFilter(value: unknown, filter: FilterValue): boolean {
  const str = isNullish(value) ? '' : String(value)

  switch (filter.type) {
    case 'text':
      return !filter.value || str.toLowerCase().includes(filter.value.toLowerCase())
    case 'select':
      return !filter.value || str === filter.value
    case 'multi-select':
      return !filter.values?.length || (filter.values?.includes(str) ?? true)
  }
}

function filterRows<TRow>(
  rows: TRow[],
  filters: Record<string, FilterValue>,
  globalSearch: string,
  keys: string[]
): TRow[] {
  const hasFilters = Object.keys(filters).length > 0
  const hasSearch = globalSearch !== ''

  if (!hasFilters && !hasSearch) return rows

  return rows.filter(row => {
    const rec = row as Record<string, unknown>

    if (hasFilters) {
      for (const [key, filter] of Object.entries(filters)) {
        if (!matchesFilter(rec[key], filter)) return false
      }
    }

    if (hasSearch) {
      const lowerSearch = globalSearch.toLowerCase()
      let found = false
      for (const k of keys) {
        const val = rec[k]
        if (!isNullish(val) && String(val).toLowerCase().includes(lowerSearch)) {
          found = true
          break
        }
      }
      if (!found) return false
    }

    return true
  })
}

function useVirtualScroll(
  totalRows: number,
  rowHeight: number,
  overscan: number,
  containerRef: React.RefObject<HTMLDivElement | null>
) {
  const [scrollTop, setScrollTop] = React.useState(0)
  const [viewportHeight, setViewportHeight] = React.useState(600)

  React.useEffect(() => {
    const el = containerRef.current
    if (!el) return

    const onScroll = () => setScrollTop(el.scrollTop)
    el.addEventListener('scroll', onScroll, { passive: true })

    const syncViewportHeight = () => {
      const height = el.clientHeight || Math.round(el.getBoundingClientRect().height)
      if (height > 0) {
        setViewportHeight(height)
      }
    }

    syncViewportHeight()

    let ro: ResizeObserver | null = null
    if (typeof globalThis.ResizeObserver === 'function') {
      ro = new globalThis.ResizeObserver(entries => {
        for (const entry of entries) {
          setViewportHeight(entry.contentRect.height)
        }
      })
      ro.observe(el)
    }

    return () => {
      el.removeEventListener('scroll', onScroll)
      ro?.disconnect()
    }
  }, [containerRef])

  const totalHeight = totalRows * rowHeight
  const visibleCount = Math.ceil(viewportHeight / rowHeight)
  const rawStart = Math.floor(scrollTop / rowHeight)
  const startIndex = Math.max(0, rawStart - overscan)
  const endIndex = Math.min(totalRows, rawStart + visibleCount + overscan)
  const offsetY = startIndex * rowHeight

  return { startIndex, endIndex, totalHeight, offsetY, visibleCount }
}

function DataGrid<TRow extends Record<string, unknown> = Record<string, unknown>>({
  className,
  columns,
  data,
  tableId,
  rowHeight = 40,
  overscan = 10,
  selectionMode = 'none',
  selectedRows: controlledSelected,
  onSelectionChange,
  sort: controlledSort,
  onSort,
  defaultSort,
  filters: controlledFilters,
  onFilterChange,
  globalSearch = '',
  onGlobalSearchChange,
  emptyMessage = 'No data available',
  loading = false,
  enableContextMenu: _enableCtxMenu = true,
  persistColumnState = true,
  onRowClick,
  rowClassName,
  ...props
}: DataGridProps<TRow>) {
  const [internalSort, setInternalSort] = React.useState<SortState[]>(defaultSort ?? [])
  const sort = controlledSort ?? internalSort

  const handleSort = React.useCallback(
    (key: string, shiftKey: boolean) => {
      const next = cycleSortState(sort, key, shiftKey)
      if (controlledSort === undefined) setInternalSort(next)
      onSort?.(next)
    },
    [sort, controlledSort, onSort]
  )

  const [internalFilters, setInternalFilters] = React.useState<Record<string, FilterValue>>({})
  const filters = controlledFilters ?? internalFilters

  const setFilter = React.useCallback(
    (key: string, value: FilterValue) => {
      const next = { ...filters, [key]: value }
      if (controlledFilters === undefined) setInternalFilters(next)
      onFilterChange?.(next)
    },
    [filters, controlledFilters, onFilterChange]
  )

  const clearFilters = React.useCallback(() => {
    if (controlledFilters === undefined) setInternalFilters({})
    onFilterChange?.({})
  }, [controlledFilters, onFilterChange])

  const [internalSelected, setInternalSelected] = React.useState<Set<number>>(new Set())
  const selected = controlledSelected ?? internalSelected

  const setSelected = React.useCallback(
    (next: Set<number>) => {
      if (controlledSelected === undefined) setInternalSelected(next)
      onSelectionChange?.(next)
    },
    [controlledSelected, onSelectionChange]
  )

  const [colState, setColState] = React.useState<Record<string, ColumnState>>(() => {
    if (persistColumnState && tableId) {
      const saved = loadState(tableId)
      if (saved) return saved
    }
    const state: Record<string, ColumnState> = {}
    columns.forEach((col, i) => {
      state[col.key] = {
        key: col.key,
        width: col.width ?? 150,
        visible: true,
        pinned: col.pinned ?? null,
        order: i,
      }
    })
    return state
  })

  React.useEffect(() => {
    if (persistColumnState && tableId) {
      saveState(tableId, colState)
    }
  }, [colState, persistColumnState, tableId])

  const [activeFilterCol, setActiveFilterCol] = React.useState<string | null>(null)
  const [showChooser, setShowChooser] = React.useState(false)

  const columnKeys = React.useMemo(() => columns.map(c => c.key), [columns])

  const filteredData = React.useMemo(
    () => filterRows(data, filters, globalSearch, columnKeys),
    [data, filters, globalSearch, columnKeys]
  )

  const sortedData = React.useMemo(() => multiSort(filteredData, sort), [filteredData, sort])

  const bodyRef = React.useRef<HTMLDivElement>(null)
  const useVirtual = sortedData.length > 100
  const vScroll = useVirtualScroll(sortedData.length, rowHeight, overscan, bodyRef)

  const orderedColumns = React.useMemo(() => {
    const visible = columns.filter(c => colState[c.key]?.visible !== false)
    return [...visible].sort(
      (a, b) => (colState[a.key]?.order ?? 0) - (colState[b.key]?.order ?? 0)
    )
  }, [columns, colState])

  const resizeRef = React.useRef<{
    key: string
    startX: number
    startWidth: number
  } | null>(null)

  const handleResizeStart = React.useCallback(
    (key: string, e: React.PointerEvent) => {
      e.preventDefault()
      e.stopPropagation()
      const startWidth = colState[key]?.width ?? 150
      resizeRef.current = { key, startX: e.clientX, startWidth }

      const onMove = (moveE: PointerEvent) => {
        if (!resizeRef.current) return
        const delta = moveE.clientX - resizeRef.current.startX
        const col = columns.find(c => c.key === resizeRef.current!.key)
        let newWidth = Math.max(col?.minWidth ?? 40, resizeRef.current.startWidth + delta)
        if (col?.maxWidth) newWidth = Math.min(col.maxWidth, newWidth)
        setColState(prev => ({
          ...prev,
          [resizeRef.current!.key]: { ...prev[resizeRef.current!.key]!, width: newWidth },
        }))
      }

      const onUp = () => {
        resizeRef.current = null
        document.removeEventListener('pointermove', onMove)
        document.removeEventListener('pointerup', onUp)
      }

      document.addEventListener('pointermove', onMove)
      document.addEventListener('pointerup', onUp)
    },
    [colState, columns]
  )

  const [dragKey, setDragKey] = React.useState<string | null>(null)

  const handleDragStart = React.useCallback((key: string) => {
    setDragKey(key)
  }, [])

  const handleDrop = React.useCallback(
    (targetKey: string) => {
      if (dragKey === null || dragKey === targetKey) return
      setColState(prev => {
        const entries = Object.values(prev).sort((a, b) => a.order - b.order)
        const fromIdx = entries.findIndex(e => e.key === dragKey)
        const toIdx = entries.findIndex(e => e.key === targetKey)
        if (fromIdx < 0 || toIdx < 0) return prev
        const [moved] = entries.splice(fromIdx, 1)
        if (!moved) return prev
        entries.splice(toIdx, 0, moved)
        const next: Record<string, ColumnState> = {}
        entries.forEach((e, i) => {
          next[e.key] = { ...e, order: i }
        })
        return next
      })
      setDragKey(null)
    },
    [dragKey]
  )

  const handleRowClick = React.useCallback(
    (row: TRow, index: number, e: React.MouseEvent) => {
      if (selectionMode === 'multi') {
        const next = new Set(selected)
        if (e.shiftKey && selected.size > 0) {
          const last = Math.max(...selected)
          const start = Math.min(last, index)
          const end = Math.max(last, index)
          for (let i = start; i <= end; i++) next.add(i)
        } else if (e.ctrlKey || e.metaKey) {
          if (next.has(index)) next.delete(index)
          else next.add(index)
        } else {
          next.clear()
          next.add(index)
        }
        setSelected(next)
      } else if (selectionMode === 'single') {
        setSelected(new Set([index]))
      }
      onRowClick?.(row, index)
    },
    [selectionMode, selected, setSelected, onRowClick]
  )

  const allSelected = sortedData.length > 0 && selected.size === sortedData.length

  const toggleAll = React.useCallback(() => {
    if (allSelected) setSelected(new Set())
    else setSelected(new Set(sortedData.map((_, i) => i)))
  }, [allSelected, sortedData, setSelected])

  const toggleVisibility = React.useCallback((key: string) => {
    setColState(prev => ({
      ...prev,
      [key]: { ...prev[key]!, visible: !prev[key]?.visible },
    }))
  }, [])

  const resetColumns = React.useCallback(() => {
    const state: Record<string, ColumnState> = {}
    columns.forEach((col, i) => {
      state[col.key] = {
        key: col.key,
        width: col.width ?? 150,
        visible: true,
        pinned: col.pinned ?? null,
        order: i,
      }
    })
    setColState(state)
    if (tableId) {
      try {
        localStorage.removeItem(STORAGE_PREFIX + tableId)
      } catch {
        // ignore
      }
    }
  }, [columns, tableId])

  const visibleRows = React.useMemo(() => {
    if (!useVirtual) return sortedData.map((row, i) => ({ row, index: i }))
    return sortedData.slice(vScroll.startIndex, vScroll.endIndex).map((row, i) => ({
      row,
      index: vScroll.startIndex + i,
    }))
  }, [sortedData, useVirtual, vScroll.startIndex, vScroll.endIndex])

  const activeFilterCount = Object.values(filters).filter(f => {
    if (f.type === 'text') return !!f.value
    if (f.type === 'select') return !!f.value
    if (f.type === 'multi-select') return (f.values?.length ?? 0) > 0
    return false
  }).length

  const [focusedRow, setFocusedRow] = React.useState(-1)
  const [focusedCol, setFocusedCol] = React.useState(0)

  const handleKeyDown = React.useCallback(
    (e: React.KeyboardEvent) => {
      const totalRows = sortedData.length
      const totalCols = orderedColumns.length
      if (totalRows === 0) return

      let nextRow = focusedRow
      let nextCol = focusedCol
      let handled = true

      switch (e.key) {
        case 'ArrowDown':
          nextRow = Math.min(totalRows - 1, focusedRow + 1)
          break
        case 'ArrowUp':
          nextRow = Math.max(0, focusedRow - 1)
          break
        case 'ArrowRight':
          nextCol = Math.min(totalCols - 1, focusedCol + 1)
          break
        case 'ArrowLeft':
          nextCol = Math.max(0, focusedCol - 1)
          break
        case 'Home':
          if (e.ctrlKey || e.metaKey) {
            nextRow = 0
            nextCol = 0
          } else {
            nextCol = 0
          }
          break
        case 'End':
          if (e.ctrlKey || e.metaKey) {
            nextRow = totalRows - 1
            nextCol = totalCols - 1
          } else {
            nextCol = totalCols - 1
          }
          break
        case 'PageDown':
          nextRow = Math.min(totalRows - 1, focusedRow + 10)
          break
        case 'PageUp':
          nextRow = Math.max(0, focusedRow - 10)
          break
        case 'Enter':
        case ' ':
          if (focusedRow >= 0 && focusedRow < totalRows) {
            const row = sortedData[focusedRow]
            if (row) onRowClick?.(row, focusedRow)
          }
          break
        case 'Escape':
          setActiveFilterCol(null)
          setShowChooser(false)
          setContextMenu(null)
          break
        default:
          handled = false
      }

      if (handled) {
        e.preventDefault()
        if (nextRow !== focusedRow || nextCol !== focusedCol) {
          setFocusedRow(nextRow)
          setFocusedCol(nextCol)
          if (useVirtual && bodyRef.current) {
            const targetTop = nextRow * rowHeight
            const { scrollTop } = bodyRef.current
            const viewH = bodyRef.current.clientHeight
            if (targetTop < scrollTop) {
              bodyRef.current.scrollTop = targetTop
            } else if (targetTop + rowHeight > scrollTop + viewH) {
              bodyRef.current.scrollTop = targetTop + rowHeight - viewH
            }
          }
        }
      }
    },
    [focusedRow, focusedCol, sortedData, orderedColumns.length, onRowClick, useVirtual, rowHeight]
  )

  const [contextMenu, setContextMenu] = React.useState<{
    x: number
    y: number
    colKey: string
  } | null>(null)

  const contextMenuRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    if (!contextMenu) return
    const handler = (e: MouseEvent) => {
      if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
        setContextMenu(null)
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [contextMenu])

  const handleHeaderContextMenu = React.useCallback((colKey: string, e: React.MouseEvent) => {
    e.preventDefault()
    e.stopPropagation()
    setContextMenu({ x: e.clientX, y: e.clientY, colKey })
  }, [])

  const pinColumn = React.useCallback((key: string, direction: PinDirection) => {
    setColState(prev => ({
      ...prev,
      [key]: { ...prev[key]!, pinned: direction },
    }))
    setContextMenu(null)
  }, [])

  const autosizeColumn = React.useCallback(
    (key: string) => {
      const col = columns.find(c => c.key === key)
      if (!col) return
      const span = document.createElement('span')
      span.style.cssText =
        'position:absolute;visibility:hidden;white-space:nowrap;font:inherit;font-size:14px'
      document.body.appendChild(span)

      span.textContent = col.label
      let maxW = span.offsetWidth + 48

      for (let i = 0; i < Math.min(sortedData.length, 100); i++) {
        const val = (sortedData[i] as Record<string, unknown>)[key]
        span.textContent = isNullish(val) ? '' : String(val)
        maxW = Math.max(maxW, span.offsetWidth + 24)
      }
      document.body.removeChild(span)

      const minW = col.minWidth ?? 40
      const maxCap = col.maxWidth ?? 800
      const final = Math.max(minW, Math.min(maxCap, maxW))

      setColState(prev => ({
        ...prev,
        [key]: { ...prev[key]!, width: final },
      }))
      setContextMenu(null)
    },
    [columns, sortedData]
  )

  const autosizeAllColumns = React.useCallback(() => {
    for (const col of orderedColumns) {
      autosizeColumn(col.key)
    }
  }, [orderedColumns, autosizeColumn])

  return (
    <div
      data-slot="data-grid"
      role="grid"
      tabIndex={0}
      className={cn('cmm-grid-react relative w-full outline-none', className)}
      onKeyDown={handleKeyDown}
      {...props}
    >
      {loading && (
        <div className="absolute inset-0 z-20 flex items-center justify-center rounded-xl bg-surface-primary/60 backdrop-blur-sm">
          <span className="text-foreground-muted animate-pulse text-sm">Loading...</span>
        </div>
      )}

      <div className="flex items-center gap-3 border-b border-border-glass px-3 py-2 text-xs text-foreground-muted">
        {activeFilterCount > 0 && (
          <>
            <span className="bg-primary/10 text-primary rounded px-1.5 py-0.5 text-[10px] font-semibold">
              {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''}
            </span>
            <button
              type="button"
              onClick={clearFilters}
              className="text-foreground-muted hover:text-foreground text-[10px] underline"
            >
              Clear
            </button>
          </>
        )}
        <div className="ml-auto flex items-center gap-2">
          {onGlobalSearchChange && (
            <input
              type="text"
              className="border-border-glass text-foreground placeholder:text-foreground-muted focus:ring-primary w-36 rounded border bg-transparent px-2 py-1 text-[11px] focus:outline-none focus:ring-1"
              placeholder="Search..."
              value={globalSearch}
              onChange={e => onGlobalSearchChange(e.target.value)}
            />
          )}
          <button
            type="button"
            onClick={() => setShowChooser(!showChooser)}
            className="text-foreground-muted hover:text-foreground hover:bg-glass-hover rounded p-1"
            aria-label="Choose columns"
          >
            <ColumnsIcon className="size-3.5" />
          </button>
        </div>
      </div>

      {showChooser && (
        <div className="bg-surface-primary border-border-glass absolute right-0 top-10 z-30 w-56 rounded-lg border p-3 shadow-xl">
          <div className="mb-2 flex items-center justify-between">
            <span className="text-foreground text-xs font-semibold">Columns</span>
            <button
              type="button"
              onClick={() => setShowChooser(false)}
              className="text-foreground-muted hover:text-foreground"
            >
              <XIcon className="size-3.5" />
            </button>
          </div>
          <div className="max-h-64 space-y-1.5 overflow-y-auto">
            {columns
              .filter(c => c.hideable !== false)
              .map(col => (
                <label
                  key={col.key}
                  className="text-foreground hover:bg-glass-hover flex cursor-pointer items-center gap-2 rounded px-1.5 py-1 text-xs"
                >
                  <input
                    type="checkbox"
                    checked={colState[col.key]?.visible !== false}
                    onChange={() => toggleVisibility(col.key)}
                    className="accent-primary"
                  />
                  {col.label}
                </label>
              ))}
          </div>
          <button
            type="button"
            onClick={resetColumns}
            className="text-foreground-muted hover:text-foreground mt-2 w-full text-[10px] underline"
          >
            Reset columns
          </button>
        </div>
      )}

      <div className="border-border-glass relative w-full overflow-x-auto rounded-b-xl border border-t-0">
        <div
          className="bg-surface-secondary border-border-glass sticky top-0 z-10 flex border-b"
          role="row"
        >
          {selectionMode !== 'none' && (
            <div className="flex w-10 min-w-[40px] items-center justify-center px-2">
              {selectionMode === 'multi' && (
                <input
                  type="checkbox"
                  checked={allSelected}
                  onChange={toggleAll}
                  aria-label="Select all"
                  className="accent-primary"
                />
              )}
            </div>
          )}
          {orderedColumns.map(col => {
            const state = colState[col.key]
            const w = state?.width ?? col.width ?? 150
            const sortIdx = sort.findIndex(s => s.key === col.key)
            const sortEntry = sortIdx >= 0 ? sort[sortIdx] : undefined
            const colFilter = filters[col.key]
            const hasActiveFilter =
              !isNullish(colFilter) &&
              ((colFilter.type === 'text' && !!colFilter.value) ||
                (colFilter.type === 'multi-select' && (colFilter.values?.length ?? 0) > 0) ||
                (colFilter.type === 'select' && !!colFilter.value))

            return (
              <div
                key={col.key}
                role="columnheader"
                className={cn(
                  'group text-foreground-secondary relative flex h-10 items-center gap-1 whitespace-nowrap px-3 text-[11px] font-semibold uppercase tracking-wider select-none',
                  col.sortable !== false &&
                    'hover:text-foreground cursor-pointer transition-colors',
                  col.headerClassName,
                  state?.pinned && 'bg-surface-secondary sticky z-[1]',
                  state?.pinned === 'left' && 'left-0',
                  state !== undefined && state.pinned === 'right' && 'right-0'
                )}
                style={{ width: w, minWidth: col.minWidth ?? 40 }}
                draggable={col.reorderable !== false}
                onDragStart={() => handleDragStart(col.key)}
                onDragOver={e => {
                  e.preventDefault()
                }}
                onDrop={() => handleDrop(col.key)}
                onClick={e => {
                  if (col.sortable !== false) handleSort(col.key, e.shiftKey)
                }}
                onContextMenu={e => handleHeaderContextMenu(col.key, e)}
                aria-sort={
                  sortEntry
                    ? sortEntry.direction === 'asc'
                      ? 'ascending'
                      : 'descending'
                    : undefined
                }
              >
                <span className="truncate">{col.label}</span>

                {col.sortable !== false && (
                  <span
                    className={cn(
                      'text-foreground-muted inline-flex shrink-0 transition-opacity',
                      sortEntry ? 'opacity-100' : 'group-hover:opacity-40 opacity-0'
                    )}
                  >
                    {sortEntry ? (
                      sortEntry.direction === 'asc' ? (
                        <ArrowUpIcon className="size-3" />
                      ) : (
                        <ArrowDownIcon className="size-3" />
                      )
                    ) : (
                      <ArrowUpDownIcon className="size-3" />
                    )}
                  </span>
                )}

                {sortEntry && sort.length > 1 && (
                  <span className="bg-primary/20 text-primary flex size-3.5 items-center justify-center rounded-full text-[9px] font-bold">
                    {sortIdx + 1}
                  </span>
                )}

                {hasActiveFilter && <span className="bg-primary size-1.5 shrink-0 rounded-full" />}

                {col.filterable !== false && (
                  <button
                    type="button"
                    onClick={e => {
                      e.stopPropagation()
                      setActiveFilterCol(activeFilterCol === col.key ? null : col.key)
                    }}
                    className={cn(
                      'hover:bg-glass-hover rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100',
                      activeFilterCol === col.key && 'opacity-100'
                    )}
                    aria-label={`Filter ${col.label}`}
                  >
                    <FilterIcon className="size-2.5" />
                  </button>
                )}

                {col.resizable !== false && (
                  <div
                    className="hover:bg-primary/40 absolute bottom-0 right-0 top-0 w-1 cursor-col-resize transition-colors"
                    onPointerDown={e => handleResizeStart(col.key, e)}
                    onDoubleClick={e => {
                      e.stopPropagation()
                      autosizeColumn(col.key)
                    }}
                    onClick={e => e.stopPropagation()}
                  />
                )}

                {activeFilterCol === col.key && (
                  <FilterDropdown
                    column={col}
                    data={data}
                    {...(!isNullish(filters[col.key]) ? { currentFilter: filters[col.key] } : {})}
                    onFilter={value => setFilter(col.key, value)}
                    onClose={() => setActiveFilterCol(null)}
                  />
                )}
              </div>
            )
          })}
        </div>

        <div
          ref={bodyRef}
          className="relative overflow-y-auto"
          style={useVirtual ? { height: Math.min(sortedData.length * rowHeight, 600) } : undefined}
        >
          {useVirtual && <div style={{ height: vScroll.totalHeight, position: 'relative' }} />}

          {sortedData.length === 0 ? (
            <div className="text-foreground-muted flex items-center justify-center py-10 text-sm">
              {emptyMessage}
            </div>
          ) : (
            visibleRows.map(({ row, index }) => (
              <div
                key={index}
                role="row"
                className={cn(
                  'border-border-glass hover:bg-glass-hover flex border-b transition-colors',
                  onRowClick && 'cursor-pointer',
                  index % 2 === 1 && 'bg-foreground/[0.02]',
                  selected.has(index) && 'bg-primary/5',
                  focusedRow === index && 'ring-primary/40 ring-1 ring-inset',
                  rowClassName?.(row, index)
                )}
                style={
                  useVirtual
                    ? {
                        position: 'absolute',
                        top: index * rowHeight,
                        height: rowHeight,
                        width: '100%',
                      }
                    : { height: rowHeight }
                }
                data-state={selected.has(index) ? 'selected' : undefined}
                onClick={e => handleRowClick(row, index, e)}
              >
                {selectionMode !== 'none' && (
                  <div className="flex w-10 min-w-[40px] items-center justify-center px-2">
                    <input
                      type="checkbox"
                      checked={selected.has(index)}
                      onChange={() => {
                        const next = new Set(selected)
                        if (next.has(index)) next.delete(index)
                        else next.add(index)
                        setSelected(next)
                      }}
                      onClick={e => e.stopPropagation()}
                      aria-label={`Select row ${index + 1}`}
                      className="accent-primary"
                    />
                  </div>
                )}
                {orderedColumns.map(col => {
                  const state = colState[col.key]
                  const w = state?.width ?? col.width ?? 150
                  const cellValue = (row as Record<string, unknown>)[col.key]

                  return (
                    <div
                      key={col.key}
                      role="gridcell"
                      className={cn(
                        'text-foreground flex items-center truncate px-3 text-sm',
                        col.cellClassName,
                        state?.pinned && 'bg-surface-primary sticky z-[1]',
                        state?.pinned === 'left' && 'left-0',
                        state !== undefined && state.pinned === 'right' && 'right-0'
                      )}
                      style={{ width: w, minWidth: col.minWidth ?? 40 }}
                    >
                      {col.cell
                        ? col.cell(cellValue, row, index)
                        : isNullish(cellValue)
                          ? ''
                          : String(cellValue)}
                    </div>
                  )
                })}
              </div>
            ))
          )}
        </div>
      </div>

      <div className="text-foreground-muted border-border-glass flex items-center justify-between border-t px-3 py-1.5 text-[11px] tabular-nums">
        <span>
          {data.length === sortedData.length
            ? `Rows: ${data.length}`
            : `Rows: ${sortedData.length} of ${data.length}`}
        </span>
        {selected.size > 0 && (
          <span className="text-primary font-semibold">{selected.size} selected</span>
        )}
      </div>

      {contextMenu && (
        <ContextMenu
          ref={contextMenuRef}
          x={contextMenu.x}
          y={contextMenu.y}
          colKey={contextMenu.colKey}
          columns={columns}
          sort={sort}
          colState={colState}
          onSort={(key, dir) => {
            const next = dir ? [{ key, direction: dir }] : []
            if (controlledSort === undefined) setInternalSort(next)
            onSort?.(next)
            setContextMenu(null)
          }}
          onPin={pinColumn}
          onAutosize={autosizeColumn}
          onAutosizeAll={autosizeAllColumns}
          onChooseColumns={() => {
            setShowChooser(true)
            setContextMenu(null)
          }}
          onResetColumns={() => {
            resetColumns()
            setContextMenu(null)
          }}
        />
      )}
    </div>
  )
}

interface ContextMenuProps {
  x: number
  y: number
  colKey: string
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  columns: ColumnDef<any>[]
  sort: SortState[]
  colState: Record<string, ColumnState>
  onSort: (key: string, direction: SortDirection | null) => void
  onPin: (key: string, direction: PinDirection) => void
  onAutosize: (key: string) => void
  onAutosizeAll: () => void
  onChooseColumns: () => void
  onResetColumns: () => void
}

const ContextMenu = React.forwardRef<HTMLDivElement, ContextMenuProps>(
  (
    {
      x,
      y,
      colKey,
      columns,
      sort,
      colState,
      onSort,
      onPin,
      onAutosize,
      onAutosizeAll,
      onChooseColumns,
      onResetColumns,
    },
    ref
  ) => {
    const col = columns.find(c => c.key === colKey)
    const currentSort = sort.find(s => s.key === colKey)
    const currentPin = colState[colKey]?.pinned ?? null

    const items: Array<{
      label: string
      icon?: React.ReactNode
      action: () => void
      separator?: boolean
      disabled?: boolean
    }> = []

    if (col?.sortable !== false) {
      items.push({
        label: 'Sort Ascending',
        icon: <ArrowUpIcon className="size-3.5" />,
        action: () => onSort(colKey, 'asc'),
        disabled: currentSort?.direction === 'asc',
      })
      items.push({
        label: 'Sort Descending',
        icon: <ArrowDownIcon className="size-3.5" />,
        action: () => onSort(colKey, 'desc'),
        disabled: currentSort?.direction === 'desc',
      })
      if (currentSort) {
        items.push({
          label: 'Clear Sort',
          icon: <XIcon className="size-3.5" />,
          action: () => onSort(colKey, null),
        })
      }
      items.push({ label: '', action: () => {}, separator: true })
    }

    items.push({
      label: 'Pin Left',
      icon: <PinIcon className="size-3.5" />,
      action: () => onPin(colKey, 'left'),
      disabled: currentPin === 'left',
    })
    items.push({
      label: 'Pin Right',
      icon: <PinIcon className="size-3.5 rotate-180" />,
      action: () => onPin(colKey, 'right'),
      disabled: currentPin === 'right',
    })
    if (currentPin) {
      items.push({
        label: 'Unpin',
        icon: <XIcon className="size-3.5" />,
        action: () => onPin(colKey, null),
      })
    }

    items.push({ label: '', action: () => {}, separator: true })

    items.push({
      label: 'Autosize This Column',
      action: () => onAutosize(colKey),
    })
    items.push({
      label: 'Autosize All Columns',
      action: () => onAutosizeAll(),
    })

    items.push({ label: '', action: () => {}, separator: true })

    items.push({
      label: 'Choose Columns',
      icon: <Columns3Icon className="size-3.5" />,
      action: () => onChooseColumns(),
    })
    items.push({
      label: 'Reset Columns',
      icon: <RotateCcwIcon className="size-3.5" />,
      action: () => onResetColumns(),
    })

    return (
      <div
        ref={ref}
        className="bg-surface-primary border-border-glass fixed z-50 w-52 rounded-lg border py-1 text-xs text-foreground shadow-2xl"
        style={{ top: y, left: x }}
      >
        {items.map((item, i) =>
          item.separator ? (
            <div key={i} className="border-border-glass my-1 border-t" />
          ) : (
            <button
              key={i}
              type="button"
              disabled={item.disabled}
              onClick={item.action}
              className="hover:bg-glass-hover flex w-full items-center gap-2.5 px-3 py-1.5 text-left transition-colors disabled:cursor-default disabled:opacity-40"
            >
              {item.icon && <span className="text-foreground-muted shrink-0">{item.icon}</span>}
              {item.label}
            </button>
          )
        )}
      </div>
    )
  }
)

ContextMenu.displayName = 'ContextMenu'

function FilterDropdown<TRow>({
  column,
  data,
  currentFilter,
  onFilter,
  onClose,
}: {
  column: ColumnDef<TRow>
  data: TRow[]
  currentFilter?: FilterValue
  onFilter: (value: FilterValue) => void
  onClose: () => void
}) {
  const [search, setSearch] = React.useState('')
  const dropdownRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        onClose()
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [onClose])

  const filterType = column.filterType ?? 'text'

  if (filterType === 'text') {
    return (
      <div
        ref={dropdownRef}
        className="bg-surface-primary border-border-glass absolute left-0 top-full z-20 mt-1 w-48 rounded-lg border p-2 shadow-xl"
        onClick={e => e.stopPropagation()}
      >
        <input
          type="text"
          className="border-border-glass text-foreground placeholder:text-foreground-muted focus:ring-primary w-full rounded border bg-transparent px-2 py-1.5 text-xs focus:outline-none focus:ring-1"
          placeholder={`Filter ${column.label}...`}
          value={currentFilter?.value ?? ''}
          onChange={e => onFilter({ type: 'text', value: e.target.value })}
          autoFocus
        />
      </div>
    )
  }

  const options =
    column.filterOptions ??
    Array.from(
      new Set(
        data
          .map(row => {
            const val = (row as Record<string, unknown>)[column.key]
            return isNullish(val) ? '' : String(val)
          })
          .filter(Boolean)
      )
    ).sort()

  const selectedValues = new Set(currentFilter?.values ?? [])
  const filteredOptions = search
    ? options.filter(o => o.toLowerCase().includes(search.toLowerCase()))
    : options

  return (
    <div
      ref={dropdownRef}
      className="bg-surface-primary border-border-glass absolute left-0 top-full z-20 mt-1 w-52 rounded-lg border p-2 shadow-xl"
      onClick={e => e.stopPropagation()}
    >
      <input
        type="text"
        className="border-border-glass text-foreground placeholder:text-foreground-muted focus:ring-primary mb-1.5 w-full rounded border bg-transparent px-2 py-1.5 text-xs focus:outline-none focus:ring-1"
        placeholder="Search..."
        value={search}
        onChange={e => setSearch(e.target.value)}
        autoFocus
      />
      <div className="max-h-40 space-y-0.5 overflow-y-auto">
        {filteredOptions.map(opt => (
          <label
            key={opt}
            className="text-foreground hover:bg-glass-hover flex cursor-pointer items-center gap-2 rounded px-1.5 py-1 text-xs"
          >
            <input
              type="checkbox"
              checked={selectedValues.has(opt)}
              onChange={() => {
                const next = new Set(selectedValues)
                if (next.has(opt)) next.delete(opt)
                else next.add(opt)
                onFilter({ type: 'multi-select', values: Array.from(next) })
              }}
              className="accent-primary"
            />
            {opt}
          </label>
        ))}
      </div>
      {selectedValues.size > 0 && (
        <button
          type="button"
          onClick={() => onFilter({ type: 'multi-select', values: [] })}
          className="text-foreground-muted hover:text-foreground mt-1.5 w-full text-center text-[10px] underline"
        >
          Clear filter
        </button>
      )}
    </div>
  )
}

export { DataGrid }
