import { html, type TemplateResult, type PropertyValues, nothing } from 'lit'
import { property, query, state } from 'lit/decorators.js'
import { live } from 'lit/directives/live.js'
import type { IApiServer } from '@api-client/core/amf/definitions/Api.js'
import { cancelEvent } from '@api-client/core/lib/events/Utils.js'
import { Events } from '../../events/amf/Events.js'
import { EventTypes } from '../../events/amf/EventTypes.js'
import type { SelectionType, ServerType } from '../../amf/types.js'
import UiDropdownList, { type UiDropdownListSelection } from '../../md/dropdown-list/internals/UiDropdownList.js'
import { ApiDocumentationBase } from './ApiDocumentationBase.js'
import type Input from '../../md/input/Input.js'
import '../../md/icons/ui-icon.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/text-field/ui-text-field.js'
import '../../md/icon-button/ui-icon-button.js'

interface ICustomServer {
  index: number
  value: string
}

interface IAutoSelectOptions {
  /**
   * The preferred URI value to use to select.
   */
  preferred?: string
  /**
   * When set to `true` it only allows to select the value from API servers or custom servers.
   * It won't select the value as a `custom` value.
   */
  noCustom?: boolean
}

/**
 * An element that presents a list of servers defined in the currently loaded API.
 * The user can select a server they prefer to use with the rest of the API documentation UI.
 *
 * The element dispatched the `apiserverchanged` event when the selection is made. This event
 * is dispatched when the selection is made automatically after querying for server data or when
 * the user decide to select a different option.
 *
 * ```javascript
 * const template = html`<api-server-picker @apiserverchanged="${this._serverChanged}"></api-server-picker>`;
 * ```
 *
 * The event is dispatched with the `detail` object that has the `value` with the current
 * base URI of the selected server, optional `id` with the graph ID of the selected server,
 * and the type of the current selection. The `type` property is one of: `server`
 * (pick is one of the predefined servers), `custom` (user entered value), or `extra`
 * (one of the added server definitions from the outside of the API specification.)
 *
 * The picker allows the user to enter a custom value of the base URI when the `custom`
 * property is set. In this case, the user can select the "custom" value in the
 * drop-down options list which then renders an input field where the user can change the value.
 * The `apiserverchanged` event is dispatched after the user commit the change in the input field (enter, blur).
 *
 * Additionally, the element offers a named `<slot>` (custom-base-uri) which renders all passed items
 * in the dropdown. All items that have `data-value` attribute set are selectable as the server value.
 *
 * ```javascript
 * const template = html`
 * <api-server-picker @apiserverchanged="${this._serverChanged}">
 *  <ui-list-item slot="custom-base-uri" data-value="http://mocking.com">Mocking service</ui-list-item>
 * </api-server-picker>`;
 * ```
 *
 * By default all server items are rendered as `<ui-list-item>`. However, you can pass any element
 * to make the experience more compelling to the user (like adding a section header).
 *
 * @fires apiserverchanged - The event dispatched when a server selection change.
 * @fires serverscountchanged - The event dispatched when a server count change.
 * @fires query - Dispatched when the element asks the context store for servers info.
 */
export default class ApiServerPicker extends ApiDocumentationBase {
  /**
   * Currently selected type of the input.
   * `server` | `extra` | `custom`
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor type: ServerType | undefined

  @state() protected accessor _graphLoaded: boolean | undefined

  /**
   * When set it automatically selected the first server from the list of servers when selection is missing.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor auto: boolean | undefined

  /**
   * When set the `Custom base URI` is rendered in the dropdown
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor custom: boolean | undefined

  /**
   * A programmatic access to the opened state of the drop down.
   * Note, this does nothing when custom element is rendered.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor opened: boolean | undefined

  /**
   * The graph domain id of the selected domain object.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor omainId: string | undefined

  /**
   * The selected domain type.
   * This is an abstract name from the api navigation.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor domainType: SelectionType | undefined

  /**
   * The current base URI value from either (in order) the baseUri, current value, or just empty string.
   * @attribute
   */
  @property({ type: String }) accessor value: string | undefined

