import { findOptionByValue, findTypeaheadMatches } from 'shared-utils/combobox/option-utils'
import {
  getInputKeyAction,
  getInputValueAction,
  checkForMatches,
  getSingleValueForInput,
} from 'shared-utils/combobox/input-utils'
import { ComboboxValue } from './combobox-value'
import type { PktTag } from '../tag'

/**
 * Event handler layer for PktCombobox.
 * Handles user interactions: input, focus, keyboard, clicks, tags.
 */
export class ComboboxHandlers extends ComboboxValue {
  protected handleInput(e: InputEvent): void {
    e.stopPropagation()
    e.stopImmediatePropagation()

    if (this.disabled) return

    this.touched = true
    const input = e.target as HTMLInputElement
    this._search = input.value
    this.checkForMatches()

    if (this.typeahead) {
      if (this._search) {
        const { filtered, suggestion } = findTypeaheadMatches(this.options, this._search)
        this._options = filtered

        if (
          e.inputType !== 'deleteContentBackward' &&
          suggestion?.label &&
          this.inputRef.value &&
          this.inputRef.value.type !== 'hidden'
        ) {
          input.value = suggestion.label
          window.setTimeout(
            () => input.setSelectionRange(this._search.length, input.value.length),
            0,
          )
          input.selectionDirection = 'backward'
        }
      } else {
        this._options = [...this.options]
      }
    }
  }

  protected handleFocus(): void {
    if (this.disabled) return

    // After selecting a value in single+typeahead, focus returns to the input.
    // Skip reopening the dropdown so screen readers announce the selected value.
    if (this._suppressNextOpen) {
      this._suppressNextOpen = false
      this._inputFocus = true
      this.requestUpdate()
      return
    }

    if (
      !this.multiple &&
      this._value[0] &&
      this.inputRef.value &&
      this.inputRef.value.type !== 'hidden'
    ) {
      this.inputRef.value.value = getSingleValueForInput(
        this._value[0],
        this.options,
        this.displayValueAs,
      )
    }
    this._inputFocus = true
    this._search = ''
    this._options = [...this.options]
    this._isOptionsOpen = true
    this.onFocus()

    this.requestUpdate()
  }

  protected handleFocusOut(e: FocusEvent): void {
    if (this.disabled || !this._isOptionsOpen) return

    const related = e.relatedTarget as Element | null
    const isFocusInsideCombobox =
      related?.closest('pkt-combobox')?.id === this.id ||
      (e.target as Element)?.getAttribute('data-focusfix') === this.id ||
      related === this.inputRef.value ||
      related === this.triggerRef.value

    if (!isFocusInsideCombobox) {
      this.closeAndProcessInput()
    }
  }

  /**
   * Shared close logic used by both focusout and outside-click handlers.
   * Processes any pending input value, then closes the dropdown.
   */
  protected closeAndProcessInput(): void {
    this._inputFocus = false
    this._addValueText = null
    this._userInfoMessage = ''
    this._search = ''

    if (this.inputRef.value && this.inputRef.value.type !== 'hidden') {
      const inputText = this.inputRef.value.value

      if (!this.multiple) {
        if (!inputText) {
          // Empty input — clear the selection
          if (this._value[0]) this.removeSelected(this._value[0])
        } else {
          // Try to match input text to an option (by value or label)
          const match = findOptionByValue(this.options, inputText)
          if (match && match.value !== this._value[0]) {
            // Input matches a different option — select it
            this._value[0] && this.removeSelected(this._value[0])
            this.setSelected(match.value)
          } else if (!match && this.allowUserInput) {
            // No match + allowUserInput — set as user value
            this._value[0] && this.removeSelected(this._value[0])
            this.addNewUserValue(inputText)
          }
          // No match, no allowUserInput — discard, keep previous selection
        }
      } else if (inputText !== '') {
        // Multi: process typed text (add/select/remove)
        const { action, value } = getInputValueAction(
          inputText,
          this._value,
          this.options,
          this.allowUserInput,
          this.multiple,
        )
        switch (action) {
          case 'addUserValue':
            this.addNewUserValue(value)
            break
          case 'selectOption':
            this.setSelected(value)
            break
          case 'removeValue':
            this.removeValue(value)
            break
        }
      }

      // Restore input to display text of current selection (or clear)
      if (!this.multiple && this._value[0]) {
        this.inputRef.value.value = getSingleValueForInput(
          this._value[0],
          this.options,
          this.displayValueAs,
        )
      } else {
        this.inputRef.value.value = ''
      }
    }
    this._isOptionsOpen = false
    this.onBlur()
  }

