import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { useState, useMemo, useRef, useCallback, useEffect, } from "react";
import { useFormContext } from "react-hook-form";
import { Input } from "./Input";
import { Button } from "./Button";
import { Dialog, Loader } from "../utils";
import { normalizeIconClass } from "../utils/iconUtils";
const SearchSelectInputInner = React.forwardRef(function SearchSelectInput({ value, onChange, onSearchPromiseFn, onSingleSearchPromiseFn, onSelectOption, dialogTitle = "Seleccione una opción", icon = "fa-search", iconPosition = "right", noResultsText = "Sin resultados", getOptionLabel, getOptionValue, getOptionDescription, renderOption, label, readOnly = false, ...inputProps }, ref) {
    const [inputText, setInputText] = useState("");
    const [dialogInputText, setDialogInputText] = useState("");
    const [isDialogOpen, setIsDialogOpen] = useState(false);
    const [options, setOptions] = useState([]);
    const [isLoading, setIsLoading] = useState(false);
    const [hasSearched, setHasSearched] = useState(false);
    const inputRef = useRef(null);
    const dialogInputRef = useRef(null);
    const justClearedRef = useRef(false);
    // Detectar modo register
    const isRegisterMode = useMemo(() => {
        return "name" in inputProps && inputProps.name !== undefined;
    }, [inputProps]);
    const fieldName = isRegisterMode && "name" in inputProps
        ? inputProps.name
        : undefined;
    // Obtener setValue del contexto del formulario
    // Para usar con register, el formulario debe estar dentro de FormProvider
    // useFormContext debe llamarse incondicionalmente (requisito de React Hooks)
    // Si no hay FormProvider y se usa en modo register, useFormContext lanzará un error
    // Para usar sin FormProvider, usar Controller en lugar de register
    const formContext = useFormContext();
    const setValue = formContext?.setValue;
    // Combinar refs
    const combinedRef = useCallback((node) => {
        inputRef.current = node;
        if (typeof ref === "function") {
            ref(node);
        }
        else if (ref) {
            ref.current = node;
        }
    }, [ref]);
    const valueGetter = useCallback((item) => {
        if (getOptionValue)
            return getOptionValue(item);
        return item["value"];
    }, [getOptionValue]);
    const labelGetter = useCallback((item) => {
        if (getOptionLabel)
            return getOptionLabel(item);
        return item["label"];
    }, [getOptionLabel]);
    const descriptionGetter = useCallback((item) => {
        if (getOptionDescription)
            return getOptionDescription(item);
        return item["description"];
    }, [getOptionDescription]);
    const handleSearch = async (text) => {
        setIsLoading(true);
        setHasSearched(true);
        const options = await onSearchPromiseFn(text);
        if (options instanceof Array) {
            setOptions(options);
        }
        else {
            setOptions(options.list);
        }
        setIsLoading(false);
    };
    const handleSelect = (option) => {
        const selectedValue = valueGetter(option);
        setIsDialogOpen(false);
        // En modo register, setear el valor usando setValue o actualizando el input nativo
        if (isRegisterMode) {
            if (setValue && fieldName) {
                setValue(fieldName, selectedValue, {
                    shouldValidate: true,
                    shouldDirty: true,
                });
            }
            else if (inputRef.current) {
                const nativeInput = inputRef.current;
                const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
                if (nativeInputValueSetter) {
                    nativeInputValueSetter.call(nativeInput, String(selectedValue ?? ""));
                }
                else {
                    nativeInput.value = String(selectedValue ?? "");
                }
                if (onChange) {
                    const changeEvent = {
                        target: nativeInput,
                        currentTarget: nativeInput,
                    };
                    onChange(changeEvent);
                }
                const inputEvent = new Event("input", {
                    bubbles: true,
                    cancelable: true,
                });
                nativeInput.dispatchEvent(inputEvent);
                const changeEventNative = new Event("change", {
                    bubbles: true,
                    cancelable: true,
                });
                nativeInput.dispatchEvent(changeEventNative);
            }
        }
        else {
            onChange?.(selectedValue);
        }
        onSelectOption?.(option, selectedValue);
        setInputText(labelGetter(option));
        setDialogInputText("");
        setOptions([]);
        setHasSearched(false);
    };
    // Función para sincronizar inputText con un valor
    const syncInputText = useCallback((currentValue) => {
        if (currentValue === undefined ||
            currentValue === null ||
            currentValue === "") {
            setInputText("");
            return;
        }
        // Si currentValue es un objeto (T) y tenemos getOptionLabel, usar directamente
        if (typeof currentValue === "object" &&
            getOptionLabel &&
            !getOptionValue) {
            try {
                const label = getOptionLabel(currentValue);
                setInputText(label);
                return;
            }
            catch {
                // Si falla, continuar con la búsqueda normal
            }
        }
        // Normalizar el value: si es un objeto (T), extraer el valor usando valueGetter
        let valueToSearch = currentValue;
        if (typeof currentValue === "object" && getOptionValue) {
            // Si currentValue es un objeto y tenemos getOptionValue, extraer el valor
            try {
                valueToSearch = getOptionValue(currentValue);
            }
            catch {
                // Si falla, no podemos usar onSingleSearchPromiseFn con un objeto
                // Intentar mostrar el label directamente si está disponible
                if (getOptionLabel) {
                    try {
                        setInputText(getOptionLabel(currentValue));
                        return;
                    }
                    catch {
                        setInputText(String(currentValue));
                        return;
                    }
                }
                setInputText(String(currentValue));
                return;
            }
        }
        // Buscar en las opciones actuales
        const matchingOption = options.find((opt) => valueGetter(opt) === valueToSearch ||
            valueGetter(opt) === currentValue);
        if (matchingOption) {
            setInputText(labelGetter(matchingOption));
            return;
        }
        // Si no se encuentra en las opciones actuales, usar onSingleSearchPromiseFn
        if (onSingleSearchPromiseFn &&
            valueToSearch !== undefined &&
            valueToSearch !== null) {
            onSingleSearchPromiseFn(valueToSearch)
                .then((foundOption) => {
                if (foundOption) {
                    setInputText(labelGetter(foundOption));
                    // Agregar la opción a las opciones disponibles si no está ya
                    setOptions((prev) => {
                        if (!prev.find((opt) => valueGetter(opt) === valueGetter(foundOption))) {
                            return [...prev, foundOption];
                        }
                        return prev;
                    });
                }
                else {
                    // Si no se encuentra, mostrar el valor como string
                    setInputText(String(valueToSearch));
                }
            })
                .catch((error) => {
                console.error("Error al buscar opción individual:", error);
                setInputText(String(valueToSearch));
            });
        }
        else {
            // Si no hay onSingleSearchPromiseFn, mostrar el valor como string
            setInputText(String(valueToSearch));
        }
    }, [
        options,
        getOptionValue,
        getOptionLabel,
        onSingleSearchPromiseFn,
        valueGetter,
        labelGetter,
    ]);
    // Sincronizar inputText cuando cambia el value (modo controlado)
    useEffect(() => {
        if (!isRegisterMode) {
            syncInputText(value);
        }
    }, [value, isRegisterMode, syncInputText]);
    // Sincronizar inputText cuando cambia el valor del formulario (modo register)
    useEffect(() => {
        if (isRegisterMode && formContext && fieldName) {
            // Sincronizar inicialmente
            const formValue = formContext.watch(fieldName);
            syncInputText(formValue);
            // Suscribirse a cambios del formulario
            const subscription = formContext.watch((_data, { name }) => {
                // Solo sincronizar cuando cambia el campo específico
                if (name === fieldName) {
                    const currentFormValue = formContext.watch(fieldName);
                    syncInputText(currentFormValue);
                }
            });
            return () => subscription.unsubscribe();
        }
    }, [isRegisterMode, formContext, fieldName, syncInputText]);
    // Hacer blur en el input del dialog cuando se abre
    useEffect(() => {
        if (isDialogOpen) {
            // El Dialog renderiza condicionalmente, así que necesitamos esperar a que el input esté montado
            // Usar requestAnimationFrame doble para asegurar que el DOM esté completamente renderizado
            const timeoutId = setTimeout(() => {
                requestAnimationFrame(() => {
                    dialogInputRef.current?.focus();
                });
            }, 50);
            return () => clearTimeout(timeoutId);
        }
    }, [isDialogOpen]);
    const getDialogBody = () => {
        return (_jsxs("div", { children: [_jsx("div", { className: "mb-2", children: _jsx(Input, { ref: dialogInputRef, value: dialogInputText, onChange: (e) => setDialogInputText(e.target.value), icon: icon, iconPosition: iconPosition, onIconClick: () => handleSearch(dialogInputText), onKeyDown: (e) => {
                            if (e.key === "Enter") {
                                e.preventDefault();
                                handleSearch(dialogInputText);
                            }
                        } }) }), _jsx("div", { children: _jsx(Loader, { isLoading: isLoading, children: !hasSearched || options.length > 0 ? (_jsx("ul", { className: "space-y-1 max-h-96 overflow-y-auto", children: _jsx("ul", { className: "space-y-1 max-h-96 overflow-y-auto", children: options.map((option, index) => {
                                    const label = labelGetter(option);
                                    const description = descriptionGetter(option);
                                    const anyOption = option;
                                    return (_jsx("li", { className: "px-3 py-2 cursor-pointer rounded-md flex items-start gap-2 text-sm\r\n                        text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors", onClick: () => handleSelect(option), children: renderOption ? (renderOption(option)) : (_jsxs(_Fragment, { children: [anyOption.icon && (_jsx("i", { className: `${normalizeIconClass(anyOption.icon)} mt-0.5 text-[var(--color-text-muted)]` })), _jsxs("div", { className: "flex flex-col flex-1", children: [_jsx("span", { className: "font-[var(--font-default)]", children: label }), description !== undefined &&
                                                            description !== null && (_jsx("span", { className: "text-xs text-[var(--color-text-secondary)]", children: description }))] })] })) }, String(valueGetter(option) ?? label ?? index)));
                                }) }) })) : (_jsx("div", { className: "px-3 py-8 text-center text-sm text-[var(--color-text-secondary)]", children: noResultsText })) }) })] }));
    };
    // Detectar si hay un valor seleccionado
    const hasValue = inputText !== "" && inputText !== undefined && inputText !== null;
    // Función para limpiar el valor y abrir el dialog
    const handleIconClick = useCallback((event) => {
        if (readOnly)
            return;
        event.preventDefault();
        event.stopPropagation();
        if (hasValue) {
            // Si hay valor, limpiarlo
            // Marcar que acabamos de limpiar para prevenir que onFocus abra el diálogo
            justClearedRef.current = true;
            if (isRegisterMode) {
                if (setValue && fieldName) {
                    setValue(fieldName, undefined, {
                        shouldValidate: true,
                        shouldDirty: true,
                    });
                }
                else if (inputRef.current) {
                    const nativeInput = inputRef.current;
                    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
                    if (nativeInputValueSetter) {
                        nativeInputValueSetter.call(nativeInput, "");
                    }
                    else {
                        nativeInput.value = "";
                    }
                    if (onChange) {
                        const changeEvent = {
                            target: nativeInput,
                            currentTarget: nativeInput,
                        };
                        onChange(changeEvent);
                    }
                    const inputEvent = new Event("input", {
                        bubbles: true,
                        cancelable: true,
                    });
                    nativeInput.dispatchEvent(inputEvent);
                    const changeEventNative = new Event("change", {
                        bubbles: true,
                        cancelable: true,
                    });
                    nativeInput.dispatchEvent(changeEventNative);
                }
            }
            else {
                onChange?.(undefined);
            }
            setInputText("");
            setIsDialogOpen(false);
            // Resetear el flag después de un pequeño delay para permitir que otros eventos se procesen
            setTimeout(() => {
                justClearedRef.current = false;
            }, 100);
        }
        else {
            // Si no hay valor, abrir el dialog
            setIsDialogOpen(true);
        }
    }, [hasValue, isRegisterMode, setValue, fieldName, onChange, readOnly]);
    // Determinar qué ícono mostrar: si hay valor, mostrar "fa-times", sino el ícono original
    // Si está en readOnly, no mostrar ningún ícono
    const displayIcon = readOnly ? undefined : hasValue ? "fa-times" : icon;
    const displayIconPosition = readOnly ? undefined : iconPosition;
    const displayOnIconClick = readOnly ? undefined : handleIconClick;
    return (_jsxs(_Fragment, { children: [_jsx(Input, { ...inputProps, ref: combinedRef, label: label, value: inputText, onChange: (e) => {
                    if (readOnly)
                        return;
                    setInputText(e.target.value);
                }, onFocus: () => {
                    if (!readOnly && !justClearedRef.current) {
                        setIsDialogOpen(true);
                    }
                }, icon: displayIcon, iconPosition: displayIconPosition, onIconClick: displayOnIconClick, readOnly: readOnly }), !readOnly && (_jsx(Dialog, { isOpen: isDialogOpen, title: dialogTitle, footer: _jsx(Button, { variant: "outline", onClick: () => setIsDialogOpen(false), children: "Cerrar" }), onClose: () => setIsDialogOpen(false), children: getDialogBody() }))] }));
});
SearchSelectInputInner.displayName = "SearchSelectInput";
// Exportar con el cast genérico para permitir uso como <SearchSelectInput<T, K>>
export const SearchSelectInput = SearchSelectInputInner;
