import {
    Children,
    cloneElement,
    isValidElement,
    useEffect,
    useMemo,
    useState,
} from 'react';
import type {
    HTMLAttributes,
    ReactElement,
    ReactNode,
    TableHTMLAttributes,
} from 'react';

import { cn } from '@/lib/utils';

type TableElement = ReactElement<TableHTMLAttributes<HTMLTableElement>>;

type ElementWithChildren = ReactElement<
    { children?: ReactNode } & Record<string, unknown>
>;

type ConfigurableTableProps = HTMLAttributes<HTMLDivElement> & {
    tableId: string;
    children: TableElement;
};

type ColumnDefinition = {
    id: string;
    label: string;
    originalIndex: number;
};

type SortDirection = 'asc' | 'desc';

type SortState = {
    columnId: string;
    direction: SortDirection;
};

type ColumnPreferences = {
    order: string[];
    hidden: string[];
};

const storagePrefix = 'parts.table-columns.';

const emptyPreferences: ColumnPreferences = {
    order: [],
    hidden: [],
};

export function ConfigurableTable({
    tableId,
    children,
    className,
    ...props
}: ConfigurableTableProps) {
    const columns = useMemo(() => getColumnDefinitions(children), [children]);
    const [preferences, setPreferences] = useState<ColumnPreferences>(() =>
        readPreferences(tableId),
    );
    const [sortState, setSortState] = useState<SortState | null>(null);

    useEffect(() => {
        setPreferences(readPreferences(tableId));
    }, [tableId]);

    useEffect(() => {
        writePreferences(tableId, preferences);
    }, [preferences, tableId]);

    const normalizedColumns = useMemo(
        () => normalizeColumns(columns, preferences),
        [columns, preferences],
    );

    const visibleColumnIds = useMemo(() => {
        const hidden = new Set(preferences.hidden);
        const visible = normalizedColumns.filter((column) => !hidden.has(column.id));

        return visible.length > 0
            ? visible.map((column) => column.id)
            : normalizedColumns.slice(0, 1).map((column) => column.id);
    }, [normalizedColumns, preferences.hidden]);

    const displayedColumns = useMemo(() => {
        const visible = new Set(visibleColumnIds);

        return normalizedColumns.filter((column) => visible.has(column.id));
    }, [normalizedColumns, visibleColumnIds]);

    useEffect(() => {
        if (
            sortState !== null &&
            !displayedColumns.some((column) => column.id === sortState.columnId)
        ) {
            setSortState(null);
        }
    }, [displayedColumns, sortState]);

    const table = useMemo(
        () =>
            renderTableWithColumns(
                children,
                displayedColumns,
                sortState,
                setSortState,
            ),
        [children, displayedColumns, sortState],
    );

    const visibleColumnCount = visibleColumnIds.length;
    const columnCount = columns.length;

    const toggleColumn = (columnId: string) => {
        setPreferences((current) => {
            const hidden = new Set(current.hidden);

            if (hidden.has(columnId)) {
                hidden.delete(columnId);

                return {
                    ...current,
                    hidden: [...hidden],
                };
            }

            if (visibleColumnIds.length <= 1) {
                return current;
            }

            hidden.add(columnId);

            return {
                ...current,
                hidden: [...hidden],
            };
        });
    };

    const moveColumn = (columnId: string, direction: -1 | 1) => {
        setPreferences((current) => {
            const order = normalizeColumns(columns, current).map(
                (column) => column.id,
            );
            const currentIndex = order.indexOf(columnId);
            const nextIndex = currentIndex + direction;

            if (
                currentIndex === -1 ||
                nextIndex < 0 ||
                nextIndex >= order.length
            ) {
                return current;
            }

            [order[currentIndex], order[nextIndex]] = [
                order[nextIndex],
                order[currentIndex],
            ];

            return {
                ...current,
                order,
            };
        });
    };

    const resetColumns = () => {
        setPreferences(emptyPreferences);
    };

    if (columnCount === 0) {
        return (
            <div className={className} {...props}>
                {children}
            </div>
        );
    }

    return (
        <div className={className} {...props}>
            <div className="border-b border-border bg-surface/70 px-3 py-2 text-sm text-muted">
                <div className="flex flex-wrap items-center justify-between gap-2">
                    <p>
                        {visibleColumnCount} of {columnCount} columns shown
                    </p>
                    <details className="w-full sm:w-auto">
                        <summary className="cursor-pointer list-none rounded-md border border-border bg-card px-3 py-1.5 text-sm font-medium text-main hover:bg-surface">
                            Columns
                        </summary>
                        <div className="mt-2 grid gap-2 rounded-lg border border-border bg-card p-3 shadow-sm sm:min-w-80">
                            {normalizedColumns.map((column, index) => {
                                const isVisible = visibleColumnIds.includes(
                                    column.id,
                                );
                                const isOnlyVisible =
                                    isVisible && visibleColumnCount === 1;

                                return (
                                    <div
                                        key={column.id}
                                        className="flex items-center gap-2 rounded-md border border-border bg-control px-2 py-2"
                                    >
                                        <label className="flex min-w-0 flex-1 items-center gap-2 text-sm text-main">
                                            <input
                                                type="checkbox"
                                                checked={isVisible}
                                                disabled={isOnlyVisible}
                                                onChange={() =>
                                                    toggleColumn(column.id)
                                                }
                                            />
                                            <span className="truncate">
                                                {column.label}
                                            </span>
                                        </label>
                                        <div className="flex shrink-0 gap-1">
                                            <button
                                                type="button"
                                                onClick={() =>
                                                    moveColumn(column.id, -1)
                                                }
                                                disabled={index === 0}
                                                className={orderButtonClassName}
                                            >
                                                Left
                                            </button>
                                            <button
                                                type="button"
                                                onClick={() =>
                                                    moveColumn(column.id, 1)
                                                }
                                                disabled={
                                                    index ===
                                                    normalizedColumns.length - 1
                                                }
                                                className={orderButtonClassName}
                                            >
                                                Right
                                            </button>
                                        </div>
                                    </div>
                                );
                            })}
                            <button
                                type="button"
                                onClick={resetColumns}
                                className="justify-self-start rounded-md border border-border px-2.5 py-1 text-xs font-medium text-muted hover:bg-surface hover:text-main"
                            >
                                Reset columns
                            </button>
                        </div>
                    </details>
                </div>
            </div>
            {table}
        </div>
    );
}

