import React, {
  Children,
  ClipboardEvent,
  cloneElement,
  FocusEvent,
  InputEvent,
  forwardRef,
  HTMLAttributes,
  isValidElement,
  KeyboardEvent,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  ReactElement,
} from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import { CFormControlWrapper, CFormControlWrapperProps } from '../form/CFormControlWrapper'
import { COneTimePasswordInput, COneTimePasswordInputProps } from './COneTimePasswordInput'

import { isValidInput, extractValidChars } from './utils'
import { getNextActiveElement, isRTL } from '../../utils'

type ComponentWithDisplayName = {
  displayName?: string
}

export interface COneTimePasswordProps
  extends CFormControlWrapperProps, Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
  /**
   * Function to generate aria-label for each input field. Receives current index (0-based) and total number of inputs.
   */
  ariaLabel?: (index: number, total: number) => string

  /**
   * Automatically submit the form when all one time password fields are filled.
   */
  autoSubmit?: boolean

  /**
   * A string of all className you want applied to the React.js OTP component.
   */
  className?: string

  /**
   * Initial value for uncontrolled React.js one time password input.
   */
  defaultValue?: string | number

  /**
   * Disable all one time password (OTP) input fields.
   */
  disabled?: boolean

  /**
   * ID attribute for the hidden input field.
   */
  id?: string

  /**
   * Enforce sequential input (users must fill fields in order).
   */
  linear?: boolean

  /**
   * Show input as password (masked characters).
   */
  masked?: boolean

  /**
   * Name attribute for the hidden input field.
   */
  name?: string

  /**
   * Callback triggered when the React.js one time password (OTP) value changes.
   */
  onChange?: (value: string) => void

  /**
   * Callback triggered when all React.js one time password (OTP) fields are filled.
   */
  onComplete?: (value: string) => void

  /**
   * Placeholder text for input fields. Single character applies to all fields, longer strings apply character-by-character.
   */
  placeholder?: string

  /**
   * Make React.js OTP input component read-only.
   */
  readOnly?: boolean

  /**
   * Makes the input field required for form validation.
   */
  required?: boolean

  /**
   * Sets the visual size of the React.js one time password (OTP) input. Use 'sm' for small or 'lg' for large input fields.
   */
  size?: 'sm' | 'lg'

  /**
   * Input validation type: 'number' for digits only, or 'text' for free text.
   */
  type?: 'number' | 'text'

  /**
   * Current value for controlled OTP input.
   */
  value?: string | number
}

