import * as React from "react";
import { useState, useEffect, useCallback, useRef } from "@wordpress/element";
import {
  Popover,
  __experimentalInputControl as InputControl,
} from "@wordpress/components";
import { __ } from "@wordpress/i18n";
import { debounce } from "lodash";
import {
  Container,
  SuggestionList,
  SuggestionItem,
  Footer,
  StyledSpinner,
  InputWrapper,
  SuffixWrapper,
  Help
} from "./AutoComplete.styles";

export type FetchSuggestionsOptions<T> = {
  page?: number;
  signal?: AbortSignal;
  selectedValue?: T | null;
  activeFilter?: string | number;
  suggestions?: T[];
};

export type AutoCompleteProps<T, C extends React.ElementType = "div"> = Omit<
  React.ComponentPropsWithoutRef<C>,
  "value" | "defaultValue" | "onInput" | "onFocus" | "onBlur"
> & {
  as?: C;
  inputRef?: React.RefObject<HTMLInputElement>;
  focusRef?: React.RefObject<HTMLDivElement>;
  label?: string;
  defaultValue?: T | null;
  fetchSuggestions: (
    query: string,
    options?: FetchSuggestionsOptions<T>
  ) => Promise<{ items: T[]; totalResults: number }>;
  renderSuggestion: (item: T, query: string, value?: T | null) => React.ReactNode;
  formatValue?: (item: T, query: string, value?: T | null) => string;
  items?: T[];
  placeholder?: string;
  help?: React.JSX.Element;
  selectOnFocus?: boolean;
  valueProp: string;
  keyProvider?: keyof T | ((item: T) => string);
  suffix?: React.JSX.Element | string;
  noResultsMessage?: string;
  onItemSelect?: (item: T, query: string, value: T | null) => void;
  onItemChange?: (item: T, query: string, value: T | null) => void;
  onInput?: (value: string) => void;
  onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
  onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
};
export const AutoComplete = <T, C extends React.ElementType = "div">({
  focusRef: passedFocusRef,
  inputRef: passedInputRef,
  label,
  defaultValue,
  fetchSuggestions,
  renderSuggestion,
  valueProp,
  help,
  placeholder = __("Search...", "justwatch-partner-integrations"),
  selectOnFocus = true,
  keyProvider,
  suffix,
  noResultsMessage = __("No results found", "justwatch-partner-integrations"),
  onItemSelect,
  onItemChange,
  onInput,
  onFocus,
  onBlur,
  ...props
}: AutoCompleteProps<T, C>): JSX.Element => {
  const generateKey = (item?: T | null): string => item
    ? typeof keyProvider === "function"
      ? keyProvider(item)
      : String(item[keyProvider as keyof T] ?? JSON.stringify(item))
    : "";

  const formatValue = (item: T | null) => {
    const valuePropValue = item && item[valueProp as keyof T];
    const formattedValue = valuePropValue ? String(valuePropValue) : "";

    return formattedValue;
  };

  const focusRef = passedFocusRef ?? useRef<HTMLDivElement | null>(null);
  const suffixRef = useRef<HTMLDivElement | null>(null);
  const inputRef = passedInputRef ?? useRef<HTMLInputElement | null>(null);
  const initialQuery = defaultValue ? formatValue(defaultValue) : "";
  const initialSuggestions = defaultValue ? [defaultValue] : [];

  const [query, setQuery] = useState(initialQuery);
  const [selectedValue, setSelectedValue] = useState<T | null>(defaultValue || null);
  const [suggestions, setSuggestions] = useState<T[]>(initialSuggestions);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [isVisible, setVisible] = useState(false);
  const [containerWidth, setContainerWidth] = useState(0);
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(false);
  const [loadingMore, setLoadingMore] = useState(false);
  const popoverRef = useRef<HTMLDivElement | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);
  const suggestionsWrapperRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (defaultValue) {
      setQuery(formatValue(defaultValue));
      setSuggestions([defaultValue]);
    }
  }
  , [defaultValue]);

  const fetchData = useCallback(
    debounce(
      async () => {
        if (abortControllerRef.current) {
          abortControllerRef.current.abort(__("Cancelled", "justwatch-partner-integrations"));
        }

        const abortController = new AbortController();
        abortControllerRef.current = abortController;

        const options = {
          page,
          signal: abortController.signal,
          selectedValue,
          suggestions,
        };

        try {
          setLoading(true);
          setError(null);
          const { items = [], totalResults = 0 } = await fetchSuggestions(query, options);

          setSuggestions((prevItems) => {
            if (page === 1) {
              return items;
            }

            return [...prevItems, ...items];
          });

          const hasMore = items.length < totalResults;

          setLoading(false);
          setLoadingMore(false);
          setHasMore(hasMore);
        } catch (error) {
          if (!abortController.signal.aborted) {
            setLoading(false);
            setLoadingMore(false);
            setError(error as Error);
          }
        } finally {
          setLoading(false);
          setLoadingMore(false);
        }
      },
      100
    ), [query, page, selectedValue, fetchSuggestions]
  );

  useEffect(() => {
    if (!focusRef.current) return;

    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        setContainerWidth(entry.contentRect.width);
      }
    });

    resizeObserver.observe(focusRef.current);

    return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
    fetchData();
  }, [query, page, fetchSuggestions]);

  const handleInputChange = (newValue?: string) => {
    setPage(1);
    setHighlightedIndex(-1);
    setQuery(newValue || "");
    setLoading(true);
    onInput?.(newValue || "");
  };

  const compareItems = (a: T, b?: T | null) => {
    if (!b) return false;
    const aKey = generateKey(a);
    const bKey = generateKey(b);

return aKey === bKey;
  };

  const handleSelect = (item: T) => {
    const oldValue = selectedValue;

    onItemSelect?.(item, query, oldValue);

    if (!compareItems(item, selectedValue)) {
      setSelectedValue(item);
      onItemChange?.(item, query, oldValue);
    }

    setVisible(false);
    setSuggestions([item]);

    const formattedValue = formatValue(item);
    setQuery(formattedValue);
    requestAnimationFrame(() => {
      if (inputRef.current) {
        inputRef.current.value = formattedValue;
        inputRef.current.blur();
      }
    });
  };

  const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
    const popoverFocus = popoverRef.current && popoverRef.current.contains(event.relatedTarget as Node);

    const suffixFocus = suffixRef.current && (
      suffixRef.current.contains(event.target as Node)
      || suffixRef.current.contains(event.relatedTarget as Node)
    );

    if (popoverFocus || suffixFocus) {
      return;
    }

    if (event.relatedTarget === focusRef.current) {
      return;
    }

    if (suffixFocus) {
      return;
    }

    setVisible(true);

    if (selectOnFocus) {
      inputRef.current?.select();
    }
    onFocus?.(event);
  };

  const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
    const popoverFocus = popoverRef.current && popoverRef.current.contains(event.relatedTarget as Node);

    if (popoverFocus) {
      return;
    }

    const focusRefFocus = focusRef.current && focusRef.current.contains(event.relatedTarget as Node);

    if (focusRefFocus) {
      return;
    }

    if (event.target === inputRef.current) {
      return;
    }

    setVisible(false);

    if (selectedValue) {
      const newQuery = formatValue(selectedValue);

      setQuery(newQuery);
      setSuggestions([selectedValue]);
    }

    onBlur?.(event);
  };

  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (!suggestions.length) return;

    if (event.key === "ArrowDown") {
      setHighlightedIndex((prevIndex) => Math.min(prevIndex + 1, suggestions.length - 1));
      event.preventDefault();
    } else if (event.key === "ArrowUp") {
      setHighlightedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
      event.preventDefault();
    } else if (event.key === "Enter" && highlightedIndex >= 0) {
      handleSelect(suggestions[highlightedIndex]);
      event.preventDefault();
    }
  };
  useEffect(() => {
    if (!suggestionsWrapperRef.current || highlightedIndex < 0) return;

    const list = suggestionsWrapperRef.current.querySelector('[role="list"]');

    if (!list) return;

    const highlightedItem = list.children[highlightedIndex] as HTMLElement | undefined;

    if (highlightedItem) {
      highlightedItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
    }
  }, [highlightedIndex]);

  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    const target = event.currentTarget;
    if (!target) return; // Ensure target is not null

    const { scrollTop, scrollHeight, clientHeight } = target;

    if (scrollHeight - scrollTop <= clientHeight + 50 && hasMore && !loadingMore) {
      setLoadingMore(true);
      setPage((prevPage) => prevPage + 1);
    }
  };

  const currentValue = query;
  const suggestionHeader = null;

  const handleSuffixMouseDown = () => {
    if (query) {
      setVisible(true);
    }
  };

  const handleSuffixChange = () => {
    window.requestAnimationFrame(() => {
      if (focusRef.current) {
        focusRef.current.focus();
      }
    });
  };

  const isPopoverOpen = isVisible && !!query;

  return (
    <>
      <div
        ref={focusRef}
        tabIndex={0}
        onFocus={handleFocus}
        onBlur={handleBlur}
        {...props}
      >
        <InputWrapper>
          <InputControl
            __next40pxDefaultSize
            ref={inputRef}
            label={label}
            placeholder={placeholder}
            value={currentValue}
            onChange={handleInputChange}
            onKeyDown={handleKeyDown}
            autoComplete="off"
            suffix={suffix ? (
              <div ref={suffixRef} tabIndex={-1}>
                <SuffixWrapper
                  onMouseDown={handleSuffixMouseDown}
                  onChange={handleSuffixChange}
                  className="jw-suffix-wrapper"
                >
                  {suffix}
                </SuffixWrapper>
              </div>

            ) : undefined}
          />
        </InputWrapper>
        {isPopoverOpen && (
          <Popover
            ref={popoverRef}
            tabIndex={-1}
            focusOnMount={false}
            anchor={inputRef.current}
            onClose={() => setVisible(false)}
            placement="bottom-start"
          >
            <Container style={{ width: containerWidth, zIndex: 100 }} onScroll={handleScroll}>
              {suggestionHeader}
              <div ref={suggestionsWrapperRef}>
                <SuggestionList
                  isBordered
                  isSeparated
                >
                  {suggestions.map((item, index) => (
                    <SuggestionItem
                      key={generateKey(item)}
                      onMouseDown={() => handleSelect(item)}
                      highlighted={index === highlightedIndex || undefined}
                    >
                      {renderSuggestion(item, query, selectedValue)}
                    </SuggestionItem>
                  ))}
                </SuggestionList>
              </div>
              {(loading || loadingMore || error || query && suggestions.length === 0) && (
                <Footer>
                  {error ? (
                    <div className="jw-error">{error.message}</div>
                  ) : (loading || loadingMore) ? (
                    <>
                      <StyledSpinner />
                      <span>{__("Loading...", "justwatch-partner-integrations")}</span>
                    </>
                  ) : suggestions.length === 0 ? (
                    <span>{noResultsMessage}</span>
                  ) : (
                    null
                  )}
                </Footer>
              )}
            </Container>
          </Popover>
        )}
      </div>
      <Help>{help}</Help>
    </>
  );
};

export default AutoComplete;