"use client";

import {
  useEffect,
  useId,
  useRef,
  useState,
  type ChangeEvent,
  type ClipboardEvent,
  type ComponentPropsWithRef,
  type FocusEvent,
  type FormEvent,
  type ReactNode,
} from "react";
import { cx, onlyDigits } from "../../utils";
import { FoxFormLabel, type FoxFormLabelRequirement } from "../fox-form-label";
import {
  foxValidationMessage,
  foxValidators,
  type FoxValidationErrors,
  type FoxValidationMessages,
  type FoxValidator,
} from "../../validation";

export type FoxInputSize = "lg" | "md" | "sm";
export type FoxInputState = "default" | "focused" | "completed" | "error" | "disabled" | "view";

/**
 * 네이티브 `<input>`이 받는 것을 그대로 넘겨받는다 — type·name·autoComplete·maxLength·
 * min·max·step·pattern·inputMode·required·readOnly·disabled 등. 이름이 겹치거나 의미가
 * 다른 것만 걷어내고 아래에서 다시 정의한다.
 */
type NativeInputProps = Omit<
  ComponentPropsWithRef<"input">,
  "size" | "onChange" | "onInput" | "className" | "children" | "hidden"
>;

export interface FoxInputProps extends NativeInputProps {
  size?: FoxInputSize;
  /** 값이 첫 인자다. 지우기 버튼은 이벤트 없이 빈 문자열로 호출한다. */
  onChange?: (value: string, event?: ChangeEvent<HTMLInputElement>) => void;
  onInput?: (value: string, event: FormEvent<HTMLInputElement>) => void;
  label?: string;
  requirement?: FoxFormLabelRequirement;
  /** 헬퍼 메시지. 색은 상태를 따라간다. */
  message?: string;
  /** 헬퍼 메시지 앞 아이콘. `currentColor`로 그려야 상태 색을 따라간다. */
  messageIcon?: ReactNode;
  /** 입력 오른쪽 아이콘. 아이콘 폰트를 넘긴다 — `<i class="fox-ico fox-ico-House" aria-hidden="true" />`. */
  icon?: ReactNode;
  /** 입력 오른쪽에 붙는 글자. 아이콘 슬롯과 달리 크기가 글자에 맞춰진다. */
  suffix?: ReactNode;
  /** 포커스 중이고 값이 있으면 지우기 버튼을 보여준다. */
  clearable?: boolean;
  clearLabel?: string;
  /** 오류 표시. `aria-invalid`가 되고 테두리·헬퍼 색이 바뀐다. */
  invalid?: boolean;
  /** 주면 모양이 그 상태로 고정된다. 없으면 포커스·값·비활성으로 브라우저가 판단한다. */
  state?: FoxInputState;
  /**
   * 숫자만 받는다. 타이핑·붙여넣기·끌어놓기·IME 어느 경로로 들어와도 숫자가 아닌 글자는
   * 그 자리에서 버리고 커서를 지킨다. `value`·`defaultValue`에 하이픈이 섞여 와도 숫자만
   * 보인다. `inputMode`를 따로 주지 않으면 `numeric`이 된다.
   */
  digitsOnly?: boolean;

  validators?: FoxValidator[];
  validationMessages?: FoxValidationMessages;
  validateOn?: "blur" | "change";
  onValidationChange?: (errors: FoxValidationErrors | null) => void;
  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
  hidden?: boolean;
  /** 배치 조정용. */
  className?: string;
}

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

/**
 * @fox 입력 필드. 네이티브 `<input>`을 감싸므로 date·password·email 등 모든 타입을 받는다.
 *
 * date 타입은 브라우저가 그리는 달력 아이콘과 피커를 그대로 쓴다 — 시안의 달력 아이콘으로
 * 바꾸지 않는다.
 *
 * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
 * (또는 개별 파티셜)로 한 번 불러와야 한다.
 */
function errorKey(errors: FoxValidationErrors | null): string {
  return errors ? Object.keys(errors).sort().join("|") : "";
}

