'use client'

import { useEffect, useRef, useState } from 'react'
import { useMergedRefs } from '@base-ui/utils/useMergedRefs'

import { useControlled, useGDS } from '../hooks/index.ts'
import { cn } from '../utils/index.ts'
import { Field } from './base/Field.tsx'
import { Input, type InputProps } from './Input.tsx'

const PADDING_CHARACTER = ' '
const TRIM_END_REGEX = new RegExp(`${PADDING_CHARACTER}+$`)

export interface OTCInputProps extends Omit<
  InputProps,
  'action' | 'maxLength' | 'onInput' | 'render' | 'type' | 'unit'
> {
  /** @default 'number' */
  type?: 'text' | 'number' | undefined
  /** @default 6 */
  length?: number | undefined
  /** @default 'one-time-code' */
  autoComplete?: InputProps['autoComplete'] | undefined
  /**
   * Return `true` or the passed string (potentially transformed, e.g. uppercased) if valid, or
   * anything falsy if not.
   */
  onInput?: (characterOrPastedString: string, atIndex: number) => string | boolean | void
}

export function OTCInput({
  ref: passedRef,
  type = 'number',
  length: passedLength = 6,
  value: controlledValue,
  defaultValue,
  onChange,
  onInput,
  autoComplete = 'one-time-code',
  className,
  ...props
}: OTCInputProps) {
  useGDS()

  const inputRef = useRef<HTMLInputElement>(null)
  const inputPassedRef = useMergedRefs(inputRef, passedRef)

  const length = Math.min(Math.max(passedLength, 1), 16)

  const [value, privateSetValue] = useControlled(controlledValue, defaultValue ?? '', onChange)
  const privatePaddedValue = useRef('')
  if (
    privatePaddedValue.current.length !== length ||
    unpadValue(privatePaddedValue.current) !== value
  ) {
    privatePaddedValue.current = padValue(value, length)
  }

  const paddedValue = privatePaddedValue.current

  const setValue = (newValue: string) => {
    const newPaddedValue = padValue(newValue, length)
    const newUnpaddedValue = unpadValue(newPaddedValue)
    privatePaddedValue.current = newPaddedValue
    privateSetValue(newUnpaddedValue)
    refreshInput()
  }

  const [focusedBoxIndex, privateSetFocusedBoxIndex] = useState<number | null>(null)
  const firstEmptyBoxIndex = ((index: number) => (index === -1 ? null : index))(
    paddedValue.indexOf(PADDING_CHARACTER),
  )
  const focusedFrom = useRef<'keyboard' | 'mouse'>('keyboard')
  const clampIndex = (index: number) => Math.min(Math.max(index, 0), length - 1)

  const setFocusedBoxIndex = (index: number | null) => {
    privateSetFocusedBoxIndex(index === null ? null : clampIndex(index))
  }

  const insertIntoValue = (atIndex: number, stringToInsert: string) => {
    const index = clampIndex(atIndex)
    let validatedStringToInsert = onInput ? onInput(stringToInsert, index) || '' : stringToInsert
    if (validatedStringToInsert === true) validatedStringToInsert = stringToInsert
    if (validatedStringToInsert === '') return
    setValue(
      paddedValue.substring(0, index) +
        validatedStringToInsert +
        paddedValue.substring(index + validatedStringToInsert.length),
    )
    setFocusedBoxIndex(index + validatedStringToInsert.length)
  }

  const deleteFromValue = (fromIndex: number, charactersToDelete: number) => {
    const index = clampIndex(fromIndex)
    setValue(
      paddedValue.substring(0, index) +
        PADDING_CHARACTER.repeat(charactersToDelete) +
        paddedValue.substring(index + charactersToDelete),
    )
  }

  const refreshInput = () => {
    window.setTimeout(() => {
      const input = inputRef.current
      if (!input) return
      if (props.readOnly) {
        input.setSelectionRange(0, length)
      } else if (focusedBoxIndex !== null) {
        input.setSelectionRange(focusedBoxIndex, focusedBoxIndex + 1)
      }
    }, 0)
  }

  useEffect(refreshInput)

  return (
    <Input
      ref={inputPassedRef}
      type={type}
      autoComplete={autoComplete}
      value={paddedValue}
      className={cn('gds-otc-input', className)}
      {...props}
      onFocus={(event) => {
        props.onFocus?.(event)
        if (event.defaultPrevented) return
        if (props.readOnly) return
        if (focusedFrom.current === 'keyboard') {
          setFocusedBoxIndex(firstEmptyBoxIndex ?? 0)
        }
        refreshInput()
      }}
      onBlur={(event) => {
        props.onBlur?.(event)
        if (event.defaultPrevented) return
        if (props.readOnly) return
        setFocusedBoxIndex(null)
      }}
      onKeyDown={(event) => {
        props.onKeyDown?.(event)
        if (event.defaultPrevented) return
        if (props.readOnly) return
        if (event.key === 'Enter') return // Allow pressing Enter to submit the form the input is in
        const index = focusedBoxIndex ?? 0
        if (event.key === 'Tab') {
          if ((event.shiftKey && index > 0) || (!event.shiftKey && index < length - 1)) {
            event.preventDefault()
            if (event.shiftKey) {
              setFocusedBoxIndex(index - 1)
            } else {
              setFocusedBoxIndex(index + 1)
            }
          }
          return
        }
        if (event.key.startsWith('Arrow')) {
          event.preventDefault()
          switch (event.key) {
            case 'ArrowLeft':
              setFocusedBoxIndex(index - 1)
              break
            case 'ArrowRight':
              setFocusedBoxIndex(index + 1)
              break
            case 'ArrowUp':
              setFocusedBoxIndex(0)
              break
            case 'ArrowDown':
              setFocusedBoxIndex(length - 1)
              break
          }
          return
        }
        if (event.key === 'Backspace' || event.key === 'Delete') {
          event.preventDefault()
          if (paddedValue[index] !== PADDING_CHARACTER) {
            deleteFromValue(index, 1)
          } else if (event.key === 'Delete') {
            deleteFromValue(index + 1, 1)
            setFocusedBoxIndex(index + 1)
          } else if (index > 0) {
            deleteFromValue(index - 1, 1)
            setFocusedBoxIndex(index - 1)
          }
          return
        }
        // Handle shortcuts
        if (event.ctrlKey || event.metaKey) {
          if (event.key === 'a') {
            event.preventDefault()
          }
          if (event.key === 'x') {
            window.setTimeout(() => deleteFromValue(index, 1), 5)
          }
          return
        }
        // Prevent entering illegal characters or the padding character
        if (
          event.key.length > 1 ||
          (type === 'number' && /\D/.test(event.key)) ||
          event.key === PADDING_CHARACTER
        ) {
          event.preventDefault()
          return
        }
        insertIntoValue(index, event.key)
      }}
      onPaste={(event) => {
        props.onPaste?.(event)
        if (event.defaultPrevented) return
        if (props.readOnly) return
        const index = focusedBoxIndex ?? 0
        event.preventDefault()
        const pastedText = event.clipboardData.getData('text')
        const filteredPastedText = (
          type === 'number' ? pastedText.replace(/\D/g, '') : pastedText
        ).replace(/\s/g, '')
        insertIntoValue(
          Math.min(Math.max(firstEmptyBoxIndex ?? 0, length - filteredPastedText.length), index),
          filteredPastedText,
        )
      }}
      renderInput={(renderProps, { inputProps }) => {
        return (
          <div {...renderProps} className={cn(renderProps.className, 'flex gap-3')}>
            <div className="pointer-events-none absolute opacity-0">
              <input {...inputProps} />
            </div>
            {Array.from({ length }).map((_, index) => (
              <div
                key={index}
                data-focus={index === focusedBoxIndex || undefined}
                className={`
                  @container-normal/gds-field grow
                  @state-read-only/field:state-read-only
                  @state-blank/field:state-blank
                  @state-focus/field:data-focus:state-focus
                  @state-enabled/field:has-[input:hover]:state-hover
                  @state-disabled/field:state-disabled
                  @prop-size-small/field:size-12
                  @prop-size-small/field:text-16
                  @prop-size-medium/field:size-14
                  @prop-size-medium/field:text-20
                  @prop-size-large/field:size-16
                  @prop-size-large/field:text-24
                `}
              >
                <Field.Box
                  data-focus={index === focusedBoxIndex || undefined} // TODO: Remove when Firefox supports style queries, as well as the `firefox:` classes below
                  className={`
                    h-full
                    firefox:@state-focus/field:not-data-focus:border-form-idle
                    firefox:@state-focus/field:not-data-focus:group-data-[status=error]/field:border-form-status-error-idle
                    firefox:@state-focus/field:not-data-focus:group-data-[status=warning]/field:border-form-status-warning-idle
                  `}
                >
                  <input
                    type="text"
                    placeholder={props.placeholder?.[index]}
                    value={paddedValue[index] === PADDING_CHARACTER ? '' : paddedValue[index]}
                    disabled={props.disabled}
                    readOnly
                    aria-readonly
                    aria-hidden
                    tabIndex={-1}
                    onMouseDown={(event) => {
                      event.preventDefault()
                      setFocusedBoxIndex(index)
                      focusedFrom.current = 'mouse'
                      inputRef.current?.focus()
                      focusedFrom.current = 'keyboard'
                    }}
                    data-focus={index === focusedBoxIndex || undefined} // TODO: Remove when Firefox supports style queries, as well as the `data-focus` variant below
                    className={`
                      size-full text-center
                      @state-focus/field:data-focus:placeholder:text-transparent
                    `}
                  />
                </Field.Box>
              </div>
            ))}
          </div>
        )
      }}
    />
  )
}

function padValue(value: string, length: number) {
  return value.substring(0, length).padEnd(length, PADDING_CHARACTER)
}

function unpadValue(paddedValue: string) {
  return paddedValue.replace(TRIM_END_REGEX, '')
}
