import { useDebouncedValue } from "@simplybusiness/mobius-hooks";
import { useEffect, useRef, useState } from "react";
import type { ComboboxOption, ComboboxOptions, ComboboxProps } from "./types";
import { filterOptions } from "./utils";

export type UseComboboxOptionsProps<T extends ComboboxOption> = Pick<
  ComboboxProps<T>,
  "options" | "asyncOptions" | "delay" | "minSearchLength"
> & {
  skipNextDebounceRef?: React.MutableRefObject<boolean>;
  inputValue?: string;
  onSearched?: (searchTerm: string) => void;
};

export function useComboboxOptions<T extends ComboboxOption>({
  options,
  asyncOptions,
  delay = 300,
  minSearchLength = 3,
  inputValue = "",
  skipNextDebounceRef,
  onSearched,
}: UseComboboxOptionsProps<T>) {
  const [filteredOptions, setFilteredOptions] = useState<
    ComboboxOptions<T> | undefined
  >(undefined);
  const debouncedInputValue = useDebouncedValue(
    inputValue,
    // Don't debounce synchronous options
    options ? 0 : delay,
  );
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  // Keep refs to latest callbacks so the fetch effect doesn't re-run when
  // their references change (e.g. due to un-memoised props in parent)
  const asyncOptionsRef = useRef(asyncOptions);
  asyncOptionsRef.current = asyncOptions;
  const onSearchedRef = useRef(onSearched);
  onSearchedRef.current = onSearched;

  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;

    const fetchOptions = async () => {
      setIsLoading(true);
      setError(null);
      try {
        if (asyncOptionsRef.current) {
          if (debouncedInputValue.length < minSearchLength) {
            setFilteredOptions(undefined);
            return;
          }
          const result = await asyncOptionsRef.current(debouncedInputValue, {
            signal,
          });
          setFilteredOptions(result);
          onSearchedRef.current?.(debouncedInputValue);
        } else if (options) {
          setFilteredOptions(filterOptions(options, debouncedInputValue));
        } else {
          setError(new Error("No options provided"));
        }
      } catch (e: unknown) {
        if (e instanceof DOMException && e.name === "AbortError") {
          // Ignore abort errors
          return;
        }
        setError(e as Error);
      } finally {
        setIsLoading(false);
      }
    };

    if (!skipNextDebounceRef?.current) {
      void fetchOptions();
    } else {
      skipNextDebounceRef.current = false;
    }

    return () => {
      controller.abort();
    };
  }, [
    debouncedInputValue,
    options,
    delay,
    minSearchLength,
    skipNextDebounceRef,
  ]);

  function updateFilteredOptions(newOptions: Promise<ComboboxOptions<T>>) {
    setIsLoading(true);
    return newOptions
      .then(setFilteredOptions)
      .catch(setError)
      .finally(() => setIsLoading(false));
  }

  return {
    filteredOptions,
    updateFilteredOptions,
    isLoading,
    error,
    isError: error != null,
  };
}
