/**
 * The sortable, paginated, column-choosable table.
 *
 * It is deliberately the *only* table primitive: pages that hand-roll a
 * `<table>` get none of this, and the divergence shows — Margin's own markup
 * cannot sort or paginate, and within Accounts the desktop empty state is a
 * bare row of text while the phone layout renders a full EmptyState for the
 * same condition. Loading, error and empty are therefore states this component
 * owns rather than states each adopting page invents, so the next page to move
 * across brings its query straight here.
 */
import { useState, type ReactNode } from 'react';
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
  type Column,
  type ColumnDef,
  type ColumnFiltersState,
  type SortingState,
  type VisibilityState,
} from '@tanstack/react-table';
import {
  ArrowDown,
  ArrowUp,
  ArrowUpDown,
  ChevronLeft,
  ChevronRight,
  RefreshCw,
  SlidersHorizontal,
  TriangleAlert,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { EmptyState, Input, Skeleton } from '@/components/ui';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
  /** One line of text for the empty case. `empty` supersedes it when given. */
  emptyMessage?: string;
  /**
   * The empty state in full — an `EmptyState` with an icon and, where there is
   * one, the action that would fill the table. Prefer it to `emptyMessage`:
   * a table that is empty because nobody has created a record yet should say
   * so and offer the way out, not print "No results." at someone.
   */
  empty?: ReactNode;
  /**
   * The first load only — react-query's `isLoading`, never `isFetching`.
   * Blanking populated rows into skeletons on every background refetch is how
   * a table flickers under the reader's cursor.
   */
  loading?: boolean;
  error?: Error | null;
  /** Wired to a retry button on the error state; omitted means no button. */
  onRetry?: () => void;
  filterColumn?: string;
  filterPlaceholder?: string;
  initialColumnVisibility?: VisibilityState;
}

