/**
 * Combobox - Searchable select with type-ahead and optional creation
 * @countermeasure/web-components/primitives/select
 */

import { type ComboboxConfig, type SelectOption } from './types'
import { resolveContainer, uniqueId } from '../utils'

const SEARCH_DEBOUNCE_MS = 150

export class Combobox {
  public readonly el: HTMLElement

  private readonly inputEl: HTMLInputElement
  private readonly dropdownEl: HTMLElement
  private readonly listboxId: string
  private optionEls: HTMLElement[] = []
  private config: ComboboxConfig
  private currentOptions: SelectOption[]
  private filteredOptions: SelectOption[] = []
  private selectedValue: string | null = null
  private highlightedIndex = -1
  private isOpen = false
  private isDestroyed = false
  private debounceTimer: ReturnType<typeof setTimeout> | null = null

  private readonly handleInputFocus: () => void
  private readonly handleInputInput: () => void
  private readonly handleInputKeydown: (e: KeyboardEvent) => void
  private readonly handleDocumentClick: (e: MouseEvent) => void

  constructor(config: ComboboxConfig) {
    this.config = config
    this.currentOptions = [...config.options]
    this.filteredOptions = [...this.currentOptions]
    this.listboxId = uniqueId('cmm-combobox-listbox')

    if (config.value !== undefined) {
      this.selectedValue = config.value
    }

    this.el = document.createElement('div')
    this.el.className = 'cmm-combobox'
    if (config.className) {
      this.el.classList.add(config.className)
    }

    this.inputEl = document.createElement('input')
    this.inputEl.type = 'text'
    this.inputEl.className = 'cmm-combobox__input'
    this.inputEl.id = uniqueId('cmm-combobox-input')
    this.inputEl.setAttribute('role', 'combobox')
    this.inputEl.setAttribute('aria-haspopup', 'listbox')
    this.inputEl.setAttribute('aria-expanded', 'false')
    this.inputEl.setAttribute('aria-controls', this.listboxId)
    this.inputEl.setAttribute('aria-autocomplete', 'list')

    if (config.placeholder) {
      this.inputEl.placeholder = config.placeholder
    }

    const comboAriaLabel = config.ariaLabel ?? config.placeholder ?? 'Select an option'
    this.inputEl.setAttribute('aria-label', comboAriaLabel)

    if (config.disabled === true) {
      this.inputEl.disabled = true
      this.el.classList.add('cmm-combobox--disabled')
    }

    if (this.selectedValue !== null) {
      const selected = this.currentOptions.find(o => o.value === this.selectedValue)
      if (selected !== undefined) {
        this.inputEl.value = selected.label
      }
    }

    this.dropdownEl = document.createElement('div')
    this.dropdownEl.id = this.listboxId
    this.dropdownEl.className = 'cmm-combobox__dropdown'
    this.dropdownEl.setAttribute('role', 'listbox')
    this.dropdownEl.style.display = 'none'

    this.renderOptions()

    this.el.appendChild(this.inputEl)
    this.el.appendChild(this.dropdownEl)

    this.handleInputFocus = () => {
      if (this.isDestroyed) {
        return
      }
      this.openDropdown()
    }

    this.handleInputInput = () => {
      if (this.isDestroyed) {
        return
      }
      this.onInputChange()
    }

    this.handleInputKeydown = (e: KeyboardEvent) => {
      if (this.isDestroyed) {
        return
      }
      this.onKeydown(e)
    }

    this.handleDocumentClick = (e: MouseEvent) => {
      if (this.isDestroyed) {
        return
      }
      const target = e.target as Node
      if (!this.el.contains(target)) {
        this.closeDropdown()
      }
    }

    this.inputEl.addEventListener('focus', this.handleInputFocus)
    this.inputEl.addEventListener('input', this.handleInputInput)
    this.inputEl.addEventListener('keydown', this.handleInputKeydown)

    if (config.container !== undefined) {
      const parent = resolveContainer(config.container)
      parent.appendChild(this.el)
    }
  }

  getValue(): string | null {
    return this.selectedValue
  }

  setValue(value: string): void {
    const option = this.currentOptions.find(o => o.value === value)
    if (option !== undefined) {
      this.selectedValue = value
      this.inputEl.value = option.label
      this.updateSelectedState()
    }
  }