export const COneTimePassword = forwardRef<HTMLDivElement, COneTimePasswordProps>(
  (
    {
      ariaLabel = (index: number, total: number) => `Digit ${index + 1} of ${total}`,
      autoSubmit = false,
      children,
      className,
      defaultValue,
      disabled = false,
      feedback,
      feedbackInvalid,
      feedbackValid,
      floatingClassName,
      floatingLabel,
      id,
      invalid,
      label,
      linear = true,
      masked = false,
      name,
      onChange,
      onComplete,
      placeholder,
      readOnly = false,
      required = false,
      size,
      text,
      tooltipFeedback,
      type = 'number',
      valid,
      value,
      ...rest
    },
    ref
  ) => {
    const inputRefs = useRef<(HTMLInputElement | null)[]>([])
    const hiddenInputRef = useRef<HTMLInputElement>(null)

    // Count valid OTP input children
    const inputCount = useMemo(() => {
      return Children.count(children)
    }, [children])

    const [inputValues, setInputValues] = useState<string[]>(() => {
      const initialValue = String(value ?? defaultValue ?? '')
      return Array.from({ length: inputCount }, (_, i) => initialValue[i] || '')
    })

    // Update input values when value prop changes (controlled mode)
    useEffect(() => {
      if (value !== undefined) {
        const valueString = String(value)
        setInputValues(Array.from({ length: inputCount }, (_, i) => valueString[i] || ''))
      }
    }, [value, inputCount])

    // Update hidden input and trigger events
    const updateValue = useCallback(
      (newValues: string[]) => {
        const newValue = newValues.join('')

        if (hiddenInputRef.current) {
          hiddenInputRef.current.value = newValue
        }

        onChange?.(newValue)

        if (newValue.length === inputCount) {
          onComplete?.(newValue)

          if (autoSubmit) {
            const form = hiddenInputRef.current?.closest('form')
            if (form && typeof form.requestSubmit === 'function') {
              form.requestSubmit()
            }
          }
        }
      },
      [autoSubmit, onChange, onComplete, inputCount]
    )

    const handleInputChange = useCallback(
      (index: number, event: InputEvent<HTMLInputElement>) => {
        const inputValue = event.currentTarget.value

        if (inputValue.length === 1 && !isValidInput(inputValue, type)) {
          return
        }

        const newValues = [...inputValues]
        newValues[index] = inputValue.length === 1 ? inputValue : ''

        setInputValues(newValues)
        updateValue(newValues)

        if (inputValue.length === 1) {
          const nextInput = getNextActiveElement(
            inputRefs.current as HTMLInputElement[],
            event.currentTarget,
            true,
            false
          )

          nextInput?.focus()
        }
      },
      [inputValues, type, updateValue]
    )

    const handleInputFocus = useCallback(
      (event: FocusEvent<HTMLInputElement>) => {
        const { target } = event

        if (target.value) {
          setTimeout(() => {
            target.select()
          }, 0)
          return
        }

        if (linear) {
          const firstEmptyInput = inputRefs.current.find((input) => !input?.value)
          if (firstEmptyInput && firstEmptyInput !== target) {
            firstEmptyInput.focus()
          }
        }
      },
      [linear]
    )

    const handleKeyDown = useCallback(
      (event: KeyboardEvent<HTMLInputElement>) => {
        const { key, target } = event

        if (key === 'Backspace' && (target as HTMLInputElement).value === '') {
          const newValues = [...inputValues]
          const prevInput = getNextActiveElement(
            inputRefs.current as HTMLInputElement[],
            target as HTMLInputElement,
            false,
            false
          )

          if (prevInput) {
            const prevIndex = inputRefs.current.indexOf(prevInput as HTMLInputElement)
            if (prevIndex !== -1) {
              newValues[prevIndex] = ''
              setInputValues(newValues)
              updateValue(newValues)
              prevInput.focus()
            }
          }

          return
        }

        if (key === 'ArrowRight') {
          if (linear && (target as HTMLInputElement).value === '') {
            return
          }

          const shouldMoveNext = !isRTL(target as HTMLInputElement)
          const nextInput = getNextActiveElement(
            inputRefs.current as HTMLInputElement[],
            target as HTMLInputElement,
            shouldMoveNext,
            false
          )
          nextInput?.focus()
          return
        }

        if (key === 'ArrowLeft') {
          const shouldMoveNext = isRTL(target as HTMLInputElement)
          const prevInput = getNextActiveElement(
            inputRefs.current as HTMLInputElement[],
            target as HTMLInputElement,
            shouldMoveNext,
            false
          )
          prevInput?.focus()
        }
      },
      [inputValues, linear, updateValue]
    )

    const handlePaste = useCallback(
      (index: number, event: ClipboardEvent<HTMLInputElement>) => {
        event.preventDefault()
        const pastedData = event.clipboardData.getData('text')
        const validChars = extractValidChars(pastedData, type)

        if (!validChars) return

        const newValues = [...inputValues]
        const startIndex = index

        for (let i = 0; i < validChars.length && startIndex + i < inputCount; i++) {
          newValues[startIndex + i] = validChars[i]
        }

        setInputValues(newValues)
        updateValue(newValues)

        // Focus the next empty input or the last filled input
        const nextEmptyIndex = startIndex + validChars.length
        if (nextEmptyIndex < inputCount) {
          inputRefs.current[nextEmptyIndex]?.focus()
        } else {
          inputRefs.current.at(-1)?.focus()
        }
      },
      [inputValues, type, updateValue, inputCount]
    )

    let inputIndex = 0

    return (
      <CFormControlWrapper
        describedby={rest['aria-describedby']}
        feedback={feedback}
        feedbackInvalid={feedbackInvalid}
        feedbackValid={feedbackValid}
        floatingClassName={floatingClassName}
        floatingLabel={floatingLabel}
        id={id}
        invalid={invalid}
        label={label}
        text={text}
        tooltipFeedback={tooltipFeedback}
        valid={valid}
      >
        <div
          className={classNames(
            'form-otp',
            {
              [`form-otp-${size}`]: size,
            },
            className
          )}
          role="group"
          ref={ref}
          {...rest}
        >
          {Children.map(children, (child, index) => {
            if (
              !isValidElement(child) ||
              (child.type as ComponentWithDisplayName)?.displayName !== 'COneTimePasswordInput'
            ) {
              return child
            }

            const inputChild = child as ReactElement<
              COneTimePasswordInputProps & React.RefAttributes<HTMLInputElement>,
              typeof COneTimePasswordInput
            >
            const currentInputIndex = inputIndex++

            return cloneElement(inputChild, {
              ...inputChild.props,
              type: masked ? 'password' : 'text',
              className: classNames(
                {
                  'is-invalid': invalid,
                  'is-valid': valid,
                },
                inputChild.props.className
              ),
              id: inputChild.props.id || (id ? `${id}-${currentInputIndex}` : undefined),
              name: inputChild.props.name || (name ? `${name}-${currentInputIndex}` : undefined),
              placeholder:
                inputChild.props.placeholder ||
                (placeholder && placeholder.length > 1
                  ? placeholder[currentInputIndex]
                  : placeholder),
              value: inputValues[currentInputIndex] || '',
              tabIndex: currentInputIndex === 0 ? 0 : inputValues[currentInputIndex - 1] ? 0 : -1,
              disabled: disabled || inputChild.props.disabled,
              readOnly: readOnly || inputChild.props.readOnly,
              required: required || inputChild.props.required,
              'aria-label':
                inputChild.props['aria-label'] || ariaLabel(currentInputIndex, inputCount),

              inputMode: type === 'number' ? 'numeric' : 'text',
              pattern: type === 'number' ? '[0-9]*' : '.*',
              onInput: (event: InputEvent<HTMLInputElement>) => {
                handleInputChange(currentInputIndex, event)
                inputChild.props.onInput?.(event)
              },
              onFocus: (event: FocusEvent<HTMLInputElement>) => {
                handleInputFocus(event)
                inputChild.props.onFocus?.(event)
              },
              onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {
                handleKeyDown(event)
                inputChild.props.onKeyDown?.(event)
              },
              onPaste: (event: ClipboardEvent<HTMLInputElement>) => {
                handlePaste(index, event)
                inputChild.props.onPaste?.(event)
              },
              ref: (el: HTMLInputElement | null) => {
                inputRefs.current[currentInputIndex] = el
              },
            })
          })}
          <input
            type="hidden"
            id={id}
            name={name}
            value={inputValues.join('')}
            disabled={disabled}
            ref={hiddenInputRef}
          />
        </div>
      </CFormControlWrapper>
    )
  }
)

COneTimePassword.propTypes = {
  ariaLabel: PropTypes.func,
  autoSubmit: PropTypes.bool,
  children: PropTypes.node,
  className: PropTypes.string,
  defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  disabled: PropTypes.bool,
  id: PropTypes.string,
  linear: PropTypes.bool,
  masked: PropTypes.bool,
  name: PropTypes.string,
  onChange: PropTypes.func,
  onComplete: PropTypes.func,
  placeholder: PropTypes.string,
  readOnly: PropTypes.bool,
  required: PropTypes.bool,
  size: PropTypes.oneOf(['sm', 'lg']),
  type: PropTypes.oneOf(['number', 'text']),
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  ...CFormControlWrapper.propTypes,
}

COneTimePassword.displayName = 'COneTimePassword'
