"use client";

import {
  useEffect,
  useId,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
  type Ref,
} from "react";
import { cx } from "../../utils";
import {
  FoxChipSelectOption,
  type FoxChipItem,
  type FoxChipSelectionMode,
} from "../fox-chip-select-option";

export type FoxChipType = "check" | "select" | "action";
export type FoxChipSize = "lg" | "md";

// 목록이 소유하는 타입이라 그쪽에 두고 여기서 다시 내보낸다 — 호출부는 칩만 알면 된다.
export type { FoxChipItem, FoxChipSelectionMode };

export interface FoxChipProps {
  /**
   * 아이콘의 유무와 누를 때 벌어지는 일을 정한다.
   *
   * - `check`: 켜고 끄는 조각. **켜졌을 때만** 글자 앞에 체크 아이콘이 붙는다.
   * - `select`: 눌러서 목록을 여는 조각. 글자 뒤에 아래 화살표가 늘 붙고, 글자 앞
   *   아이콘은 `icon`을 넘긴 때만 나온다. 값을 고르면 켜짐으로 보인다.
   * - `action`: 아이콘 없이 켜고 끄는 조각. 상태별 모양은 `check`와 같다.
   */
  type?: FoxChipType;
  size?: FoxChipSize;
  /**
   * 조각에 보이는 글자.
   *
   * `select`·단일 선택에서는 아무것도 고르지 않았을 때의 문구다 — 값을 고르면 그 옵션의
   * 라벨이 대신 보인다(고른 것이 무엇인지가 조각 위에 남아야 필터로 쓸 수 있다).
   *
   * ⚠️ 다중 선택에서는 언제나 이 글자가 그대로 보인다. 고른 것이 여럿이라 하나를 골라
   * 보여 줄 수 없고, 개수를 붙이는 형식(`기간 2`)은 시안에 없어 임의로 만들지 않았다.
   * 켜짐(남색 채움)이 "무언가 걸려 있다"는 표시를 대신한다. **확인 필요.**
   */
  label: ReactNode;
  /**
   * 보이는 글자를 대신할 읽어 줄 이름. 넘기지 않으면 글자가 그대로 이름이 된다.
   *
   * 조각 여러 개가 같은 글자를 쓰는 자리에 적어 준다 — 예를 들어 기간·지역 필터가 나란히
   * 있고 둘 다 "전체"라면, 소리로는 구분되지 않는다.
   */
  ariaLabel?: string;
  disabled?: boolean;
  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
  hidden?: boolean;

  // ── check·action 전용 ────────────────────────────────────────────────────────
  /** 주면 제어 컴포넌트가 된다 — 켜짐 여부를 호출부가 소유하고 조각이 스스로 바꾸지 않는다. */
  checked?: boolean;
  /** 비제어로 쓸 때의 초기 켜짐 여부. `checked`를 주면 무시된다. */
  defaultChecked?: boolean;
  /**
   * 켜지거나 꺼질 때 호출된다.
   *
   * ⚠️ 이름이 `onChange`가 아니다. 이 패키지의 기준은 "네이티브 요소를 감싸면 `onChange`,
   * 감싸는 요소가 없는 커스텀 위젯이면 `on…Change`"인데, 이 조각은 네이티브
   * `<input type="checkbox">`가 아니라 `aria-pressed`를 든 버튼이라 뒤쪽이다
   * (`FoxSelect.onValueChange`와 같은 이유).
   */
  onCheckedChange?: (checked: boolean) => void;

