import { html, nothing, PropertyValues } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import strings from '@/translations/no.json'
import { PktElement } from '@/base-elements/element'
import { repeat } from 'lit/directives/repeat.js'
import { classMap } from 'lit/directives/class-map.js'
import type { IPktComboboxOption } from 'shared-types/combobox'
import { uuidish } from 'shared-utils/utils'
import { filterOptionsBySearch } from 'shared-utils/combobox/option-utils'
import {
  isLetterOrSpace,
  createTypeaheadHandler,
  findTypeaheadOptionMatch,
} from 'shared-utils/combobox/typeahead'
import {
  focusAndScrollIntoView,
  getOptionElements,
  focusNextOption as navFocusNext,
  focusPreviousOption as navFocusPrev,
  focusFirstOption as navFocusFirst,
  focusLastOption as navFocusLast,
  focusFirstOrSelectedOption as navFocusFirstOrSelected,
} from 'shared-utils/combobox/keyboard-navigation'

declare global {
  interface HTMLElementTagNameMap {
    'pkt-listbox': PktListbox
  }
}

export interface IPktListbox {
  options: IPktComboboxOption[]
  isOpen: boolean
  disabled: boolean
  includeSearch: boolean
  isMultiSelect: boolean
  allowUserInput: boolean
  maxIsReached: boolean
  customUserInput: string | null
  searchPlaceholder: string | null
  searchValue: string | null
  maxLength: number
  userMessage: string | null
}

export class PktListbox extends PktElement implements IPktListbox {
  @property({ type: String }) id: string = uuidish()
  @property({ type: String }) label: string | null = null
  @property({ type: Array }) options: IPktComboboxOption[] = []
  @property({ type: Boolean, reflect: true, attribute: 'is-open' }) isOpen: boolean = false
  @property({ type: Boolean }) disabled: boolean = false
  @property({ type: Boolean, attribute: 'include-search' }) includeSearch: boolean = false
  @property({ type: Boolean, attribute: 'is-multi-select' }) isMultiSelect: boolean = false
  @property({ type: Boolean, attribute: 'allow-user-input' }) allowUserInput: boolean = false
  @property({ type: Boolean, attribute: 'max-is-reached' }) maxIsReached: boolean = false
  @property({ type: String, attribute: 'custom-user-input' }) customUserInput: string | null = null
  @property({ type: String, attribute: 'search-placeholder' }) searchPlaceholder: string | null =
    null
  @property({ type: String, attribute: 'search-value' }) searchValue: string | null = null
  @property({ type: Number, attribute: 'max-length' }) maxLength: number = 0
  @property({ type: String, attribute: 'user-message' }) userMessage: string | null = null

  private _selectedOptions: number = 0
  private typeahead = createTypeaheadHandler()

  @state() private _filteredOptions: IPktComboboxOption[] = []

  // Lifecycle methods
  connectedCallback(): void {
    super.connectedCallback()
    if (this.includeSearch && !this.searchValue) {
      this.searchValue = ''
    }
    if (this.options.length > 0) {
      this.filterOptions()
    }
    this.setAttribute('tabindex', '-1')
    this.addEventListener('focus', this.focusFirstOrSelectedOption)
  }

  disconnectedCallback(): void {
    super.disconnectedCallback()
    this.typeahead.reset()
  }

