import { property, state } from 'lit/decorators.js'
import { Ref, createRef } from 'lit/directives/ref.js'
import { PktInputElement } from '@/base-elements/input-element'
import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
import { isMaxSelectionReached } from 'shared-utils/combobox/option-utils'
import specs from 'componentSpecs/combobox.json'

import type { IPktComboboxOption, TPktComboboxTagPlacement } from './combobox-types'
import PktListbox from '../listbox'

/**
 * Base class for PktCombobox.
 * Declares all reactive properties, state, refs, and simple helpers.
 */
export class ComboboxBase extends PktInputElement {
  constructor() {
    super()
    this.optionsController = new PktOptionsSlotController(this)
  }

  // Props / Attributes
  @property({ type: String, reflect: true }) value: string | string[] = ''
  @property({ type: Array }) options: IPktComboboxOption[] = []
  @property({ type: Array, attribute: 'default-options' }) defaultOptions: IPktComboboxOption[] = []
  @property({ type: Boolean, attribute: 'allow-user-input' }) allowUserInput: boolean = false
  @property({ type: Boolean }) typeahead: boolean = false
  @property({ type: Boolean, attribute: 'include-search' }) includeSearch: boolean = false
  @property({ type: String, attribute: 'search-placeholder' }) searchPlaceholder: string = ''
  @property({ type: Boolean }) multiple: boolean = false
  @property({ type: Number }) maxlength: number | null = null
  @property({ type: String, attribute: 'display-value-as' }) displayValueAs: string =
    specs.props.displayValueAs.default
  @property({ type: String, attribute: 'tag-placement' })
  tagPlacement: TPktComboboxTagPlacement | null = null
  // Internal use only — syncs to _isOptionsOpen in updated(), but does not
  // reliably open the listbox as a declarative attribute (requires focus state
  // and populated _options). Not part of the public API / component spec.
  // Used in tests to set initial open state without relying on focus or options population.
  @property({ type: Boolean, attribute: 'open' }) isOpen: boolean = false

  // Internal state
  @state() override _options: IPktComboboxOption[] = []
  @state() protected _value: string[] = []
  @state() protected _isOptionsOpen = false
  @state() protected _userInfoMessage: string = ''
  @state() protected _addValueText: string | null = null
  @state() protected _maxIsReached: boolean = false
  @state() protected _search: string = ''
  @state() protected _inputFocus: boolean = false
  protected _internalValueSync = false
  protected _optionsFromSlot = false
  protected _lastSlotGeneration = 0
  /** When true, the next handleFocus call will not reopen the dropdown. */
  protected _suppressNextOpen = false

  // Refs
  protected readonly inputRef: Ref<HTMLInputElement> = createRef()
  protected readonly triggerRef: Ref<HTMLDivElement> = createRef()
  protected readonly listboxRef: Ref<PktListbox> = createRef()

  protected get _hasTextInput(): boolean {
    return this.typeahead || this.allowUserInput
  }

  protected get _selectionDescription(): string | undefined {
    if (!this.multiple || this._value.length === 0) return undefined
    return `${this._value.length} valgt`
  }

  /**
   * Focuses the appropriate trigger element after closing the listbox.
   * Select-only: the combobox input div. Editable: the text input.
   */
  protected focusTrigger(): void {
    if (this._hasTextInput) {
      this.inputRef.value?.focus()
    } else {
      this.triggerRef.value?.focus()
    }
  }

  /**
   * Parses the value prop into an internal string array.
   */
  protected parseValue(): string[] {
    if (Array.isArray(this.value)) {
      return this.multiple ? this.value : this.value.length > 0 ? [this.value[0]] : []
    }
    if (this.value && this.multiple) {
      return this.value.split(',')
    }
    if (this.value) {
      return [this.value]
    }
    return []
  }

  /**
   * Updates the _maxIsReached state flag.
   */
  protected updateMaxReached(): void {
    this._maxIsReached = isMaxSelectionReached(this._value.length, this.maxlength)
  }

  /**
   * Syncs the public value property from internal _value state and dispatches
   * events if the value content changed. Always sets this.value as a string
   * to prevent array→string reflect cascades.
   */
  protected syncValueAndDispatch(oldInternal: string[]): void {
    const newInternal = this._value

    // Sync public value as a string (avoids array→string attribute reflect cascade)
    const newPublicStr = this.multiple ? newInternal.join(',') : newInternal[0] || ''
    const currentPublicStr = Array.isArray(this.value)
      ? this.value.join(',')
      : String(this.value || '')

    if (newPublicStr !== currentPublicStr) {
      this._internalValueSync = true
      this.value = newPublicStr
    }

    // Dispatch events if value content changed
    if (oldInternal?.join(',') !== newInternal.join(',')) {
      const eventValue = this.multiple ? [...newInternal] : newInternal[0] || ''
      this.onChange(eventValue)
    } else if (newInternal.length === 0 && oldInternal && oldInternal.length > 0) {
      this.clearInputValue()
    }
  }

  /**
   * Override onChange to skip the base class touched guard.
   * The base class returns early on the first call (setting touched = true but not
   * dispatching events). Combobox needs consistent event dispatch regardless of
   * touched state.
   */
  protected override onChange(value: string | string[]): void {
    this.touched = true
    super.onChange(value)
  }

  /**
   * No-op override of the base class valueChanged.
   * The base class version sets both this.value AND this._value, which creates
   * an infinite _value → valueChanged → value → parseValue → _value loop.
   * Combobox handles value sync and event dispatch in syncValueAndDispatch() instead.
   */
  protected override valueChanged(): void {
    // Intentionally empty — combobox manages value sync in updated()
  }
}