export function FoxInput({
  size = "md",
  onChange,
  onInput,
  label,
  requirement,
  message,
  messageIcon,
  icon,
  suffix,
  clearable = false,
  clearLabel = "입력 지우기",
  invalid = false,
  state,
  digitsOnly = false,
  validators,
  validationMessages,
  validateOn = "blur",
  onValidationChange,
  hidden = false,
  className,
  id,
  value,
  defaultValue,
  ref,
  ...rest
}: FoxInputProps) {
  const autoId = useId();

  const fieldValue = digitsOnly && value !== undefined ? onlyDigits(String(value)) : value;
  const fieldDefaultValue =
    digitsOnly && defaultValue !== undefined ? onlyDigits(String(defaultValue)) : defaultValue;

  // 지우기 버튼은 값이 있을 때만 뜬다. 비제어 입력은 DOM만 알고 있으므로 직접 따라간다.
  const [typedIn, setTypedIn] = useState(() => String(fieldDefaultValue ?? "").length > 0);
  const fieldRef = useRef<HTMLInputElement>(null);

  const [draft, setDraft] = useState(() => String(fieldDefaultValue ?? ""));
  const [touched, setTouched] = useState(false);

  const validationErrors =
    validators && validators.length > 0
      ? foxValidators.compose(validators)(
          value !== undefined ? String(value) : draft
        )
      : null;

  const errorsKey = errorKey(validationErrors);
  const latestRef = useRef({ errors: validationErrors, notify: onValidationChange });
  useEffect(() => {
    latestRef.current = { errors: validationErrors, notify: onValidationChange };
  });
  useEffect(() => {
    latestRef.current.notify?.(latestRef.current.errors);
  }, [errorsKey]);

  if (hidden) {
    return null;
  }

  const fieldId = id ?? `${autoId}-field`;
  const messageId = `${autoId}-message`;
  const hasValue = value !== undefined ? String(value).length > 0 : typedIn;

  const showValidation = validateOn === "change" || touched;
  const validationText = showValidation
    ? foxValidationMessage(validationErrors, validationMessages)
    : undefined;

  const callerErrored = invalid || state === "error";
  const shownMessage = callerErrored ? message : (validationText ?? message);
  const errored = callerErrored || validationText !== undefined;

  // 호출부 ref와 내부 ref를 함께 채운다 — 지우기가 비제어 입력의 DOM 값을 비워야 한다.
  const attachField = (node: HTMLInputElement | null) => {
    fieldRef.current = node;
    if (typeof ref === "function") {
      ref(node);
    } else if (ref) {
      (ref as { current: HTMLInputElement | null }).current = node;
    }
  };

  // 숫자 전용이면 DOM 값을 그 자리에서 고쳐 쓴다. 커서 앞에서 지운 글자 수만큼 커서를 당겨
  // 제자리를 지킨다 — 값을 통째로 바꾸면 브라우저가 커서를 끝으로 보내기 때문이다.
  const readFieldValue = (field: HTMLInputElement): string => {
    const raw = field.value;
    if (!digitsOnly) {
      return raw;
    }
    const clean = onlyDigits(raw);
    if (clean === raw) {
      return raw;
    }
    const caret = field.selectionStart;
    field.value = clean;
    if (caret !== null) {
      const position = onlyDigits(raw.slice(0, caret)).length;
      field.setSelectionRange(position, position);
    }
    return clean;
  };

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const next = readFieldValue(event.target);
    if (value === undefined) {
      setTypedIn(next.length > 0);
    }
    setDraft(next);
    onChange?.(next, event);
  };

  const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
    setTouched(true);
    rest.onBlur?.(event);
  };

  // 숫자 전용 붙여넣기는 숫자만 추려 직접 넣는다. 브라우저는 maxLength를 붙여넣은 원문에 먼저
  // 적용해 `123-45-67890`의 뒷자리를 잘라 버리므로 기본 동작에 맡길 수 없다.
  const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
    rest.onPaste?.(event);
    if (!digitsOnly || event.defaultPrevented) {
      return;
    }
    event.preventDefault();
    const digits = onlyDigits(event.clipboardData.getData("text"));
    if (digits === "") {
      return;
    }
    const field = event.currentTarget;
    if (document.execCommand("insertText", false, digits)) {
      return;
    }
    // execCommand가 없는 환경 — 값을 직접 넣고 input 이벤트를 흘린다. React가 노드에 심어 둔
    // 값 추적기를 피하려면 프로토타입의 setter를 써야 onChange가 돈다.
    const start = field.selectionStart ?? field.value.length;
    const end = field.selectionEnd ?? start;
    let next = field.value.slice(0, start) + digits + field.value.slice(end);
    if (field.maxLength > 0) {
      next = next.slice(0, field.maxLength);
    }
    const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
    setValue?.call(field, next);
    const caret = Math.min(start + digits.length, next.length);
    field.setSelectionRange(caret, caret);
    field.dispatchEvent(new Event("input", { bubbles: true }));
  };

  const handleInput = (event: FormEvent<HTMLInputElement>) => {
    onInput?.(readFieldValue(event.target as HTMLInputElement), event);
  };

  const handleClear = () => {
    const field = fieldRef.current;
    if (field) {
      // 비제어 입력은 DOM을 직접 비운다. 제어 입력이면 onChange를 받은 호출부가 비운다.
      field.value = "";
      field.focus();
    }
    setTypedIn(false);
    setDraft("");
    onChange?.("");
  };

  return (
    <div
      className={cx("fox-input", SIZE_CLASS[size], className)}
      data-state={state}
    >
      {label && (
        <FoxFormLabel htmlFor={fieldId} requirement={requirement}>
          {label}
        </FoxFormLabel>
      )}

      <div className="fox-input__box">
        <input
          {...rest}
          inputMode={rest.inputMode ?? (digitsOnly ? "numeric" : undefined)}
          ref={attachField}
          id={fieldId}
          className="fox-input__field"
          value={fieldValue}
          defaultValue={fieldDefaultValue}
          aria-invalid={errored || undefined}
          aria-describedby={shownMessage ? messageId : undefined}
          onChange={handleChange}
          onInput={handleInput}
          onBlur={handleBlur}
          onPaste={handlePaste}
        />

        {clearable && hasValue && (
          <button
            type="button"
            className="fox-input__clear"
            aria-label={clearLabel}
            onClick={handleClear}
          >
            <i className="fox-ico fox-ico-XCircle" aria-hidden="true" />
          </button>
        )}

        {suffix && <span className="fox-input__suffix">{suffix}</span>}

        {icon && (
          <span className="fox-input__icon" aria-hidden="true">
            {icon}
          </span>
        )}
      </div>

      {shownMessage && (
        <p className="fox-input__message" id={messageId}>
          {messageIcon && (
            <span className="fox-input__message-icon" aria-hidden="true">
              {messageIcon}
            </span>
          )}
          {shownMessage}
        </p>
      )}
    </div>
  );
}
