import { DebouncedFunc, debounce } from 'lodash-es';
import { useEffect, useMemo, useRef } from 'react';

export type DebounceOptions = {
  leading?: boolean;
  trailing?: boolean;
  maxWait?: number;
};

/**
 * Custom hook that creates a debounced version of a callback function
 *
 * The function being passed in does not need to be memoized externally.
 *
 * @param callback - The function to debounce
 * @param delay - The delay in milliseconds (default: 500)
 * @param options - Debounce options
 * @returns A debounced function with `cancel()` and `flush()` methods
 *
 * @example
 * const debouncedSearch = useDebounceCallback(searchApi, 300);
 * // Call debouncedSearch.cancel() to cancel pending invocations
 * // Call debouncedSearch.flush() to immediately invoke pending callback
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- `any` used for proper generic type inference
export default function useDebounceCallback<T extends (...args: any[]) => any>(
  callback: T,
  delay = 500,
  options?: DebounceOptions
): DebouncedFunc<T> {
  // Update underlying function when it changes
  const callbackRef = useRef(callback);
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Update debounced callback when any options change
  const { leading = false, trailing = true, maxWait } = options || {};
  const debouncedCallback = useMemo(() => {
    const options: DebounceOptions = { leading, trailing };
    if (maxWait !== undefined) {
      options.maxWait = maxWait;
    }
    return debounce(
      (...args: Parameters<T>) => callbackRef.current(...args),
      delay,
      options
    );
  }, [delay, leading, trailing, maxWait]);
  const debouncedCallbackRef = useRef<DebouncedFunc<T>>(debouncedCallback);

  useEffect(() => {
    // Debounced callback reference for use by the return value
    debouncedCallbackRef.current = debouncedCallback;

    // Cancel any pending debounced callbacks when unmounting
    return () => {
      debouncedCallback.cancel();
    };
  }, [debouncedCallback]);

  // Return a memoized function reference that will not change
  return useMemo(() => {
    const wrappedFunc: DebouncedFunc<T> = (...args: Parameters<T>) =>
      debouncedCallbackRef.current(...args);

    wrappedFunc.cancel = () => debouncedCallbackRef.current.cancel();
    wrappedFunc.flush = () => debouncedCallbackRef.current.flush();

    return wrappedFunc;
  }, []);
}
