import { html, LitElement, PropertyValues, TemplateResult } from 'lit'
import { property, query, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { setDisabled } from '../../../lib/disabled.js'
import type UiOption from './Option.js'
import type { UiMenuElement } from '../../menu/ui-menu.js'

import '../../text-field/ui-outlined-text-field.js'
import '../../menu/ui-menu.js'
import '../../icons/ui-icon.js'
import '@material/web/focus/md-focus-ring.js'

export interface UiSelectChangeEvent {
  value: string | undefined
  item: UiOption | null
}

/**
 * Material Design 3 Select component that behaves like an outlined text field with dropdown.
 *
 * @fires change - Dispatched when the selection changes. The event is non-bubbling and non-cancelable.
 *                 The `event.detail` object contains the `value` and `item` properties.
 * @fires open - Dispatched when the dropdown opens
 * @fires close - Dispatched when the dropdown closes
 */
export default class UiSelect extends LitElement {
  static readonly formAssociated = true
  #internals = this.attachInternals()

  /**
   * The value has a private member so that we can set the value without triggering
   * the side effects.
   */
  #value: string | undefined

  /**
   * The currently selected value. Corresponds to the `value` attribute of the selected `ui-option`.
   * When set programmatically, it will update the selected option if a matching option exists.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select value="apple">
   *   <ui-option value="apple">Apple</ui-option>
   *   <ui-option value="banana">Banana</ui-option>
   * </ui-select>
   * ```
   */
  get value(): string | undefined {
    return this.#value
  }

  @property({ type: String })
  set value(newValue: string | undefined) {
    const oldValue = this.#value
    if (newValue === oldValue) return
    this.#value = newValue
    this.requestUpdate()
  }

  /**
   * The name attribute for form submission. This value will be used as the key
   * when the form is submitted.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select name="country" value="us">
   *   <ui-option value="us">United States</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: String }) accessor name: string | undefined

  /**
   * The label text displayed in the select field. Provides accessible labeling
   * and is shown as the floating label in the outlined text field.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select label="Select a country">
   *   <ui-option value="us">United States</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: String }) accessor label: string | undefined

  /**
   * Whether the select is required for form validation. When true, the select
   * must have a value selected for the form to be valid.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select required label="Required field">
   *   <ui-option value="option1">Option 1</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: Boolean }) accessor required = false

  /**
   * Whether the select is in an invalid state. This is typically set automatically
   * during validation, but can be set manually to indicate validation errors.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select invalid invalidText="Please select a valid option">
   *   <ui-option value="option1">Option 1</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: Boolean }) accessor invalid: boolean | undefined

  /**
   * The error message to display when the select is invalid. This text is shown
   * below the select field when `invalid` is true.
   *
   * @attribute
   * @example
   * ```html
   * <ui-select invalid invalidText="This field is required">
   *   <ui-option value="option1">Option 1</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: String }) accessor invalidText: string | undefined

  /**
   * @attribute
   */
  @property({ type: String }) accessor supportingText: string | undefined

  /**
   * Whether the select is disabled. When disabled, the select cannot be interacted
   * with and will not receive focus or respond to user input.
   *
   * @default false
   * @attribute
   * @example
   * ```html
   * <ui-select disabled label="Disabled select">
   *   <ui-option value="option1">Option 1</ui-option>
   * </ui-select>
   * ```
   */
  @property({ type: Boolean, reflect: true }) accessor disabled = false

  /**
   * Whether the dropdown menu is currently open. This property reflects the
   * current state of the dropdown and can be set programmatically to open/close it.
   *
   * @default false
   * @example
   * ```javascript
   * // Open the dropdown programmatically
   * selectElement.open = true;
   *
   * // Close the dropdown
   * selectElement.open = false;
   * ```
   */
  @property({ type: Boolean, reflect: true }) accessor open = false

  @state() accessor selectedOption: UiOption | null = null
  @state() accessor ariaActiveDescendant: string | undefined
  @query('.menu') accessor menu!: UiMenuElement

  /**
   * Returns the currently selected option element. This provides access to the
   * full `ui-option` element, not just its value.
   *
   * @readonly
   * @example
   * ```javascript
   * const select = document.querySelector('ui-select');
   * const selectedItem = select.selectedItem;
   * if (selectedItem) {
   *   console.log('Selected option:', selectedItem.textContent);
   * }
   * ```
   */
  get selectedItem(): UiOption | null {
    return this.selectedOption
  }

  /**
   * Returns the text content that should be displayed in the select field.
   * This is the rendered value of the currently selected option.
   *
   * @readonly
   * @example
   * ```javascript
   * const select = document.querySelector('ui-select');
   * console.log('Display text:', select.renderValue);
   * ```
   */
  get renderValue(): string {
    const item = this.selectedOption
    return item ? item.renderValue : ''
  }

  /**
   * Returns the form element that contains this select, if any.
   * Part of the form-associated custom element API.
   *
   * @readonly
   */
  get form(): HTMLFormElement | null {
    return this.#internals.form
  }

  /**
   * Returns the validity state of the select element.
   * Part of the form-associated custom element API.
   *
   * @readonly
   */
  get validity(): ValidityState {
    return this.#internals.validity
  }

  /**
   * Returns the validation message for the select element.
   * Part of the form-associated custom element API.
   *
   * @readonly
   */
  get validationMessage(): string {
    return this.#internals.validationMessage
  }

  /**
   * Returns whether the select element will be validated when the form is submitted.
   * Part of the form-associated custom element API.
   *
   * @readonly
   */
  get willValidate(): boolean {
    return this.#internals.willValidate
  }

  /**
   * Checks the validity of the select element and returns true if valid.
   * Part of the form-associated custom element API.
   *
   * @returns {boolean} True if the element is valid, false otherwise
   * @example
   * ```javascript
   * const select = document.querySelector('ui-select');
   * if (!select.checkValidity()) {
   *   console.log('Select is invalid:', select.validationMessage);
   * }
   * ```
   */
  checkValidity(): boolean {
    return this.#internals.checkValidity()
  }

  constructor() {
    super()
    this.addEventListener('click', this.handleClick.bind(this))
    this.addEventListener('blur', this.handleBlur.bind(this))
    this.addEventListener('keydown', this.handleKeydown.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.setAttribute('role', 'combobox')
    this.setAttribute('aria-haspopup', 'listbox')
    this.setAttribute('aria-controls', 'menu')
    if (!this.disabled) {
      this.setAttribute('tabindex', '0')
    }
  }

  /**
   * Resets the select to its initial state. Called automatically when the parent
   * form is reset. Part of the form-associated custom element API.
   *
   * @example
   * ```javascript
   * const select = document.querySelector('ui-select');
   * select.formResetCallback(); // Clears the selection
   * ```
   */
  formResetCallback(): void {
    this.value = undefined
  }

  /**
   * Restores the select's state from saved form data. Called automatically when
   * the browser restores form state. Part of the form-associated custom element API.
   *
   * @param {string | null} state - The saved state to restore
   */
  formStateRestoreCallback(state: string | null): void {
    this.value = state ?? undefined
  }

  /**
   * Validates the select element and updates its validity state. This is called
   * automatically during property changes, but can be called manually to trigger validation.
   *
   * @example
   * ```javascript
   * const select = document.querySelector('ui-select');
   * select.validate();
   * if (select.invalid) {
   *   console.log('Validation failed:', select.invalidText);
   * }
   * ```
   */
  validate(): void {
    let message = ''
    if (this.required && !this.value) {
      message = 'Please select an item.'
      this.#internals.setValidity({ valueMissing: true }, message)
    } else {
      this.#internals.setValidity({})
    }
    this.invalid = !this.#internals.validity.valid
    this.invalidText = message
  }

  protected override willUpdate(changedProperties: PropertyValues<this>): void {
    super.willUpdate(changedProperties)
    if (changedProperties.has('disabled')) {
      setDisabled(this, this.disabled)
    }
    if (changedProperties.has('open')) {
      this.handleOpenChange()
    }
    if (changedProperties.has('value')) {
      this.setCurrentOption()
      this.#internals.setFormValue(this.value ?? null)
      this.validate()
    }
    if (changedProperties.has('label')) {
      if (this.label) {
        this.setAttribute('aria-label', this.label)
      } else {
        this.removeAttribute('aria-label')
      }
    }
  }

  protected async setCurrentOption(): Promise<void> {
    await this.updateComplete
    if (this.value) {
      const options = this.querySelectorAll<UiOption>('ui-option')
      this.selectedOption = Array.from(options).find((option) => option.value === this.value) || null
    } else {
      this.selectedOption = null
    }
  }

  protected handleKeydown(e: KeyboardEvent): void {
    if (this.disabled || e.defaultPrevented) return
    if (this.open) {
      switch (e.key) {
        case 'Tab': {
          // If menu is open and Tab is pressed, close it and allow normal tab navigation
          if (this.open) {
            this.open = false
          }
          break
        }
        case 'Escape': {
          if (this.open) {
            e.preventDefault()
            this.open = false
            this.focus() // Return focus to the select element
          }
          break
        }
        case 'ArrowDown':
          e.preventDefault()
          this.menu.highlightNext()
          return
        case 'ArrowUp':
          e.preventDefault()
          this.menu.highlightPrevious()
          return
        case 'Home':
          e.preventDefault()
          this.menu.highlightFirst()
          return
        case 'End':
          e.preventDefault()
          this.menu.highlightLast()
          return
        case 'Enter':
        case ' ':
          if (this.menu.highlightListItem) {
            e.preventDefault()
            this.menu.notifySelect(this.menu.highlightListItem as UiOption)
          }
          return
      }
    } else {
      switch (e.key) {
        case 'Enter':
        case ' ': {
          if (!this.open) {
            e.preventDefault()
            this.open = true
          }
          break
        }
        case 'ArrowDown':
        case 'ArrowUp': {
          if (!this.open) {
            e.preventDefault()
            this.open = true
          }
          // If menu is open, let the menu handle arrow keys
          break
        }
      }
    }
  }

  protected handleBlur(e: FocusEvent): void {
    if (this.disabled) return

    // Check if focus is moving to the menu or one of its children
    const relatedTarget = e.relatedTarget as HTMLElement

    if (relatedTarget && this.contains(relatedTarget)) {
      // Focus is moving to the menu, keep it open
      return
    }

    // Close the menu when focus leaves the component
    this.open = false
  }

  protected handleClick(e: Event): void {
    if (this.disabled || e.defaultPrevented) return
    e.preventDefault()
    e.stopPropagation()
    this.open = true
  }

  protected async handleOpenChange(): Promise<void> {
    const menu = this.menu
    if (!menu) {
      // The status can be set before the menu is rendered
      return
    }
    this.setAttribute('aria-expanded', String(this.open))
    if (this.open) {
      menu.showPopover()
      // menu.focus()
      if (this.selectedOption) {
        this.menu.highlightItem(this.selectedOption)
      } else {
        this.menu.highlightFirst()
      }
      this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))
      this.focus()
    } else {
      menu.hidePopover()
      this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))
    }
  }

  handleSelect(e: CustomEvent<{ item: UiOption }>): void {
    e.stopPropagation()
    const item = e.detail.item
    this.selectedOption = item
    this.#value = item.value
    this.open = false

    // Dispatch change event
    const changeEvent = new CustomEvent<UiSelectChangeEvent>('change', {
      detail: { value: this.value, item: item },
      bubbles: false,
      composed: true,
    })
    this.dispatchEvent(changeEvent)
    this.focus()
  }

  handleHighlightChange(e: CustomEvent<{ item: UiOption | null }>): void {
    this.ariaActiveDescendant = e.detail.item?.id
  }

  handleMenuClose(): void {
    this.open = false
    this.focus()
  }

  override render(): TemplateResult {
    const classes = classMap({
      'ui-select': true,
      'open': this.open,
      'disabled': this.disabled,
    })
    return html`${this.renderFocusRing()}
      <div class="${classes}" aria-activedescendant=${this.ariaActiveDescendant || ''}>
        ${this.renderInput()} ${this.renderMenu()}
      </div> `
  }

  protected renderInput(): TemplateResult {
    return html`<ui-outlined-text-field
      .name=${this.name}
      .label=${this.label}
      .value=${this.renderValue}
      .disabled=${this.disabled}
      .required=${this.required}
      readonly
      tabindex="-1"
      inert
      aria-hidden="true"
      .invalid=${this.invalid}
      .invalidText=${this.invalidText || ''}
      .supportingText=${this.supportingText || ''}
      class="input"
    >
      <ui-icon slot="suffix">arrow_drop_down</ui-icon>
    </ui-outlined-text-field>`
  }

  protected renderMenu(): TemplateResult {
    return html`<ui-menu
      id="menu"
      class="menu"
      popover="auto"
      selector="ui-option"
      @select="${this.handleSelect}"
      @close="${this.handleMenuClose}"
      @highlightchange="${this.handleHighlightChange}"
    >
      <slot></slot>
    </ui-menu>`
  }

  protected renderFocusRing(): TemplateResult {
    return html`<md-focus-ring part="focus-ring" class="focus-ring" .control="${this as HTMLElement}"></md-focus-ring>`
  }
}
