import { useCallback, useMemo } from 'react'
import {
  Box,
  Checkbox,
  IconButton,
  Table as MuiTable,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TablePagination,
  TableRow as MuiTableRow,
  TableSortLabel,
  type TableProps as MuiTableProps,
} from '@mui/material'
import type { TablePaginationActionsProps } from '@mui/material/TablePagination/TablePaginationActions'
import { FirstPage as FirstPageIcon } from '@mui/icons-material'
import { KeyboardArrowLeft } from '@mui/icons-material'
import { KeyboardArrowRight } from '@mui/icons-material'
import { LastPage as LastPageIcon } from '@mui/icons-material'
import { DEFAULT_TABLE_LABELS, type TableLabels } from './labels'
import {
  DEFAULT_TABLE_PAGE_SIZE_OPTIONS,
  type TableColumn,
  type TableRow,
  type TableSortDirection,
  type TableSortState,
} from './types'
import { styles } from './style'

export interface TableUIProps<T extends TableRow = TableRow> {
  columns: readonly TableColumn[]
  /** Already-paginated, already-sorted rows for the current view. */
  rows: readonly T[]
  /** Total row count (across all pages). Required for pagination footer. */
  total: number
  page: number
  pageSize: number
  pageSizeOptions?: readonly number[]
  sort?: TableSortState
  /**
   * Column name to use as the row identity. Drives selection lookup,
   * React keys, and aria labels. Defaults to `'id'` — point it at
   * another column when your rows don't carry an `id` field.
   */
  keyColumn?: string
  /** Selected row ids. Destination-owned. */
  selection?: readonly (string | number)[]
  selectable?: boolean
  onSortChange?: (next: TableSortState) => void
  onPageChange?: (page: number) => void
  onPageSizeChange?: (pageSize: number) => void
  onSelectionChange?: (next: readonly (string | number)[]) => void
  onRowClick?: (row: T) => void
  onRowHover?: (row: T | null) => void
  labels?: Partial<TableLabels>
  /** Row rendered when `rows` is empty for the current page. */
  emptyContent?: React.ReactNode
  /**
   * Forwarded to MUI's `<Table size>` — `'small'` for compact rows,
   * `'medium'` for the default density. Leave `undefined` (the default)
   * to let MUI's own default kick in.
   */
  size?: MuiTableProps['size']
}

/**
 * Pure renderer for a paginated, sortable, optionally-selectable table.
 * Has no widget-store coupling — `<Table>` (the bridge) reads from the store
 * and feeds this UI with already-projected data.
 */
