import React, {FC, useState, useRef, useMemo, useEffect} from "react";
import Select, {components, Props} from "react-select";
import classNames from "classnames";
import {__, CouponsPlus} from "../../globals";
import {
    MultiSelectSearchItem,
    MultiSelectSearchItemContext,
    MultiSelectSearchItemOptions, MultiSelectSearchItemProps
} from "./MultiSelectSearchItem";
import AsyncSelect, {AsyncProps} from "react-select/async";
import {get, merge} from "lodash";
import {fieldsCache, fieldsCacheContainer} from "../FieldsCache";
import {Button} from "../cards/Button";

export type RemoteOptions = {
    type: 'remote',
    category: string, // the category of the data to fetch, e.g. 'products', 'categories', 'tags', 'users'
    where: string, //
    path: string,
    searchArgumentName?: string | 'search',
    labelProperty?: string, // the property to use as the name of the option, default is 'name'
    idProperty?: string, // the property to use as the id, default is 'id'
    excludeIdsArgumentName?: string,
    arguments?: Record<string, any>,
}

export type MultiSelectSearchProps<Option extends SelectOption> = {
    options: Option[] | RemoteOptions,
    onSelectedValue: (value: Option) => void,
    selectedValues?: Option[],
    selectedValue?: MultiSelectSearchProps<Option>['options'] extends RemoteOptions ? Option : Option['id'],
    autoFocus?: boolean,
    autoFocusTrigger?: number,
    placeholder?: () => string,
    isSearchable?: boolean,
    openMenuOnClick?: boolean,
    size?: 'large' | 'small' | 'extra-small',
    width?: 'full' | 'auto',
    style?: 'normal' | 'transparent-gray' | 'gray' | 'gray-lighter',
    showMode?: 'always' | 'focus-only',
    MultiSelectSearchItemProps?: Partial<MultiSelectSearchItemProps>,
    requiredLengthToSearch?: number,
    customOptionValueResolver?: (option: Option) => unknown,
    forceMenuToOpen?: boolean // false, designed for when you control the state outside.
}

export type SelectOption<T extends any = string> = {
    id: string,
    value: T,
    label: string,
}

// Check if the element's center point is visible in the viewport and not clipped by scrollable ancestors
const isElementVisibleInViewport = (element: HTMLElement | null): boolean => {
    if (!element || typeof window === 'undefined' || typeof document === 'undefined') {
        return false
    }

    const rect = element.getBoundingClientRect()
    const centerX = (rect.left + rect.right) / 2
    const centerY = (rect.top + rect.bottom) / 2

    const vw = window.innerWidth || document.documentElement.clientWidth
    const vh = window.innerHeight || document.documentElement.clientHeight

    if (centerX < 0 || centerX > vw || centerY < 0 || centerY > vh) {
        return false
    }

    let current: HTMLElement | null = element.parentElement
    while (current) {
        const styles = window.getComputedStyle(current)
        const clips = ['auto', 'scroll', 'hidden', 'clip', 'overlay']
        if (clips.includes(styles.overflowY) || clips.includes(styles.overflowX)) {
            const cr = current.getBoundingClientRect()
            if (centerX < cr.left || centerX > cr.right || centerY < cr.top || centerY > cr.bottom) {
                return false
            }
        }
        current = current.parentElement
    }

    return true
}

// When multiple MultiSelectSearch instances mount in the same React commit,
// only the last one should auto-focus (the newest/bottom-most one).
let pendingMountFocusRef: React.MutableRefObject<any> | null = null