  /**
   * The `baseUri` property overrides any server definition and turns the picker into the
   * "custom" mode.
   * @attribute
   */
  @property({ type: String }) accessor baseUri: string | undefined

  protected _debounceValue?: number

  /**
   * Holds the size of rendered custom servers.
   */
  protected _customNodesCount: number

  /**
   * A list of custom items rendered in the slot.
   */
  @state() protected accessor _customItems: ICustomServer[]

  @state() protected accessor _servers: IApiServer[] | undefined

  /**
   * Returns a list of custom servers found in the element's DOM.
   * For an item to be included in the list it has to have the `data-value="..."` attribute set.
   */
  get customServers(): string[] {
    return this._customItems.map((i) => i.value)
  }

  /**
   * Returns a list of servers defined in the API.
   */
  get apiServers(): string[] {
    return (this._servers || []).map((i) => i.url)
  }

  /**
   * @returns Total number of list items being rendered.
   */
  get serversCount(): number {
    const { custom, _servers = [], _customItems } = this
    const offset = custom ? 1 : 0
    const serversCount = _servers.length + _customItems.length + offset
    return serversCount
  }

  protected _onApiServerChange: EventListener | null = null

  /**
   * @returns Previously registered callback function for the `api-server-changed` event.
   */
  get onapiserverchange(): EventListener | null {
    return this._onApiServerChange
  }

  /**
   * @param value A callback function to be called  when `api-server-changed` event is dispatched.
   */
  set onapiserverchange(value: EventListener | null) {
    const old = this._onApiServerChange
    if (old) {
      this.removeEventListener(EventTypes.Server.serverChange, old)
    }
    const isFn = typeof value === 'function'
    if (isFn) {
      this._onApiServerChange = value
      this.addEventListener(EventTypes.Server.serverChange, value)
    } else {
      this._onApiServerChange = null
    }
  }

  protected _onServersCountChangeValue: EventListener | null = null

  /**
   * @returns Previously registered callback function for the `servers-count-changed` event.
   */
  get onserverscountchange(): EventListener | null {
    return this._onServersCountChangeValue || null
  }

  /**
   * @param value A callback function to be called when `servers-count-changed` event is dispatched.
   */
  set onserverscountchange(value: EventListener | null) {
    const old = this._onServersCountChangeValue
    if (old) {
      this.removeEventListener(EventTypes.Server.serverCountChange, old)
    }
    const isFn = typeof value === 'function'
    if (isFn) {
      this._onServersCountChangeValue = value
      this.addEventListener(EventTypes.Server.serverCountChange, value)
    } else {
      this._onServersCountChangeValue = null
    }
  }

  protected _boundEvent: () => void

  @query('.uri-input')
  protected accessor customInput: Input | null = null

  constructor() {
    super()
    this.queryDebouncerTimeout = 1
    this._customNodesCount = 0
    this._graphLoaded = false
    this._customItems = []
    this._boundEvent = (): void => {
      this._processDebounce()
    }
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.eventsTarget.addEventListener(EventTypes.Store.graphChange, this._boundEvent)
    this._processDebounce()
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.eventsTarget.removeEventListener(EventTypes.Store.graphChange, this._boundEvent)
    if (this._debounceValue) {
      clearTimeout(this._debounceValue)
      this._debounceValue = undefined
    }
  }

  protected override shouldUpdate(_changedProperties: PropertyValues<this>): boolean {
    if (_changedProperties.has('value')) {
      if (this.baseUri) {
        this.value = this.baseUri
        return false
      }
      // const oldValue = _changedProperties.get('value');
      // const newValue = this.value;

      // if (newValue && this._isValueCustom(newValue) && !this.custom) {
      //   // eslint-disable-next-line no-console
      //   console.warn(`Trying to set a custom value on the selector but the "custom" is not set.`);
      //   this.value = oldValue;
      //   return _changedProperties.size !== 1;
      // }
    }
    return true
  }

