/**
 * TagInput - Chip/tag input with autocomplete suggestions
 * @countermeasure/web-components/form/tag-input
 */

import { type TagInputConfig, type TagSuggestion } from './types'
import { resolveContainer, uniqueId } from '../../primitives/utils'

export type { TagInputConfig, TagSuggestion } from './types'

export class TagInput {
  public readonly el: HTMLElement

  private readonly inputEl: HTMLInputElement
  private readonly tagsContainer: HTMLElement
  private readonly dropdownEl: HTMLElement
  private readonly listboxId: string
  private readonly inputId: string
  private config: TagInputConfig
  private tags: string[] = []
  private filteredSuggestions: TagSuggestion[] = []
  private highlightedIndex = -1
  private isDropdownOpen = false
  private isDestroyed = false
  private debounceTimer: ReturnType<typeof setTimeout> | null = null

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

  constructor(config: TagInputConfig) {
    this.config = config
    this.listboxId = uniqueId('cmm-taginput-listbox')
    this.inputId = uniqueId('cmm-taginput-input')
    this.tags = config.tags ? [...config.tags] : []

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

    // Tags + input container (styled to look like a single input)
    this.tagsContainer = document.createElement('div')
    this.tagsContainer.className = 'cmm-tag-input__container'
    this.tagsContainer.addEventListener('click', () => {
      if (!this.isDestroyed) this.inputEl.focus()
    })

    this.inputEl = document.createElement('input')
    this.inputEl.id = this.inputId
    this.inputEl.type = 'text'
    this.inputEl.className = 'cmm-tag-input__input'
    this.inputEl.placeholder = config.placeholder ?? 'Add tag...'
    this.inputEl.setAttribute('role', 'combobox')
    this.inputEl.setAttribute('aria-expanded', 'false')
    this.inputEl.setAttribute('aria-controls', this.listboxId)
    this.inputEl.setAttribute('aria-autocomplete', 'list')
    this.inputEl.setAttribute('autocomplete', 'off')

    if (this.isMaxReached()) {
      this.inputEl.disabled = true
    }

    this.tagsContainer.appendChild(this.inputEl)
    this.renderTags()
    this.el.appendChild(this.tagsContainer)

    // Dropdown for suggestions
    this.dropdownEl = document.createElement('div')
    this.dropdownEl.id = this.listboxId
    this.dropdownEl.className = 'cmm-tag-input__dropdown'
    this.dropdownEl.setAttribute('role', 'listbox')
    this.dropdownEl.style.display = 'none'
    this.el.appendChild(this.dropdownEl)

    // Bind event handlers
    this.handleInputKeydown = (e: KeyboardEvent) => {
      if (this.isDestroyed) return
      this.onInputKeydown(e)
    }

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

    this.handleInputPaste = (e: ClipboardEvent) => {
      if (this.isDestroyed) return
      this.onPaste(e)
    }

    this.handleInputFocus = () => {
      if (this.isDestroyed) return
      if (this.inputEl.value.length > 0) {
        this.onInputChange()
      }
    }

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

    this.inputEl.addEventListener('keydown', this.handleInputKeydown)
    this.inputEl.addEventListener('input', this.handleInputInput)
    this.inputEl.addEventListener('paste', this.handleInputPaste)
    this.inputEl.addEventListener('focus', this.handleInputFocus)
    document.addEventListener('click', this.handleDocumentClick)

    const parent = resolveContainer(config.container)
    parent.appendChild(this.el)
  }

  getTags(): string[] {
    return [...this.tags]
  }

  setTags(tags: string[]): void {
    this.tags = [...tags]
    this.renderTags()
    this.updateInputState()
    this.config.onChange?.(this.getTags())
  }

  addTag(tag: string): boolean {
    const trimmed = tag.trim()
    if (trimmed === '') return false
    if (this.isMaxReached()) return false
    if (this.config.allowDuplicates !== true && this.tags.includes(trimmed)) return false

    this.tags.push(trimmed)
    this.renderTags()
    this.updateInputState()
    this.config.onChange?.(this.getTags())
    return true
  }

  removeTag(tag: string): boolean {
    const idx = this.tags.indexOf(tag)
    if (idx === -1) return false
    this.tags.splice(idx, 1)
    this.renderTags()
    this.updateInputState()
    this.config.onChange?.(this.getTags())
    return true
  }

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

