import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type FocusEvent,
  type KeyboardEvent
} from "react";
import { ChevronDown, Search } from "lucide-react";
import { usePlatformLanguage } from "../i18n/LanguageContext";

export type SearchableSelectOption = {
  value: string;
  label: string;
  description?: string | null;
  searchText?: string | null;
  disabled?: boolean;
};

export type SearchableSelectLoadOptions = (
  query: string,
  options: { limit: number; signal: AbortSignal }
) => Promise<readonly SearchableSelectOption[]>;

export type SearchableSelectCreateCustomOption = (
  value: string
) => SearchableSelectOption | null;

export type SearchableSelectProps = {
  id?: string;
  value: string;
  onChange: (
    value: string,
    option: SearchableSelectOption | null
  ) => void;
  options?: readonly SearchableSelectOption[];
  loadOptions?: SearchableSelectLoadOptions;
  createCustomOption?: SearchableSelectCreateCustomOption;
  selectedOption?: SearchableSelectOption | null;
  "aria-label"?: string;
  placeholder?: string;
  searchPlaceholder?: string;
  emptyText?: string;
  loadingText?: string;
  errorText?: string;
  disabled?: boolean;
  required?: boolean;
  minQueryLength?: number;
  searchLimit?: number;
  debounceMs?: number;
  className?: string;
};

const EMPTY_OPTIONS: readonly SearchableSelectOption[] = [];

export function filterSearchableSelectOptions(
  options: readonly SearchableSelectOption[],
  query: string,
  limit = 50
): SearchableSelectOption[] {
  const needle = query.trim().toLocaleLowerCase();
  const filtered = needle
    ? options.filter((option) =>
        [
          option.label,
          option.description ?? "",
          option.searchText ?? "",
          option.value
        ]
          .join(" ")
          .toLocaleLowerCase()
          .includes(needle)
      )
    : [...options];
  return filtered.slice(0, Math.max(1, Math.floor(limit)));
}