  protected handleBlur(): void {
    this._inputFocus = false
    this.onBlur()
  }

  protected handleInputClick(e: MouseEvent): void {
    if (this.disabled) {
      e.preventDefault()
      e.stopImmediatePropagation()
      return
    }
    if (this._hasTextInput) {
      this.inputRef.value?.focus()
      this.requestUpdate()
    } else {
      // Select-only: toggle the listbox
      e.stopImmediatePropagation()
      e.preventDefault()
      this._isOptionsOpen = !this._isOptionsOpen
      if (this._isOptionsOpen) {
        this.listboxRef.value?.focusFirstOrSelectedOption()
      }
    }
  }

  protected handlePlaceholderClick(e: MouseEvent): void {
    if (this.disabled) return
    e.stopPropagation()
    if (this._hasTextInput && this.inputRef.value) {
      this.inputRef.value.focus()
      this._inputFocus = true
      this.requestUpdate()
    } else {
      this._isOptionsOpen = !this._isOptionsOpen
      this.requestUpdate()
    }
  }

  protected handleSelectOnlyKeydown(e: KeyboardEvent): void {
    if (this.disabled) return

    switch (e.key) {
      case 'Enter':
      case ' ':
      case 'ArrowDown':
      case 'ArrowUp':
        e.preventDefault()
        if (!this._isOptionsOpen) {
          this._isOptionsOpen = true
          this.listboxRef.value?.focusFirstOrSelectedOption()
        } else {
          this._isOptionsOpen = false
        }
        break
      case 'Escape':
        if (this._isOptionsOpen) {
          e.preventDefault()
          this._isOptionsOpen = false
        }
        break
      case 'Home':
      case 'End':
        e.preventDefault()
        if (!this._isOptionsOpen) {
          this._isOptionsOpen = true
        }
        this.listboxRef.value?.focusFirstOrSelectedOption()
        break
      case 'ArrowLeft':
        if (this.multiple && this._value.length > 0) {
          e.preventDefault()
          this.focusTag(this._value.length - 1)
        }
        break
      case 'Backspace':
      case 'Delete':
        if (this.multiple && this._value.length > 0) {
          e.preventDefault()
          this.removeSelected(this._value[this._value.length - 1])
        }
        break
    }
  }

  protected handleOptionToggled(e: CustomEvent) {
    this.toggleValue(e.detail)
  }

  protected handleSearch(e: CustomEvent) {
    e.stopPropagation()
    this._search = e.detail.toLowerCase()
    this.checkForMatches()
  }