  private buildRootClass(): string {
    const classes = ['cmm-tag-input']
    if (this.config.className) classes.push(this.config.className)
    return classes.join(' ')
  }

  private isMaxReached(): boolean {
    return this.config.maxTags !== undefined && this.tags.length >= this.config.maxTags
  }

  private renderTags(): void {
    const chips = this.tagsContainer.querySelectorAll('.cmm-tag-input__tag')
    chips.forEach(c => {
      c.remove()
    })

    for (const tag of this.tags) {
      const chip = this.createChipEl(tag)
      this.tagsContainer.insertBefore(chip, this.inputEl)
    }
  }

  private createChipEl(tag: string): HTMLElement {
    const chip = document.createElement('span')
    chip.className = 'cmm-tag-input__tag'
    chip.setAttribute('data-tag', tag)

    // Try to find matching suggestion for color
    const suggestion = this.findSuggestion(tag)
    if (suggestion?.color) {
      chip.style.setProperty('--tag-color', suggestion.color)
      chip.classList.add('cmm-tag-input__tag--colored')
    }

    const label = document.createElement('span')
    label.className = 'cmm-tag-input__tag-label'
    label.textContent = suggestion?.label ?? tag
    chip.appendChild(label)

    const removeBtn = document.createElement('button')
    removeBtn.type = 'button'
    removeBtn.className = 'cmm-tag-input__tag-remove'
    removeBtn.setAttribute('aria-label', `Remove ${tag}`)
    removeBtn.innerHTML =
      '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M9 3L3 9M3 3L9 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
    removeBtn.addEventListener('click', e => {
      e.stopPropagation()
      if (!this.isDestroyed) this.removeTag(tag)
    })
    chip.appendChild(removeBtn)

    return chip
  }

  private findSuggestion(tag: string): TagSuggestion | undefined {
    if (this.config.suggestions === undefined || typeof this.config.suggestions === 'function') {
      return undefined
    }
    return this.config.suggestions.find(s => s.value === tag)
  }

  private updateInputState(): void {
    if (this.isMaxReached()) {
      this.inputEl.disabled = true
      this.inputEl.placeholder = 'Max tags reached'
      this.closeDropdown()
    } else {
      this.inputEl.disabled = false
      this.inputEl.placeholder = this.config.placeholder ?? 'Add tag...'
    }
  }

  private onInputKeydown(e: KeyboardEvent): void {
    const delimiter = this.config.delimiter ?? ','

    switch (e.key) {
      case 'Enter': {
        e.preventDefault()
        if (this.isDropdownOpen && this.highlightedIndex >= 0) {
          const suggestion = this.filteredSuggestions[this.highlightedIndex]
          if (suggestion !== undefined) {
            this.addTag(suggestion.value)
            this.inputEl.value = ''
            this.closeDropdown()
          }
        } else if (this.inputEl.value.trim() !== '') {
          this.addTag(this.inputEl.value)
          this.inputEl.value = ''
          this.closeDropdown()
        }
        break
      }
      case 'Backspace': {
        if (this.inputEl.value === '' && this.tags.length > 0) {
          const last = this.tags[this.tags.length - 1]
          if (last !== undefined) {
            this.removeTag(last)
          }
        }
        break
      }
      case 'Escape': {
        if (this.isDropdownOpen) {
          e.preventDefault()
          this.closeDropdown()
        }
        break
      }
      case 'ArrowDown': {
        if (this.isDropdownOpen) {
          e.preventDefault()
          const next =
            this.highlightedIndex < this.filteredSuggestions.length - 1
              ? this.highlightedIndex + 1
              : 0
          this.setHighlightedIndex(next)
        }
        break
      }
      case 'ArrowUp': {
        if (this.isDropdownOpen) {
          e.preventDefault()
          const prev =
            this.highlightedIndex > 0
              ? this.highlightedIndex - 1
              : this.filteredSuggestions.length - 1
          this.setHighlightedIndex(prev)
        }
        break
      }
      default: {
        if (e.key === delimiter && this.inputEl.value.trim() !== '') {
          e.preventDefault()
          this.addTag(this.inputEl.value)
          this.inputEl.value = ''
          this.closeDropdown()
        }
      }
    }
  }