export default function SearchableSelect({
  id,
  value,
  onChange,
  options = EMPTY_OPTIONS,
  loadOptions,
  createCustomOption,
  selectedOption,
  "aria-label": ariaLabel = "Select an option",
  placeholder = "Select an option",
  searchPlaceholder = "Search...",
  emptyText = "No matching options.",
  loadingText = "Loading...",
  errorText = "Options could not be loaded.",
  disabled = false,
  required = false,
  minQueryLength = 0,
  searchLimit = 50,
  debounceMs = 200,
  className = ""
}: SearchableSelectProps) {
  const { translateText } = usePlatformLanguage();
  const generatedId = useId().replace(/[^a-zA-Z0-9_-]/g, "-");
  const pickerId = id || `searchable-select-${generatedId}`;
  const listboxId = `${pickerId}-results`;
  const statusId = `${pickerId}-status`;
  const rootRef = useRef<HTMLDivElement | null>(null);
  const resolvedSelected = useMemo(
    () =>
      selectedOption ??
      options.find((option) => option.value === value) ??
      null,
    [options, selectedOption, value]
  );
  const selectedLabel = resolvedSelected?.label ?? value;
  const [inputValue, setInputValue] = useState(selectedLabel);
  const [results, setResults] =
    useState<readonly SearchableSelectOption[]>(EMPTY_OPTIONS);
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [failed, setFailed] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const normalizedMinLength = Math.max(0, Math.floor(minQueryLength));
  const normalizedLimit = Math.max(1, Math.min(200, Math.floor(searchLimit)));
  const visibleResults = useMemo(() => {
    const customValue = inputValue.trim();
    const customOption = customValue ? createCustomOption?.(customValue) : null;
    if (
      !customOption
      || results.some((option) => option.value === customOption.value)
    ) {
      return results;
    }
    return [...results, customOption];
  }, [createCustomOption, inputValue, results]);

  useEffect(() => {
    if (!open) setInputValue(selectedLabel);
  }, [open, selectedLabel]);

  useEffect(() => {
    if (!open || disabled) {
      setLoading(false);
      setFailed(false);
      setResults(EMPTY_OPTIONS);
      return;
    }
    const query = inputValue.trim();
    if (query.length < normalizedMinLength) {
      setLoading(false);
      setFailed(false);
      setResults(EMPTY_OPTIONS);
      return;
    }
    if (!loadOptions) {
      setResults(filterSearchableSelectOptions(options, query, normalizedLimit));
      setLoading(false);
      setFailed(false);
      return;
    }

    const controller = new AbortController();
    const timer = window.setTimeout(() => {
      setLoading(true);
      setFailed(false);
      void loadOptions(query, {
        limit: normalizedLimit,
        signal: controller.signal
      })
        .then((nextOptions) => {
          if (!controller.signal.aborted) setResults(nextOptions);
        })
        .catch((error: unknown) => {
          if (
            controller.signal.aborted ||
            (error instanceof DOMException && error.name === "AbortError")
          ) {
            return;
          }
          setResults(EMPTY_OPTIONS);
          setFailed(true);
        })
        .finally(() => {
          if (!controller.signal.aborted) setLoading(false);
        });
    }, Math.max(0, Math.floor(debounceMs)));

    return () => {
      controller.abort();
      window.clearTimeout(timer);
    };
  }, [
    debounceMs,
    disabled,
    inputValue,
    loadOptions,
    normalizedLimit,
    normalizedMinLength,
    open,
    options
  ]);

  useEffect(() => {
    const selectedIndex = visibleResults.findIndex(
      (option) => option.value === value && !option.disabled
    );
    const firstEnabledIndex = visibleResults.findIndex(
      (option) => !option.disabled
    );
    setActiveIndex(selectedIndex >= 0 ? selectedIndex : firstEnabledIndex);
  }, [value, visibleResults]);

  function choose(option: SearchableSelectOption) {
    if (disabled || option.disabled) return;
    onChange(option.value, option);
    setInputValue(option.label);
    setOpen(false);
    setActiveIndex(-1);
  }

  function moveActive(delta: 1 | -1) {
    const enabledIndexes = visibleResults
      .map((option, index) => ({ option, index }))
      .filter(({ option }) => !option.disabled)
      .map(({ index }) => index);
    if (!enabledIndexes.length) {
      setActiveIndex(-1);
      return;
    }
    const currentPosition = enabledIndexes.indexOf(activeIndex);
    const nextPosition =
      currentPosition < 0
        ? delta > 0
          ? 0
          : enabledIndexes.length - 1
        : (currentPosition + delta + enabledIndexes.length) %
          enabledIndexes.length;
    setActiveIndex(enabledIndexes[nextPosition]);
  }

  function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
    if (event.key === "ArrowDown") {
      event.preventDefault();
      setOpen(true);
      moveActive(1);
      return;
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      setOpen(true);
      moveActive(-1);
      return;
    }
    if (event.key === "Enter" && open && activeIndex >= 0) {
      const option = visibleResults[activeIndex];
      if (option && !option.disabled) {
        event.preventDefault();
        choose(option);
      }
      return;
    }
    if (event.key === "Escape") {
      event.preventDefault();
      setOpen(false);
      setInputValue(selectedLabel);
      setActiveIndex(-1);
    }
  }

  function closeOnFocusLeave(event: FocusEvent<HTMLDivElement>) {
    const nextTarget = event.relatedTarget;
    if (nextTarget instanceof Node && rootRef.current?.contains(nextTarget)) {
      return;
    }
    setOpen(false);
    setInputValue(selectedLabel);
    setActiveIndex(-1);
  }

  const minimumLengthMissing =
    inputValue.trim().length < normalizedMinLength;
  const visibleStatus = loading
    ? loadingText
    : failed
      ? errorText
      : minimumLengthMissing
        ? `Type at least ${normalizedMinLength} characters.`
        : !visibleResults.length
          ? emptyText
          : "";
  const rootClassName = ["searchable-select", className]
    .filter(Boolean)
    .join(" ");

  return (
    <div
      ref={rootRef}
      className={rootClassName}
      onBlur={closeOnFocusLeave}
    >
      <div className="searchable-select-control">
        <Search size={17} aria-hidden="true" />
        <input
          id={pickerId}
          type="search"
          role="combobox"
          value={inputValue}
          disabled={disabled}
          required={required && !value}
          placeholder={translateText(
            open ? searchPlaceholder : placeholder
          )}
          aria-label={translateText(ariaLabel)}
          aria-autocomplete="list"
          aria-expanded={open}
          aria-controls={listboxId}
          aria-activedescendant={
            open && activeIndex >= 0
              ? `${pickerId}-option-${activeIndex}`
              : undefined
          }
          aria-describedby={statusId}
          aria-busy={loading}
          onFocus={() => {
            if (!open) setInputValue("");
            setOpen(true);
          }}
          onChange={(event) => {
            setInputValue(event.target.value);
            setOpen(true);
            setFailed(false);
            if (value) onChange("", null);
          }}
          onKeyDown={handleKeyDown}
        />
        <ChevronDown size={17} aria-hidden="true" />
      </div>
      {open && !disabled && (
        <div
          id={listboxId}
          className="searchable-select-results"
          role="listbox"
          aria-label={translateText(ariaLabel)}
        >
          {visibleResults.map((option, index) => (
            <button
              key={option.value}
              id={`${pickerId}-option-${index}`}
              type="button"
              role="option"
              aria-selected={option.value === value}
              className={[
                "searchable-select-option",
                option.value === value ? "is-selected" : "",
                index === activeIndex ? "is-active" : ""
              ]
                .filter(Boolean)
                .join(" ")}
              disabled={option.disabled}
              onMouseEnter={() => {
                if (!option.disabled) setActiveIndex(index);
              }}
              onClick={() => choose(option)}
            >
              <strong>{option.label}</strong>
              {option.description && <small>{option.description}</small>}
            </button>
          ))}
          {visibleStatus && (
            <p className="searchable-select-message">
              {translateText(visibleStatus)}
            </p>
          )}
        </div>
      )}
      <span id={statusId} className="sr-only" aria-live="polite">
        {translateText(visibleStatus)}
      </span>
    </div>
  );
}