  // ── select 전용 ─────────────────────────────────────────────────────────────
  /** 글자 앞에 붙는 아이콘. 넘기지 않으면 자리를 만들지 않는다. `select`에서만 쓴다. */
  icon?: ReactNode;
  /** 열었을 때 보여 줄 목록. 비어 있으면 목록을 열지 않는다. */
  options?: FoxChipItem[];
  /**
   * 하나만 고를지 여러 개를 고를지 정한다. 값 prop이 계열마다 다르다 —
   * `single`은 `value`·`defaultValue`·`onValueChange`(문자열), `multiple`은
   * `values`·`defaultValues`·`onValuesChange`(문자열 배열)를 쓴다.
   *
   * 목록 항목의 모양도 여기서 갈린다(단일은 라디오, 다중은 체크박스). 고른 뒤 닫히는
   * 규칙도 마찬가지다 — 컴포넌트 주석의 "닫히는 규칙" 참고.
   */
  selectionMode?: FoxChipSelectionMode;
  /** 단일 선택에서만 쓴다. 주면 제어 컴포넌트가 된다 — 고른 값을 호출부가 소유한다. */
  value?: string;
  /** 단일 선택에서만 쓴다. 비제어일 때의 초기 값. `value`를 주면 무시된다. */
  defaultValue?: string;
  /** 단일 선택에서만 쓴다. 값이 바뀔 때 호출된다. */
  onValueChange?: (value: string) => void;
  /** 다중 선택에서만 쓴다. 주면 제어 컴포넌트가 된다. */
  values?: string[];
  /** 다중 선택에서만 쓴다. 비제어일 때의 초기 값들. `values`를 주면 무시된다. */
  defaultValues?: string[];
  /** 다중 선택에서만 쓴다. 켜고 끌 때마다 **바뀐 뒤의 전체 목록**이 넘어온다. */
  onValuesChange?: (values: string[]) => void;
  /** 모바일에만 보이는 확인 버튼의 글자. */
  confirmLabel?: string;

  id?: string;
  /** 배치 조정용. 모양이 달라야 하면 여기 말고 `type`이나 모디파이어를 추가한다. */
  className?: string;
  /** 조각(버튼)을 가리킨다 — 호출부가 포커스를 옮길 때 쓴다. */
  ref?: Ref<HTMLButtonElement>;
}

/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */
const TYPE_CLASS: Record<FoxChipType, string> = {
  check: "fox-chip--check",
  select: "fox-chip--select",
  action: "fox-chip--action",
};

const SIZE_CLASS: Record<FoxChipSize, string> = {
  lg: "fox-chip--lg",
  md: "fox-chip--md",
};

/**
 * @fox 칩. 알약보다 각진 작은 조각이고 **켜짐 여부를 갖는다** — 이것이 `FoxTag`와의 차이다.
 * 태그는 눌러서 사라지거나 어디로 넘어가는 조각이라 상태가 남지 않고, 칩은 누른 결과가
 * 자기 자신에게 남는다.
 *
 * 켜짐은 계열에 따라 다른 것에서 온다: `check`·`action`은 눌러서 직접 켜고 끄고,
 * `select`는 값을 고르면 켜진 것으로 보인다. 어느 쪽이든 모양 규칙은 한 벌이다.
 *
 * `check`·`action`은 `aria-pressed`를 든 버튼이다 — 체크박스가 아니다. 폼으로 값을
 * 보내야 하면 호출부가 `<input type="hidden">`을 함께 놓는다(칩은 여러 개가 한 묶음으로
 * 하나의 값을 이루는 자리에 쓰여서, 이름 한 개를 칩 하나에 묶을 수 없다).
 *
 * `select`는 `FoxSelect`와 달리 **콤보박스가 아니다.** 열리는 `FoxChipSelectOption`이
 * listbox가 아니라 체크박스·라디오 목록이라, 칩은 그것을 여닫는 버튼(`aria-expanded`)일
 * 뿐이고 항목 이동은 네이티브 입력이 알아서 한다. `aria-activedescendant`로 가상 커서를
 * 옮기던 규칙이 여기에는 없다.
 *
 * ── 닫히는 규칙 (`select`) ───────────────────────────────────────────────────
 *
 * `selectionMode`가 정한다. 데스크톱 기준이다.
 *
 * - `single`: 하나를 고르면 **그 자리에서 반영되고 목록이 닫힌다.** 더 고를 것이 없으니
 *   닫지 않을 이유가 없다.
 * - `multiple`: 켜고 꺼도 **목록이 열린 채로 남는다.** 여러 개를 연달아 고르는 것이 목적이라
 *   하나 누를 때마다 닫히면 쓸 수 없다. 닫는 것은 **바깥을 누르는 것**이고(Escape·Tab으로
 *   빠져나가는 것도 같다), 그때까지의 체크는 이미 `onValuesChange`로 반영돼 있다 — 닫기가
 *   곧 확정이 아니라서 취소 개념이 없다.
 *
 * 모바일에는 목록 맨 아래에 확인 버튼이 붙는다(시안 주석 "모바일만 노출"). 그 버튼은 값을
 * 확정하는 것이 아니라 **닫기만 한다** — 위와 같은 이유로 이미 반영돼 있기 때문이다.
 * 데스크톱에서는 CSS가 그 버튼을 숨긴다.
 *
 * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
 * (또는 개별 파티셜)로 한 번 불러와야 한다.
 */
