"use client";

import {
  Fragment,
  useEffect,
  useId,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
  type Ref,
} from "react";
import { cx } from "../../utils";
import { FoxFormLabel, type FoxFormLabelRequirement } from "../fox-form-label";
import { FoxSelectOption } from "../fox-select-option";
import { FoxSelectOptionItem } from "../fox-select-option-item";

export type FoxSelectSize = "sm" | "md" | "lg";
export type FoxSelectSelectionMode = "single" | "multiple";

/** 옵션 하나의 데이터. 그리는 일은 `FoxSelectOptionItem`이 한다. */
export interface FoxSelectItem {
  value: string;
  label: ReactNode;
  /** 옵션 라벨 뒤에 붙는 아이콘. 없으면 텍스트만 보여준다. */
  icon?: ReactNode;
  /** 위험한 선택지(시안의 `state=danger`). 글자·아이콘이 danger 색으로 바뀐다. */
  danger?: boolean;
}

export interface FoxSelectProps {
  size: FoxSelectSize;
  options: FoxSelectItem[];
  /**
   * 하나만 고를지 여러 개를 고를지 정한다. 값 prop이 계열마다 다르다 —
   * `single`은 `value`·`defaultValue`·`onValueChange`(문자열), `multiple`은
   * `values`·`defaultValues`·`onValuesChange`(문자열 배열)를 쓴다.
   *
   * `multiple`이면 트리거에 고른 옵션의 라벨이 **쉼표로 이어져** 보이고, 하나를 골라도
   * 목록이 닫히지 않는다 — 여러 개를 연달아 고르는 것이 목적이라 누를 때마다 닫히면 쓸 수
   * 없다. 닫는 것은 바깥 클릭·Escape·Tab이다(`FoxChip`의 select와 같은 규칙).
   */
  selectionMode?: FoxSelectSelectionMode;
  /** 단일 선택에서만 쓴다. 주면 제어 컴포넌트가 된다 — 호출부가 소유하고, select가 스스로 바꾸지 않는다. */
  value?: string;
  /**
   * 비제어로 쓸 때의 초기 값. `value`를 주면 무시된다.
   *
   * 버튼 계열과 달리 값 상태를 안에 둘 수 있게 열어 둔 이유는, 폼 한 칸을 놓기 위해
   * 호출부마다 `useState`를 반복하는 비용이 크기 때문이다(사용자 승인).
   */
  defaultValue?: string;
  /**
   * 값이 바뀔 때 호출된다.
   *
   * ⚠️ 이름을 `onChange`로 바꾸지 않는다. 이 패키지의 기준은 "네이티브 요소를 감싸면
   * `onChange`(그 네이티브 것을 `Omit`으로 걷어내고 대신한다), 감싸는 요소가 없는 커스텀
   * 위젯이면 `onValueChange`"다. FoxInput·FoxTextArea·FoxRadio는 앞쪽이고, 이 컴포넌트는
   * 네이티브 `<select>`가 아니라 버튼 + 리스트박스라 뒤쪽이다.
   *
   * 게다가 `FoxEmail`·`FoxPhoneNumber`가 이 이름으로 호출하고 있어 바꾸면 함께 깨진다.
   */
  onValueChange?: (value: string) => void;
  /** 다중 선택에서만 쓴다. 주면 제어 컴포넌트가 된다. */
  values?: string[];
  /** 다중 선택에서만 쓴다. 비제어일 때의 초기 값들. `values`를 주면 무시된다. */
  defaultValues?: string[];
  /** 다중 선택에서만 쓴다. 켜고 끌 때마다 **바뀐 뒤의 전체 목록**이 넘어온다. */
  onValuesChange?: (values: string[]) => void;
  /** 아무 옵션도 선택되지 않았을 때 트리거에 보여줄 문구. */
  placeholder?: ReactNode;
  /** Figma 시안의 error 상태. */
  error?: boolean;
  disabled?: boolean;
  /**
   * Figma 시안의 view(조회 전용) 상태. 값은 보이되 바꿀 수 없다 — 내부적으로는
   * `disabled`를 걸되, disabled(입력 불가 안내)와 시각적으로 구분해 보여준다.
   */
  readOnly?: boolean;
  /** 필드 위 라벨. 넘기지 않으면 라벨을 렌더링하지 않는다. */
  label?: ReactNode;
  /**
   * 라벨을 두지 않는 자리(시안의 필터 셀렉트 등)에서 트리거를 읽어 줄 이름.
   * `label`이 있으면 그쪽이 이름이라 무시된다(`FoxSelectText`와 같은 규칙).
   */
  ariaLabel?: string;
  /** 라벨 뒤 필수·선택 표시. `label`이 없으면 의미 없다. */
  requirement?: FoxFormLabelRequirement;
  /** select 아래 힌트 메시지. 넘기지 않으면 힌트 영역을 렌더링하지 않는다. */
  hint?: ReactNode;
  /** 힌트 메시지 앞에 붙는 아이콘. `hint`가 없으면 의미 없다. */
  hintIcon?: ReactNode;
  id?: string;
  /** 배치 조정용. 모양이 달라야 하면 여기 말고 모디파이어를 추가한다. */
  className?: string;
  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
  hidden?: boolean;
  /** 포인터가 올라오면 `true`, 벗어나면 `false`. */
  onHoverChange?: (hovered: boolean) => void;
  /** 트리거 버튼을 가리킨다 — 호출부가 포커스를 옮길 때 쓴다. */
  ref?: Ref<HTMLButtonElement>;
}

