import type { DragEndEvent } from '@dnd-kit/core';
import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import {
  SortableContext,
  useSortable,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { ColumnDef, ExpandedState, Row } from '@tanstack/react-table';
import {
  flexRender,
  getCoreRowModel,
  getExpandedRowModel,
  useReactTable,
} from '@tanstack/react-table';
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
  GripVertical,
} from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

export interface PaginationState {
  current_page: number;
  last_page: number;
  per_page: number;
  total: number;
}

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
  searchKey?: string;
  searchPlaceholder?: string;
  emptyText?: string;
  onReorder?: (items: TData[]) => void;
  getRowId?: (item: TData) => string | number;
  toolbar?: React.ReactNode;
  renderSubRow?: (row: Row<TData>, searchValue?: string) => React.ReactNode;
  defaultExpanded?: boolean;
  pagination?: PaginationState;
  onPageChange?: (page: number) => void;
  onPerPageChange?: (perPage: number) => void;
  onSearchChange?: (search: string) => void;
  searchValue?: string;
}

const DragHandleContext = React.createContext<{
  listeners?: Record<string, unknown>;
  attributes?: Record<string, string>;
}>({});

export function DragHandleTrigger() {
  const { listeners, attributes } = React.useContext(DragHandleContext);

  return (
    <Button
      variant="ghost"
      size="icon"
      className="-ml-2 cursor-grab active:cursor-grabbing"
      {...listeners}
      {...attributes}
    >
      <GripVertical className="h-4 w-4 text-muted-foreground" />
    </Button>
  );
}

function SortableTableRow({
  id,
  children,
}: {
  id: string;
  children: React.ReactNode;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.4 : undefined,
  };

  return (
    <DragHandleContext.Provider value={{ listeners, attributes }}>
      <TableRow
        ref={setNodeRef}
        style={style}
        data-state={isDragging ? 'dragging' : undefined}
      >
        {children}
      </TableRow>
    </DragHandleContext.Provider>
  );
}

function useDebounce(callback: (value: string) => void, delay: number) {
  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);

  return React.useCallback(
    (value: string) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callback(value);
      }, delay);
    },
    [callback, delay],
  );
}

