import React, {FC, useMemo, useState, useRef, useCallback, useEffect} from "react";
import classNames from "classnames";
import parse from "html-react-parser";
import {SelectOption} from "./MultiSelectSearch";
import {__} from "../../globals";
import {Container} from "../cards/Container";

export type MultiSelectListProps<Option extends SelectOption> = {
    options: Option[],
    selectedValues: Option['id'][],
    onSelectedValue: (value: Option) => void,
    onRemovedValue: (value: Option) => void,
    placeholder?: string,
    maxHeight?: number,
    autoSelectSearchOnMount?: boolean,
}

const CheckedIcon: FC<{className?: string}> = ({className}) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
        <path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clipRule="evenodd" />
    </svg>
)

const RemoveIcon: FC<{className?: string}> = ({className}) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
        <path fillRule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clipRule="evenodd" />
    </svg>
)

const StackedActionIcon: FC<{action: 'select' | 'deselect', className?: string}> = ({action, className}) => {
    const Icon = action === 'select' ? CheckedIcon : RemoveIcon

    return (
        <span className={classNames("relative inline-block w-[calc(16px+6px)]", className)}>
            <Icon className="absolute top-0.5 left-[6px] min-w-4 min-h-4 max-w-4 max-h-4 text-gray-450 bg-white rounded-full"/>
            <Icon className="relative z-10 min-w-4 min-h-4 max-w-4 max-h-4 text-gray-450 bg-white rounded-full"/>
        </span>
    )
}

/**
 * Normalizes a string for accent-insensitive comparison.
 * "Canción" becomes "cancion", "café" becomes "cafe", etc.
 */
const normalizeForSearch = (str: string): string => {
    return str
        .toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '')
}

/**
 * Fuzzy match: checks if all characters in the query appear in the target in order.
 * "aa" matches "ama", "ct" matches "category", etc.
 */
const fuzzyMatch = (target: string, query: string): boolean => {
    const normalizedTarget = normalizeForSearch(target)
    const normalizedQuery = normalizeForSearch(query)

    let targetIndex = 0

    for (const char of normalizedQuery) {
        const foundIndex = normalizedTarget.indexOf(char, targetIndex)
        if (foundIndex === -1) {
            return false
        }
        targetIndex = foundIndex + 1
    }

    return true
}

const getOptionElementId = (optionId: string) => `multi-select-option-${encodeURIComponent(optionId)}`
const addedToSelectedToastDurationMs = 2000