const orderButtonClassName = cn(
    'rounded border border-border px-2 py-1 text-xs font-medium text-muted',
    'hover:bg-surface hover:text-main disabled:cursor-not-allowed disabled:opacity-40',
);

const sortCollator = new Intl.Collator(undefined, {
    numeric: true,
    sensitivity: 'base',
});

function getColumnDefinitions(table: TableElement): ColumnDefinition[] {
    const header = Children.toArray(table.props.children).find((child) =>
        isElementType(child, 'thead'),
    );

    if (!isValidElementWithChildren(header)) {
        return [];
    }

    const headerRow = Children.toArray(header.props.children).find((child) =>
        isElementType(child, 'tr'),
    );

    if (!isValidElementWithChildren(headerRow)) {
        return [];
    }

    return Children.toArray(headerRow.props.children)
        .filter(isValidElementWithChildren)
        .map((cell, index) => {
            const label = getNodeText(cell.props.children) || `Column ${index + 1}`;

            return {
                id: getColumnId(label, index),
                label,
                originalIndex: index,
            };
        });
}

function normalizeColumns(
    columns: ColumnDefinition[],
    preferences: ColumnPreferences,
): ColumnDefinition[] {
    const columnById = new Map(columns.map((column) => [column.id, column]));
    const orderedColumns = preferences.order
        .map((columnId) => columnById.get(columnId))
        .filter((column): column is ColumnDefinition => Boolean(column));
    const orderedIds = new Set(orderedColumns.map((column) => column.id));
    const newColumns = columns.filter((column) => !orderedIds.has(column.id));

    return [...orderedColumns, ...newColumns];
}

