import React, { useCallback, useEffect, useId, useRef, useState } from "react";

export type CustomSelectOption = {
  value: string;
  label: string;
};

export interface CustomSelectProps {
  ariaLabel?: string;
  value: string;
  options: ReadonlyArray<CustomSelectOption>;
  onChange: (value: string) => void;
  disabled?: boolean;
  placeholder?: string;
  className?: string;
  triggerClassName?: string;
  menuClassName?: string;
}

export function CustomSelect({
  ariaLabel,
  value,
  options,
  onChange,
  disabled = false,
  placeholder = "Select…",
  className = "",
  triggerClassName = "",
  menuClassName = "",
}: CustomSelectProps): React.ReactElement {
  const [isOpen, setIsOpen] = useState(false);
  const [focusIndex, setFocusIndex] = useState(-1);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const listRef = useRef<HTMLUListElement | null>(null);
  const id = useId();

  const selectedOption = options.find((option) => option.value === value);

  const close = useCallback(() => {
    setIsOpen(false);
    setFocusIndex(-1);
  }, []);

  useEffect(() => {
    if (!isOpen) {
      return undefined;
    }

    function handleClickOutside(event: MouseEvent) {
      if (
        containerRef.current &&
        !containerRef.current.contains(event.target as Node)
      ) {
        close();
      }
    }

    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, [close, isOpen]);

  useEffect(() => {
    if (!isOpen || focusIndex < 0) {
      return;
    }

    const items =
      listRef.current?.querySelectorAll<HTMLLIElement>('[role="option"]');
    items?.[focusIndex]?.scrollIntoView({ block: "nearest" });
  }, [focusIndex, isOpen]);

  function openWithCurrentValue() {
    setIsOpen(true);
    setFocusIndex(options.findIndex((option) => option.value === value));
  }

  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
    if (disabled) {
      return;
    }

    switch (event.key) {
      case "Enter":
      case " ":
        event.preventDefault();
        if (isOpen && focusIndex >= 0) {
          onChange(options[focusIndex].value);
          close();
        } else {
          openWithCurrentValue();
        }
        break;
      case "ArrowDown":
        event.preventDefault();
        if (!isOpen) {
          openWithCurrentValue();
        } else {
          setFocusIndex((prev) =>
            prev < options.length - 1 ? prev + 1 : prev,
          );
        }
        break;
      case "ArrowUp":
        event.preventDefault();
        if (!isOpen) {
          openWithCurrentValue();
        } else {
          setFocusIndex((prev) => (prev > 0 ? prev - 1 : prev));
        }
        break;
      case "Home":
        event.preventDefault();
        if (isOpen && options.length > 0) {
          setFocusIndex(0);
        }
        break;
      case "End":
        event.preventDefault();
        if (isOpen && options.length > 0) {
          setFocusIndex(options.length - 1);
        }
        break;
      case "Escape":
        event.preventDefault();
        close();
        break;
      case "Tab":
        close();
        break;
      default:
        break;
    }
  }

  return (
    <div
      ref={containerRef}
      className={`b3-wvs-custom-select ${className}`.trim()}
      onKeyDown={handleKeyDown}
    >
      <button
        type="button"
        aria-label={ariaLabel}
        aria-haspopup="listbox"
        aria-expanded={isOpen}
        aria-controls={`${id}-menu`}
        disabled={disabled}
        onClick={() => {
          if (disabled) {
            return;
          }

          if (isOpen) {
            close();
            return;
          }

          openWithCurrentValue();
        }}
        className={`b3-wvs-custom-select-trigger ${triggerClassName}`.trim()}
      >
        <span
          className={selectedOption ? "" : "b3-wvs-custom-select-placeholder"}
        >
          {selectedOption?.label ?? placeholder}
        </span>
        <svg
          className={`b3-wvs-custom-select-chevron ${isOpen ? "is-open" : ""}`}
          viewBox="0 0 20 20"
          fill="currentColor"
          aria-hidden="true"
        >
          <path
            fillRule="evenodd"
            d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
            clipRule="evenodd"
          />
        </svg>
      </button>

      {isOpen ? (
        <ul
          ref={listRef}
          id={`${id}-menu`}
          role="listbox"
          aria-activedescendant={
            focusIndex >= 0 ? `${id}-option-${focusIndex}` : undefined
          }
          className={`b3-wvs-custom-select-menu ${menuClassName}`.trim()}
        >
          {options.map((option, index) => {
            const isSelected = option.value === value;
            const isFocused = index === focusIndex;

            return (
              <li
                key={option.value}
                id={`${id}-option-${index}`}
                role="option"
                aria-selected={isSelected}
                className={[
                  "b3-wvs-custom-select-option",
                  isSelected ? "is-selected" : "",
                  isFocused ? "is-focused" : "",
                ]
                  .filter(Boolean)
                  .join(" ")}
                onMouseEnter={() => setFocusIndex(index)}
                onMouseDown={(event) => {
                  event.preventDefault();
                  onChange(option.value);
                  close();
                }}
              >
                <span>{option.label}</span>
                {isSelected ? (
                  <svg
                    className="b3-wvs-custom-select-check"
                    viewBox="0 0 20 20"
                    fill="currentColor"
                    aria-hidden="true"
                  >
                    <path
                      fillRule="evenodd"
                      d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
                      clipRule="evenodd"
                    />
                  </svg>
                ) : null}
              </li>
            );
          })}
        </ul>
      ) : null}
    </div>
  );
}