export function TableUI<T extends TableRow = TableRow>({
  columns,
  rows,
  total,
  page,
  pageSize,
  pageSizeOptions = DEFAULT_TABLE_PAGE_SIZE_OPTIONS,
  sort,
  keyColumn = 'id',
  selection,
  selectable = false,
  onSortChange,
  onPageChange,
  onPageSizeChange,
  onSelectionChange,
  onRowClick,
  onRowHover,
  labels,
  emptyContent,
  size,
}: TableUIProps<T>) {
  if (process.env.NODE_ENV !== 'production') {
    // Dev-time guard: a nullish identity collapses every row into the
    // same selection-set entry, which manifests as "click one → all
    // appear selected". Surface it loudly instead of silently degrading.
    const missing = rows.some((r) => r[keyColumn] == null)
    if (missing) {
      // eslint-disable-next-line no-console
      console.error(
        `<TableUI>: rows are missing the identity column \`${keyColumn}\`. ` +
          'Set the `keyColumn` prop to a column present on every row, or ' +
          'add the column to the data.',
      )
    }
  }
  const _labels = useMemo(
    () => ({ ...DEFAULT_TABLE_LABELS, ...labels }),
    [labels],
  )
  // Auto-hide the pagination footer when every row already fits in the
  // smallest available page size — the Rows-per-page selector + prev /
  // next buttons would just be inert noise. Consumers force the footer
  // on by passing a smaller `pageSizeOptions[0]` than `total`.
  const minPageSize = Math.min(...pageSizeOptions)
  const showPagination = total > minPageSize
  // Own the `ActionsComponent` slot ourselves so the Meridian theme's
  // `theme.components.MuiTablePagination.defaultProps.ActionsComponent`
  // (which depends on `react-intl`) doesn't leak into the library. The
  // MUI / Meridian style overrides for `MuiIconButton` /
  // `MuiTablePagination` still apply.
  const PaginationActions = useMemo(
    () => makePaginationActions(_labels),
    [_labels],
  )
  const selectionSet = useMemo(
    () => new Set<string | number>(selection ?? []),
    [selection],
  )
  // Resolve each row's identity from `keyColumn`, falling back to the row's
  // index when the cell is nullish. The dev-time guard above surfaces the
  // misconfiguration loudly, but in production we must NOT collapse every
  // nullish-keyed row into the same selection-set entry (that's the
  // "click one → all selected" bug) — a per-index fallback keeps ids
  // distinct and React keys stable for a given page.
  const resolveRowId = useCallback(
    (row: T, index: number): string | number =>
      (row[keyColumn] ?? index) as string | number,
    [keyColumn],
  )
  const pageRowIds = useMemo(() => rows.map(resolveRowId), [rows, resolveRowId])
  const allOnPageSelected =
    pageRowIds.length > 0 && pageRowIds.every((id) => selectionSet.has(id))
  const someOnPageSelected =
    !allOnPageSelected && pageRowIds.some((id) => selectionSet.has(id))

  const handleSort = (columnId: string) => {
    if (!onSortChange) return
    const sameCol = sort?.columnId === columnId
    const nextDir: TableSortDirection =
      sameCol && sort?.direction === 'asc' ? 'desc' : 'asc'
    onSortChange({ columnId, direction: nextDir })
  }

  const handleSelectAllOnPage = () => {
    if (!onSelectionChange) return
    if (allOnPageSelected) {
      onSelectionChange(
        (selection ?? []).filter((id) => !pageRowIds.includes(id)),
      )
    } else {
      const merged = new Set<string | number>(selection ?? [])
      for (const id of pageRowIds) merged.add(id)
      onSelectionChange([...merged])
    }
  }

  const handleSelectRow = (rowId: string | number) => {
    if (!onSelectionChange) return
    const next = new Set<string | number>(selection ?? [])
    if (next.has(rowId)) next.delete(rowId)
    else next.add(rowId)
    onSelectionChange([...next])
  }

  return (
    <Box sx={styles.container}>
      <TableContainer>
        <MuiTable stickyHeader size={size} sx={styles.table}>
          <TableHead>
            <MuiTableRow>
              {selectable ? (
                <TableCell padding='checkbox' sx={styles.headerCell}>
                  <Checkbox
                    checked={allOnPageSelected}
                    indeterminate={someOnPageSelected}
                    onChange={handleSelectAllOnPage}
                    inputProps={{ 'aria-label': _labels.selectAll }}
                  />
                </TableCell>
              ) : null}
              {columns.map((column) => {
                const isSorted = sort?.columnId === column.id
                const direction = isSorted ? sort?.direction : undefined
                return (
                  <TableCell
                    key={column.id}
                    align={column.align}
                    sortDirection={direction}
                    sx={styles.headerCell}
                    style={
                      column.width != null ? { width: column.width } : undefined
                    }
                  >
                    {column.sortable && onSortChange ? (
                      <TableSortLabel
                        active={isSorted}
                        direction={direction ?? 'asc'}
                        onClick={() => handleSort(column.id)}
                      >
                        {column.label}
                      </TableSortLabel>
                    ) : (
                      column.label
                    )}
                  </TableCell>
                )
              })}
            </MuiTableRow>
          </TableHead>
          <TableBody>
            {rows.length === 0 ? (
              <MuiTableRow>
                <TableCell
                  colSpan={columns.length + (selectable ? 1 : 0)}
                  sx={styles.empty}
                >
                  {emptyContent ?? null}
                </TableCell>
              </MuiTableRow>
            ) : (
              rows.map((row, index) => {
                const rowId = resolveRowId(row, index)
                const isSelected = selectionSet.has(rowId)
                return (
                  <MuiTableRow
                    key={rowId}
                    hover
                    selected={isSelected}
                    onClick={() => onRowClick?.(row)}
                    onMouseEnter={() => onRowHover?.(row)}
                    onMouseLeave={() => onRowHover?.(null)}
                    sx={{
                      ...styles.row,
                      ...(onRowClick ? styles.rowClickable : null),
                    }}
                  >
                    {selectable ? (
                      <TableCell padding='checkbox'>
                        <Checkbox
                          checked={isSelected}
                          onClick={(e) => {
                            e.stopPropagation()
                            handleSelectRow(rowId)
                          }}
                          inputProps={{
                            'aria-label': _labels.selectRow(rowId),
                          }}
                        />
                      </TableCell>
                    ) : null}
                    {columns.map((column) => (
                      <TableCell key={column.id} align={column.align}>
                        {column.formatter
                          ? column.formatter(row[column.id], row)
                          : stringifyCell(row[column.id])}
                      </TableCell>
                    ))}
                  </MuiTableRow>
                )
              })
            )}
          </TableBody>
        </MuiTable>
      </TableContainer>
      {showPagination ? (
        <TablePagination
          component='div'
          count={total}
          page={page}
          rowsPerPage={pageSize}
          rowsPerPageOptions={[...pageSizeOptions]}
          onPageChange={(_, next) => onPageChange?.(next)}
          onRowsPerPageChange={(e) => {
            const next = parseInt(e.target.value, 10)
            onPageSizeChange?.(next)
          }}
          labelRowsPerPage={_labels.rowsPerPage}
          labelDisplayedRows={({ from, to, count }) =>
            _labels.paginationOf(from, to, count)
          }
          ActionsComponent={PaginationActions}
          sx={styles.pagination}
        />
      ) : null}
    </Box>
  )
}