function renderTableWithColumns(
    table: TableElement,
    columns: ColumnDefinition[],
    sortState: SortState | null,
    setSortState: (sortState: SortState | null) => void,
): TableElement {
    const indexes = columns.map((column) => column.originalIndex);
    const transformedChildren = Children.toArray(table.props.children).map(
        (child) => {
            if (
                isElementType(child, 'thead') ||
                isElementType(child, 'tbody') ||
                isElementType(child, 'tfoot')
            ) {
                return renderTableSectionWithColumns(
                    child,
                    indexes,
                    columns,
                    sortState,
                    setSortState,
                );
            }

            return child;
        },
    );

    return cloneElement(table, undefined, transformedChildren);
}

function renderTableSectionWithColumns(
    section: ReactElement<{ children?: ReactNode }>,
    indexes: number[],
    columns: ColumnDefinition[],
    sortState: SortState | null,
    setSortState: (sortState: SortState | null) => void,
): ReactElement {
    const sortColumn =
        sortState === null
            ? null
            : columns.find((column) => column.id === sortState.columnId) || null;
    const rows =
        isElementType(section, 'tbody') &&
        sortState !== null &&
        sortColumn !== null
            ? sortRows(
                  Children.toArray(section.props.children),
                  sortColumn.originalIndex,
                  sortState.direction,
              )
            : Children.toArray(section.props.children);
    const transformedRows = rows.map(
        (child) => {
            if (!isElementType(child, 'tr')) {
                return child;
            }

            return renderRowWithColumns(
                child,
                indexes,
                isElementType(section, 'thead') ? columns : [],
                sortState,
                setSortState,
            );
        },
    );

    return cloneElement(section, undefined, transformedRows);
}

function renderRowWithColumns(
    row: ReactElement<{ children?: ReactNode }>,
    indexes: number[],
    columns: ColumnDefinition[],
    sortState: SortState | null,
    setSortState: (sortState: SortState | null) => void,
): ReactElement {
    const cells = Children.toArray(row.props.children);
    const transformedCells: ReactNode[] = [];

    indexes.forEach((index, visibleIndex) => {
        const cell = cells[index];

        if (cell !== undefined && cell !== null) {
            transformedCells.push(
                renderCellWithSort(
                    cell,
                    columns[visibleIndex] || null,
                    sortState,
                    setSortState,
                ),
            );
        }
    });

    return cloneElement(row, undefined, transformedCells);
}

function renderCellWithSort(
    cell: ReactNode,
    column: ColumnDefinition | null,
    sortState: SortState | null,
    setSortState: (sortState: SortState | null) => void,
): ReactNode {
    if (
        column === null ||
        !isValidElementWithChildren(cell) ||
        !isElementType(cell, 'th') ||
        !isSortableColumn(column)
    ) {
        return cell;
    }

    const isActive = sortState?.columnId === column.id;
    const nextDirection: SortDirection =
        isActive && sortState.direction === 'asc' ? 'desc' : 'asc';
    const ariaSort = isActive
        ? sortState.direction === 'asc'
            ? 'ascending'
            : 'descending'
        : 'none';
    const sortedDirection = sortState?.direction;

    return cloneElement(
        cell,
        {
            ...cell.props,
            'aria-sort': ariaSort,
        },
        <button
            type="button"
            aria-label={`Sort ${column.label} ${nextDirection}`}
            onClick={() =>
                setSortState({
                    columnId: column.id,
                    direction: nextDirection,
                })
            }
            className="group inline-flex w-full cursor-pointer items-center gap-1 text-left font-[inherit] text-inherit"
        >
            <span>{cell.props.children}</span>
            <span className="text-[10px] font-semibold uppercase text-muted opacity-0 transition group-hover:opacity-70 group-focus-visible:opacity-70">
                sort
            </span>
            {isActive && sortedDirection !== undefined ? (
                <span className="text-[10px] font-semibold uppercase text-main">
                    {sortedDirection}
                </span>
            ) : null}
        </button>,
    );
}

