import type { ChangeEvent, ClipboardEvent, InputHTMLAttributes, KeyboardEvent, ReactNode } from 'react';
import { forwardRef, useCallback, useId, useMemo } from 'react';
import { digitsOnly, lettersOnly } from './inputMask';
import './input.css';

/** Updates DOM value in a way that works with React controlled inputs. */
function setNativeInputValue(input: HTMLInputElement, value: string) {
  const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
  setter?.call(input, value);
}

/** Restricts typed / pasted characters (digits-only or letters-only). */
export type InputCharacterMask = 'none' | 'digits' | 'letters';

export type InputProps = {
  label?: ReactNode;
  error?: boolean;
  errorMessage?: string;
  helperText?: string;
  /** Appended control (e.g. password visibility toggle), input-group layout. */
  endAdornment?: ReactNode;
  /**
   * Filters input: `digits` keeps 0–9; `letters` keeps Unicode letters and spaces (no digits).
   * Prefer {@link NumericInput} / {@link AlphabetInput} for clearer intent at call sites.
   */
  characterMask?: InputCharacterMask;
  /** Emits on each change (mirrors Angular valueChange). */
  onValueChange?: (value: string) => void;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
    onChange?: InputHTMLAttributes<HTMLInputElement>['onChange'];
  };

const LETTER_OR_SPACE = /^[\p{L}\s]$/u;

