'use client';

import { useMemo, useState } from 'react';
import {
  flexRender,
  getCoreRowModel,
  useReactTable,
  type ColumnDef,
  type Row,
  type RowSelectionState,
  type VisibilityState,
} from '@tanstack/react-table';
import { ArrowDown, ArrowUp, ArrowUpDown, Loader2 } from 'lucide-react';

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';

export interface DataTablePagination {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

export interface BulkAction {
  label: string;
  icon?: React.ElementType;
  variant?: 'default' | 'destructive';
  onClick: (selectedIds: string[]) => void;
}

interface DataTableProps<TData> {
  columns: ColumnDef<TData, unknown>[];
  data: TData[];
  pagination?: DataTablePagination;
  onPaginationChange?: (page: number, pageSize: number) => void;
  sort?: { field: string; direction: 'asc' | 'desc' };
  onSortChange?: (field: string, direction: 'asc' | 'desc') => void;
  rowSelection?: RowSelectionState;
  onRowSelectionChange?: (selection: RowSelectionState) => void;
  bulkActions?: BulkAction[];
  emptyState?: React.ReactNode;
  isLoading?: boolean;
  getRowId?: (row: TData) => string;
  onRowClick?: (row: TData) => void;
  /**
   * Optional row class hook — return a string of Tailwind utilities
   * applied to the `<TableRow>`. Use for visual grouping (e.g. tinting
   * berths by mooring letter so the kanban-like grid reads at a glance).
   */
  getRowClassName?: (row: TData) => string | undefined;
  /**
   * Mobile card renderer. When provided, the table is hidden below `lg:`
   * and replaced with a vertical list of cards built from this callback.
   * The same TanStack `table` instance powers both views, so pagination,
   * sort, and selection stay in sync across the breakpoint.
   */
  cardRender?: (row: Row<TData>) => React.ReactNode;
  /**
   * Optional grouping key for the mobile card list. When set, consecutive
   * rows that share the same returned key are visually grouped under a
   * header showing the key. Rendered only on mobile (next to cardRender);
   * the desktop table is unaffected. Useful for berths-by-area,
   * documents-by-folder, etc. — pre-sort the data on the same key so
   * adjacent rows already share groups.
   */
  mobileGroupBy?: (row: TData) => string | null | undefined;
  /**
   * Per-column visibility map. Keys are column IDs, values mean
   * "currently visible". Columns absent from the map are visible by
   * default — newly-added columns surface for existing users without
   * needing a preferences migration.
   */
  columnVisibility?: VisibilityState;
}

export function DataTable<TData>({
  columns,
  data,
  pagination,
  onPaginationChange,
  sort,
  onSortChange,
  rowSelection: externalSelection,
  onRowSelectionChange,
  bulkActions,
  emptyState,
  isLoading,
  getRowId,
  onRowClick,
  getRowClassName,
  cardRender,
  mobileGroupBy,
  columnVisibility,
}: DataTableProps<TData>) {
  const [internalSelection, setInternalSelection] = useState<RowSelectionState>({});
  const rowSelectionState = externalSelection ?? internalSelection;
  const setRowSelection = onRowSelectionChange ?? setInternalSelection;

  // Memoize the assembled columns array. perf-test-auditor HIGH H2:
  // TanStack docs explicitly warn that rebuilding `columns` on every
  // render resets the table's internal state (sort, filter, sizing).
  // Re-derive only when the source columns or bulkActions presence
  // actually change.
  const hasBulkActions = Boolean(bulkActions && bulkActions.length > 0);
  const allColumns = useMemo<ColumnDef<TData, unknown>[]>(() => {
    const cols: ColumnDef<TData, unknown>[] = [];
    if (hasBulkActions) {
      cols.push({
        id: 'select',
        header: ({ table }) => (
          <Checkbox
            checked={
              table.getIsAllPageRowsSelected() ||
              (table.getIsSomePageRowsSelected() && 'indeterminate')
            }
            onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
            aria-label="Select all"
            className="translate-y-[2px]"
          />
        ),
        cell: ({ row }) => (
          <Checkbox
            checked={row.getIsSelected()}
            onCheckedChange={(value) => row.toggleSelected(!!value)}
            aria-label="Select row"
            className="translate-y-[2px]"
            onClick={(e) => e.stopPropagation()}
          />
        ),
        enableSorting: false,
        size: 40,
      });
    }
    cols.push(...columns);
    return cols;
  }, [columns, hasBulkActions]);

  const table = useReactTable({
    data,
    columns: allColumns,
    getCoreRowModel: getCoreRowModel(),
    manualPagination: true,
    manualSorting: true,
    rowCount: pagination?.total ?? data.length,
    state: {
      rowSelection: rowSelectionState,
      pagination: pagination
        ? { pageIndex: pagination.page - 1, pageSize: pagination.pageSize }
        : undefined,
      columnVisibility,
    },
    onRowSelectionChange: (updater) => {
      const newSelection = typeof updater === 'function' ? updater(rowSelectionState) : updater;
      setRowSelection(newSelection);
    },
    getRowId: getRowId as (row: TData, index: number) => string,
    enableRowSelection: !!bulkActions?.length,
  });

  const selectedIds = Object.keys(rowSelectionState).filter((k) => rowSelectionState[k]);

  function handleSort(columnId: string) {
    if (!onSortChange) return;
    if (sort?.field === columnId) {
      onSortChange(columnId, sort.direction === 'asc' ? 'desc' : 'asc');
    } else {
      onSortChange(columnId, 'asc');
    }
  }

  function getSortIcon(columnId: string) {
    if (sort?.field !== columnId) return <ArrowUpDown className="ml-1 h-3.5 w-3.5" />;
    return sort.direction === 'asc' ? (
      <ArrowUp className="ml-1 h-3.5 w-3.5" />
    ) : (
      <ArrowDown className="ml-1 h-3.5 w-3.5" />
    );
  }

  const rows = table.getRowModel().rows;

  return (
    <div className="space-y-2">
      <div className={cn('rounded-md border overflow-x-auto', cardRender && 'hidden lg:block')}>
        <Table>
          <TableHeader className="sticky top-0 z-10 bg-muted/50">
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead
                    key={header.id}
                    style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
                    className={cn(
                      header.column.getCanSort() && onSortChange && 'cursor-pointer select-none',
                    )}
                    onClick={() => {
                      if (
                        header.column.getCanSort() &&
                        onSortChange &&
                        header.column.id !== 'select'
                      ) {
                        handleSort(header.column.id);
                      }
                    }}
                  >
                    <div className="flex items-center">
                      {header.isPlaceholder
                        ? null
                        : flexRender(header.column.columnDef.header, header.getContext())}
                      {header.column.getCanSort() &&
                        onSortChange &&
                        header.column.id !== 'select' &&
                        header.column.id !== 'actions' &&
                        getSortIcon(header.column.id)}
                    </div>
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {isLoading ? (
              <TableRow>
                <TableCell colSpan={allColumns.length} className="h-40 text-center">
                  <div className="flex items-center justify-center gap-2">
                    <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
                    <span className="text-muted-foreground">Loading...</span>
                  </div>
                </TableCell>
              </TableRow>
            ) : table.getRowModel().rows.length === 0 ? (
              <TableRow>
                <TableCell colSpan={allColumns.length} className="h-40">
                  {emptyState ?? (
                    <div className="text-center text-muted-foreground">No results.</div>
                  )}
                </TableCell>
              </TableRow>
            ) : (
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  data-state={row.getIsSelected() && 'selected'}
                  className={cn(onRowClick && 'cursor-pointer', getRowClassName?.(row.original))}
                  onClick={() => onRowClick?.(row.original)}
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </div>

      {/* Mobile card list */}
      {cardRender && (
        <ul className="lg:hidden flex flex-col gap-2">
          {isLoading ? (
            <li className="rounded-md border bg-card p-6 text-center">
              <Loader2 className="mx-auto h-5 w-5 animate-spin text-muted-foreground" />
            </li>
          ) : rows.length === 0 ? (
            <li className="rounded-md border bg-card p-6 text-center text-sm text-muted-foreground">
              {emptyState ?? 'No results.'}
            </li>
          ) : (
            (() => {
              // Walk rows once, emitting a section header <li> every time
              // the groupBy key changes. Keeps the existing flex-col gap-2
              // rhythm; the header sits above the first card of each group
              // with a faint top divider for visual rest between blocks.
              let lastGroup: string | null | undefined;
              const nodes: React.ReactNode[] = [];
              rows.forEach((row, i) => {
                const group = mobileGroupBy ? mobileGroupBy(row.original) : undefined;
                if (mobileGroupBy && group !== lastGroup) {
                  nodes.push(
                    <li key={`__group_${group ?? '_none'}_${i}`} className="px-1 pt-3">
                      <div className="flex items-center gap-3 text-base font-bold tracking-tight text-foreground">
                        <span>{group ?? 'Other'}</span>
                        <span aria-hidden className="h-px flex-1 bg-border" />
                      </div>
                    </li>,
                  );
                  lastGroup = group;
                }
                nodes.push(<li key={row.id}>{cardRender(row)}</li>);
              });
              return nodes;
            })()
          )}
        </ul>
      )}

      {/* Pagination — render whenever pagination is defined so the
          page-size selector is reachable even on single-page tables.
          Prev/Next group only renders when there's actually more than
          one page. */}
      {pagination && (
        <div className="flex items-center justify-between px-2 gap-3 flex-wrap">
          <div className="text-sm text-muted-foreground">
            {selectedIds.length > 0
              ? `${selectedIds.length} of ${pagination.total} row(s) selected`
              : `${pagination.total} row(s) total`}
          </div>
          <div className="flex items-center gap-3">
            {/* Page-size selector — "All" maps to the validator's
                max(1000) cap. If a port has more than 1000 active rows
                the user paginates; we don't quietly drop rows. */}
            <label className="text-sm text-muted-foreground inline-flex items-center gap-1.5">
              Show
              <select
                value={String(pagination.pageSize)}
                onChange={(e) => {
                  const next = e.target.value === 'all' ? 1000 : Number(e.target.value);
                  onPaginationChange?.(1, next);
                }}
                className="h-8 rounded-md border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
              >
                <option value="25">25</option>
                <option value="50">50</option>
                <option value="100">100</option>
                <option value="250">250</option>
                <option value="all">All</option>
              </select>
            </label>
            {pagination.totalPages > 1 && (
              <div className="flex items-center gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  disabled={pagination.page <= 1}
                  onClick={() => onPaginationChange?.(pagination.page - 1, pagination.pageSize)}
                >
                  Previous
                </Button>
                <span className="text-sm text-muted-foreground">
                  Page {pagination.page} of {pagination.totalPages}
                </span>
                <Button
                  variant="outline"
                  size="sm"
                  disabled={pagination.page >= pagination.totalPages}
                  onClick={() => onPaginationChange?.(pagination.page + 1, pagination.pageSize)}
                >
                  Next
                </Button>
              </div>
            )}
          </div>
        </div>
      )}

      {/* Bulk actions bar */}
      {bulkActions && selectedIds.length > 0 && (
        <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-lg border bg-background px-4 py-3 shadow-lg animate-in slide-in-from-bottom-4">
          <span className="text-sm font-medium">{selectedIds.length} selected</span>
          {bulkActions.map((action) => (
            <Button
              key={action.label}
              variant={action.variant ?? 'default'}
              size="sm"
              onClick={() => action.onClick(selectedIds)}
            >
              {action.icon && <action.icon className="mr-1.5 h-4 w-4" />}
              {action.label}
            </Button>
          ))}
        </div>
      )}
    </div>
  );
}
