import { useOnUnmount } from "@simplybusiness/mobius-hooks";
import classNames from "classnames/dedupe";
import type React from "react";
import type { FocusEvent } from "react";
import { useEffect, useId, useRef, useState } from "react";
import { useBreakpoint } from "../../hooks";
import { TextField } from "../TextField";
import { VisuallyHidden } from "../VisuallyHidden";
import { Listbox } from "./Listbox"; // Import Listbox component
import type { ComboboxOption, ComboboxProps } from "./types";
import { useComboboxHighlight } from "./useComboboxHighlight";
import { useComboboxOptions } from "./useComboboxOptions";
import { getOptionLabel, getOptionValue, isOptionGroup } from "./utils";
import "./Combobox.css";

const ComboboxInner = <T extends ComboboxOption>({
  ref,
  ...props
}: ComboboxProps<T>) => {
  const {
    id,
    defaultValue,
    value,
    options,
    asyncOptions,
    delay,
    minSearchLength,
    onSelected,
    className,
    placeholder,
    icon,
    onBlur,
    onFocus,
    onChange,
    // onSearched, // unused prop, consider removing
    optionComponent,
    optionTestIdPrefix,
    errorMessage,
    ...otherProps
  } = props;
  // Avoid re-fetching after selecting an option
  const skipNextDebounceRef = useRef(false);
  const fallbackRef = useRef<HTMLInputElement>(null);
  const [inputValue, setInputValue] = useState(defaultValue || "");
  const [isOpen, setIsOpen] = useState(false);
  const [isChanging, setIsChanging] = useState(false);
  const { filteredOptions, updateFilteredOptions, isLoading, error } =
    useComboboxOptions({
      options,
      asyncOptions,
      inputValue,
      delay,
      minSearchLength,
      skipNextDebounceRef,
    });
  const [validationError, setValidationError] = useState(
    error?.message || errorMessage,
  );
  const {
    highlightedIndex,
    highlightedGroupIndex,
    highlightNextOption,
    highlightPreviousOption,
    highlightFirstOption,
    highlightLastOption,
    clearHighlight,
  } = useComboboxHighlight(filteredOptions);

  const inputRef = ref || fallbackRef;
  const listboxId = useId();
  const statusId = useId();
  const blurTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const userInteractedRef = useRef(false);
  const justSelectedRef = useRef(false);
  const { down } = useBreakpoint();
  const isMobile = down("md");

  useEffect(() => {
    setValidationError(error?.message || errorMessage);
  }, [error, errorMessage]);

  // Helper to create properly-typed empty value based on option type
  const getEmptyValue = (): T => {
    // Check first available option to determine if we're using string or object options
    const firstOption = filteredOptions
      ? isOptionGroup(filteredOptions)
        ? filteredOptions[0]?.options[0]
        : filteredOptions[0]
      : options
        ? isOptionGroup(options)
          ? options[0]?.options[0]
          : options[0]
        : undefined;

    // If options are strings, return empty string
    if (typeof firstOption === "string") {
      return "" as T;
    }

    // If options are objects, return empty object with same shape
    return { label: "", value: "" } as T;
  };

  const handleFocus = (e: FocusEvent) => {
    onFocus?.(e);
    if (!filteredOptions || filteredOptions.length === 0) return;
    if (blurTimeoutRef.current) {
      clearTimeout(blurTimeoutRef.current);
      blurTimeoutRef.current = null;
    }

    // Check if this is natural focus (user click/Tab) or programmatic focus
    const isNaturalFocus =
      userInteractedRef.current || e.relatedTarget !== null;
    if (userInteractedRef.current) {
      userInteractedRef.current = false;
    }

    // Block opening only if programmatic focus right after selection
    if (justSelectedRef.current && !isNaturalFocus) {
      return;
    }

    // Open dropdown for natural focus
    if (isNaturalFocus) {
      setIsOpen(true);
      justSelectedRef.current = false;
    }
  };

  useEffect(() => {
    if (!inputRef || typeof inputRef === "function") return;
    const inputElement = inputRef.current;
    if (!inputElement) return;

    const handleMouseDown = () => {
      // Track that user clicked/interacted with input
      userInteractedRef.current = true;
    };

    inputElement.addEventListener("mousedown", handleMouseDown);
    return () => {
      inputElement.removeEventListener("mousedown", handleMouseDown);
    };
  }, [inputRef]);

  useOnUnmount(() => {
    if (blurTimeoutRef.current) {
      clearTimeout(blurTimeoutRef.current);
    }
  });

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;
    setInputValue(newValue);
    setValidationError(undefined);
    justSelectedRef.current = false;
    setIsChanging(true);
    // Only open immediately for sync options; async options controlled by useEffect
    if (!asyncOptions) {
      setIsOpen(true);
    }
    clearHighlight();
    onChange?.(e);
  };

  const handleOptionSelect = (option: T) => {
    const val = getOptionValue(option);

    // Allow empty values to pass through
    if (!val && val !== "") return;

    if (
      typeof option === "object" &&
      "callback" in option &&
      option.callback &&
      typeof option.callback === "function"
    ) {
      justSelectedRef.current = true;
      setTimeout(() => {
        if (inputRef && typeof inputRef !== "function" && inputRef.current) {
          inputRef.current.focus();
        }
      }, 0);
      const callbackPromise = option.callback();
      updateFilteredOptions(callbackPromise);
      callbackPromise
        .then(() => {
          setIsOpen(true);
          setIsChanging(true);
        })
        .catch(() => {
          // error handled inside updateFilteredOptions via setError
        });
      return;
    }

    // Prevent re-fetching options after selecting an option
    skipNextDebounceRef.current = true;
    justSelectedRef.current = true;

    setIsChanging(false);
    setValidationError(undefined);
    setIsOpen(false);
    setInputValue(val);
    onSelected?.(option);
  };

  const getFirstOption = () => {
    if (!filteredOptions) return undefined;
    if (isOptionGroup(filteredOptions)) {
      return filteredOptions[0]?.options[0];
    }

    return filteredOptions[0];
  };

  const getHighlightedOption = () => {
    if (!filteredOptions) return undefined;
    if (highlightedIndex === -1) return undefined;

    if (isOptionGroup(filteredOptions)) {
      const group = filteredOptions[highlightedGroupIndex];
      return group?.options[highlightedIndex];
    }

    return filteredOptions[highlightedIndex];
  };

  const getHighlightedOptionId = () => {
    const option = getHighlightedOption();
    if (!option) return undefined;

    if (isOptionGroup(filteredOptions)) {
      return `${listboxId}-option-${highlightedGroupIndex}-${highlightedIndex}`;
    }

    return `${listboxId}-option-${highlightedIndex}`;
  };

  const handleBlur = (e: FocusEvent<Element, Element>) => {
    // Force selection if user has matched an entry by typing (not already selected)
    // Defer this to allow natural focus flow to complete first
    if (!justSelectedRef.current) {
      const typedText = inputValue.trim();
      const typedTextLower = typedText.toLowerCase();
      const highlightedOption = getHighlightedOption();
      const label = getOptionLabel(highlightedOption);

      if (typedTextLower === label?.toLowerCase()) {
        // Exact match with an option
        setTimeout(() => {
          handleOptionSelect(highlightedOption as T);
        }, 0);
      } else if (typedText === "") {
        // Allow empty values
        setTimeout(() => {
          handleOptionSelect(getEmptyValue());
        }, 0);
      } else {
        // Invalid value (not in options and not empty)
        setValidationError(
          errorMessage || "Please select an option from the list",
        );
        setTimeout(() => {
          setInputValue("");
        }, 0);
      }
    }

    blurTimeoutRef.current = setTimeout(() => {
      onBlur?.(e);
      setIsOpen(false);
      setIsChanging(false);
    }, 150);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        justSelectedRef.current = false;
        setIsOpen(true);
        highlightNextOption();
        break;
      case "ArrowUp":
        e.preventDefault();
        justSelectedRef.current = false;
        setIsOpen(true);
        highlightPreviousOption();
        break;
      case "Home":
        e.preventDefault();
        justSelectedRef.current = false;
        setIsOpen(true);
        highlightFirstOption();
        break;
      case "End":
        e.preventDefault();
        justSelectedRef.current = false;
        setIsOpen(true);
        highlightLastOption();
        break;
      case "Enter":
        e.preventDefault();
        if (isOpen) {
          const selectedOption = getHighlightedOption() || getFirstOption();
          if (selectedOption) {
            handleOptionSelect(selectedOption);
          }
        }
        break;
      case "Escape":
        e.preventDefault();
        setInputValue("");
        setIsOpen(false);
        clearHighlight();
        break;
      default:
      // Do nothing
    }
  };

  useEffect(() => {
    if (value) {
      setInputValue(value);
    }
  }, [value]);

  // Open and close the combobox based on async filtered options
  useEffect(() => {
    if (asyncOptions && isChanging) {
      setIsOpen(!!filteredOptions && filteredOptions.length > 0);
    }
  }, [filteredOptions, asyncOptions, isChanging]);

  const classes = classNames(
    "mobius mobius-combobox",
    {
      "mobius-combobox--is-expanded": isOpen,
      "mobius-combobox--is-loading": isLoading,
      "mobius-combobox--is-mobile": isMobile,
    },
    className,
  );

  const getStatusMessage = () => {
    if (isLoading) return "Loading options";
    if (!filteredOptions || filteredOptions.length === 0) {
      return isChanging ? "No options found" : "";
    }
    const count = isOptionGroup(filteredOptions)
      ? filteredOptions.reduce((sum, group) => sum + group.options.length, 0)
      : filteredOptions.length;
    return isOpen && isChanging
      ? `${count} option${count === 1 ? "" : "s"} available`
      : "";
  };

  return (
    <div id={id} data-testid="mobius-combobox__wrapper" className={classes}>
      <VisuallyHidden
        role="status"
        aria-live="polite"
        id={statusId}
        elementType="div"
        className="mobius-combobox__status"
      >
        {getStatusMessage()}
      </VisuallyHidden>
      <TextField
        {...otherProps}
        className="mobius-combobox__input"
        role="combobox"
        value={inputValue}
        placeholder={placeholder}
        onFocus={handleFocus}
        onBlur={handleBlur}
        onKeyDown={handleKeyDown}
        onChange={handleInputChange}
        autoComplete="off"
        aria-describedby={isLoading ? statusId : undefined}
        aria-autocomplete="list"
        aria-haspopup="listbox"
        aria-controls={isOpen ? listboxId : undefined}
        aria-expanded={isOpen}
        aria-activedescendant={
          highlightedIndex === -1 ? undefined : getHighlightedOptionId()
        }
        prefixInside={icon}
        ref={inputRef}
        errorMessage={errorMessage || validationError || error?.message}
      />
      <Listbox
        id={listboxId}
        isOpen={isOpen}
        isLoading={isLoading}
        options={filteredOptions}
        highlightedIndex={highlightedIndex}
        highlightedGroupIndex={highlightedGroupIndex}
        onOptionSelect={handleOptionSelect}
        optionComponent={optionComponent}
        optionTestIdPrefix={optionTestIdPrefix}
      />
    </div>
  );
};

export const Combobox = ComboboxInner;
