import { ComponentPublicInstance, defineComponent, h, ref, computed, nextTick, watch } from 'vue'
import { CFormControlWrapper } from '../form/CFormControlWrapper'
import { COneTimePasswordInput } from './COneTimePasswordInput'
import { isValidInput, extractValidChars } from './utils'
import { getNextActiveElement, isRTL } from '../../utils'

const COneTimePassword = defineComponent({
  name: 'COneTimePassword',
  inheritAttrs: false,
  props: {
    /**
     * Function to generate aria-label for each input field. Receives current index (0-based) and total number of inputs.
     */
    ariaLabel: {
      type: Function,
      default: (index: number, total: number) => `Digit ${index + 1} of ${total}`,
    },
    /**
     * Automatically submit the form when all one time password fields are filled.
     */
    autoSubmit: {
      type: Boolean,
      default: false,
    },
    /**
     * Disable all one time password (OTP) input fields.
     */
    disabled: {
      type: Boolean,
      default: false,
    },
    /**
     * Initial value for uncontrolled Vue.js one time password input.
     */
    defaultValue: [String, Number],
    /**
     * Provide valuable, actionable feedback.
     */
    feedback: String,
    /**
     * Provide valuable, actionable feedback.
     */
    feedbackInvalid: String,
    /**
     * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
     */
    feedbackValid: String,
    /**
     * A string of all className you want applied to the floating label wrapper.
     */
    floatingClassName: String,
    /**
     * Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
     */
    floatingLabel: String,
    /**
     * ID attribute for the hidden input field.
     */
    id: String,
    /**
     * Set component validation state to invalid.
     */
    invalid: Boolean,
    /**
     * Add a caption for a component.
     */
    label: String,
    /**
     * Enforce sequential input (users must fill fields in order).
     */
    linear: {
      type: Boolean,
      default: true,
    },
    /**
     * Show input as password (masked characters).
     */
    masked: {
      type: Boolean,
      default: false,
    },
    /**
     * The default name for a value passed using v-model.
     */
    modelValue: [String, Number],
    /**
     * Name attribute for the hidden input field.
     */
    name: String,
    /**
     * Placeholder text for input fields. Single character applies to all fields, longer strings apply character-by-character.
     */
    placeholder: String,
    /**
     * Make Vue.js OTP input component read-only.
     */
    readonly: {
      type: Boolean,
      default: false,
    },
    /**
     * Makes the input field required for form validation.
     */
    required: {
      type: Boolean,
      default: false,
    },
    /**
     * Sets the visual size of the Vue.js one time password (OTP) input. Use 'sm' for small or 'lg' for large input fields.
     */
    size: {
      type: String,
      validator: (value: string) => ['sm', 'lg'].includes(value),
    },
    /**
     * Add helper text to the component.
     */
    text: String,
    /**
     * Display validation feedback in a styled tooltip.
     */
    tooltipFeedback: Boolean,
    /**
     * Input validation type: 'number' for digits only, or 'text' for free text.
     */
    type: {
      type: String,
      default: 'number',
      validator: (value: string) => ['number', 'text'].includes(value),
    },
    /**
     * Set component validation state to valid.
     */
    valid: Boolean,
    /**
     * The current value of the one time password input.
     */
    value: [String, Number],
  },
  emits: [
    /**
     * Callback triggered when the Vue.js one time password (OTP) value changes.
     */
    'update:modelValue',
    /**
     * Callback triggered when the Vue.js one time password (OTP) value changes.
     */
    'change',
    /**
     * Callback triggered when all Vue.js one time password (OTP) fields are filled.
     */
    'complete',
  ],
  setup(props, { attrs, slots, emit }) {
    const inputRefs = ref<(HTMLInputElement | null)[]>([])
    const hiddenInputRef = ref<HTMLInputElement | null>(null)
    const inputValues = ref<string[]>([])

    // Count valid OTP input children
    const inputCount = computed(() => {
      return inputRefs.value.filter((ref) => ref !== null).length
    })

    // Initialize input values
    const initializeValues = () => {
      const initialValue = String(props.modelValue ?? props.value ?? props.defaultValue ?? '')
      inputValues.value = Array.from({ length: inputCount.value }, (_, i) => initialValue[i] || '')
    }

    // Watch for changes in modelValue or value (controlled mode)
    watch(
      () => props.modelValue ?? props.value,
      (newValue) => {
        if (newValue !== undefined) {
          const valueString = String(newValue)
          inputValues.value = Array.from(
            { length: inputCount.value },
            (_, i) => valueString[i] || ''
          )
        }
      },
      { immediate: true }
    )

    // Watch for changes in inputCount
    watch(inputCount, initializeValues, { immediate: true })

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

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

      emit('update:modelValue', newValue)
      emit('change', newValue)

      if (newValue.length === inputCount.value) {
        emit('complete', newValue)

        if (props.autoSubmit) {
          nextTick(() => {
            const form = hiddenInputRef.value?.closest('form') as HTMLFormElement
            if (form && typeof form.requestSubmit === 'function') {
              form.requestSubmit()
            }
          })
        }
      }
    }

    // Write `chars` across the slots starting at `startIndex`, then sync focus, the
    // hidden form value and auto-submit. Shared by paste and by multi-character
    // `input` events.
    const distributeChars = (startIndex: number, chars: string) => {
      const newValues = [...inputValues.value]

      // A value at least as long as the field is a complete code: fill from the
      // first slot, whichever slot happens to be focused.
      const start = chars.length >= inputCount.value ? 0 : startIndex

      for (let i = 0; i < chars.length && start + i < inputCount.value; i++) {
        newValues[start + i] = chars[i]
      }

      inputValues.value = newValues
      updateValue(newValues)

      // Focus the next empty input or the last filled one
      const nextEmptyIndex = start + chars.length
      const nextInput =
        nextEmptyIndex < inputCount.value
          ? inputRefs.value[nextEmptyIndex]
          : inputRefs.value[inputRefs.value.length - 1]

      nextInput?.focus()
    }

    const handleInputChange = (index: number, event: Event) => {
      const target = event.target as HTMLInputElement
      const inputValue = target.value

      // SMS autofill, password managers, dictation and IME commits insert the
      // whole code at once and fire `input`, not `paste`. Spread it across the
      // slots instead of leaving it in one of them.
      if (inputValue.length > 1) {
        const chars = extractValidChars(inputValue, props.type as 'number' | 'text')

        // The slot is bound to the state, so restore what it holds before Vue
        // re-renders — an all-invalid value leaves the state untouched.
        target.value = inputValues.value[index] || ''

        if (chars) {
          distributeChars(index, chars)
        }

        return
      }

      if (inputValue.length === 1 && !isValidInput(inputValue, props.type as 'number' | 'text')) {
        return
      }

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

      inputValues.value = newValues
      updateValue(newValues)

      if (inputValue.length === 1) {
        const nextInput = getNextActiveElement(
          inputRefs.value.filter(Boolean) as HTMLInputElement[],
          target,
          true,
          false
        )
        nextInput?.focus()
      }
    }

    const handleInputFocus = (event: FocusEvent) => {
      const target = event.target as HTMLInputElement

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

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

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

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

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

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

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

      if (key === 'ArrowLeft') {
        const shouldMoveNext = isRTL(target as HTMLInputElement)
        const prevInput = getNextActiveElement(
          inputRefs.value.filter(Boolean) as HTMLInputElement[],
          target as HTMLInputElement,
          shouldMoveNext,
          false
        )
        prevInput?.focus()
      }
    }

    const handlePaste = (index: number, event: ClipboardEvent) => {
      event.preventDefault()
      const pastedData = event.clipboardData?.getData('text') || ''
      const validChars = extractValidChars(pastedData, props.type as 'number' | 'text')

      if (!validChars) {
        return
      }

      distributeChars(index, validChars)
    }

    return () => {
      if (!slots.default) {
        return null
      }

      const children = slots.default()
      let inputIndex = 0

      // `inputCount` is derived from the refs, so it is still 0 on the first
      // render — the slot attributes need the count the markup already carries.
      const slotCount = children.filter(
        (child) => (child.type as { name?: string })?.name === 'COneTimePasswordInput'
      ).length

      const processedChildren = children?.map((child) => {
        if (child.type && (child.type as { name?: string }).name === 'COneTimePasswordInput') {
          const currentInputIndex = inputIndex++

          return h(COneTimePasswordInput, {
            ...child.props,
            key: `otp-input-${currentInputIndex}`,
            type: props.masked ? 'password' : 'text',
            class: [
              {
                'is-invalid': props.invalid,
                'is-valid': props.valid,
              },
              child.props?.class,
            ],
            id: child.props?.id || (props.id ? `${props.id}-${currentInputIndex}` : undefined),
            name:
              child.props?.name || (props.name ? `${props.name}-${currentInputIndex}` : undefined),
            placeholder:
              child.props?.placeholder ||
              (props.placeholder && props.placeholder.length > 1
                ? props.placeholder[currentInputIndex]
                : props.placeholder),
            value: inputValues.value[currentInputIndex] || '',
            tabindex:
              currentInputIndex === 0 ? 0 : inputValues.value[currentInputIndex - 1] ? 0 : -1,
            disabled: props.disabled || child.props?.disabled,
            readonly: props.readonly || child.props?.readonly,
            required: props.required || child.props?.required,
            'aria-label':
              child.props?.['aria-label'] || props.ariaLabel!(currentInputIndex, inputCount.value),
            inputmode: props.type === 'number' ? 'numeric' : 'text',
            pattern: props.type === 'number' ? '[0-9]*' : '.*',
            // Only the first slot advertises the one-time code, so SMS autofill
            // and password managers target a single field instead of every slot,
            // and it has to fit a whole code while empty.
            autocomplete:
              child.props?.autocomplete ?? (currentInputIndex === 0 ? 'one-time-code' : 'off'),
            maxlength:
              child.props?.maxlength ??
              (currentInputIndex === 0 && !inputValues.value[0] ? slotCount : 1),
            enterkeyhint:
              child.props?.enterkeyhint ?? (currentInputIndex === slotCount - 1 ? 'done' : 'next'),
            onInput: (event: Event) => {
              handleInputChange(currentInputIndex, event)
            },
            onFocus: handleInputFocus,
            onKeydown: (event: KeyboardEvent) => {
              handleKeyDown(event)
            },
            onPaste: (event: ClipboardEvent) => {
              handlePaste(currentInputIndex, event)
            },
            ref: (el: Element | ComponentPublicInstance | null) => {
              // Get the actual DOM element - handle both direct elements and component instances
              if (el) {
                // If it's a component instance, get the DOM element
                const domElement = (el as ComponentPublicInstance).$el ?? el
                // Ensure it's actually an HTMLInputElement
                inputRefs.value[currentInputIndex] =
                  domElement && domElement.tagName === 'INPUT'
                    ? (domElement as HTMLInputElement)
                    : (el as HTMLInputElement)
              } else {
                inputRefs.value[currentInputIndex] = null
              }
            },
          })
        }
        return child
      })

      return h(
        CFormControlWrapper,
        {
          ...(typeof attrs['aria-describedby'] === 'string' && {
            describedby: attrs['aria-describedby'],
          }),
          feedback: props.feedback,
          feedbackInvalid: props.feedbackInvalid,
          feedbackValid: props.feedbackValid,
          floatingClassName: props.floatingClassName,
          floatingLabel: props.floatingLabel,
          id: props.id,
          invalid: props.invalid,
          label: props.label,
          text: props.text,
          tooltipFeedback: props.tooltipFeedback,
          valid: props.valid,
        },
        {
          default: () => [
            h(
              'div',
              {
                class: [
                  'form-otp',
                  {
                    [`form-otp-${props.size}`]: props.size,
                  },
                  attrs.class,
                ],
                role: 'group',
                ...attrs,
              },
              [
                ...processedChildren,
                h('input', {
                  type: 'hidden',
                  id: props.id,
                  name: props.name,
                  value: inputValues.value.join(''),
                  disabled: props.disabled,
                  ref: hiddenInputRef,
                }),
              ]
            ),
          ],
        }
      )
    }
  },
})

export { COneTimePassword }