  setOptions(options: SelectOption[]): void {
    this.currentOptions = [...options]
    this.filteredOptions = [...this.currentOptions]
    this.renderOptions()
  }

  setDisabled(disabled: boolean): void {
    this.inputEl.disabled = disabled
    if (disabled) {
      this.el.classList.add('cmm-combobox--disabled')
      if (this.isOpen) {
        this.closeDropdown()
      }
    } else {
      this.el.classList.remove('cmm-combobox--disabled')
    }
  }

  focus(): void {
    this.inputEl.focus()
  }

  destroy(): void {
    this.isDestroyed = true
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer)
    }
    this.closeDropdown()
    this.inputEl.removeEventListener('focus', this.handleInputFocus)
    this.inputEl.removeEventListener('input', this.handleInputInput)
    this.inputEl.removeEventListener('keydown', this.handleInputKeydown)
    document.removeEventListener('click', this.handleDocumentClick)
    this.el.remove()
  }

  private onInputChange(): void {
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer)
    }

    this.debounceTimer = setTimeout(() => {
      const query = this.inputEl.value.toLowerCase()
      this.config.onSearch?.(query)

      if (query) {
        this.filteredOptions = this.currentOptions.filter(o =>
          o.label.toLowerCase().includes(query)
        )
      } else {
        this.filteredOptions = [...this.currentOptions]
      }

      this.renderOptions()

      if (!this.isOpen) {
        this.openDropdown()
      }

      this.setHighlightedIndex(0)
    }, SEARCH_DEBOUNCE_MS)
  }

  private openDropdown(): void {
    if (this.isOpen || this.inputEl.disabled) {
      return
    }
    this.isOpen = true
    this.dropdownEl.style.display = ''
    this.inputEl.setAttribute('aria-expanded', 'true')

    this.positionDropdown()
    document.addEventListener('click', this.handleDocumentClick)
  }

  private closeDropdown(): void {
    if (!this.isOpen) {
      return
    }
    this.isOpen = false
    this.dropdownEl.style.display = 'none'
    this.inputEl.setAttribute('aria-expanded', 'false')
    this.inputEl.removeAttribute('aria-activedescendant')
    this.highlightedIndex = -1
    this.clearHighlight()

    document.removeEventListener('click', this.handleDocumentClick)
  }

  private renderOptions(): void {
    this.dropdownEl.innerHTML = ''
    this.optionEls = []

    for (const option of this.filteredOptions) {
      this.appendOption(option)
    }

    if (this.config.creatable === true) {
      const query = this.inputEl.value.trim()
      if (query) {
        const exactMatch = this.currentOptions.some(
          o => o.label.toLowerCase() === query.toLowerCase()
        )
        if (!exactMatch) {
          this.appendCreateOption(query)
        }
      }
    }
  }

  private appendOption(option: SelectOption): void {
    const optionEl = document.createElement('div')
    const optionId = uniqueId('cmm-combobox-option')
    optionEl.id = optionId
    optionEl.className = 'cmm-combobox__option'
    optionEl.setAttribute('role', 'option')
    optionEl.setAttribute('data-value', option.value)
    optionEl.textContent = option.label

    if (option.disabled === true) {
      optionEl.classList.add('cmm-combobox__option--disabled')
      optionEl.setAttribute('aria-disabled', 'true')
    }

    if (option.value === this.selectedValue) {
      optionEl.classList.add('cmm-combobox__option--selected')
      optionEl.setAttribute('aria-selected', 'true')
    } else {
      optionEl.setAttribute('aria-selected', 'false')
    }

    optionEl.addEventListener('mousedown', e => {
      e.preventDefault()
      if (option.disabled !== true) {
        this.selectOption(option.value, option.label)
      }
    })

    this.dropdownEl.appendChild(optionEl)
    this.optionEls.push(optionEl)
  }

  private appendCreateOption(query: string): void {
    const createEl = document.createElement('div')
    const createId = uniqueId('cmm-combobox-create')
    createEl.id = createId
    createEl.className = 'cmm-combobox__option cmm-combobox__create'
    createEl.setAttribute('role', 'option')
    createEl.setAttribute('data-create', query)
    createEl.textContent = `Create: ${query}`

    createEl.addEventListener('mousedown', e => {
      e.preventDefault()
      this.config.onCreate?.(query)
      this.inputEl.value = query
      this.selectedValue = query
      this.closeDropdown()
    })

    this.dropdownEl.appendChild(createEl)
    this.optionEls.push(createEl)
  }

  private selectOption(value: string, label: string): void {
    this.selectedValue = value
    this.inputEl.value = label
    this.updateSelectedState()
    this.closeDropdown()
    this.config.onChange?.(value)
  }

  private updateSelectedState(): void {
    for (const el of this.optionEls) {
      const val = el.getAttribute('data-value')
      if (val === this.selectedValue) {
        el.classList.add('cmm-combobox__option--selected')
        el.setAttribute('aria-selected', 'true')
      } else {
        el.classList.remove('cmm-combobox__option--selected')
        el.setAttribute('aria-selected', 'false')
      }
    }
  }

  private positionDropdown(): void {
    const inputRect = this.inputEl.getBoundingClientRect()
    const spaceBelow = window.innerHeight - inputRect.bottom
    const spaceAbove = inputRect.top
    const dropdownHeight = 256

    if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
      this.dropdownEl.classList.add('cmm-combobox__dropdown--above')
      this.dropdownEl.classList.remove('cmm-combobox__dropdown--below')
    } else {
      this.dropdownEl.classList.add('cmm-combobox__dropdown--below')
      this.dropdownEl.classList.remove('cmm-combobox__dropdown--above')
    }
  }

  private getSelectableOptionEls(): HTMLElement[] {
    return this.optionEls.filter(el => !el.classList.contains('cmm-combobox__option--disabled'))
  }

  private setHighlightedIndex(index: number): void {
    this.clearHighlight()
    const selectable = this.getSelectableOptionEls()
    if (selectable.length === 0) {
      this.highlightedIndex = -1
      return
    }

    this.highlightedIndex = Math.max(0, Math.min(index, selectable.length - 1))
    const el = selectable[this.highlightedIndex]
    if (el !== undefined) {
      el.classList.add('cmm-combobox__option--highlighted')
      el.scrollIntoView({ block: 'nearest' })
      this.inputEl.setAttribute('aria-activedescendant', el.id)
    }
  }

  private clearHighlight(): void {
    for (const el of this.optionEls) {
      el.classList.remove('cmm-combobox__option--highlighted')
    }
  }

  private onKeydown(e: KeyboardEvent): void {
    const selectable = this.getSelectableOptionEls()

    switch (e.key) {
      case 'ArrowDown': {
        e.preventDefault()
        if (!this.isOpen) {
          this.openDropdown()
          this.setHighlightedIndex(0)
        } else {
          const next = this.highlightedIndex < selectable.length - 1 ? this.highlightedIndex + 1 : 0
          this.setHighlightedIndex(next)
        }
        break
      }
      case 'ArrowUp': {
        e.preventDefault()
        if (this.isOpen) {
          const prev = this.highlightedIndex > 0 ? this.highlightedIndex - 1 : selectable.length - 1
          this.setHighlightedIndex(prev)
        }
        break
      }
      case 'Enter': {
        e.preventDefault()
        if (this.isOpen && this.highlightedIndex >= 0) {
          const highlighted = selectable[this.highlightedIndex]
          if (highlighted !== undefined) {
            const createVal = highlighted.getAttribute('data-create')
            if (createVal !== null) {
              this.config.onCreate?.(createVal)
              this.inputEl.value = createVal
              this.selectedValue = createVal
              this.closeDropdown()
            } else {
              const value = highlighted.getAttribute('data-value')
              if (value !== null) {
                const option = this.filteredOptions.find(o => o.value === value)
                if (option !== undefined) {
                  this.selectOption(option.value, option.label)
                }
              }
            }
          }
        }
        break
      }
      case 'Escape':
        if (this.isOpen) {
          e.preventDefault()
          this.closeDropdown()
        }
        break
      case 'Tab':
        this.closeDropdown()
        break
    }
  }
}

export { type ComboboxConfig } from './types'
