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;
    groupBy?: (item: TData) => string;
    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 = 'Ketikkan sesuatu...',
    emptyText = 'Tidak ada data.',
    onReorder,
    getRowId,
    toolbar,
    renderSubRow,
    defaultExpanded = false,
    groupBy,
    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 overflow-hidden 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>
                                ) : (
                                    (() => {
                                        let previousGroup: string | null =
                                            null;

                                        return table
                                            .getRowModel()
                                            .rows.map((row) => {
                                                const group = groupBy
                                                    ? groupBy(row.original)
                                                    : null;
                                                const showGroupHeader =
                                                    groupBy &&
                                                    group !== previousGroup;
                                                previousGroup = group;

                                                return (
                                                    <React.Fragment
                                                        key={row.id}
                                                    >
                                                        {showGroupHeader && (
                                                            <TableRow className="bg-muted/50 hover:bg-muted/50">
                                                                <TableCell
                                                                    colSpan={
                                                                        visibleColumns.length
                                                                    }
                                                                    className="py-2 text-sm font-semibold"
                                                                >
                                                                    {group}
                                                                </TableCell>
                                                            </TableRow>
                                                        )}
                                                        <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>
        </>
    );
}