export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
  {
    label,
    placeholder = '',
    disabled = false,
    readOnly,
    error = false,
    errorMessage = '',
    helperText = '',
    required,
    'aria-label': ariaLabelProp,
    'aria-describedby': ariaDescribedByProp,
    minLength,
    maxLength,
    min,
    max,
    type = 'text',
    id: idProp,
    inputMode,
    pattern,
    value,
    defaultValue,
    onValueChange,
    onChange,
    onClick,
    onKeyDown,
    onPaste,
    className,
    endAdornment,
    characterMask = 'none',
    ...rest
  },
  ref
) {
  const uid = useId();
  const inputId = idProp ?? `ccl-input-${uid.replace(/:/g, '')}`;
  const labelId = `${inputId}-label`;
  const hintId = `${inputId}-hint`;
  const errorId = `${inputId}-error`;

  const describedBy = useMemo(() => {
    const ids: string[] = [];
    if (ariaDescribedByProp) ids.push(ariaDescribedByProp);
    if (helperText) ids.push(hintId);
    if (error && errorMessage) ids.push(errorId);
    return ids.length ? ids.join(' ') : undefined;
  }, [ariaDescribedByProp, helperText, hintId, error, errorMessage, errorId]);

  const showLabel = label != null && label !== '';

  const inputClassName = ['ccl-input', endAdornment ? 'ccl-input-group__control' : '', className]
    .filter(Boolean)
    .join(' ');

  const applyMask = useCallback(
    (raw: string) => {
      if (characterMask === 'digits') return digitsOnly(raw);
      if (characterMask === 'letters') return lettersOnly(raw);
      return raw;
    },
    [characterMask]
  );

  const handleChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      const el = e.currentTarget;
      let next = el.value;
      if (characterMask !== 'none' && !readOnly && !disabled) {
        next = applyMask(next);
        if (next !== el.value) {
          setNativeInputValue(el, next);
        }
      }
      onChange?.(e);
      onValueChange?.(next);
    },
    [applyMask, characterMask, disabled, onChange, onValueChange, readOnly]
  );

  const handleKeyDown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      if (characterMask === 'none' || readOnly || disabled) {
        onKeyDown?.(e);
        return;
      }
      const mod = e.ctrlKey || e.metaKey || e.altKey;
      const allowedNav = [
        'Backspace',
        'Delete',
        'Tab',
        'Escape',
        'Enter',
        'ArrowLeft',
        'ArrowRight',
        'ArrowUp',
        'ArrowDown',
        'Home',
        'End',
      ].includes(e.key);
      if (mod || allowedNav || e.key.length !== 1) {
        onKeyDown?.(e);
        return;
      }
      if (characterMask === 'digits') {
        const isDigit = /^[0-9]$/.test(e.key);
        if (!isDigit) e.preventDefault();
      } else if (characterMask === 'letters') {
        const ok = LETTER_OR_SPACE.test(e.key);
        if (!ok) e.preventDefault();
      }
      onKeyDown?.(e);
    },
    [characterMask, disabled, onKeyDown, readOnly]
  );

  const handlePaste = useCallback(
    (e: ClipboardEvent<HTMLInputElement>) => {
      if (characterMask === 'none' || readOnly || disabled) {
        onPaste?.(e);
        return;
      }
      const paste = e.clipboardData.getData('text');
      if (!paste) {
        onPaste?.(e);
        return;
      }
      const dirty =
        characterMask === 'digits' ? /\D/.test(paste) : paste.replace(/[\p{L}\s]/gu, '').length > 0;
      if (dirty) {
        e.preventDefault();
        const el = e.currentTarget;
        const start = el.selectionStart ?? el.value.length;
        const end = el.selectionEnd ?? el.value.length;
        const merged = el.value.slice(0, start) + paste + el.value.slice(end);
        const next = applyMask(merged);
        setNativeInputValue(el, next);
        el.setSelectionRange(next.length, next.length);
        const synthetic = {
          ...e,
          target: el,
          currentTarget: el,
        } as unknown as ChangeEvent<HTMLInputElement>;
        onChange?.(synthetic);
        onValueChange?.(next);
      }
      onPaste?.(e);
    },
    [applyMask, characterMask, disabled, onChange, onPaste, onValueChange, readOnly]
  );

  const handleClick = useCallback(
    (e: React.MouseEvent<HTMLInputElement>) => {
      onClick?.(e);
      if (e.defaultPrevented) return;
      if (disabled || readOnly) return;
      const el = e.currentTarget;
      const dateLike =
        type === 'date' || type === 'datetime-local' || type === 'month' || type === 'week' || type === 'time';
      if (!dateLike) return;
      try {
        (el as HTMLInputElement & { showPicker?: () => void }).showPicker?.();
      } catch {
        // ignore
      }
    },
    [disabled, onClick, readOnly, type]
  );

  const resolvedInputMode = characterMask === 'digits' ? 'numeric' : inputMode;
  const resolvedPattern = characterMask === 'digits' ? '[0-9]*' : pattern;

  const control = (
    <input
      ref={ref}
      id={inputId}
      className={inputClassName}
      type={type}
      placeholder={placeholder}
      disabled={disabled}
      readOnly={readOnly}
      required={required}
      value={value}
      defaultValue={defaultValue}
      aria-label={ariaLabelProp}
      aria-describedby={describedBy}
      aria-invalid={error}
      aria-required={required}
      minLength={minLength}
      maxLength={maxLength}
      min={min}
      max={max}
      inputMode={resolvedInputMode}
      pattern={resolvedPattern}
      onChange={handleChange}
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      onPaste={handlePaste}
      {...rest}
    />
  );

  return (
    <div className="ccl-input-wrapper">
      {showLabel ? (
        <label htmlFor={inputId} id={labelId} className="ccl-input__label">
          {label}
          {required ? (
            <span className="ccl-input__required" aria-label="required">
              *
            </span>
          ) : null}
        </label>
      ) : null}

      {endAdornment ? (
        <div className="ccl-input-group">
          {control}
          <div className="ccl-input-group__append">{endAdornment}</div>
        </div>
      ) : (
        control
      )}

      {helperText ? (
        <span id={hintId} className="ccl-input__helper">
          {helperText}
        </span>
      ) : null}

      {error && errorMessage ? (
        <span id={errorId} className="ccl-input__error" role="alert">
          {errorMessage}
        </span>
      ) : null}
    </div>
  );
});