function stringifyCell(value: unknown): React.ReactNode {
  if (value == null) return ''
  if (typeof value === 'string' || typeof value === 'number') return value
  if (typeof value === 'boolean') return String(value)
  if (Array.isArray(value) || typeof value === 'object') {
    return JSON.stringify(value)
  }
  return ''
}

/**
 * Build the `ActionsComponent` used by `<TablePagination>` — four MUI
 * `IconButton`s for first / previous / next / last. We provide this
 * ourselves so the Meridian theme's `defaultProps.ActionsComponent`
 * (which depends on `react-intl`) is bypassed at the call site without
 * disturbing the rest of the Meridian theme. The factory closes over
 * the merged label set so aria-labels stay consistent with the rest
 * of the table's i18n surface.
 */
function makePaginationActions(labels: TableLabels) {
  return function PaginationActions({
    count,
    page,
    rowsPerPage,
    onPageChange,
  }: TablePaginationActionsProps) {
    const lastPage = Math.max(0, Math.ceil(count / rowsPerPage) - 1)
    return (
      <Box sx={styles.paginationActions}>
        <IconButton
          size='small'
          onClick={(e) => onPageChange(e, 0)}
          disabled={page === 0}
          aria-label={labels.firstPage}
        >
          <FirstPageIcon fontSize='small' />
        </IconButton>
        <IconButton
          size='small'
          onClick={(e) => onPageChange(e, page - 1)}
          disabled={page === 0}
          aria-label={labels.previousPage}
        >
          <KeyboardArrowLeft fontSize='small' />
        </IconButton>
        <IconButton
          size='small'
          onClick={(e) => onPageChange(e, page + 1)}
          disabled={page >= lastPage}
          aria-label={labels.nextPage}
        >
          <KeyboardArrowRight fontSize='small' />
        </IconButton>
        <IconButton
          size='small'
          onClick={(e) => onPageChange(e, lastPage)}
          disabled={page >= lastPage}
          aria-label={labels.lastPage}
        >
          <LastPageIcon fontSize='small' />
        </IconButton>
      </Box>
    )
  }
}