export const MultiSelectList = <Option extends SelectOption>({
    options,
    selectedValues,
    onSelectedValue,
    onRemovedValue,
    placeholder,
    maxHeight = 300,
    autoSelectSearchOnMount = false,
}: MultiSelectListProps<Option>) => {
    const [searchQuery, setSearchQuery] = useState('')
    const [highlightedIndex, setHighlightedIndex] = useState(0)
    const [addedToSelectedToastCount, setAddedToSelectedToastCount] = useState(0)
    const inputRef = useRef<HTMLInputElement>(null)
    const listRef = useRef<HTMLDivElement>(null)
    const selectedSectionRef = useRef<HTMLDivElement>(null)
    const scrollTargetRef = useRef<string | null>(null)
    const pendingSelectionActionRef = useRef<'add' | 'remove' | null>(null)
    const pendingSelectionSourceRef = useRef<'click' | 'keyboard' | null>(null)
    const addedToSelectedToastCountRef = useRef(0)
    const addedToSelectedToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

    const focusAndSelectSearchInput = useCallback(() => {
        requestAnimationFrame(() => {
            inputRef.current?.focus()
            inputRef.current?.select()
        })
    }, [])

    const showAddedToSelectedToast = useCallback(() => {
        const nextCount = addedToSelectedToastCountRef.current + 1
        addedToSelectedToastCountRef.current = nextCount
        setAddedToSelectedToastCount(nextCount)

        if (addedToSelectedToastTimerRef.current) {
            clearTimeout(addedToSelectedToastTimerRef.current)
        }

        addedToSelectedToastTimerRef.current = setTimeout(() => {
            addedToSelectedToastCountRef.current = 0
            setAddedToSelectedToastCount(0)
            addedToSelectedToastTimerRef.current = null
        }, addedToSelectedToastDurationMs)
    }, [])

    const isSearching = searchQuery.trim().length > 0
    const navigateHelperText = useMemo(
        () => parse(
            __('Use * to navigate').replace(
                '*',
                `                <div class="flex items-center gap-0">
                    <kbd class="grid place-content-center  min-w-[14px] min-h-[14px] max-w-[14px] max-h-[14px] bg-gray-150 text-gray-400 rounded-[3px]"> 
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="min-w-[10px] min-h-[10px] max-w-[10px] max-h-[10px]">
                      <path fill-rule="evenodd" d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z" clip-rule="evenodd" />
                    </svg>
                    </kbd>
                    <kbd class="grid place-content-center  min-w-[14px] min-h-[14px] max-w-[14px] max-h-[14px] bg-gray-150 text-gray-400 rounded-[3px]"> 
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="min-w-[10px] min-h-[10px] max-w-[10px] max-h-[10px]">
                      <path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v8.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.22 3.22V2.75A.75.75 0 0 1 8 2Z" clip-rule="evenodd" />
                    </svg>
                    </kbd>
                </div>`
            )
        ),
        []
    )

    const returnHelperText = useMemo(
        () => parse(
            __('Press * to select/deselect').replace(
                '*',
                `<kbd class="grid place-content-center  min-w-5 min-h-[14px] max-w-5 max-h-[14px] bg-gray-150 text-gray-550 rounded-[3px]"> 
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="min-w-[10px] min-h-[10px] max-w-[10px] max-h-[10px] rotate-180">
                      <path stroke-linecap="round" stroke-linejoin="round" d="m15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3" />
                    </svg>
                    </kbd>`
            )
        ),
        []
    )

    const returnToSelectHelperText = useMemo(
        () => parse(
            __('Press * to select').replace(
                '*',
                `<kbd class="grid place-content-center  min-w-5 min-h-[14px] max-w-5 max-h-[14px] bg-gray-150 text-gray-550 rounded-[3px]"> 
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="min-w-[10px] min-h-[10px] max-w-[10px] max-h-[10px] rotate-180">
                      <path stroke-linecap="round" stroke-linejoin="round" d="m15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3" />
                    </svg>
                    </kbd>`
            )
        ),
        []
    )

    const returnToDeselectHelperText = useMemo(
        () => parse(
            __('Press * to deselect').replace(
                '*',
                `<kbd class="grid place-content-center  min-w-5 min-h-[14px] max-w-5 max-h-[14px] bg-gray-150 text-gray-550 rounded-[3px]"> 
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="min-w-[10px] min-h-[10px] max-w-[10px] max-h-[10px] rotate-180">
                      <path stroke-linecap="round" stroke-linejoin="round" d="m15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3" />
                    </svg>
                    </kbd>`
            )
        ),
        []
    )


    const isSelected = useCallback((option: Option) => selectedValues.includes(option.id), [selectedValues])

    // Split options into selected and unselected.
    // Selected items follow selection order, not options array order.
    const {selectedOptions, unselectedOptions} = useMemo(() => {
        const uniqueSelectedIds = [...new Set(selectedValues)]
        const optionsById = new Map(options.map(option => [option.id, option]))
        const selectedIdSet = new Set(uniqueSelectedIds)

        const selected = uniqueSelectedIds
            .map(id => optionsById.get(id))
            .filter((option): option is Option => Boolean(option))

        const unselected = options.filter(option => !selectedIdSet.has(option.id))

        return {selectedOptions: selected, unselectedOptions: unselected}
    }, [options, selectedValues])

    // Filtered options when searching (includes both selected and unselected)
    const filteredOptions = useMemo(() => {
        if (!isSearching) {
            return []
        }

        return options.filter(option => {
            return fuzzyMatch(option.label || '', searchQuery) ||
                   fuzzyMatch(String(option.value || ''), searchQuery) ||
                   fuzzyMatch(option.id || '', searchQuery)
        })
    }, [options, searchQuery, isSearching])

    const nonSearchOptions = useMemo(
        () => [...selectedOptions, ...unselectedOptions],
        [selectedOptions, unselectedOptions]
    )

    const navigableOptions = isSearching ? filteredOptions : nonSearchOptions

    // Reset highlighted index when search query changes
    useEffect(() => {
        setHighlightedIndex(0)
    }, [searchQuery])

    useEffect(() => {
        if (!navigableOptions.length) {
            setHighlightedIndex(0)
            return
        }

        setHighlightedIndex(prev => Math.min(prev, navigableOptions.length - 1))
    }, [navigableOptions.length])

    useEffect(() => {
        if (!listRef.current || !navigableOptions.length) return

        const highlightedElement = listRef.current.querySelector(`[data-index="${highlightedIndex}"]`)
        highlightedElement?.scrollIntoView({block: 'nearest'})
    }, [highlightedIndex, isSearching, navigableOptions.length])

    useEffect(() => {
        if (!autoSelectSearchOnMount) return

        const frame = requestAnimationFrame(() => {
            inputRef.current?.focus()
            inputRef.current?.select()
        })

        return () => cancelAnimationFrame(frame)
    }, [autoSelectSearchOnMount])

    useEffect(() => {
        const container = listRef.current
        if (!container) return

        const targetId = scrollTargetRef.current
        scrollTargetRef.current = null

        if (targetId) {
            const el = document.getElementById(targetId)

            if (el && container.contains(el)) {
                const containerRect = container.getBoundingClientRect()
                const elementRect = el.getBoundingClientRect()
                const elementTop = elementRect.top - containerRect.top
                const elementBottom = elementTop + elementRect.height

                if (elementTop < 0) {
                    container.scrollTop += elementTop - 8
                } else if (elementBottom > containerRect.height) {
                    container.scrollTop += elementBottom - containerRect.height + 8
                }
            }
        }

        const selectionAction = pendingSelectionActionRef.current
        pendingSelectionActionRef.current = null
        pendingSelectionSourceRef.current = null

        if (selectionAction !== 'add' || !selectedSectionRef.current) {
            return
        }

        const containerRect = container.getBoundingClientRect()
        const selectedRect = selectedSectionRef.current.getBoundingClientRect()
        const selectedSectionIsVisible = selectedRect.bottom > containerRect.top && selectedRect.top < containerRect.bottom

        if (selectedSectionIsVisible) {
            if (addedToSelectedToastTimerRef.current) {
                clearTimeout(addedToSelectedToastTimerRef.current)
                addedToSelectedToastTimerRef.current = null
            }
            addedToSelectedToastCountRef.current = 0
            setAddedToSelectedToastCount(0)
            return
        }

        showAddedToSelectedToast()
    }, [selectedValues, showAddedToSelectedToast])

    useEffect(() => {
        return () => {
            if (addedToSelectedToastTimerRef.current) {
                clearTimeout(addedToSelectedToastTimerRef.current)
            }
        }
    }, [])

    const handleOptionClick = (option: Option, selectionSource: 'click' | 'keyboard' = 'click') => {
        const alreadySelected = isSelected(option)
        pendingSelectionActionRef.current = alreadySelected ? 'remove' : 'add'
        pendingSelectionSourceRef.current = selectionSource

        // Grab the next sibling's element id before the list reshuffles
        scrollTargetRef.current = null
        if (selectionSource === 'click' && navigableOptions.length > 1) {
            const idx = navigableOptions.findIndex(({id}) => id === option.id)
            const next = navigableOptions[idx + 1] ?? navigableOptions[idx - 1]
            if (next) scrollTargetRef.current = getOptionElementId(next.id)
        } else if (selectionSource === 'keyboard' && !alreadySelected) {
            scrollTargetRef.current = getOptionElementId(option.id)
        }

        if (alreadySelected) {
            onRemovedValue(option)
        } else {
            onSelectedValue(option)
        }

        if (isSearching) {
            setSearchQuery('')
            setHighlightedIndex(0)
        }

        focusAndSelectSearchInput()
    }

    const handleSelectAll = useCallback(() => {
        const selectedIdSet = new Set(selectedValues)

        options.forEach(option => {
            if (!selectedIdSet.has(option.id)) {
                onSelectedValue(option)
            }
        })

        focusAndSelectSearchInput()
    }, [options, selectedValues, onSelectedValue, focusAndSelectSearchInput])

    const handleDeselectAll = useCallback(() => {
        selectedOptions.forEach(option => onRemovedValue(option))
        focusAndSelectSearchInput()
    }, [selectedOptions, onRemovedValue, focusAndSelectSearchInput])

    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        const maxIndex = navigableOptions.length - 1

        switch (e.key) {
            case 'ArrowDown':
                if (!navigableOptions.length) return
                e.preventDefault()
                setHighlightedIndex(prev => Math.min(prev + 1, maxIndex))
                break
            case 'ArrowUp':
                if (!navigableOptions.length) return
                e.preventDefault()
                setHighlightedIndex(prev => Math.max(prev - 1, 0))
                break
            case 'Enter':
                if (!navigableOptions.length) return
                e.preventDefault()
                if (navigableOptions[highlightedIndex]) {
                    handleOptionClick(navigableOptions[highlightedIndex], 'keyboard')
                }
                break
            case 'Escape':
                if (isSearching) {
                    e.preventDefault()
                    setSearchQuery('')
                    setHighlightedIndex(0)
                }
                break
        }
    }

    const renderOptionItem = (option: Option, index: number, isHighlighted: boolean = false) => {
        const selected = isSelected(option)

        return (
            <div
                id={getOptionElementId(option.id)}
                key={option.id}
                data-index={index}
                data-option-id={option.id}
                onClick={() => handleOptionClick(option)}
                className={classNames(
                    "group w-full flex justify-between gap-2 px-4 py-[6px] rounded-3 cursor-pointer transition-all ease scale-100 --active:scale-[.99]",
                    {
                        "bg-gray-150 bg-opacity-50": isHighlighted,
                        "hover:bg-gray-150 hover:bg-opacity-40 -": selected,
                        "hover:bg-gray-150 hover:bg-opacity-40": !selected && !isHighlighted,
                    }
                )}
            >
                <div className="flex items-center gap-2">
                    {selected ? (
                        <div className="relative w-5 h-5 flex-shrink-0">
                            <CheckedIcon className="absolute inset-0 w-5 h-5 text-gray-750 transition-all duration-500 ease group-hover:opacity-0 --group-hover:scale-75"/>
                            <RemoveIcon className="absolute inset-0 w-5 h-5 text-gray-750 transition-all duration-500 ease opacity-0 scale-75 group-hover:opacity-100 group-hover:scale-110 group-active:scale-[.99]" />
                        </div>
                    ) : (
                        <div className="relative w-5 h-5 flex-shrink-0">
                            <div className="rounded-full border-px border-gray-200 min-w-5 min-h-5 max-w-5 max-h-5"></div>
                            <CheckedIcon className="absolute inset-0 w-5 h-5 text-gray-350 transition-all duration-500 ease opacity-0 scale-75 group-hover:opacity-100 group-hover:scale-100"/>
                        </div>
                    )}
                    <span className={classNames(
                        "text-1x font-normal",
                        {
                            "text-gray-850": selected,
                            "text-gray-700": !selected,
                        }
                    )}>
                    {option.label}
                </span>
                </div>
                {<div className="text-gray-400 self-end flex items-center gap-2">
                    {isHighlighted && (selected ? returnToDeselectHelperText : returnToSelectHelperText)}
                    {!isHighlighted && (selected ? <span className="text-smaller-1 text-gray-250">{__('Remove +')}</span> : <span className="text-smaller-1 text-gray-250">{__('Add +')}</span>)}
                </div>}
            </div>
        )
    }

    return (
        <div className={'grid gap-2 place-content-stretch w-full'}>
            <div className=" flex items-center justify-end gap-2">
                <button
                    type="button"
                    onClick={handleSelectAll}
                    className="flex items-center gap-1 text-smaller-1 text-gray-500 hover:text-gray-650 hover:bg-gray-150 rounded-full px-2 py-1 transition-colors"
                >
                    <StackedActionIcon action="select"/>
                    <span>{__('Select all')}</span>
                </button>
                {selectedOptions.length > 0 && (
                    <button
                        type="button"
                        onClick={handleDeselectAll}
                        className="flex items-center gap-1 text-smaller-1 text-gray-500 hover:text-gray-650 hover:bg-gray-150 rounded-full px-2 py-1 transition-colors"
                    >
                        <StackedActionIcon action="deselect"/>
                        <span>{__('Deselect all')}</span>
                    </button>
                )}
            </div>
            <Container alignment={'left'} padding={'p-0'} margin={'mt-0'}>
                {/* Options List */}
                <div className="relative flex flex-col gap-y-0 w-full">
                    {addedToSelectedToastCount > 0 && (
                        <div
                            className="pointer-events-none absolute top-2 left-1/2 -translate-x-1/2"
                            style={{zIndex: 2147483647}}
                        >
                            <div className="bg-gray-550 text-white rounded-2 px-4 py-2 text-smaller-1 shadow-lg whitespace-nowrap">
                                {addedToSelectedToastCount === 1
                                    ? __('Added 1 item to Selected')
                                    : __('Added * items to Selected').replace('*', String(addedToSelectedToastCount))}
                            </div>
                        </div>
                    )}
                    <div
                        ref={listRef}
                        className="overflow-y-auto w-full p-4"
                        style={{maxHeight}}
                    >
                        {isSearching ? (
                            // Filtered results when searching
                            filteredOptions.length === 0 ? (
                                <div className="px-4 py-3 text-gray-400 text-base w-full flex items-center justify-center">
                                    {__('No options found')}
                                </div>
                            ) : (
                                filteredOptions.map((option, index) =>
                                    renderOptionItem(option, index, index === highlightedIndex)
                                )
                            )
                        ) : (
                            // Split view: selected first, then unselected
                            <div className="flex flex-col gap-3">
                                {selectedOptions.length > 0 && (
                                    <div ref={selectedSectionRef}>
                                        <div className="px-4 py-2 text-smaller-1 font-normal text-gray-400">
                                            {__('Selected')}
                                        </div>
                                        {selectedOptions.map((option, index) =>
                                            renderOptionItem(option, index, index === highlightedIndex)
                                        )}
                                    </div>
                                )}
                                {unselectedOptions.length > 0 && (
                                    <div>
                                        <div className="px-4 py-2 text-smaller-1 font-normal text-gray-400">
                                            {__('Available')}
                                        </div>
                                        {unselectedOptions.map((option, index) => {
                                            const optionIndex = selectedOptions.length + index
                                            return renderOptionItem(option, optionIndex, optionIndex === highlightedIndex)
                                        })}
                                    </div>
                                )}
                                {selectedOptions.length === 0 && unselectedOptions.length === 0 && (
                                    <div className="px-4 py-3 text-gray-400 text-sm">
                                        {__('No options available')}
                                    </div>
                                )}
                            </div>
                        )}
                    </div>

                    {/* Search Input */}
                    <div className="--border-t --border-gray-200 pt-1 pb-4 px-4 w-full flex flex-col gap-1">
                        <div className="flex items-center gap-1 text-gray-300">
                            <p className="text-smaller-1 flex items-center gap-1">{navigateHelperText}</p>
                            <div className="min-w-1 min-h-1 max-w-1 max-h-1 rounded-full bg-gray-250"></div>
                            <p className="text-smaller-1 flex items-center gap-1">{returnHelperText}</p>
                        </div>
                        <input
                            ref={inputRef}
                            type="text"
                            value={searchQuery}
                            onChange={(e) => setSearchQuery(e.target.value)}
                            onKeyDown={handleKeyDown}
                            placeholder={placeholder || __('Search...')}
                            className="w-full py-1 px-5 border border-gray-150 rounded-[22px] text-base caret-gray-400 transition --bg-transparent focus:outline-none focus:border-blue-50  ring-gray-200 ring-opacity-30 ring-offset-1 placeholder:text-gray-300"
                        />
                    </div>
                </div>
            </Container>
        </div>
    )
}