export function DataTable<TData, TValue>({
  columns,
  data,
  emptyMessage = 'No results.',
  empty,
  loading = false,
  error = null,
  onRetry,
  filterColumn,
  filterPlaceholder = 'Filter results',
  initialColumnVisibility = {},
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
    initialColumnVisibility,
  );

  const table = useReactTable({
    data,
    columns,
    state: { sorting, columnFilters, columnVisibility },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
  });
  const activeFilter = filterColumn ? table.getColumn(filterColumn) : undefined;
  const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide());
  const rows = table.getRowModel().rows;
  const columnCount = Math.max(table.getVisibleLeafColumns().length, 1);
  // Precedence matters: an error that arrives mid-load must not be reported as
  // "no results", which is a true statement and a false explanation.
  const state = error ? 'error' : loading ? 'loading' : rows.length ? 'rows' : 'empty';
  // Only when there is no dataset at all. Disabling the filter whenever the
  // table looks empty would trap the reader inside a search that matched
  // nothing, with no way to clear it.
  const controlsDisabled = state === 'loading' || state === 'error';
  const filterValue = (activeFilter?.getFilterValue() as string | undefined) ?? '';

  return (
    <div className="flex flex-col gap-3">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        {activeFilter ? (
          <Input
            type="search"
            value={filterValue}
            onChange={(event) => activeFilter.setFilterValue(event.target.value)}
            placeholder={filterPlaceholder}
            aria-label={filterPlaceholder}
            disabled={controlsDisabled}
            className="sm:max-w-xs"
          />
        ) : (
          <span />
        )}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="outline" className="tap sm:ml-auto" disabled={controlsDisabled}>
              <SlidersHorizontal data-icon="inline-start" aria-hidden />
              Columns
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>Visible columns</DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuGroup>
              {hideableColumns.map((column) => (
                <DropdownMenuCheckboxItem
                  key={column.id}
                  checked={column.getIsVisible()}
                  onCheckedChange={(visible) => column.toggleVisibility(Boolean(visible))}
                >
                  {columnLabel(column.id)}
                </DropdownMenuCheckboxItem>
              ))}
            </DropdownMenuGroup>
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      <div className="scroll-x rounded-xl border border-border bg-surface">
        <Table className="min-w-[44rem]">
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(header.column.columnDef.header, header.getContext())}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {state === 'rows'
              ? rows.map((row) => (
                  <TableRow key={row.id} data-state={row.getIsSelected() ? 'selected' : undefined}>
                    {row.getVisibleCells().map((cell) => (
                      <TableCell key={cell.id}>
                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
                      </TableCell>
                    ))}
                  </TableRow>
                ))
              : null}

            {/* Skeletons in the real grid, not one bar over the whole table:
                the column widths the reader is about to get are part of the
                answer, and settling into them costs nothing to show. */}
            {state === 'loading'
              ? Array.from({ length: SKELETON_ROWS }, (_, index) => (
                  <TableRow key={`skeleton-${index}`} aria-hidden>
                    {Array.from({ length: columnCount }, (_, cell) => (
                      <TableCell key={cell}>
                        <Skeleton className="h-4 w-full max-w-[12rem]" />
                      </TableCell>
                    ))}
                  </TableRow>
                ))
              : null}

            {state === 'error' ? (
              <MessageRow colSpan={columnCount}>
                <EmptyState
                  icon={<TriangleAlert aria-hidden />}
                  title="Results unavailable"
                  description={error?.message}
                  action={
                    onRetry ? (
                      <Button variant="outline" className="tap" onClick={onRetry}>
                        <RefreshCw data-icon="inline-start" aria-hidden />
                        Try again
                      </Button>
                    ) : undefined
                  }
                />
              </MessageRow>
            ) : null}

            {state === 'empty' ? (
              <MessageRow colSpan={columnCount}>
                {/* A filter that matched nothing is not an empty table, and
                    telling someone to create their first record when they have
                    simply mistyped a search is how a product loses trust. */}
                {data.length > 0 ? (
                  <EmptyState
                    title="No results match"
                    description="Nothing here matches the current filter."
                    action={
                      filterValue ? (
                        <Button
                          variant="outline"
                          className="tap"
                          onClick={() => activeFilter?.setFilterValue('')}
                        >
                          Clear filter
                        </Button>
                      ) : undefined
                    }
                  />
                ) : (
                  (empty ?? <p className="py-10 text-sm text-muted">{emptyMessage}</p>)
                )}
              </MessageRow>
            ) : null}
          </TableBody>
        </Table>
      </div>

      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        {/* "0 results · Page 1 of 1" while a request is still in flight is a
            count of something nobody has counted yet. */}
        <p className="text-sm text-muted" aria-live="polite">
          {state === 'loading'
            ? 'Loading results…'
            : state === 'error'
              ? 'Results could not be loaded.'
              : `${table.getFilteredRowModel().rows.length} result${
                  table.getFilteredRowModel().rows.length === 1 ? '' : 's'
                } · Page ${table.getState().pagination.pageIndex + 1} of ${Math.max(
                  table.getPageCount(),
                  1,
                )}`}
        </p>
        <div className="flex items-center justify-between gap-2 sm:justify-end">
          <Select
            value={String(table.getState().pagination.pageSize)}
            onValueChange={(value) => table.setPageSize(Number(value))}
            disabled={controlsDisabled}
          >
            <SelectTrigger className="h-11 w-[7.5rem]" aria-label="Rows per page">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectGroup>
                {[10, 20, 50].map((pageSize) => (
                  <SelectItem key={pageSize} value={String(pageSize)}>
                    {pageSize} rows
                  </SelectItem>
                ))}
              </SelectGroup>
            </SelectContent>
          </Select>
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="tap"
            onClick={() => table.previousPage()}
            disabled={!table.getCanPreviousPage()}
            aria-label="Previous page"
          >
            <ChevronLeft aria-hidden />
          </Button>
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="tap"
            onClick={() => table.nextPage()}
            disabled={!table.getCanNextPage()}
            aria-label="Next page"
          >
            <ChevronRight aria-hidden />
          </Button>
        </div>
      </div>
    </div>
  );
}

export function DataTableColumnHeader<TData, TValue>({
  column,
  title,
}: {
  column: Column<TData, TValue>;
  title: string;
}) {
  if (!column.getCanSort()) return <span>{title}</span>;

  const direction = column.getIsSorted();
  const SortIcon = direction === 'asc' ? ArrowUp : direction === 'desc' ? ArrowDown : ArrowUpDown;

  return (
    <Button
      type="button"
      variant="ghost"
      size="sm"
      className="-ml-3 min-h-11"
      onClick={() => column.toggleSorting(direction === 'asc')}
      aria-label={`Sort by ${title}${direction ? `, currently ${direction}ending` : ''}`}
    >
      {title}
      <SortIcon data-icon="inline-end" aria-hidden />
    </Button>
  );
}

/** Enough to read as a table settling in, few enough not to imply a page size. */
const SKELETON_ROWS = 5;

/**
 * One cell spanning the grid, for the states that replace the rows.
 *
 * `h-40` rather than the rows' natural height so that loading, empty and error
 * occupy roughly the same space: a table that changes height as it resolves
 * pushes whatever sits beneath it around the screen.
 */
function MessageRow({ colSpan, children }: { colSpan: number; children: ReactNode }) {
  return (
    <TableRow className="hover:bg-transparent">
      <TableCell colSpan={colSpan} className="h-40 p-0 text-center align-middle">
        <div className="flex items-center justify-center">{children}</div>
      </TableCell>
    </TableRow>
  );
}

function columnLabel(value: string): string {
  return value
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/_/g, ' ')
    .replace(/^./, (character) => character.toUpperCase());
}
