import {
    forwardRef,
    memo,
    PropsWithoutRef,
    ReactElement,
    ReactNode,
    RefAttributes,
    useCallback,
    useMemo,
} from 'react';
import {
    type GestureResponderEvent,
    type ViewStyle,
    type SectionListProps,
    type SectionListRenderItemInfo,
    SectionList,
    View,
} from 'react-native';
import { typedMemo } from '../../hocs';
import withKeyboardAccessibility, {
    useCurrentIndexStoreValue,
} from '../../hocs/withKeyboardAccessibility';
import { useControlledValue, UseSearchableProps } from '../../hooks';
import { useSearchInputProps } from '../../hooks/useSearchable';
import { TextInput } from '../TextInput';
import { TouchableRipple } from '../TouchableRipple';
import { optionListStyles } from './utils';

type DefaultSectionT<TItem> = {
    data: TItem[];
    [key: string]: any;
};

type DefaultItemT = {
    id: string | number;
    [key: string]: any;
};

export type OptionListRenderItemInfo<
    TItem = DefaultItemT,
    TSection = DefaultSectionT<TItem>,
> = SectionListRenderItemInfo<TItem, TSection> & {
    focused: boolean;
    onPress: (e?: GestureResponderEvent) => void;
    [key: string]: any;
};

// To make a correct type inference
export type IOptionList = <ItemType = DefaultItemT, TSectionType = DefaultSectionT<ItemType>>(
    props: PropsWithoutRef<Props<ItemType, TSectionType>> & RefAttributes<SectionList<ItemType>>,
) => ReactElement;

export type Props<TItem = DefaultItemT, TSection = DefaultSectionT<TItem>> = UseSearchableProps &
    Omit<SectionListProps<TItem, {}>, 'sections' | 'renderItem'> & {
        records: TSection[];
        containerStyle?: ViewStyle;
        searchInputContainerStyle?: ViewStyle;
        /*
         * when set to true, the items will be selectable. Each item will be wrapped around by TouchableRipple component. Whenever they're pressed, onSelectedItem function will trigger
         * */
        selectable?: boolean;
        /*
         * when set to true, multiple items can be selected and selectedItem will be an array. onSelectItem's argument will be an array.
         * */
        multiple?: boolean;
        /*
         * Expects an array of TItem in multiple mode. If the item already exists in the array, it will be removed.
         * */
        selection?: TItem | TItem[] | null;
        /*
         * passes the current selectedItem. Will be an array in multiple mode. Item is the specific item which is pressed
         * */
        onSelectionChange?: (
            selection: TItem | TItem[] | null,
            item: TItem,
            event?: GestureResponderEvent,
        ) => void;
        renderItem: (
            info: SectionListRenderItemInfo<TItem, TSection> & {
                onPress: (e?: GestureResponderEvent) => void;
                focused: boolean;
            },
        ) => ReactNode;
        enableKeyboardNavigation?: boolean;
        onCancel?: () => void;
    };

const getIdToIndexMapFromRecords = <
    TItem extends DefaultItemT = DefaultItemT,
    TSection extends DefaultSectionT<TItem> = DefaultSectionT<TItem>,
>(
    records: TSection[],
) => {
    const flattenRecords = records.reduce((acc, item, sectionIndex: Number) => {
        return acc.concat((item.data || []).map((t: any) => ({ ...t, sectionIndex })));
    }, [] as TItem[]);

    return flattenRecords.reduce((acc, item, currentIndex) => {
        acc[item.id] = currentIndex;

        return acc;
    }, {} as Record<number | string, number>);
};

const OptionList = <
    TItem extends DefaultItemT = DefaultItemT,
    TSection extends DefaultSectionT<TItem> = DefaultSectionT<TItem>,