  protected override updated(_changedProperties: PropertyValues<this>): void {
    super.updated(_changedProperties)
    if (_changedProperties.has('domainType') || _changedProperties.has('domainId')) {
      this._processDebounce()
    }
    if (_changedProperties.has('custom')) {
      this.notifyServersCount()
      if (!this.custom && this.type === 'custom') {
        this.disabledCustomUri()
      }
    }

    if (_changedProperties.has('auto') && this.auto && !this.value) {
      this._autoSelect({ preferred: this.value })
    }
    if (_changedProperties.has('value')) {
      this._valueChanged(this.value)
    }
    if (_changedProperties.has('baseUri') && this.baseUri) {
      this.value = this.baseUri
      this.type = 'custom'
    }
  }

  /**
   * Calls the `queryGraph()` function in a debouncer.
   */
  protected _processDebounce(): void {
    if (this._debounceValue) {
      clearTimeout(this._debounceValue)
    }
    this._debounceValue = setTimeout(() => {
      this._debounceValue = undefined
      this.processGraph()
    }, this.queryDebouncerTimeout) as unknown as number
  }

  /**
   * Queries for the current API server data from the store.
   */
  override async processGraph(): Promise<void> {
    this._graphLoaded = false
    let oldValue = this.value
    const oldCustom = this.isValueCustom(oldValue)
    await this._queryServers()
    const isCustom = this.isValueCustom(oldValue)
    this._graphLoaded = true
    if (!oldCustom && isCustom) {
      oldValue = undefined
    }
    const noCustom = !(oldCustom && isCustom)
    this._autoSelect({ preferred: oldValue, noCustom })
    this.notifyServersCount()
  }

