/**
 * Select - Custom dropdown select component
 * @countermeasure/web-components/primitives/select
 */

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

export class Select {
  public readonly el: HTMLElement

  private readonly triggerEl: HTMLButtonElement
  private readonly dropdownEl: HTMLElement
  private readonly listboxId: string
  private readonly triggerId: string
  private searchInputEl: HTMLInputElement | null = null
  private liveRegionEl: HTMLElement | null = null
  private optionEls: HTMLElement[] = []
  private config: SelectConfig
  private currentOptions: SelectOption[]
  private selectedValue: string | null = null
  private highlightedIndex = -1
  private isOpen = false
  private isDestroyed = false

  private readonly handleTriggerClick: () => void
  private readonly handleTriggerKeydown: (e: KeyboardEvent) => void
  private readonly handleDocumentClick: (e: MouseEvent) => void
  private readonly handleDropdownKeydown: (e: KeyboardEvent) => void

  constructor(config: SelectConfig) {
    this.config = config
    this.currentOptions = [...config.options]
    this.listboxId = uniqueId('cmm-select-listbox')
    this.triggerId = uniqueId('cmm-select-trigger')

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

    this.el = document.createElement('div')
    this.el.className = this.buildRootClass()

    this.triggerEl = document.createElement('button')
    this.triggerEl.type = 'button'
    this.triggerEl.id = this.triggerId
    this.triggerEl.className = 'cmm-select__trigger'
    this.triggerEl.setAttribute('role', 'combobox')
    this.triggerEl.setAttribute('aria-haspopup', 'listbox')
    this.triggerEl.setAttribute('aria-expanded', 'false')
    this.triggerEl.setAttribute('aria-controls', this.listboxId)
    this.triggerEl.setAttribute('aria-owns', this.listboxId)

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

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

    this.updateTriggerContent()

    const chevron = document.createElement('span')
    chevron.className = 'cmm-select__chevron'
    chevron.setAttribute('aria-hidden', 'true')
    chevron.innerHTML =
      '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
    this.triggerEl.appendChild(chevron)

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

    if (config.searchable === true) {
      this.buildSearchInput()
      this.buildLiveRegion()
    }

    this.renderOptions(this.currentOptions)

    this.el.appendChild(this.triggerEl)
    this.el.appendChild(this.dropdownEl)

    this.handleTriggerClick = () => {
      if (this.isDestroyed) {
        return
      }
      if (this.isOpen) {
        this.close()
      } else {
        this.open()
      }
    }

    this.handleTriggerKeydown = (e: KeyboardEvent) => {
      if (this.isDestroyed) {
        return
      }
      this.onTriggerKeydown(e)
    }

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

    this.handleDropdownKeydown = (e: KeyboardEvent) => {
      if (this.isDestroyed) {
        return
      }
      this.onDropdownKeydown(e)
    }

    this.triggerEl.addEventListener('click', this.handleTriggerClick)
    this.triggerEl.addEventListener('keydown', this.handleTriggerKeydown)
    this.dropdownEl.addEventListener('keydown', this.handleDropdownKeydown)

    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.updateTriggerContent()
      this.updateSelectedState()
    }
  }

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

  setDisabled(disabled: boolean): void {
    this.triggerEl.disabled = disabled
    if (disabled) {
      this.el.classList.add('cmm-select--disabled')
      if (this.isOpen) {
        this.close()
      }
    } else {
      this.el.classList.remove('cmm-select--disabled')
    }
  }

  open(): void {
    if (this.isOpen || this.triggerEl.disabled) {
      return
    }
    this.isOpen = true
    this.dropdownEl.style.display = ''
    this.triggerEl.setAttribute('aria-expanded', 'true')
    this.triggerEl.classList.add('cmm-select__trigger--open')

    this.positionDropdown()

    if (this.searchInputEl !== null) {
      this.searchInputEl.value = ''
      this.searchInputEl.focus()
      this.renderOptions(this.currentOptions)
    }

    const selectedIndex = this.getSelectableOptions().findIndex(o => o.value === this.selectedValue)
    this.setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0)

    document.addEventListener('click', this.handleDocumentClick)
    this.config.onOpen?.()
  }

  close(): void {
    if (!this.isOpen) {
      return
    }
    this.isOpen = false
    this.dropdownEl.style.display = 'none'
    this.triggerEl.setAttribute('aria-expanded', 'false')
    this.triggerEl.classList.remove('cmm-select__trigger--open')
    this.triggerEl.removeAttribute('aria-activedescendant')
    this.highlightedIndex = -1
    this.clearHighlight()

    document.removeEventListener('click', this.handleDocumentClick)
    this.config.onClose?.()
  }

  destroy(): void {
    this.isDestroyed = true
    this.close()
    this.triggerEl.removeEventListener('click', this.handleTriggerClick)
    this.triggerEl.removeEventListener('keydown', this.handleTriggerKeydown)
    this.dropdownEl.removeEventListener('keydown', this.handleDropdownKeydown)
    document.removeEventListener('click', this.handleDocumentClick)
    if (this.liveRegionEl !== null) {
      this.liveRegionEl.remove()
      this.liveRegionEl = null
    }
    this.el.remove()
  }

  private buildRootClass(): string {
    const classes = ['cmm-select']
    if (this.config.size !== undefined) {
      classes.push(`cmm-select--${this.config.size}`)
    }
    if (this.config.className) {
      classes.push(this.config.className)
    }
    return classes.join(' ')
  }

  private buildSearchInput(): void {
    const searchWrap = document.createElement('div')
    searchWrap.className = 'cmm-select__search-wrap'

    this.searchInputEl = document.createElement('input')
    this.searchInputEl.type = 'text'
    this.searchInputEl.className = 'cmm-select__search'
    this.searchInputEl.placeholder = 'Search...'
    this.searchInputEl.setAttribute('aria-label', 'Search options')

    this.searchInputEl.addEventListener('input', () => {
      const query = this.searchInputEl?.value.toLowerCase() ?? ''
      let filtered: SelectOption[]
      if (query) {
        filtered = this.currentOptions.filter(o => o.label.toLowerCase().includes(query))
        this.renderOptions(filtered)
      } else {
        filtered = this.currentOptions
        this.renderOptions(this.currentOptions)
      }
      this.setHighlightedIndex(0)

      // Announce results count to screen readers
      this.announceResultsCount(filtered.length, query)
    })

    searchWrap.appendChild(this.searchInputEl)
    this.dropdownEl.appendChild(searchWrap)
  }

  private buildLiveRegion(): void {
    this.liveRegionEl = document.createElement('div')
    this.liveRegionEl.setAttribute('role', 'status')
    this.liveRegionEl.setAttribute('aria-live', 'polite')
    this.liveRegionEl.setAttribute('aria-atomic', 'true')
    this.liveRegionEl.className = 'cmm-sr-only'
    this.liveRegionEl.style.cssText = `
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    `
    this.el.appendChild(this.liveRegionEl)
  }

  private announceResultsCount(count: number, query: string): void {
    if (this.liveRegionEl === null) return

    if (query === '') {
      this.liveRegionEl.textContent = ''
      return
    }

    if (count === 0) {
      this.liveRegionEl.textContent = 'No results found'
    } else if (count === 1) {
      this.liveRegionEl.textContent = '1 result available'
    } else {
      this.liveRegionEl.textContent = `${String(count)} results available`
    }
  }

  private renderOptions(options: SelectOption[]): void {
    // Remove existing option elements but keep search input
    const existingOptions = this.dropdownEl.querySelectorAll(
      '.cmm-select__option, .cmm-select__group-header'
    )
    existingOptions.forEach(el => {
      el.remove()
    })

    this.optionEls = []
    const groups = new Map<string, SelectOption[]>()
    const ungrouped: SelectOption[] = []

    for (const option of options) {
      if (option.group) {
        const groupList = groups.get(option.group)
        if (groupList !== undefined) {
          groupList.push(option)
        } else {
          groups.set(option.group, [option])
        }
      } else {
        ungrouped.push(option)
      }
    }

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

    for (const [groupName, groupOptions] of groups) {
      const header = document.createElement('div')
      header.className = 'cmm-select__group-header'
      header.textContent = groupName
      header.setAttribute('aria-hidden', 'true')
      this.dropdownEl.appendChild(header)

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

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

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

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

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

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

  private selectOption(value: string): void {
    this.selectedValue = value
    this.updateTriggerContent()
    this.updateSelectedState()
    this.close()
    this.triggerEl.focus()
    this.config.onChange?.(value)
  }

  private updateTriggerContent(): void {
    const textSpan = this.triggerEl.querySelector('.cmm-select__value')
    const chevronSpan = this.triggerEl.querySelector('.cmm-select__chevron')

    const selectedOption = this.currentOptions.find(o => o.value === this.selectedValue)

    if (textSpan !== null) {
      textSpan.remove()
    }

    const span = document.createElement('span')
    if (selectedOption !== undefined) {
      span.className = 'cmm-select__value'
      span.textContent = selectedOption.label
    } else {
      span.className = 'cmm-select__value cmm-select__placeholder'
      span.textContent = this.config.placeholder ?? 'Select...'
    }

    if (chevronSpan !== null) {
      this.triggerEl.insertBefore(span, chevronSpan)
    } else {
      this.triggerEl.insertBefore(span, this.triggerEl.firstChild)
    }
  }

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

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

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

  private getSelectableOptions(): SelectOption[] {
    return this.currentOptions.filter(o => o.disabled !== true)
  }

  private getSelectableOptionEls(): HTMLElement[] {
    return this.optionEls.filter(el => !el.classList.contains('cmm-select__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-select__option--highlighted')
      el.scrollIntoView({ block: 'nearest' })
      this.triggerEl.setAttribute('aria-activedescendant', el.id)
    }
  }

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

  private onTriggerKeydown(e: KeyboardEvent): void {
    switch (e.key) {
      case 'ArrowDown':
      case 'ArrowUp':
      case 'Enter':
      case ' ':
        e.preventDefault()
        if (!this.isOpen) {
          this.open()
        }
        break
      case 'Escape':
        if (this.isOpen) {
          e.preventDefault()
          this.close()
        }
        break
    }
  }

  private onDropdownKeydown(e: KeyboardEvent): void {
    const selectable = this.getSelectableOptionEls()
    if (selectable.length === 0) {
      return
    }

    switch (e.key) {
      case 'ArrowDown': {
        e.preventDefault()
        const next = this.highlightedIndex < selectable.length - 1 ? this.highlightedIndex + 1 : 0
        this.setHighlightedIndex(next)
        break
      }
      case 'ArrowUp': {
        e.preventDefault()
        const prev = this.highlightedIndex > 0 ? this.highlightedIndex - 1 : selectable.length - 1
        this.setHighlightedIndex(prev)
        break
      }
      case 'Enter': {
        e.preventDefault()
        const highlighted = selectable[this.highlightedIndex]
        if (highlighted !== undefined) {
          const value = highlighted.getAttribute('data-value')
          if (value !== null) {
            this.selectOption(value)
          }
        }
        break
      }
      case 'Escape':
        e.preventDefault()
        this.close()
        this.triggerEl.focus()
        break
      case 'Tab':
        this.close()
        break
    }
  }
}

export { type SelectConfig, type SelectOption } from './types'