export const MultiSelectSearch = <Option extends SelectOption>({
                                                                   options,
                                                                    onSelectedValue,
                                                                    selectedValues,
                                                                    selectedValue,
                                                                    autoFocus,
                                                                    autoFocusTrigger,
                                                                    placeholder,
                                                                    isSearchable,
                                                                    size = 'large',
                                                                   width = 'full',
                                                                   style = 'normal',
                                                                   MultiSelectSearchItemProps,
                                                                   requiredLengthToSearch = 3,
                                                                   customOptionValueResolver,
                                                                   openMenuOnClick,
                                                                   showMode = 'always',
                                                                   forceMenuToOpen
}: MultiSelectSearchProps<Option>) => {
    const [focused, setFocused] = useState(false);
    const [query, setQuery] = useState('');
    const selectRef = useRef<any>(null);
    const hasFocusedOnMountRef = useRef(false)
    const previousAutoFocusTriggerRef = useRef<number | undefined>(autoFocusTrigger)
    const [menuPortalTarget, setMenuPortalTarget] = useState<HTMLElement | null>(null);

    useEffect(() => {
        if (typeof document !== 'undefined') {
            setMenuPortalTarget(document.body);
        }
    }, []);

    if (Array.isArray(options) && ['string', 'number'].includes(typeof selectedValue)) {
        selectedValue = options.find(({id}) => id === selectedValue) as unknown as SelectOption['id']
    }

    const localOptions: Option[] = useMemo(() => {
        return Array.isArray(options) ? options : []
    }, [options])

    const isMulti = Array.isArray(selectedValues)
    const shouldAutoFocus = typeof autoFocus === 'boolean' ? autoFocus : isMulti
    const searchable = typeof isSearchable === 'undefined' ? (isMulti ? true : false) : isSearchable
    const excludeSelected = ({id}) => Array.isArray(selectedValues) ? !(selectedValues.find(option => option.id.toString() === id.toString())) : (selectedValue as SelectOption | undefined)?.id !== id

        useEffect(() => {
        if (!shouldAutoFocus) {
            return
        }

        const hasTriggerChanged = typeof autoFocusTrigger === 'number' && autoFocusTrigger !== previousAutoFocusTriggerRef.current
        previousAutoFocusTriggerRef.current = autoFocusTrigger

        if (hasFocusedOnMountRef.current && !hasTriggerChanged) {
            return
        }
        hasFocusedOnMountRef.current = true

        if (hasTriggerChanged) {
            // Explicit trigger (user selected/removed in THIS instance): focus directly
            requestAnimationFrame(() => {
                const focusTarget: HTMLElement | null = selectRef.current?.controlRef ?? selectRef.current?.inputRef ?? null
                if (!isElementVisibleInViewport(focusTarget)) return
                selectRef.current?.focus()
            })
        } else {
            // Mount focus: only the last-mounted instance in a batch gets focus.
            // Retries if not visible yet (e.g. inside animating popup/screen transition).
            pendingMountFocusRef = selectRef
            const attemptFocus = (retriesLeft: number) => {
                if (pendingMountFocusRef !== selectRef) return

                const focusTarget: HTMLElement | null = selectRef.current?.controlRef ?? selectRef.current?.inputRef ?? null
                if (!isElementVisibleInViewport(focusTarget)) {
                    if (retriesLeft > 0) {
                        setTimeout(() => attemptFocus(retriesLeft - 1), 350)
                    } else {
                        pendingMountFocusRef = null
                    }
                    return
                }

                pendingMountFocusRef = null
                selectRef.current?.focus()
            }
            requestAnimationFrame(() => attemptFocus(2))
        }
    }, [shouldAutoFocus, autoFocusTrigger])

    const optionsWithoutSelected = useMemo(
        () => localOptions.filter(excludeSelected),
        [options, selectedValues]
    )

    const type: 'local' | 'remote' = Array.isArray(options) ? 'local' : 'remote'
    const isRemote = type === 'remote';

    openMenuOnClick = typeof openMenuOnClick === 'undefined' ? !isRemote : openMenuOnClick;

    const SelectOrAsyncSelect = type === 'remote' ? AsyncSelect : Select

    let optionsToPass: Props<Option, false> | AsyncProps<Option, true, any>;

    if (isRemote) {
        optionsToPass = {
            loadOptions: (inputValue: string, resolve: (options: Option[]) => void) => {
                // Minimum characters to trigger search
                if (inputValue.length < requiredLengthToSearch) {
                    resolve([]);
                    return;
                }

                const remoteOptions = options as RemoteOptions;
                const category = remoteOptions.category
                const where = remoteOptions.where
                const labelProperty = remoteOptions.labelProperty || 'name'
                const idProperty = remoteOptions.idProperty || 'id'
                const isAdminAjax = where === 'admin-ajax.php'
                const needsNonce = isAdminAjax || remoteOptions.needsNonce

                const base = isAdminAjax ? where : get(CouponsPlus.urls, where || '')
                const endpoint = options.path || ''
                const uri = base + endpoint //`https://dummyjson.com/products/search?q=${encodeURIComponent(inputValue)}`; // Replace with your
                const parameters = {
                    [remoteOptions.searchArgumentName || 'search']: inputValue,
                    ...(remoteOptions.arguments || {})
                };

                if (isAdminAjax) {
                    parameters.action = 'woocommerce_json_search_products_and_variations'
                    parameters.couponsPlusMode = 'tree'
                }

                if (needsNonce) {
                    parameters.security = CouponsPlus.security.nonces.search
                    parameters.couponsPlusDashboardNonce = CouponsPlus.security.nonces.dashboard
                }

                if (typeof remoteOptions.excludeIdsArgumentName !== 'undefined') {
                    parameters[remoteOptions.excludeIdsArgumentName!] = selectedValues.map(option => option.id)
                }

                // Build query string from parameters
                const queryParams = new URLSearchParams();
                Object.entries(parameters).forEach(([key, value]) => {
                    queryParams.append(key, value);
                });

                // Check if URI already has query parameters
                const finalUrl = uri.includes('?')
                    ? `${uri}&${queryParams.toString()}`
                    : `${uri}?${queryParams.toString()}`;

                fetch(finalUrl, {
                    method: 'GET',
                    headers: {
                        'X-WP-Nonce': CouponsPlus.security.nonces.rest
                    },
                })
                    .then(response => {
                        if (!response.ok) {
                            throw new Error('Network response was not ok');
                        }
                        return response.json();
                    })
                    .then(data => {
                        const mappedOptions: Option[] = data.filter(excludeSelected).map((item: any) => {

                            const customOption = customOptionValueResolver?.(item);

                            const option = ({
                                id: item[idProperty].toString(),
                                // pass a custom options value resolver
                                value: typeof customOption !== 'undefined' ? customOption : (item.ancestors?.length ? {
                                    type: 'hierarchy',
                                    value: [
                                        ...item.ancestors,
                                        item[labelProperty]
                                    ],
                                } : item[labelProperty]),
                                label: item[labelProperty]
                            }) as SelectOption

                            fieldsCacheContainer.get(category).set(option);

                            return option;
                        });

                        resolve(mappedOptions);
                    }).catch(error => {
                    console.error('Error fetching options:', error);
                    resolve([]);
                });
            }
        };
    } else {
        optionsToPass = {
            options: optionsWithoutSelected
        };
    }

    // for single selects only, when true this iwll show the full selected option even if its large, if false, this will show an ellipsis
    const selectedOptionNoHiddenOverflow = !isMulti;

    // this is useful for places where we have the structure to select multiple options but a particular feature only supports one option,
    // we still want to show the label of the "selected" option (which is the only one supported)
    const onlyASingleOption = Array.isArray(options) && options.length === 1;

    const isHidden = showMode === 'focus-only' && !focused;
    const menuIsOpen = (forceMenuToOpen === true && focused) ? true : (searchable && !openMenuOnClick ? (query?.length || 0) !== 0 : undefined);

    const SearchLabelPlaceholder = __('Type to search')

    return <><SelectOrAsyncSelect
        ref={selectRef}
        {...optionsToPass}
        classNamePrefix="cp-select"
        blurInputOnSelect={false}
        isMulti={isMulti}
        onFocus={() => setFocused(true)}
        onBlur={() => {
            setFocused(false)
        }}
        onChange={value => {
            onSelectedValue(Array.isArray(value) ? value[0] : value);
        }}
        isDisabled={onlyASingleOption}
        isSearchable={searchable}
        value={!isMulti ? selectedValue : null} // always null when multi, we are going to show the selected values elsewhere
        placeholder={(isMulti ? (openMenuOnClick ? <div className="flex items-center gap-2">
            {placeholder?.() || <span>{SearchLabelPlaceholder}</span>}
            <span>{__('or')}</span>
            <div
                className="py-[2px] px-[6px] rounded-3 bg-gray-150 bg-opacity-70 text-gray-400 text-smaller-1 flex items-center gap-1 hover:cursor-pointer">
                <span>{__('click to open')}</span>
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5"
                     stroke="currentColor" className="h-3 w-3 text-gray-400">
                    <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
                </svg>
            </div>
        </div> : placeholder?.() || `${SearchLabelPlaceholder}...`) : placeholder?.())}
        components={{
            IndicatorSeparator: () => null,
            DropdownIndicator: onlyASingleOption ? null : DropdownIndicator(isMulti, size),
            MenuList: PortalMenuList,
            Option: (props) => <Option {...{...props, ...(MultiSelectSearchItemProps || {})}}
                                       itemOptions={{context: MultiSelectSearchItemContext.Menu} as MultiSelectSearchItemOptions}
                                       isMulti={isMulti}/>
        }}

        onInputChange={value => {
            setQuery(value);
        }}
        menuIsOpen={menuIsOpen}
        captureMenuScroll={false}
        menuShouldBlockScroll={false}
        noOptionsMessage={({inputValue}) => inputValue.length < requiredLengthToSearch ? __('Type at least 3 characters to search') : __('Nothing found.')}
        menuPortalTarget={menuPortalTarget || undefined}
        menuPosition={'fixed'}
        styles={{
            menu: (base) => ({
                ...base,
                marginTop: -1,
                animation: 'fadeInDown 0.15s ease-in-out',
            }),
            menuList: (base) => ({
                ...base,
            }),
            menuPortal: (base) => ({
                ...base,
                zIndex: 2147483647,
                pointerEvents: 'auto',
            }),
        }}
        classNames={{
            container: () => classNames({
                'inline-flex': width === 'auto',
                'hidden': isHidden,
            }),
            control: ({
                          isFocused,
                          menuIsOpen
                      }) => classNames('rounded-[24px] caret-gray-400 transition --bg-transparent active:scale-[.90] transition-all duration-200 hover:cursor-pointer', {
                'inline-flex': width === 'auto',
                '--bg-white bg-gray-100 bg-opacity-60 border-[1px]': style === 'normal',
                'border-none bg-transparent hover:bg-gray-200 hover:bg-opacity-60 hover:border-none': !menuIsOpen && style === 'transparent-gray',
                '!bg-gray-200 !bg-opacity-60 border-none': menuIsOpen && style === 'transparent-gray',
                'border-none bg-gray-200 bg-opacity-60': !menuIsOpen && style === 'transparent-gray',
                '!bg-gray-200 !bg-opacity-60 border-none --gray': style === 'gray',
                '!bg-gray-150 !bg-opacity-60 border-none --gray': style === 'gray-lighter',
                'px-3 h-11': size === 'large',
                'px-1 !min-h-7 h-7': size === 'small' && !searchable,
                'px-1 min-h-10 h-7': size === 'small' && searchable,
                'px-[2px] min-h-3': size === 'extra-small',
                'text-base ': size === 'large' || size === 'small',
                'text-smaller-1': size === 'extra-small',
                'border-gray-150': !isFocused,
                'border-blue-50 --border-gray-300 --ring --ring-[3px] --ring-blue-normal ring-gray-200 ring-opacity-30 ring-offset-1': isFocused && style === 'normal',
                'border-none '/*border-none bg-opacity-60 ring ring-[2px] ring-gray-150 ring-offset-1'*/: isFocused && style === 'transparent-gray',
                'shadow-none': isFocused,
                'rounded-b-none': isMulti && menuIsOpen,
                '!flex-wrap-initial': selectedOptionNoHiddenOverflow,
            }),
            placeholder: () => 'text-gray-300 text-base',
            menu: () => classNames('shadow-none rounded-2 border-px border-gray-150 bg-white px-2 py-2 max-w-auto', {
                'rounded-t-0 -mt-px border-t-0': isMulti,
                'w-[max-content]': !isMulti,
            }),
            option: ({
                         isSelected,
                         isFocused
                     }) => classNames('px-4 rounded-3 hover:!bg-gray-100 text-base text-gray-650', {
                '!bg-gray-100': isFocused,
                '!bg-transparent': !isFocused,
                'bg-none': isSelected,
            }),
            singleValue: (props) => classNames('text-semibold', {
                'text-gray-700': style !== 'gray-lighter',
                'text-gray-550': style === 'gray-lighter',
                'text-base': size === 'large',
                'text-smaller-1': size === 'small' || size === 'extra-small',
                '!overflow-initial': selectedOptionNoHiddenOverflow,
            }),
            valueContainer: (props) => classNames({
                'pr-0': !isMulti && !onlyASingleOption
            })
        }}
    />
        {isHidden && <Button size={'extra-small'} onClick={() => {
            setFocused(true)
            setTimeout(() => {
                selectRef.current?.focus()
            }, 100)
        }}>{__('Select items')}</Button>}
    </>;
}