const SIZE_CLASS: Record<FoxSelectSize, string> = {
  sm: "fox-select--sm",
  md: "fox-select--md",
  lg: "fox-select--lg",
};

/**
 * @fox 커스텀 드롭다운. 네이티브 `<select>`는 옵션 목록 자체를 브라우저가 그려서
 * 스타일링할 수 없기 때문에, 버튼(트리거) + 리스트박스로 직접 구현한다.
 *
 * 버튼 계열과 달리 상태를 갖는다 — 열림 여부와, 비제어로 쓸 때의 값이다. `value`를 주면
 * 값은 호출부가 소유하고 열림 여부만 안에 남는다.
 *
 * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
 * (또는 개별 파티셜)로 한 번 불러와야 한다.
 */
export function FoxSelect({
  size,
  options,
  selectionMode = "single",
  value,
  defaultValue,
  onValueChange,
  values,
  defaultValues,
  onValuesChange,
  placeholder = "선택해주세요",
  error,
  disabled = false,
  readOnly = false,
  label,
  ariaLabel,
  requirement,
  hint,
  hintIcon,
  id,
  className,
  hidden = false,
  onHoverChange,
  ref,
}: FoxSelectProps) {
  const isDisabled = disabled || readOnly;
  const isMultiple = selectionMode === "multiple";
  const [open, setOpen] = useState(false);
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
  const [uncontrolledValues, setUncontrolledValues] = useState<string[]>(defaultValues ?? []);
  const [activeIndex, setActiveIndex] = useState(-1);
  const rootRef = useRef<HTMLDivElement>(null);

  const currentValue = value ?? uncontrolledValue;
  const currentValues = values ?? uncontrolledValues;

  const isSelected = (optionValue: string) =>
    isMultiple ? currentValues.includes(optionValue) : optionValue === currentValue;

  // 화살표가 어디서 시작할지 정한다. 다중 선택에서는 켜진 것 중 첫 번째다.
  const selectedIndex = options.findIndex((option) => isSelected(option.value));
  const selectedOptions = options.filter((option) => isSelected(option.value));

  // 트리거에 보이는 것. 다중이면 고른 라벨을 쉼표로 잇는다 — 라벨이 ReactNode일 수 있어
  // `join`을 쓰지 못하므로 사이에 구분자를 끼워 넣는다.
  const displayValue =
    selectedOptions.length === 0 ? (
      placeholder
    ) : isMultiple ? (
      selectedOptions.map((option, index) => (
        <Fragment key={option.value}>
          {index > 0 ? ", " : null}
          {option.label}
        </Fragment>
      ))
    ) : (
      selectedOptions[0].label
    );

  const generatedId = useId();
  const triggerId = id ?? generatedId;
  const listboxId = `${triggerId}-listbox`;
  const labelId = `${triggerId}-label`;
  // 값이 아니라 순번으로 만든다 — 옵션 값에 공백·특수문자가 들어와도 id가 깨지지 않는다.
  const optionId = (index: number) => `${triggerId}-opt-${index}`;

  useEffect(() => {
    if (!open) return;
    function handlePointerDown(event: PointerEvent) {
      if (!rootRef.current?.contains(event.target as Node)) {
        setOpen(false);
      }
    }
    // 포인터만 보면 Tab으로 빠져나갈 때 리스트가 열린 채 남는다.
    //
    // 포커스가 아무 데도 가지 않은 경우(relatedTarget이 null)는 여기서 닫지 않는다 —
    // 바깥의 포커스 못 받는 곳을 누른 것이고, 그건 위 pointerdown이 이미 처리한다.
    // 이걸 닫기로 치면 옵션을 누르는 순간에도 닫혀 클릭이 성사되지 않는다.
    function handleFocusOut(event: FocusEvent) {
      const next = event.relatedTarget as Node | null;
      if (next && !rootRef.current?.contains(next)) {
        setOpen(false);
      }
    }
    document.addEventListener("pointerdown", handlePointerDown);
    const root = rootRef.current;
    root?.addEventListener("focusout", handleFocusOut);
    return () => {
      document.removeEventListener("pointerdown", handlePointerDown);
      root?.removeEventListener("focusout", handleFocusOut);
    };
  }, [open]);

  function openList() {
    setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
    setOpen(true);
  }

  /**
   * 제어 컴포넌트일 때는 내부 값을 건드리지 않는다 — 건드리면 호출부가 값을 거부해도
   * 내부에만 반영되어 둘이 조용히 갈라진다.
   */
  function commit(next: string) {
    if (isMultiple) {
      // 이미 켜져 있으면 끈다. 순서는 `options`가 정한 순서를 따라가게 다시 추린다 —
      // 누른 순서대로 쌓으면 같은 조합인데도 배열이 달라져 비교·저장이 흔들리고, 트리거에
      // 이어 붙는 글자 순서도 목록과 어긋난다.
      const nextValues = currentValues.includes(next)
        ? currentValues.filter((item) => item !== next)
        : options
            .filter((option) => option.value === next || currentValues.includes(option.value))
            .map((option) => option.value);

      if (values === undefined) setUncontrolledValues(nextValues);
      onValuesChange?.(nextValues);
      // 목록을 닫지 않는다 — 여러 개를 연달아 고르는 것이 목적이다.
      return;
    }

    if (value === undefined) setUncontrolledValue(next);
    onValueChange?.(next);
    setOpen(false);
  }

  function moveActive(delta: number) {
    setActiveIndex((current) => {
      const count = options.length;
      if (count === 0) return current;
      return (current + delta + count) % count;
    });
  }

  function handleTriggerKeyDown(event: KeyboardEvent<HTMLButtonElement>) {
    if (isDisabled) return;

    if (!open) {
      if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        openList();
      }
      return;
    }

    switch (event.key) {
      case "ArrowDown":
        event.preventDefault();
        moveActive(1);
        break;
      case "ArrowUp":
        event.preventDefault();
        moveActive(-1);
        break;
      case "Home":
        event.preventDefault();
        setActiveIndex(0);
        break;
      case "End":
        event.preventDefault();
        setActiveIndex(options.length - 1);
        break;
      case "Enter":
      case " ":
        event.preventDefault();
        if (activeIndex >= 0) {
          commit(options[activeIndex].value);
        }
        break;
      case "Escape":
        event.preventDefault();
        setOpen(false);
        break;
      default:
        break;
    }
  }

  // 훅을 전부 부른 뒤에 빠져나간다 — 위쪽에서 반환하면 훅 호출 순서가 깨진다.
  if (hidden) {
    return null;
  }

  return (
    <div
      ref={rootRef}
      className={cx("fox-select", SIZE_CLASS[size], className)}
      onMouseEnter={onHoverChange ? () => onHoverChange(true) : undefined}
      onMouseLeave={onHoverChange ? () => onHoverChange(false) : undefined}
    >
      {label ? (
        <FoxFormLabel id={labelId} htmlFor={triggerId} requirement={requirement}>
          {label}
        </FoxFormLabel>
      ) : null}

      <div className="fox-select__control">
        <button
          ref={ref}
          type="button"
          id={triggerId}
          role="combobox"
          disabled={isDisabled}
          aria-label={label ? undefined : ariaLabel}
          aria-haspopup="listbox"
          aria-expanded={open}
          // 리스트박스는 열렸을 때만 렌더되므로, 닫힌 동안 없는 id를 가리키지 않게 한다.
          aria-controls={open ? listboxId : undefined}
          // 화살표로 옮겨 다니는 옵션을 보조기술에 알린다. 이게 없으면 `--active` 강조가
          // 시각 전용이라 스크린리더 사용자는 이동을 알 수 없다.
          aria-activedescendant={open && activeIndex >= 0 ? optionId(activeIndex) : undefined}
          aria-invalid={error || undefined}
          aria-readonly={readOnly || undefined}
          className={cx(
            "fox-select__trigger",
            selectedOptions.length > 0 && "fox-select__trigger--completed"
          )}
          onClick={() => (open ? setOpen(false) : openList())}
          onKeyDown={handleTriggerKeyDown}
        >
          <span className="fox-select__value">{displayValue}</span>
          <ChevronIcon className="fox-select__icon" />
        </button>

        {open ? (
          <FoxSelectOption
            id={listboxId}
            labelledBy={label ? labelId : undefined}
            multiselectable={isMultiple}
          >
            {options.map((option, index) => (
              <FoxSelectOptionItem
                key={option.value}
                id={optionId(index)}
                label={option.label}
                icon={option.icon}
                danger={option.danger}
                selected={isSelected(option.value)}
                active={index === activeIndex}
                onHover={() => setActiveIndex(index)}
                onSelect={() => commit(option.value)}
              />
            ))}
          </FoxSelectOption>
        ) : null}
      </div>

      {hint ? (
        <div className="fox-select__hint">
          {hintIcon ? (
            <span className="fox-select__hint-icon" aria-hidden="true">
              {hintIcon}
            </span>
          ) : null}
          <span className="fox-select__hint-message">{hint}</span>
        </div>
      ) : null}
    </div>
  );
}

// 시안 아이콘. viewBox가 24×24 정사각형이고 화살표가 그 안에 여백을 두고 들어 있다 —
// 여백이 SVG에 내장돼 있어 슬롯이 커지고 작아질 때 여백도 같이 비례한다(CSS 패딩 불필요).
// `fill`은 시안의 리터럴 대신 `currentColor`로 두어 색을 토큰이 정한다.
function ChevronIcon({ className }: { className?: string }) {
  return <i className={`fox-ico fox-ico-CaretDown ${className ?? ""}`.trim()} aria-hidden="true" />;
}