function sortRows(
    rows: ReactNode[],
    columnIndex: number,
    direction: SortDirection,
): ReactNode[] {
    return rows
        .map((row, originalIndex) => ({
            originalIndex,
            row,
            value: getRowSortValue(row, columnIndex),
        }))
        .sort((first, second) => {
            const comparison = compareSortValues(
                first.value,
                second.value,
                direction,
            );

            if (comparison === 0) {
                return first.originalIndex - second.originalIndex;
            }

            return comparison;
        })
        .map(({ row }) => row);
}

function getRowSortValue(row: ReactNode, columnIndex: number): string {
    if (!isElementType(row, 'tr')) {
        return '';
    }

    const cell = Children.toArray(row.props.children)[columnIndex];

    if (!isValidElementWithChildren(cell)) {
        return '';
    }

    return getNodeSortValue(cell);
}

function compareSortValues(
    first: string,
    second: string,
    direction: SortDirection,
): number {
    const firstValue = first.trim();
    const secondValue = second.trim();

    if (firstValue === '' && secondValue === '') {
        return 0;
    }

    if (firstValue === '') {
        return 1;
    }

    if (secondValue === '') {
        return -1;
    }

    const comparison = sortCollator.compare(firstValue, secondValue);

    return direction === 'asc' ? comparison : -comparison;
}

function isSortableColumn(column: ColumnDefinition): boolean {
    return column.label.trim().toLowerCase() !== 'actions';
}

function getNodeSortValue(node: ElementWithChildren): string {
    const explicitSortValue = getPropText(node.props, [
        'data-sort-value',
        'data-value',
    ]);

    if (explicitSortValue !== '') {
        return explicitSortValue;
    }

    return getNodeText(node.props.children);
}

function readPreferences(tableId: string): ColumnPreferences {
    if (typeof window === 'undefined') {
        return emptyPreferences;
    }

    try {
        const stored = window.localStorage.getItem(`${storagePrefix}${tableId}`);

        if (!stored) {
            return emptyPreferences;
        }

        const parsed = JSON.parse(stored) as Partial<ColumnPreferences>;

        return {
            order: Array.isArray(parsed.order) ? parsed.order : [],
            hidden: Array.isArray(parsed.hidden) ? parsed.hidden : [],
        };
    } catch {
        return emptyPreferences;
    }
}

function writePreferences(tableId: string, preferences: ColumnPreferences) {
    if (typeof window === 'undefined') {
        return;
    }

    window.localStorage.setItem(
        `${storagePrefix}${tableId}`,
        JSON.stringify(preferences),
    );
}

function getColumnId(label: string, index: number): string {
    const slug = label
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '');

    return `${slug || 'column'}-${index}`;
}

function getNodeText(node: ReactNode): string {
    if (typeof node === 'string' || typeof node === 'number') {
        return String(node).trim();
    }

    if (Array.isArray(node)) {
        return node.map(getNodeText).join(' ').replace(/\s+/g, ' ').trim();
    }

    if (isValidElementWithChildren(node)) {
        const childrenText = getNodeText(node.props.children);

        if (childrenText !== '') {
            return childrenText;
        }

        return getPropText(node.props, [
            'aria-label',
            'title',
            'label',
            'status',
            'name',
            'value',
        ]);
    }

    return '';
}

function getPropText(
    props: Record<string, unknown>,
    names: string[],
): string {
    for (const name of names) {
        const value = props[name];

        if (
            typeof value === 'string' ||
            typeof value === 'number' ||
            typeof value === 'boolean'
        ) {
            const text = String(value).trim();

            if (text !== '') {
                return text;
            }
        }
    }

    return '';
}

function isElementType(
    node: ReactNode,
    type: string,
): node is ElementWithChildren {
    return isValidElement(node) && node.type === type;
}

function isValidElementWithChildren(node: ReactNode): node is ElementWithChildren {
    return isValidElement(node);
}