function PortalMenuList(props: any) {
    const originalOnWheel = props.innerProps?.onWheel

    return (
        <components.MenuList
            {...props}
            innerProps={{
                ...(props.innerProps || {}),
                onWheel: (event: React.WheelEvent<HTMLDivElement>) => {
                    const list = event.currentTarget

                    if (list.scrollHeight > list.clientHeight) {
                        event.preventDefault()
                        event.stopPropagation()

                        const maxScrollTop = list.scrollHeight - list.clientHeight
                        list.scrollTop = Math.max(0, Math.min(maxScrollTop, list.scrollTop + event.deltaY))
                        return
                    }

                    if (typeof originalOnWheel === 'function') {
                        originalOnWheel(event)
                    }
                },
            }}
        />
    )
}

function DropdownIndicator(isMulti: boolean, size: "large" | "small" | "extra-small" = 'large') {
    return (props: any) => {
        const iconClasses = classNames('text-gray-400', {
            'h-5 w-5': size === 'large',
            'h-4 w-4': size === 'small' || size === 'extra-small',
        })
        return (
            <components.DropdownIndicator {...props} className={classNames(`${props.className}  ${!isMulti ? 'pl-0' : ''}`, {
                'p-2': size === 'large',
                'p-1': size === 'small',
                'p-[2px]': size === 'extra-small',
            })}>
                {isMulti ? <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5"
                                stroke="currentColor" className={iconClasses}>
                    <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
                </svg> : <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
                              className={iconClasses}>
                    <path fillRule="evenodd"
                          d="M11.47 4.72a.75.75 0 0 1 1.06 0l3.75 3.75a.75.75 0 0 1-1.06 1.06L12 6.31 8.78 9.53a.75.75 0 0 1-1.06-1.06l3.75-3.75Zm-3.75 9.75a.75.75 0 0 1 1.06 0L12 17.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-3.75 3.75a.75.75 0 0 1-1.06 0l-3.75-3.75a.75.75 0 0 1 0-1.06Z"
                          clipRule="evenodd"/>
                </svg>}
            </components.DropdownIndicator>
        );
    }
        ;
}

function Option(props: any) {
    return (
        <components.Option {...props} className={'active:scale-[.97] transition-all duration-75'}>
            <MultiSelectSearchItem data={props.data}
                                   options={props.options ? merge(props.itemOptions, props.options) : props.itemOptions}
                                   isMulti={props.isMulti}
                                   showID={typeof props.showID !== 'undefined' ? props.showID : props.isMulti}/>
        </components.Option>
    );
}