  protected handleInputKeydown(e: KeyboardEvent): void {
    // Backspace has special DOM-dependent conditions
    if (e.key === 'Backspace') {
      const inputEmpty = !this.inputRef.value?.value
      if (!this._search && inputEmpty && this.multiple && this._value.length > 0)
        this.removeLastValue(e)
      return
    }

    if (e.key === 'ArrowLeft' && this.multiple && this._value.length > 0) {
      const input = this.inputRef.value
      if (input && input.selectionStart === 0 && !input.value) {
        e.preventDefault()
        this.focusTag(this._value.length - 1)
        return
      }
    }

    const action = getInputKeyAction(e.key, e.shiftKey, this.multiple)
    if (!action) return

    // When the dropdown is closed, let Tab move focus naturally.
    if (e.key === 'Tab' && !this._isOptionsOpen) return

    // For Tab/'focusListbox': only focus the listbox if it has focusable options.
    // If the listbox is empty (no matches), close and let Tab move focus naturally.
    if (action === 'focusListbox' && e.key === 'Tab') {
      const hasFocusable = this.listboxRef.value?.querySelector(
        '[role="option"]:not([data-disabled]), [data-type="new-option"]',
      )
      if (!hasFocusable) {
        this.closeAndProcessInput()
        return
      }
    }

    e.preventDefault()
    switch (action) {
      case 'addValue':
        this.addValue()
        break
      case 'focusListbox':
        this.listboxRef.value?.focusFirstOrSelectedOption()
        break
      case 'closeOptions':
        this._isOptionsOpen = false
        // Don't refocus — the text input already has focus, and focusing
        // it again would trigger handleFocus which reopens the dropdown
        break
    }
  }

  protected handleTagRemove(value: string | null): void {
    this.removeSelected(value)
    if (this._hasTextInput && this.inputRef.value) {
      this._inputFocus = true
      this.inputRef.value.focus()
      this.requestUpdate()
    }
  }

  protected getInsideTags(): HTMLElement[] {
    return Array.from(
      this.querySelectorAll<HTMLElement>('.pkt-combobox__input .pkt-combobox__tag-list pkt-tag'),
    )
  }

  protected focusTag(index: number): void {
    const tags = this.getInsideTags()
    tags.forEach((tag, i) => {
      ;(tag as PktTag).buttonTabindex = i === index ? 0 : -1
    })
    // Focus the button inside the target pkt-tag
    const btn = tags[index]?.querySelector('button')
    btn?.focus()
  }

  protected resetTagTabindices(): void {
    const tags = this.getInsideTags()
    tags.forEach((tag) => {
      ;(tag as PktTag).buttonTabindex = -1
    })
  }

  protected handleTagKeydown(e: KeyboardEvent, index: number): void {
    e.stopPropagation()
    const returnFocusToTrigger = () => {
      this.resetTagTabindices()
      if (this._hasTextInput && this.inputRef.value) {
        this.inputRef.value.focus()
      } else {
        this.triggerRef.value?.focus()
      }
    }

    switch (e.key) {
      case 'ArrowLeft':
        e.preventDefault()
        if (index > 0) {
          this.focusTag(index - 1)
        }
        break
      case 'ArrowRight':
        e.preventDefault()
        if (index < this._value.length - 1) {
          this.focusTag(index + 1)
        } else {
          returnFocusToTrigger()
        }
        break
      case 'Backspace':
      case 'Delete':
        e.preventDefault()
        {
          const val = this._value[index]
          const nextIndex = index >= this._value.length - 1 ? index - 1 : index
          this.removeSelected(val)
          if (nextIndex >= 0) {
            this.requestUpdate()
            this.updateComplete.then(() => this.focusTag(nextIndex))
          } else {
            returnFocusToTrigger()
          }
        }
        break
      case 'Tab':
        // Let Tab move focus naturally, but reset roving tabindex
        // so the next Tab into the combobox lands on the trigger, not a tag
        this.resetTagTabindices()
        break
      case 'Escape':
        e.preventDefault()
        returnFocusToTrigger()
        break
    }
  }

  protected checkForMatches() {
    const inputValue = this.inputRef.value?.value || this._search || ''
    const result = checkForMatches(
      inputValue,
      this._value,
      this.options,
      this.allowUserInput,
      this.multiple,
    )

    if (result.shouldRemoveValue) {
      this.removeValue(this._value[0])
    }
    if (result.shouldResetInput) {
      this.resetComboboxInput(false)
      return
    }

    this._addValueText = result.addValueText
    this._userInfoMessage = result.userInfoMessage
  }
}
