import {DndContext, DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
import {restrictToParentElement, restrictToVerticalAxis} from '@dnd-kit/modifiers';
import {SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable';
import {
    __InputWrapperProps,
    Box,
    BoxProps,
    Factory,
    Input,
    MantineSpacing,
    Stack,
    StylesApiProps,
    useProps,
    useStyles,
} from '@mantine/core';
import {useDidUpdate} from '@mantine/hooks';
import {ForwardedRef, ReactNode} from 'react';

import {CustomComponentThemeExtend, identity} from '../../utils/createFactoryComponent.js';
import classes from './Collection.module.css';
import {CollectionAddButton} from './CollectionAddButton.js';
import {CollectionColumnDef} from './CollectionColumn.types.js';
import {CollectionProvider} from './CollectionContext.js';
import {CollectionItem} from './CollectionItem.js';
import {CollectionLayout} from './layouts/CollectionLayout.types.js';
import {CollectionLayouts} from './layouts/CollectionLayouts.js';

/**
 * Base props shared by both column-based and children-based patterns
 */
interface BaseCollectionProps<T> extends __InputWrapperProps, BoxProps, StylesApiProps<CollectionFactory> {
    /**
     * The default value each new item should have
     */
    newItem: T | (() => T);
    /**
     * The list of items to display inside the collection
     *
     * @default []
     */
    value?: T[];
    /**
     * Defines how each item is uniquely identified. It is highly recommended that you specify this prop to an ID that makes sense.
     *
     * This method is required when using this component with ReactHookForm.
     *
     * @see {@link https://react-hook-form.com/api/usefieldarray/} for using a collection with ReactHookForm.
     *
     * @param originalItem The original item
     * @param itemIndex The index of the original item
     */
    getItemId?: (originalItem: T, itemIndex: number) => string;
    /**
     * Unused, has no effect
     */
    onFocus?: () => void;
    /**
     * Function called whenever the value needs to be updated
     *
     * @param value The whole list of items after the change
     */
    onChange?: (value: T[]) => void;
    /**
     * Function called after an item is removed from the collection using the remove button
     *
     * @param itemIndex The index of the item that was removed
     */
    onRemoveItem?: (itemIndex: number) => void;
    /**
     * Function that gets called whenever a collection item needs to be reordered
     *
     * @param payload The origin and destination index of the item to reorder
     */
    onReorderItem?: (payload: {from: number; to: number}) => void;
    /**
     * Function that gets called when a new item needs to be added to the collection
     *
     * @param value The value of the item to insert
     * @param index The index of the new item to insert
     */
    onInsertItem?: (value: T, index: number) => void;
    /**
     * Whether the collection should have drag and drop behavior enabled
     *
     * @default false
     */
    draggable?: boolean;
    /**
     * Whether the collection is disabled, or in other words in read only mode
     *
     * @default false
     */
    disabled?: boolean;
    /**
     * Whether the collection is readOnly. If true, the collection will not allow adding or removing items
     *
     * @default false
     */
    readOnly?: boolean;
    /**
     * Function that determines if the add item button should be enabled given the current items of the collection.
     * The button is always enabled if this props remains undefined
     *
     * @param values The current items of the collection
     */
    allowAdd?: boolean | ((values: T[]) => boolean);
    /**
     * The label of the add item button
     *
     * @default "Add item"
     */
    addLabel?: ReactNode;
    /**
     * The tooltip text displayed when hovering over the disabled add item button
     *
     * @default 'There is already an empty item'
     */
    addDisabledTooltip?: string;
    /**
     * The gap between the collection items
     *
     * @default 'md'
     */
    gap?: MantineSpacing;
    /**
     * Whether the collection is required. When required is true, the collection will hide the remove button if there is only one item
     *
     * @default false
     */
    required?: boolean;
}

/**
 * Collection with column-based layout
 */
interface CollectionWithColumns<T> extends BaseCollectionProps<T> {
    /**
     * Column definitions for the collection
     */
    columns: Array<CollectionColumnDef<T>>;

    /**
     * Layout component to use for rendering
     * @default CollectionLayouts.Horizontal
     */
    layout?: CollectionLayout;

    /**
     * Must not have children when using columns
     */
    children?: never;
}

/**
 * Collection with legacy children render prop
 */
interface CollectionWithChildren<T> extends BaseCollectionProps<T> {
    /**
     * A render function called for each item passed in the `value` prop.
     *
     * @param item The current item's value
     * @param index The current item's index
     */
    children: (item: T, index: number) => ReactNode;

    /**
     * Must not have columns when using children
     */
    columns?: never;

    /**
     * Must not have layout when using children
     */
    layout?: never;
}

/**
 * Collection props - either columns OR children, never both
 */
export type CollectionProps<T> = CollectionWithColumns<T> | CollectionWithChildren<T>;

export type CollectionStylesNames = 'root' | 'item' | 'items' | 'itemDragging' | 'dragHandle' | 'removeButton';

export type CollectionFactory = Factory<{
    props: CollectionProps<unknown>;
    ref: HTMLDivElement;
    stylesNames: CollectionStylesNames;
}>;

const defaultProps: Partial<CollectionProps<unknown>> = {
    draggable: false,
    addLabel: 'Add item',
    addDisabledTooltip: 'There is already an empty item',
    disabled: false,
    readOnly: false,
    gap: 'md',
    required: false,
    getItemId: ({id}: any) => id,
};

export const Collection = <T,>(props: CollectionProps<T> & {ref?: ForwardedRef<HTMLDivElement>}) => {
    const {
        value,
        onChange,
        onRemoveItem,
        onReorderItem,
        onInsertItem,
        disabled,
        readOnly,
        draggable,
        children,
        columns,
        layout,
        gap,
        required,
        newItem,
        addLabel,
        addDisabledTooltip,
        allowAdd,
        label,
        labelProps,
        withAsterisk,
        description,
        descriptionProps,
        error,
        errorProps,
        getItemId,
        ref,

        // Style props
        style,
        className,
        classNames,
        styles,
        unstyled,
        ...others
    } = useProps('Collection', defaultProps as CollectionProps<T>, props);

    // Runtime validation: ensure columns and children are mutually exclusive
    if (columns && children) {
        throw new Error('Collection: Cannot use both "columns" and "children" props.');
    }

    if (layout && !columns) {
        throw new Error('Collection: "layout" prop can only be used with "columns" prop.');
    }

    const getStyles = useStyles<CollectionFactory>({
        name: 'Collection',
        classes,
        props,
        className,
        style,
        classNames,
        styles,
        unstyled,
    });
    const sensors = useSensors(
        useSensor(PointerSensor),
        useSensor(KeyboardSensor, {
            coordinateGetter: sortableKeyboardCoordinates,
        }),
    );

    const canEdit = !disabled && !readOnly;
    const items = value ?? [];
    const hasOnlyOneItem = items.length === 1;

    /**
     * Enforcing onChange when the value is modified will make sure the errors are carried through.
     */
    useDidUpdate(() => {
        onChange?.(items);
    }, [JSON.stringify(items)]);

    const isRequired = typeof withAsterisk === 'boolean' ? withAsterisk : required;
    const _label = label ? (
        <Input.Label required={isRequired} {...labelProps}>
            {label}
        </Input.Label>
    ) : null;

    const _description = description ? (
        <Input.Description {...descriptionProps}>{description}</Input.Description>
    ) : null;
    const _error = error ? (
        <Input.Error {...errorProps} pt="xs">
            {error}
        </Input.Error>
    ) : null;
    const _header =
        _label || _description ? (
            <Stack gap="xxs" pb="xs">
                {_label}
                {_description}
            </Stack>
        ) : null;

    const standardizedItems = items.map((item, index) => ({id: getItemId?.(item, index) ?? String(index), data: item}));

    const getIndex = (id: string) => standardizedItems.findIndex((item) => item.id === id);

    const handleDragEnd = ({over, active}: DragEndEvent): void => {
        if (over) {
            const activeIndex = getIndex(String(active.id));
            const overIndex = getIndex(String(over.id));
            if (activeIndex !== overIndex) {
                onReorderItem?.({from: activeIndex, to: overIndex});
            }
        }
    };

    const addAllowed = typeof allowAdd === 'boolean' ? allowAdd : (allowAdd?.(items) ?? true);

    const handleAdd = () => {
        const newItemValue = typeof newItem === 'function' ? (newItem as () => T)() : newItem;
        onInsertItem?.(newItemValue, items?.length ?? 0);
    };

    const _addButton = canEdit ? (
        <CollectionAddButton
            addLabel={addLabel}
            addDisabledTooltip={addDisabledTooltip}
            addAllowed={addAllowed}
            onAdd={handleAdd}
        />
    ) : null;

    // Column-based layout pattern
    if (columns) {
        const Layout = layout || CollectionLayouts.Horizontal;

        return (
            <CollectionProvider value={{getStyles, columns: columns as Array<CollectionColumnDef<unknown>>}}>
                <DndContext
                    onDragEnd={handleDragEnd}
                    sensors={sensors}
                    modifiers={[restrictToVerticalAxis, restrictToParentElement]}
                >
                    <SortableContext items={standardizedItems} strategy={verticalListSortingStrategy}>
                        <Box ref={ref} {...others} {...getStyles('root')}>
                            {_header}
                            <Layout>
                                <Layout.Header
                                    draggable={draggable && canEdit}
                                    removable={canEdit && !(isRequired && hasOnlyOneItem)}
                                />
                                <Layout.Body
                                    items={items}
                                    onRemove={canEdit ? onRemoveItem : undefined}
                                    removable={canEdit && !(isRequired && hasOnlyOneItem)}
                                    draggable={draggable && canEdit}
                                    disabled={disabled}
                                    readOnly={readOnly}
                                    getItemId={getItemId}
                                    gap={gap}
                                />
                            </Layout>
                            {_addButton}
                            {_error}
                        </Box>
                    </SortableContext>
                </DndContext>
            </CollectionProvider>
        );
    }

    // Legacy children render prop pattern
    const renderedItems = standardizedItems.map((item, index) => (
        <CollectionItem
            key={item.id}
            id={item.id}
            disabled={!canEdit}
            draggable={draggable}
            onRemove={() => onRemoveItem?.(index)}
            removable={!(isRequired && hasOnlyOneItem)}
        >
            {children(item.data, index)}
        </CollectionItem>
    ));

    return (
        <CollectionProvider value={{getStyles}}>
            <DndContext
                onDragEnd={handleDragEnd}
                sensors={sensors}
                modifiers={[restrictToVerticalAxis, restrictToParentElement]}
            >
                <SortableContext items={standardizedItems} strategy={verticalListSortingStrategy}>
                    <Box ref={ref} {...others} {...getStyles('root')}>
                        {_header}
                        <Stack gap={gap} {...getStyles('items')}>
                            {renderedItems}
                            {_addButton}
                        </Stack>
                        {_error}
                    </Box>
                </SortableContext>
            </DndContext>
        </CollectionProvider>
    );
};

Collection.displayName = 'Collection';

Collection.extend = identity as CustomComponentThemeExtend<CollectionFactory>;

Collection.Layouts = CollectionLayouts;