  private onInputChange(): void {
    const query = this.inputEl.value.trim()
    if (query === '' || this.config.suggestions === undefined) {
      this.closeDropdown()
      return
    }

    if (this.debounceTimer !== null) clearTimeout(this.debounceTimer)

    this.debounceTimer = setTimeout(() => {
      void this.fetchSuggestions(query)
    }, 150)
  }

  private async fetchSuggestions(query: string): Promise<void> {
    if (this.config.suggestions === undefined) return

    if (typeof this.config.suggestions === 'function') {
      this.showLoadingState()
      try {
        const result = this.config.suggestions(query)
        const suggestions = result instanceof Promise ? await result : result
        this.filteredSuggestions = this.filterExisting(suggestions)
        this.renderSuggestions()
      } catch {
        this.closeDropdown()
      }
    } else {
      const lowerQuery = query.toLowerCase()
      const filtered = this.config.suggestions.filter(s => {
        const searchText = (s.label ?? s.value).toLowerCase()
        return searchText.includes(lowerQuery)
      })
      this.filteredSuggestions = this.filterExisting(filtered)
      this.renderSuggestions()
    }
  }

  private filterExisting(suggestions: TagSuggestion[]): TagSuggestion[] {
    if (this.config.allowDuplicates === true) return suggestions
    return suggestions.filter(s => !this.tags.includes(s.value))
  }

  private renderSuggestions(): void {
    this.dropdownEl.innerHTML = ''

    if (this.filteredSuggestions.length === 0) {
      this.closeDropdown()
      return
    }

    for (let i = 0; i < this.filteredSuggestions.length; i++) {
      const suggestion = this.filteredSuggestions[i]
      if (suggestion === undefined) continue
      const suggestionValue = suggestion.value
      const optionEl = document.createElement('div')
      const optionId = uniqueId('cmm-taginput-option')
      optionEl.id = optionId
      optionEl.className = 'cmm-tag-input__option'
      optionEl.setAttribute('role', 'option')
      optionEl.setAttribute('aria-selected', 'false')
      optionEl.setAttribute('data-index', String(i))

      if (suggestion.color !== undefined) {
        const dot = document.createElement('span')
        dot.className = 'cmm-tag-input__option-dot'
        dot.style.backgroundColor = suggestion.color
        optionEl.appendChild(dot)
      }

      const text = document.createElement('span')
      text.className = 'cmm-tag-input__option-text'
      text.textContent = suggestion.label ?? suggestion.value

      if (suggestion.category !== undefined) {
        const cat = document.createElement('span')
        cat.className = 'cmm-tag-input__option-category'
        cat.textContent = suggestion.category
        optionEl.appendChild(text)
        optionEl.appendChild(cat)
      } else {
        optionEl.appendChild(text)
      }

      optionEl.addEventListener('click', e => {
        e.stopPropagation()
        if (this.isDestroyed) return
        this.addTag(suggestionValue)
        this.inputEl.value = ''
        this.closeDropdown()
        this.inputEl.focus()
      })

      this.dropdownEl.appendChild(optionEl)
    }

    this.openDropdown()
    this.highlightedIndex = -1
  }

  private showLoadingState(): void {
    this.dropdownEl.innerHTML = ''
    const spinner = document.createElement('div')
    spinner.className = 'cmm-tag-input__loading'
    spinner.innerHTML =
      '<svg class="cmm-tag-input__spinner" width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 10" /></svg> Loading...'
    this.dropdownEl.appendChild(spinner)
    this.openDropdown()
  }

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

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

  private setHighlightedIndex(index: number): void {
    const options = this.dropdownEl.querySelectorAll<HTMLElement>('.cmm-tag-input__option')
    for (const opt of options) {
      opt.classList.remove('cmm-tag-input__option--highlighted')
      opt.setAttribute('aria-selected', 'false')
    }

    this.highlightedIndex = index
    const target = options[index]
    if (target !== undefined) {
      target.classList.add('cmm-tag-input__option--highlighted')
      target.setAttribute('aria-selected', 'true')
      target.scrollIntoView({ block: 'nearest' })
      this.inputEl.setAttribute('aria-activedescendant', target.id)
    }
  }

  private onPaste(e: ClipboardEvent): void {
    const text = e.clipboardData?.getData('text')
    if (text === undefined || text === '') return

    const delimiter = this.config.delimiter ?? ','
    if (text.includes(delimiter)) {
      e.preventDefault()
      const parts = text.split(delimiter)
      for (const part of parts) {
        this.addTag(part)
      }
    }
  }
}
