"use client";

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

export type FoxSelectTextSize = "lg" | "md" | "sm";

export interface FoxSelectTextProps {
  size: FoxSelectTextSize;
  /** `FoxSelect`와 같은 데이터 모양이다 — 열리는 목록이 같은 조각이므로 타입도 같이 쓴다. */
  options: FoxSelectItem[];
  /** 주면 제어 컴포넌트가 된다 — 호출부가 소유하고, 이 요소가 스스로 바꾸지 않는다. */
  value?: string;
  /** 비제어로 쓸 때의 초기 값. `value`를 주면 무시된다. */
  defaultValue?: string;
  /** 값이 바뀔 때 호출된다(`FoxSelect.onValueChange`와 같은 이름·같은 근거). */
  onValueChange?: (value: string) => void;
  /** 아무 옵션도 선택되지 않았을 때 보여줄 문구. */
  placeholder?: ReactNode;
  disabled?: boolean;
  /**
   * 글자를 대신할 읽어 줄 이름.
   *
   * 폼 라벨이 없는 컴포넌트라 보이는 글자가 곧 이름이다. 그런데 그 글자가 고른 값
   * ("2026년 8월")이라 무엇을 고르는 자리인지는 소리로 전달되지 않는다 — `"조회 기간"`처럼
   * 적어 준다.
   */
  /**
   * 오른쪽 표시를 바꾼다. 기본은 펼침을 뜻하는 캐럿이고, 시안이 다른 뜻을 담을 때만 준다
   * (예: 정렬 셀렉트의 위아래 화살표). 장식이라 `aria-hidden`인 자리다.
   */
  icon?: ReactNode;
  ariaLabel?: string;
  /** 이름 역할을 하는 요소의 id. `ariaLabel`보다 우선한다. */
  labelledBy?: string;
  id?: string;
  /** 배치 조정용. 모양이 달라야 하면 여기 말고 모디파이어를 추가한다. */
  className?: string;
  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
  hidden?: boolean;
  /** 트리거를 가리킨다 — 호출부가 포커스를 옮길 때 쓴다. */
  ref?: Ref<HTMLButtonElement>;
}

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

/**
 * @fox 글자만 있는 셀렉트. 테두리도 고정 높이도 없고, 고른 값이 그대로 글자로 보인다.
 *
 * `FoxSelect`와 같은 콤보박스지만 **폼 한 칸이 아니다** — 라벨·힌트·에러·필수 표시가 없고,
 * lg는 글자가 heading/md(제목용 20px bold)다. 화면의 제목이나 목록 위 정렬 기준처럼 "지금
 * 무엇을 보고 있는지"를 글자로 보여 주면서 그 자리에서 바꾸게 하는 자리에 쓴다
 * (예: `2026년 8월 ∨`, `최신순 ∨`). 값을 입력받아 서버로 보내는 자리라면 `FoxSelect`다.
 *
 * 열리는 목록은 `FoxSelect`와 같은 `FoxSelectOption`·`FoxSelectOptionItem`을 쓴다 — 트리거
 * 모양만 다르고 목록은 같아야 한다.
 *
 * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
 * (또는 개별 파티셜)로 한 번 불러와야 한다.
 */
export function FoxSelectText({
  size,
  options,
  value,
  defaultValue,
  onValueChange,
  placeholder = "선택해주세요",
  disabled = false,
  icon,
  ariaLabel,
  labelledBy,
  id,
  className,
  hidden = false,
  ref,
}: FoxSelectTextProps) {
  const [open, setOpen] = useState(false);
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
  const [activeIndex, setActiveIndex] = useState(-1);
  const rootRef = useRef<HTMLDivElement>(null);

  const currentValue = value ?? uncontrolledValue;
  const selectedIndex = options.findIndex((option) => option.value === currentValue);
  const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;

  const generatedId = useId();
  const triggerId = id ?? generatedId;
  const listboxId = `${triggerId}-listbox`;
  // 값이 아니라 순번으로 만든다 — 옵션 값에 공백·특수문자가 들어와도 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이 이미 처리한다 — 여기서 닫으면
    // 옵션을 누르는 순간에도 닫혀 클릭이 성사되지 않는다(`FoxSelect`와 같은 근거).
    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 (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 handleKeyDown(event: KeyboardEvent<HTMLButtonElement>) {
    if (disabled) return;

    if (!open) {
      if (event.key === "ArrowDown" || event.key === "ArrowUp") {
        event.preventDefault();
        openList();
      }
      // Enter·Space는 막지 않는다 — 네이티브 버튼의 클릭이 그대로 열어 준다.
      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 (
    // `FoxSelectOption`은 `position: absolute`라 위치 기준이 되는 조상이 필요하다
    // (`fox-select__control`과 같은 역할).
    <div ref={rootRef} className={cx("fox-select-text", SIZE_CLASS[size], className)}>
      <button
        ref={ref}
        type="button"
        id={triggerId}
        role="combobox"
        disabled={disabled}
        aria-haspopup="listbox"
        aria-expanded={open}
        // 목록은 열렸을 때만 렌더되므로, 닫힌 동안 없는 id를 가리키지 않게 한다.
        aria-controls={open ? listboxId : undefined}
        // 화살표로 옮겨 다니는 옵션을 보조기술에 알린다. 이게 없으면 `--active` 강조가
        // 시각 전용이라 스크린리더 사용자는 이동을 알 수 없다.
        aria-activedescendant={open && activeIndex >= 0 ? optionId(activeIndex) : undefined}
        aria-label={labelledBy ? undefined : ariaLabel}
        aria-labelledby={labelledBy}
        className="fox-select-text__trigger"
        onClick={() => (open ? setOpen(false) : openList())}
        onKeyDown={handleKeyDown}
      >
        <span className="fox-select-text__value">{selectedOption?.label ?? placeholder}</span>
        <span className="fox-select-text__icon" aria-hidden="true">
          {icon ?? <ChevronMark />}
        </span>
      </button>

      {open ? (
        <FoxSelectOption
          id={listboxId}
          labelledBy={labelledBy}
          className="fox-select-text__list"
        >
          {options.map((option, index) => (
            <FoxSelectOptionItem
              key={option.value}
              id={optionId(index)}
              label={option.label}
              icon={option.icon}
              danger={option.danger}
              selected={option.value === currentValue}
              active={index === activeIndex}
              onHover={() => setActiveIndex(index)}
              onSelect={() => commit(option.value)}
            />
          ))}
        </FoxSelectOption>
      ) : null}
    </div>
  );
}

// 시안 아이콘. `FoxChip`의 것과 같은 글리프다 — 이 패키지는 컴포넌트마다 자기 글리프를
// 안에 두고(`FoxTag`·`FoxSelect`도 그렇다), 아이콘 모듈을 따로 만들지 않는다.
// `fill`은 시안의 리터럴 대신 `currentColor`로 두어 색을 크기 규칙이 정하게 한다.
// `width`·`height`를 쓰지 않는다 — 크기는 슬롯이 정하고 SVG는 100%로 따라온다.
function ChevronMark() {
  return <i className="fox-ico fox-ico--bold fox-ico-CaretDown" aria-hidden="true" />;
}