export function DataTable<TData, TValue>({
  columns,
  data,
  searchKey,
  searchPlaceholder = 'Cari...',
  emptyText = 'Tidak ada data.',
  onReorder,
  getRowId,
  toolbar,
  renderSubRow,
  defaultExpanded = false,
  pagination,
  onPageChange,
  onPerPageChange,
  onSearchChange,
  searchValue,
}: DataTableProps<TData, TValue>) {
  const [expanded, setExpanded] = React.useState<ExpandedState>(() => {
    if (!defaultExpanded || !data.length) {
      return {};
    }

    const initial: Record<string, boolean> = {};
    data.forEach((item, index) => {
      initial[String(index)] = true;
    });

    return initial;
  });

  const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');

  React.useEffect(() => {
    setLocalSearch(searchValue ?? '');
  }, [searchValue]);

  const isServerMode = !!pagination && !!onPageChange;
  const isSortable = !!onReorder && !!getRowId;

  const handleSearchDebounced = useDebounce(
    (value: string) => onSearchChange?.(value),
    300,
  );

  const totalPages = pagination?.last_page ?? 1;
  const currentPage = pagination?.current_page ?? 1;

  const visibleColumns = [
    {
      id: 'no',
      header: () => <span className="block text-center">No</span>,
      cell: ({ row }) => (
        <span className="block text-center">
          {isServerMode
            ? (currentPage - 1) * (pagination?.per_page ?? 25) +
            row.index +
            1
            : row.index + 1}
        </span>
      ),
      meta: {
        className: 'w-[50px] text-center',
        headerClassName: 'w-[50px] text-center',
      },
    } as ColumnDef<TData, TValue>,
    ...(isSortable
      ? [
        {
          id: 'drag',
          header: '',
          cell: () => <DragHandleTrigger />,
          meta: {
            className: 'w-[40px]',
            headerClassName: 'w-[40px]',
          },
        } as ColumnDef<TData, TValue>,
      ]
      : []),
    ...columns,
  ];

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: { distance: 8 },
    }),
    useSensor(KeyboardSensor),
  );

  const table = useReactTable({
    data,
    columns: visibleColumns,
    getCoreRowModel: getCoreRowModel(),
    getExpandedRowModel: renderSubRow ? getExpandedRowModel() : undefined,
    onExpandedChange: setExpanded,
    state: {
      expanded,
    },
  });

  const itemIds = React.useMemo(
    () => data.map((item) => String(getRowId ? getRowId(item) : '')),
    [data, getRowId],
  );

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;

    if (!over || active.id === over.id) {
      return;
    }

    const oldIndex = itemIds.indexOf(String(active.id));
    const newIndex = itemIds.indexOf(String(over.id));

    if (oldIndex === -1 || newIndex === -1) {
      return;
    }

    const reordered = [...data];
    const [moved] = reordered.splice(oldIndex, 1);
    reordered.splice(newIndex, 0, moved);

    onReorder?.(reordered);
  }

  function handleSearchChange(value: string) {
    setLocalSearch(value);

    if (isServerMode) {
      handleSearchDebounced(value);
    }
  }

  return (
    <>
      {(searchKey || toolbar || isServerMode) && (
        <div className="flex items-center gap-2">
          {searchKey && (
            <Input
              placeholder={searchPlaceholder}
              value={localSearch}
              onChange={(event) =>
                handleSearchChange(event.target.value)
              }
              className="max-w-sm"
            />
          )}
          {toolbar}
          <div className="ml-auto flex items-center gap-2">
            {isServerMode && onPerPageChange && (
              <Select
                value={String(pagination?.per_page ?? 25)}
                onValueChange={(value) =>
                  onPerPageChange(Number(value))
                }
              >
                <SelectTrigger className="h-8 w-[70px]">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="25">25</SelectItem>
                  <SelectItem value="50">50</SelectItem>
                  <SelectItem value="100">100</SelectItem>
                  <SelectItem value="999999">
                    Semua
                  </SelectItem>
                </SelectContent>
              </Select>
            )}
          </div>
        </div>
      )}
      <Card className="shrink-0 bg-sidebar p-0">
        <CardContent className="p-0">
          <Table>
            <TableHeader>
              {table.getHeaderGroups().map((headerGroup) => (
                <TableRow key={headerGroup.id}>
                  {headerGroup.headers.map((header) => (
                    <TableHead
                      key={header.id}
                      className={
                        (
                          header.column.columnDef
                            .meta as {
                              headerClassName?: string;
                            }
                        )?.headerClassName
                      }
                    >
                      {header.isPlaceholder
                        ? null
                        : flexRender(
                          header.column.columnDef
                            .header,
                          header.getContext(),
                        )}
                    </TableHead>
                  ))}
                </TableRow>
              ))}
            </TableHeader>
            <TableBody>
              {table.getRowModel().rows?.length ? (
                isSortable ? (
                  <DndContext
                    sensors={sensors}
                    onDragEnd={handleDragEnd}
                  >
                    <SortableContext
                      items={itemIds}
                      strategy={
                        verticalListSortingStrategy
                      }
                    >
                      {table
                        .getRowModel()
                        .rows.map((row) => (
                          <SortableTableRow
                            key={row.id}
                            id={String(
                              getRowId!(
                                row.original,
                              ),
                            )}
                          >
                            {row
                              .getVisibleCells()
                              .map((cell) => (
                                <TableCell
                                  key={
                                    cell.id
                                  }
                                  className={
                                    (
                                      cell
                                        .column
                                        .columnDef
                                        .meta as {
                                          className?: string;
                                        }
                                    )
                                      ?.className
                                  }
                                >
                                  {flexRender(
                                    cell
                                      .column
                                      .columnDef
                                      .cell,
                                    cell.getContext(),
                                  )}
                                </TableCell>
                              ))}
                          </SortableTableRow>
                        ))}
                    </SortableContext>
                  </DndContext>
                ) : (
                  table.getRowModel().rows.map((row) => (
                    <React.Fragment key={row.id}>
                      <TableRow
                        data-state={
                          row.getIsSelected() &&
                          'selected'
                        }
                      >
                        {row
                          .getVisibleCells()
                          .map((cell) => (
                            <TableCell
                              key={cell.id}
                              className={
                                (
                                  cell.column
                                    .columnDef
                                    .meta as {
                                      className?: string;
                                    }
                                )?.className
                              }
                            >
                              {flexRender(
                                cell.column
                                  .columnDef
                                  .cell,
                                cell.getContext(),
                              )}
                            </TableCell>
                          ))}
                      </TableRow>
                      {renderSubRow &&
                        row.getIsExpanded() && (
                          <TableRow>
                            <TableCell
                              colSpan={
                                visibleColumns.length
                              }
                              className="bg-muted/50 p-0"
                            >
                              <div className="p-4">
                                {renderSubRow(
                                  row,
                                  localSearch,
                                )}
                              </div>
                            </TableCell>
                          </TableRow>
                        )}
                    </React.Fragment>
                  ))
                )
              ) : (
                <TableRow>
                  <TableCell
                    colSpan={visibleColumns.length}
                    className="h-24 text-center"
                  >
                    {emptyText}
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>
        </CardContent>
      </Card>

      <div className="flex items-center justify-end gap-2">
        <span className="text-sm text-muted-foreground">
          {isServerMode
            ? `Halaman ${currentPage} dari ${totalPages}`
            : `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`}
        </span>
        <div className="flex items-center gap-1">
          {isServerMode ? (
            <>
              <Button
                variant="outline"
                size="sm"
                onClick={() => onPageChange(1)}
                disabled={currentPage <= 1}
              >
                <ChevronsLeft className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => onPageChange(currentPage - 1)}
                disabled={currentPage <= 1}
              >
                <ChevronLeft className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => onPageChange(currentPage + 1)}
                disabled={currentPage >= totalPages}
              >
                <ChevronRight className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => onPageChange(totalPages)}
                disabled={currentPage >= totalPages}
              >
                <ChevronsRight className="h-4 w-4" />
              </Button>
            </>
          ) : (
            <>
              <Button
                variant="outline"
                size="sm"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                <ChevronsLeft className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                <ChevronLeft className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                <ChevronRight className="h-4 w-4" />
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  table.setPageIndex(table.getPageCount() - 1)
                }
                disabled={!table.getCanNextPage()}
              >
                <ChevronsRight className="h-4 w-4" />
              </Button>
            </>
          )}
        </div>
      </div>
    </>
  );
}