>(
    {
        query,
        onQueryChange,
        searchInputProps: dirtySearchInputProps,
        searchable,
        containerStyle = {},
        searchInputContainerStyle = {},
        style: styleProp,
        records,
        multiple = false,
        selectable,
        selection: selectionProp,
        onSelectionChange: onSelectionChangeProp,
        renderItem: renderItemProp,
        testID,
        ...rest
    }: Props<TItem, TSection>,
    ref: any,
) => {
    const [selection, onSelectionChange] = useControlledValue<TItem | TItem[] | null>({
        value: selectionProp,
        onChange: onSelectionChangeProp,
    });

    const searchInputProps = useSearchInputProps(dirtySearchInputProps);

    const { containerStyles, searchInputContainerStyles, style } = useMemo(() => {
        const { container, searchInputContainer } = optionListStyles;

        return {
            containerStyles: [container, containerStyle],
            searchInputContainerStyles: [searchInputContainer, searchInputContainerStyle],
            style: styleProp,
        };
    }, [containerStyle, searchInputContainerStyle, styleProp]);

    // To get the actual flatten indexes
    const idToIndexMap = useMemo(() => getIdToIndexMapFromRecords(records), [records]);

    const onPressItem = useCallback(
        (item: TItem, event?: GestureResponderEvent) => {
            const isSelected = Array.isArray(selection)
                ? selection.find(sItem => sItem?.id === item.id)
                : selection?.id === item.id;

            onSelectionChange(
                // if multiple we push the item into an array and if it's already exists we filter them
                multiple
                    ? Array.isArray(selection)
                        ? isSelected
                            ? selection.filter(sItem => sItem.id !== item.id)
                            : [...selection, item]
                        : [item]
                    : item,
                item,
                event,
            );
        },
        [multiple, onSelectionChange, selection],
    );

    const renderItem = useCallback(
        (info: SectionListRenderItemInfo<TItem, TSection>) => {
            return (
                <OptionListItem
                    // TODO - fix ts issues
                    renderItem={renderItemProp}
                    info={info}
                    testID={testID && `${testID}-${info.item.id}`}
                    index={idToIndexMap[info.item.id as keyof typeof idToIndexMap] as number}
                    onPressItem={onPressItem}
                    selectable={selectable}
                />
            );
        },
        [idToIndexMap, testID, onPressItem, renderItemProp, selectable],
    );

    const keyExtractor = useCallback((item: TItem) => `${item.id}`, []);

    return (
        <View style={containerStyles}>
            <>
                {searchable && (
                    <View style={searchInputContainerStyles}>
                        <TextInput
                            value={query}
                            onChangeText={onQueryChange}
                            {...searchInputProps}
                        />
                    </View>
                )}
            </>
            <SectionList
                ref={ref}
                keyExtractor={keyExtractor}
                testID={testID}
                // TODO - fix ts issues
                {...(rest as any)}
                sections={records}
                renderItem={renderItem as SectionListProps<TItem, TSection>['renderItem']}
                style={style}
            />
        </View>
    );
};

type OptionListItemProps<TItem = DefaultItemT, TSection = DefaultSectionT<TItem>> = Pick<
    Props<TItem, TSection>,
    'renderItem'
> & {
    info: SectionListRenderItemInfo<TItem, TSection>;
    onPressItem?: (item: TItem, e?: GestureResponderEvent) => void;
    selectable?: boolean;
    index: number;
    testID?: string;
};

const OptionListItem = typedMemo(
    <
        TItem extends DefaultItemT = DefaultItemT,
        TSection extends DefaultSectionT<TItem> = DefaultSectionT<TItem>,
    >({
        info,
        renderItem,
        selectable,
        onPressItem,
        index,
        testID,
    }: OptionListItemProps<TItem, TSection>) => {
        const focused = useCurrentIndexStoreValue(state => {
            return state.currentIndex === index;
        });
        const onPress = useCallback(
            (event?: GestureResponderEvent) => {
                onPressItem?.(info.item, event);
            },
            [info.item, onPressItem],
        );

        const renderItemInfo = useMemo(
            () => ({
                ...info,
                focused,
                onPress,
            }),
            [focused, info, onPress],
        );

        if (selectable && info.item?.selectable !== false) {
            return (
                <TouchableRipple testID={testID} onPress={onPress}>
                    {renderItem(renderItemInfo)}
                </TouchableRipple>
            );
        }

        return <>{renderItem(renderItemInfo)}</>;
    },
);

export default memo(withKeyboardAccessibility(forwardRef(OptionList))) as IOptionList;