  updated(changedProperties: PropertyValues) {
    if (changedProperties.has('options') || changedProperties.has('searchValue')) {
      this.filterOptions()
    }
    super.updated(changedProperties)
  }

  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    if (name === 'options' || name === 'search-value') {
      this.filterOptions()
    }
    super.attributeChangedCallback(name, _old, value)
  }

  // Render methods
  private get _hasOptions(): boolean {
    return this._filteredOptions.length > 0 || this.options.length > 0
  }

  render() {
    return html`
      <div
        class=${classMap({
          'pkt-listbox': true,
          'pkt-listbox__open': this.isOpen,
          'pkt-txt-16-light': true,
        })}
        role=${ifDefined(this._hasOptions ? 'listbox' : undefined)}
        aria-multiselectable=${ifDefined(
          this._hasOptions && this.isMultiSelect ? 'true' : undefined,
        )}
        aria-label=${ifDefined(this._hasOptions ? (this.label ?? undefined) : undefined)}
      >
        <div class="pkt-listbox__banners">
          ${this.renderSearch()} ${this.renderMaximumReachedBanner()} ${this.renderUserMessage()}
          ${this.renderEmptyMessage()} ${this.renderNewOptionBanner()}
        </div>
        <ul class="pkt-listbox__options" role="presentation">
          ${this.renderList()}
        </ul>
      </div>
      <div aria-live="polite" class="pkt-visually-hidden">${this.userMessage}</div>
    `
  }

  private renderCheckboxOrCheckIcon(option: IPktComboboxOption, index: number) {
    return this.isMultiSelect
      ? html`
          <input
            class="pkt-input-check__input-checkbox"
            type="checkbox"
            role="presentation"
            tabindex="-1"
            value=${option.value}
            @change=${(e: Event) => {
              e.stopPropagation()
            }}
            .checked=${option.selected}
            aria-labelledby=${this.id + '-option-label-' + index}
            ?disabled=${this.disabled || option.disabled || (this.maxIsReached && !option.selected)}
          />
        `
      : option.selected
        ? html`<pkt-icon name="check-big"></pkt-icon>`
        : nothing
  }

  private renderEmptyMessage() {
    if (this.options.length > 0 || this._filteredOptions.length > 0 || this.userMessage) {
      return nothing
    }
    return html`<div class="pkt-listbox__banner pkt-listbox__banner--empty">
      <pkt-icon
        class="pkt-listbox__banner-icon"
        name="exclamation-mark-circle"
        size="large"
      ></pkt-icon>
      Tom liste
    </div>`
  }

  private renderList() {
    return html`
      ${repeat(
        this._filteredOptions,
        (option) => option.value,
        (option, index) => html`
          <li
            @click=${() => {
              this.toggleOption(option)
            }}
            aria-selected=${option.selected ? 'true' : 'false'}
            @keydown=${this.handleOptionKeydown}
            class=${classMap({
              'pkt-listbox__option': true,
              'pkt-listbox__option--selected': Boolean(!this.isMultiSelect && option.selected),
              'pkt-listbox__option--checkBox': this.isMultiSelect,
            })}
            tabindex="${this.disabled || option.disabled ? '-1' : '0'}"
            data-index=${index}
            data-value=${option.value}
            data-selected=${option.selected ? 'true' : 'false'}
            ?data-disabled=${this.disabled ||
            option.disabled ||
            (this.maxIsReached && !option.selected)}
            aria-disabled=${this.disabled ||
            option.disabled ||
            (this.maxIsReached && !option.selected)
              ? 'true'
              : 'false'}
            role="option"
            id=${`${this.id}-${index}`}
          >
            ${this.renderCheckboxOrCheckIcon(option, index)}
            <span class="pkt-listbox__option-label" id=${this.id + '-option-label-' + index}>
              ${option.prefix
                ? html`<span class="pkt-listbox__option-prefix">${option.prefix}</span>`
                : nothing}
              ${option.label || option.value}
            </span>
            ${option.description
              ? html`<span class="pkt-listbox__option-description pkt-txt-14-light"
                  >${option.description}</span
                >`
              : nothing}
          </li>
        `,
      )}
    `
  }

  private renderNewOptionBanner() {
    return this.allowUserInput && this.customUserInput
      ? html`
          <div
            class="pkt-listbox__banner pkt-listbox__banner--new-option pkt-listbox__option"
            data-type="new-option"
            data-value=${this.customUserInput}
            data-selected="false"
            tabindex="0"
            @click=${() =>
              this.toggleOption({
                value: this.customUserInput || '',
              })}
            @keydown=${this.handleOptionKeydown}
          >
            <pkt-icon class="pkt-listbox__banner-icon" name="plus-sign" size="large"></pkt-icon>
            Legg til "${this.customUserInput}"
          </div>
        `
      : nothing
  }

  private renderMaximumReachedBanner() {
    this._selectedOptions = this.options.filter((option) => option.selected).length

    return this.isMultiSelect && this._selectedOptions > 0 && this.maxLength > 0
      ? html`
          <div class="pkt-listbox__banner pkt-listbox__banner--maximum-reached">
            ${this._selectedOptions} av maks ${this.maxLength} mulige er valgt.
          </div>
        `
      : nothing
  }

  private renderUserMessage() {
    return this.userMessage
      ? html`<div class="pkt-listbox__banner pkt-listbox__banner--user-message">
          <pkt-icon
            class="pkt-listbox__banner-icon"
            name="exclamation-mark-circle"
            size="large"
          ></pkt-icon>
          ${this.userMessage}
        </div>`
      : nothing
  }

  private renderSearch() {
    return this.includeSearch
      ? html`
          <div class="pkt-listbox__search">
            <span class="pkt-listbox__search-icon">
              <pkt-icon name="magnifying-glass-small" size="large"></pkt-icon>
            </span>
            <input
              class="pkt-txt-16-light"
              type="text"
              aria-label="Søk i listen"
              form=""
              placeholder=${this.searchPlaceholder || strings.forms.search.placeholder}
              @input=${this.handleSearchInput}
              @keydown=${this.handleSearchKeydown}
              .value=${this.searchValue}
              data-type="searchbox"
              ?disabled=${this.disabled}
              ?readonly=${this.disabled}
              role="searchbox"
            />
          </div>
        `
      : nothing
  }

  // Event handlers
  private handleSearchInput(e: InputEvent) {
    this.searchValue = (e.target as HTMLInputElement).value
    this.dispatchEvent(
      new CustomEvent('search', {
        detail: this.searchValue,
        bubbles: false,
      }),
    )
  }

  private handleSearchKeydown(e: KeyboardEvent) {
    switch (e.key) {
      case 'Enter':
        e.preventDefault()
        break
      case 'ArrowUp':
      case 'Escape':
        this.closeOptions()
        e.preventDefault()
        break
      case 'ArrowDown':
        this.focusFirstOrSelectedOption()
        break
      case 'Tab':
        this.tabClose()
        break
    }
  }

  private handleOptionKeydown(e: KeyboardEvent) {
    const target = e.currentTarget as HTMLElement
    const value = target.dataset.value
    const itemType = target.dataset.type
    const isValueSelected = target.dataset.selected === 'true'

    if (
      !getOptionElements(this).length &&
      (!this.customUserInput || (!this.allowUserInput && this.customUserInput)) &&
      itemType !== 'new-option' &&
      itemType !== 'searchbox'
    ) {
      return
    }

    switch (e.key) {
      case ' ':
      case 'Enter':
        this.toggleOption(target)
        e.preventDefault()
        break

      case 'Backspace':
        if (value) {
          if (isValueSelected) {
            this.toggleOption(target)
          } else {
            this.closeOptions()
          }
        }
        e.preventDefault()
        break

      case 'Escape':
        this.closeOptions()
        e.preventDefault()
        break
      case 'Tab':
        // Don't preventDefault — let Tab move focus naturally
        this.tabClose()
        break

      case 'ArrowDown':
        if (e.altKey) {
          navFocusLast(this)
        } else {
          if (itemType === 'searchbox' || itemType === 'new-option') {
            navFocusFirst(this)
          } else {
            navFocusNext(target)
          }
        }
        e.preventDefault()
        break

      case 'ArrowUp':
        if (e.altKey) {
          navFocusFirst(this)
        } else {
          if (target.dataset.index === '0' && this.includeSearch) {
            const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
            searchInput && searchInput.focus()
          } else if (target.dataset.index === '0' && this.customUserInput) {
            const newOption = this.querySelector('[data-type="new-option"]') as HTMLElement
            newOption && newOption.focus()
          } else {
            navFocusPrev(target, this, this.includeSearch)
          }
        }
        e.preventDefault()
        break

      case 'Home':
        navFocusFirst(this)
        e.preventDefault()
        break

      case 'End':
        navFocusLast(this)
        e.preventDefault()
        break

      default:
        if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
          this.selectAll()
          e.preventDefault()
        }
        if (isLetterOrSpace(e.key)) {
          this.handleTypeAhead(e.key)
          e.preventDefault()
        }
        break
    }
  }

  // Focus management methods (delegates to shared utils)
  focusFirstOrSelectedOption() {
    navFocusFirstOrSelected(this, {
      disabled: this.disabled,
      allowUserInput: this.allowUserInput,
      customUserInput: this.customUserInput,
      includeSearch: this.includeSearch,
    })
  }

  // Event dispatching methods
  private toggleOption(option: IPktComboboxOption | HTMLElement) {
    const optionDisabled = option instanceof HTMLElement ? option.dataset.disabled : option.disabled
    if (this.disabled || optionDisabled) return
    const value = option instanceof HTMLElement ? option.dataset.value : option.value
    this.dispatchEvent(
      new CustomEvent('option-toggle', {
        detail: value,
        bubbles: false,
      }),
    )
  }

  private selectAll() {
    this.dispatchEvent(new CustomEvent('select-all', { bubbles: false }))
  }

  private closeOptions() {
    this.dispatchEvent(new CustomEvent('close-options', { bubbles: false }))
  }

  private tabClose() {
    this.dispatchEvent(new CustomEvent('tab-close', { bubbles: false }))
  }

  // Filtering and typeahead methods
  filterOptions() {
    this._filteredOptions = filterOptionsBySearch(this.options, this.searchValue)
  }

  private handleTypeAhead(char: string) {
    const searchString = this.typeahead.append(char)
    const options = getOptionElements(this)
    const matchIndex = findTypeaheadOptionMatch(options, searchString)
    if (matchIndex >= 0) {
      focusAndScrollIntoView(options[matchIndex])
    }
  }
}

try {
  customElement('pkt-listbox')(PktListbox)
} catch (e) {
  console.warn('Forsøker å definere <pkt-listbox>, men den er allerede definert')
}