export function FoxChip({
  type = "check",
  size = "lg",
  label,
  ariaLabel,
  disabled = false,
  hidden = false,
  checked,
  defaultChecked = false,
  onCheckedChange,
  icon,
  options,
  selectionMode = "single",
  value,
  defaultValue,
  onValueChange,
  values,
  defaultValues,
  onValuesChange,
  confirmLabel,
  id,
  className,
  ref,
}: FoxChipProps) {
  const [uncontrolledChecked, setUncontrolledChecked] = useState(defaultChecked);
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
  const [uncontrolledValues, setUncontrolledValues] = useState<string[]>(defaultValues ?? []);
  const [open, setOpen] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const isSelect = type === "select";
  const isMultiple = selectionMode === "multiple";
  const items = options ?? [];

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

  // 두 계열의 상태를 한 모양(배열)으로 모아 목록에 넘긴다 — 목록은 계열마다 다른 prop을
  // 알 필요가 없다.
  const selectedValues = isMultiple
    ? currentValues
    : currentValue
      ? [currentValue]
      : [];

  const selectedItem = !isMultiple
    ? items.find((item) => item.value === currentValue)
    : undefined;

  const generatedId = useId();
  const chipId = id ?? generatedId;
  const listId = `${chipId}-list`;

  // 켜짐의 출처가 계열마다 다르다. select는 고른 값이 하나라도 있으면 켜진 것이고(다중에서도
  // 개수와 무관하게 켜짐/꺼짐 둘뿐이다), 나머지는 호출부가 준 `checked`(없으면 내부 상태)다.
  const isChecked = isSelect ? selectedValues.length > 0 : (checked ?? uncontrolledChecked);

  useEffect(() => {
    if (!open) return;
    function handlePointerDown(event: PointerEvent) {
      if (!rootRef.current?.contains(event.target as Node)) {
        setOpen(false);
      }
    }
    // 포인터만 보면 Tab으로 빠져나갈 때 목록이 열린 채 남는다. 포커스가 아무 데도 가지
    // 않은 경우(relatedTarget이 null)는 위 pointerdown이 이미 처리한다.
    //
    // 목록 안의 체크박스·라디오로 포커스가 옮겨 가는 것은 닫기가 아니다 — 그것들도
    // `rootRef` 안이라 아래 조건에 걸리지 않는다.
    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 toggle() {
    const next = !isChecked;
    if (checked === undefined) setUncontrolledChecked(next);
    onCheckedChange?.(next);
  }

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

      if (values === undefined) setUncontrolledValues(nextValues);
      onValuesChange?.(nextValues);
      // 목록을 닫지 않는다. 여러 개를 연달아 고르는 것이 목적이라, 하나 누를 때마다 닫히면
      // 쓸 수 없다. 닫는 것은 바깥 클릭·Escape·Tab이고 그건 위 effect와 키 처리가 맡는다.
      return;
    }

    if (value === undefined) setUncontrolledValue(next);
    onValueChange?.(next);
    // 단일 선택은 고르는 순간 할 일이 끝나므로 닫는다.
    close();
  }

  function close() {
    setOpen(false);
    // 목록 안에 포커스가 있었다면 갈 곳이 사라진다 — 여는 버튼으로 돌려놓는다.
    triggerRef.current?.focus();
  }

  function handleKeyDown(event: KeyboardEvent<HTMLButtonElement>) {
    if (disabled || !isSelect) return;

    if (!open) {
      if (event.key === "ArrowDown") {
        event.preventDefault();
        setOpen(true);
      }
      // Enter·Space는 막지 않는다 — 네이티브 버튼의 클릭이 그대로 열어 준다.
      return;
    }

    if (event.key === "Escape") {
      event.preventDefault();
      close();
    }
  }

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

  // 호출부 ref와 내부 ref를 함께 채운다 — 목록을 닫을 때 포커스를 돌려놓으려면 안에서도
  // 버튼을 쥐어야 한다.
  const attachTrigger = (node: HTMLButtonElement | null) => {
    triggerRef.current = node;
    if (typeof ref === "function") {
      ref(node);
    } else if (ref) {
      (ref as { current: HTMLButtonElement | null }).current = node;
    }
  };

  const chip = (
    <button
      ref={isSelect ? attachTrigger : ref}
      type="button"
      id={chipId}
      disabled={disabled}
      aria-label={ariaLabel}
      // select는 목록을 여닫는 버튼이다 — listbox가 아니라 체크박스·라디오 묶음을 열므로
      // `role="combobox"`가 아니라 그냥 버튼 + `aria-expanded`다.
      aria-expanded={isSelect ? open : undefined}
      // 목록은 열렸을 때만 렌더되므로, 닫힌 동안 없는 id를 가리키지 않게 한다.
      aria-controls={isSelect && open ? listId : undefined}
      aria-pressed={isSelect ? undefined : isChecked}
      className={cx(
        "fox-chip",
        TYPE_CLASS[type],
        SIZE_CLASS[size],
        isChecked && "fox-chip--checked",
        // 목록을 감싸는 자리가 따로 있어서, select는 바깥 div가 className을 받는다.
        !isSelect && className
      )}
      onClick={isSelect ? () => setOpen((current) => !current) : toggle}
      onKeyDown={handleKeyDown}
    >
      {/* 앞 아이콘. check는 켜졌을 때만 체크를, select는 호출부가 준 것을 놓는다. */}
      {type === "check" && isChecked ? (
        <span className="fox-chip__icon" aria-hidden="true">
          <CheckMark />
        </span>
      ) : null}
      {isSelect && icon ? (
        <span className="fox-chip__icon" aria-hidden="true">
          {icon}
        </span>
      ) : null}

      <span className="fox-chip__label">{selectedItem?.label ?? label}</span>

      {isSelect ? (
        <span className="fox-chip__icon fox-chip__arrow" aria-hidden="true">
          <ChevronMark />
        </span>
      ) : null}
    </button>
  );

  if (!isSelect) {
    return chip;
  }

  // `FoxChipSelectOption`은 `position: absolute`라 위치 기준이 되는 조상이 필요하다.
  return (
    <div ref={rootRef} className={cx("fox-chip-select", className)}>
      {chip}
      {open && items.length > 0 ? (
        <FoxChipSelectOption
          id={listId}
          labelledBy={chipId}
          // 칩의 크기를 그대로 물려준다 — 크기 이름이 같고, 항목의 글자 크기도 칩과 같은
          // 값으로 떨어진다(lg 17px, md 15px).
          size={size}
          selectionMode={selectionMode}
          options={items}
          values={selectedValues}
          onSelect={select}
          confirmLabel={confirmLabel}
          onConfirm={close}
        />
      ) : null}
    </div>
  );
}

// 시안 아이콘 둘. `fill`을 시안의 리터럴(흰색·#6D7882) 대신 `currentColor`로 두면 색을
// 상태 규칙이 정한다 — 남색 배경 위에서 흰색으로, 비활성에서 흐린 회색으로 따라간다.
// `width`·`height`를 쓰지 않는다 — 크기는 슬롯이 정하고 SVG는 100%로 따라온다.

// check 계열의 체크 표시.
function CheckMark() {
  return <i className="fox-ico fox-ico--bold fox-ico-Check" aria-hidden="true" />;
}

// select 계열의 아래 화살표.
function ChevronMark() {
  return <i className="fox-ico fox-ico-CaretDown" aria-hidden="true" />;
}