  /**
   * Queries for the current servers value.
   */
  protected async _queryServers(): Promise<void> {
    const { domainId, domainType } = this
    let endpointId
    let methodId
    if (domainType === 'operation') {
      methodId = domainId
    } else if (domainType === 'resource') {
      endpointId = domainId
    }
    try {
      const info = await Events.Server.query(this, {
        endpointId,
        methodId,
      })
      this._servers = info
    } catch (e) {
      const ex = e as Error
      this._servers = undefined
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for API servers: ${ex.message}`, this.localName)
    }
  }

  /**
   * Checks whether the passed value would be rendered as a "custom" value.
   * The value is "custom" if it cannot be matched with server value for an API defined server or
   * a custom server passed in the DOM.
   *
   * @param value The value to test.
   * @returns `true` when the value would be rendered as "custom".
   */
  isValueCustom(value?: string): boolean {
    const { _servers = [], _customItems } = this
    const srv = _servers.some((s) => s.url === value)
    if (srv) {
      return false
    }
    const custom = _customItems.some((s) => s.value === value)
    if (custom) {
      return false
    }
    // returns false for no value.
    return !!value
  }

  /**
   * This only makes sure whether the set value is a "custom" value while no selection is made.
   *
   * @param value The current value.
   */
  protected _valueChanged(value?: string): void {
    if (!value || !this.custom || !this._graphLoaded) {
      return
    }

    const { _servers = [], _customItems, _graphLoaded } = this
    if (_graphLoaded) {
      return
    }
    if (_servers.some((s) => s.url === value)) {
      return
    }
    if (_customItems.some((s) => s.value === value)) {
      return
    }
    this.value = value
    this.type = 'custom'
    Events.Server.serverChange(this, value, this.type)
  }

  /**
   * Automatically selects one of available values.
   *
   * When the `preferred` is set it checks whether this value is on the servers list. If so this value is used.
   * If none match then it checks for custom servers and if they have this value then it is set.
   *
   * Otherwise it takes files value of servers or custom servers (when no servers) and sets this value instead.
   *
   * Caveats:
   * - it does nothing when `auto` is not set
   * - it does nothing when `type` is `custom`
   * - it won't set the "first" server or custom server value when the graph is being loaded.
   *
   * @param preferred The preferred value to be selected.
   */
  protected _autoSelect({ preferred, noCustom }: IAutoSelectOptions = {}): void {
    const { auto, type } = this
    if (type === 'custom') {
      if (preferred) {
        const isCustom = this.isValueCustom(preferred)
        if (!isCustom) {
          this.type = undefined
        } else {
          this.value = preferred
          Events.Server.serverChange(this, preferred || '', this.type as ServerType)
          return
        }
      } else {
        return
      }
    }
    if (!auto) {
      return
    }
    const { _servers = [], _customItems } = this
    const srv = _servers.find((s) => s.url === preferred)
    if (srv) {
      this.value = preferred
      this.type = 'server'
      Events.Server.serverChange(this, preferred || '', this.type as ServerType)
      return
    }
    const custom = _customItems.find((s) => s.value === preferred)
    if (custom) {
      this.value = custom.value
      this.type = 'extra'
      Events.Server.serverChange(this, custom.value, this.type as ServerType)
      return
    }
    if (!this._graphLoaded) {
      // do not pre-select values before servers query ends.
      return
    }

    // todo: if anything is wrong with auto-select and the custom values, remove these lines.
    if (this.custom && preferred && !noCustom) {
      this.value = preferred
      this.type = 'custom'
      Events.Server.serverChange(this, preferred, this.type as ServerType)
      return
    }

    const [firstServer] = _servers
    if (firstServer) {
      this.value = firstServer.url
      this.type = 'server'
      Events.Server.serverChange(this, firstServer.url, this.type as ServerType)
      return
    }
    const [firstCustom] = _customItems
    if (firstCustom) {
      this.value = firstCustom.value
      this.type = 'extra'
      Events.Server.serverChange(this, firstCustom.value, this.type as ServerType)
    }
  }

  protected handleDropdownOpen(e: Event): void {
    const node = e.target as UiDropdownList
    this.opened = node.open
  }

  protected handleSelection(e: CustomEvent<UiDropdownListSelection>): void {
    e.preventDefault()
    const { _graphLoaded, _customItems } = this
    if (!_graphLoaded) {
      return
    }
    const { item } = e.detail
    const { value } = item.dataset
    if (!value) {
      return
    }
    if (value === 'custom') {
      this.enableCustomUri()
      return
    }
    const custom = _customItems.find((i) => i.value === value)
    this.value = value
    if (custom) {
      this.type = 'extra'
    } else {
      this.type = 'server'
    }
    Events.Server.serverChange(this, value, this.type)
  }

  protected handleSlot(e: Event): void {
    const slot = e.target as HTMLSlotElement
    const elements = slot.assignedElements({ flatten: true })
    const servers: ICustomServer[] = []
    elements.forEach((element, index) => {
      const value = element.getAttribute('data-value')
      if (!value) {
        return
      }
      element.setAttribute('role', 'menuitem')
      servers.push({
        index,
        value,
      })
    })
    this._customItems = servers
    this.notifyServersCount()
  }

  /**
   * Dispatches the `servers-count-changed` event with the current number of rendered servers.
   */
  protected notifyServersCount(): void {
    Events.Server.serverCountChange(this, this.serversCount)
  }

  protected handleCustomChange(e: Event): void {
    const { value } = e.target as Input
    this.value = value
    Events.Server.serverChange(this, this.value, this.type as ServerType)
  }

  /**
   * Resets current selection to a default value.
   */
  protected disabledCustomUri(): void {
    const { baseUri } = this
    if (baseUri) {
      this.value = baseUri
      this.type = 'custom'
    } else {
      this.value = ''
      this.type = 'server'
    }
    Events.Server.serverChange(this, '', this.type)
    this.opened = false
    this._autoSelect()
  }

  protected async enableCustomUri(): Promise<void> {
    this.type = 'custom'
    this.value = ''
    this.opened = false
    Events.Server.serverChange(this, this.value, this.type)
    await this.updateComplete
    const { customInput } = this
    customInput?.focus()
  }

  /**
   * Removes the currently rendered servers from the element.
   * This allow to programmatically manipulate the list of servers by removing all API related definitions.
   */
  removeServers(): void {
    this._servers = []
  }

  override render(): TemplateResult {
    const { type } = this
    const isCustom = type === 'custom'
    return html` ${isCustom ? this.renderCustomUriInput() : ''} ${this.renderSelector(isCustom)} `
  }

  /**
   * @returns Template result for the custom input.
   */
  protected renderCustomUriInput(): TemplateResult {
    const { value = '', baseUri } = this
    return html`
      <ui-text-field
        class="uri-input"
        .value="${live(value)}"
        label="API base URI"
        type="url"
        ?readOnly="${!!baseUri}"
        @change="${this.handleCustomChange}"
        @select="${cancelEvent}"
      >
        <ui-icon-button
          slot="suffix"
          aria-label="Activate to clear and close the input"
          title="Clears and closes the input"
          ?hidden="${!!baseUri}"
          @click="${this.disabledCustomUri}"
        >
          <ui-icon role="presentation" icon="close"></ui-icon>
        </ui-icon-button>
      </ui-text-field>
    `
  }

  /**
   * @returns Template result for the drop down element.
   */
  protected renderSelector(hidden: boolean): TemplateResult {
    const { value, opened = false } = this
    const finalValue = value === '' ? undefined : value
    return html`
      <ui-dropdown-list
        class="api-server-dropdown"
        closeOnOutsideClick
        matchTriggerWidth
        ?hidden="${hidden}"
        .open="${opened}"
        @open="${this.handleDropdownOpen}"
        @select="${this.handleSelection}"
      >
        <ui-text-field
          class="trigger"
          label="Select server"
          readOnly
          .value="${finalValue || ''}"
          @select="${cancelEvent}"
        >
          <ui-icon icon="arrowDropDown" role="presentation" slot="suffix"></ui-icon>
        </ui-text-field>
        <ui-list slot="dropdown" role="menu" selector="[data-value]" selectActive>
          ${this.renderSelectorList()}
        </ui-list>
      </ui-dropdown-list>
    `
  }

  /**
   * Call the render functions for
   * - Server options (from AMF Model)
   * - Custom URI option
   * - Extra slot
   * @returns The combination of all options
   */
  protected renderSelectorList(): TemplateResult {
    return html` ${this.renderServerList()} ${this.renderSlot()} ${this.renderCustomUri()} `
  }

  /**
   * @returns The template for the custom URI list item.
   */
  protected renderCustomUri(): TemplateResult | typeof nothing {
    const { custom } = this
    if (!custom) {
      return nothing
    }
    return html`
      <ui-list-item role="menuitem" data-value="custom" class="custom-option">Custom base URI</ui-list-item>
    `
  }

  /**
   * @returns Template result for the drop down list options for current servers
   */
  protected renderServerList(): TemplateResult[] | typeof nothing {
    const { _servers } = this
    if (!Array.isArray(_servers)) {
      return nothing
    }
    return _servers.map((server) => this.renderServerListItem(server))
  }

  /**
   * @param server The server definition
   * @returns The template for a server list item.
   */
  protected renderServerListItem(server: IApiServer): TemplateResult {
    return html`
      <ui-list-item role="menuitem" data-value="${server.url}" data-item="server-dropdown-option"
        >${server.url}</ui-list-item
      >
    `
  }

  protected renderSlot(): TemplateResult {
    return html`<slot name="custom-base-uri" @slotchange="${this.handleSlot}"></slot>`
  }
}
