/* eslint-disable @typescript-eslint/unified-signatures */
import { html, PropertyValues, TemplateResult } from 'lit'
import { queryRequestHeaders } from '@api-client/core/lib/definitions/HttpDefinitions.js'
import { Events as CoreEvents } from '@api-client/core/events/Events.js'
import { type IProperty, Property } from '@api-client/core/models/Property.js'
import { HeadersArray } from '@api-client/core/models/HeadersArray.js'
import { property, state } from 'lit/decorators.js'
import { live } from 'lit/directives/live.js'
import ApiElement from '../ApiElement.js'
import type Input from '../../md/input/Input.js'
import type SwitchElement from '../../md/switch/internals/SwitchElement.js'
import { EventTypes } from '../../events/EventTypes.js'
import { SnackNotifications } from '../../md/notification/SnackNotifications.js'
import '../../md/button/ui-button.js'
import '../../md/icon-button/ui-icon-button.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/switch/ui-switch.js'
import '../../md/divider/ui-divider.js'
import '../../md/icons/ui-icon.js'

const nameSuggestions = queryRequestHeaders().map((item) => item.key)

/**
 * An editor that creates HTTP headers as a data model that can be translated into a headers string.
 */
export default class HeadersForm extends ApiElement {
  /**
   * When set the editor is in read only mode.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor readOnly = false

  /**
   * Whether the text editor is rendered instead of the form.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor text = false

  /**
   * The headers model.
   */
  @property({ type: Array }) accessor model: IProperty[] = []

  @state() protected accessor internalModel = new HeadersArray()

  @state() protected accessor textValue: string | undefined

  protected autocompleteMap: Record<string, string[]> = {}

  constructor() {
    super()
    this.handleGlobalContentType = this.handleGlobalContentType.bind(this)
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.eventsTarget.addEventListener(
      EventTypes.Http.Request.State.contentTypeChange,
      this.handleGlobalContentType as EventListener
    )
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.eventsTarget.removeEventListener(
      EventTypes.Http.Request.State.contentTypeChange,
      this.handleGlobalContentType as EventListener
    )
  }

  protected override update(cp: PropertyValues<this>): void {
    if (cp.has('model')) {
      if (this.model) {
        this.internalModel = HeadersArray.from(this.model)
        this.model.forEach((item, index) => this.setupValueAutocomplete(item.name, index))
      } else {
        this.internalModel = new HeadersArray()
      }
    }
    if (cp.has('text')) {
      if (this.text) {
        this.textValue = this.internalModel.toRawString()
      } else {
        this.textValue = undefined
      }
    }

    super.update(cp)
  }

  protected setupHeaders(headers?: string): void {
    this.autocompleteMap = {}
    if (headers) {
      this.internalModel = HeadersArray.fromHeaders(headers)
      this.model = this.internalModel.toJSON()
    } else {
      this.internalModel = new HeadersArray()
      this.model = []
    }
    this.requestUpdate()
  }

  /**
   * Updates header value. It the header does not exists at the position it adds a new header.
   * @param index The position at which the header is located
   * @param value Header value
   */
  updateHeaderValue(index: number, value: string): void

  /**
   * Updates header value. It the header does not exists at the position it adds a new header.
   * @param name The name of the header to update.
   * @param value Header value
   */
  updateHeaderValue(name: string, value: string): void

  /**
   * Updates header value. It the header does not exists at the position it adds a new header.
   * @param nameOrIndex Header name or index
   * @param value Header value
   */
  updateHeaderValue(nameOrIndex: string | number, value: string): void {
    const { internalModel } = this
    let index: number
    if (typeof nameOrIndex === 'number') {
      index = nameOrIndex
    } else {
      const lowerName = nameOrIndex.toLowerCase()
      index = internalModel.findIndex((item) => (item.name || '').toLocaleLowerCase() === lowerName)
    }
    if (index === -1) {
      if (typeof nameOrIndex === 'string') {
        this.addHeader(nameOrIndex, value)
      } else {
        this.addHeader('', value)
      }
      this.propagateModelChange()
      return
    }
    internalModel[index].value = value
    this.model[index].value = value
    if (this.text) {
      this.textValue = internalModel.toRawString()
    }
    this.propagateModelChange()
    this.requestUpdate()
  }

  /**
   * Updates header name. It the header does not exists at the position it adds a new header.
   * @param index The position at which the header is located
   * @param value The header name
   */
  updateHeaderName(index: number, value: string): void

  /**
   * Updates header name. It the header does not exists at the position it adds a new header.
   * @param name The name of the header to update.
   * @param value The header name
   */
  updateHeaderName(name: string, value: string): void

  /**
   * Updates header name. It the header does not exists at the position it adds a new header.
   * @param nameOrIndex The header name or index
   * @param value The header name
   */
  updateHeaderName(nameOrIndex: string | number, value: string): void {
    const { internalModel } = this
    let index: number
    if (typeof nameOrIndex === 'number') {
      index = nameOrIndex
    } else {
      const lowerName = nameOrIndex.toLowerCase()
      index = internalModel.findIndex((item) => (item.name || '').toLocaleLowerCase() === lowerName)
    }
    if (index === -1) {
      this.addHeader(value)
      return
    }
    internalModel[index].name = value
    this.model[index].name = value
    this.setupValueAutocomplete(value, index)
    if (this.text) {
      this.textValue = internalModel.toRawString()
    }
    this.propagateModelChange()
    this.requestUpdate()
  }

  protected setupValueAutocomplete(name: string, index: number): void {
    const query = name.toLowerCase()
    const items = queryRequestHeaders(query)
    const item = items.find((i) => i.key.toLowerCase() === query)
    if (item && item.autocomplete) {
      this.autocompleteMap[`header-value-${index}`] = item.autocomplete
    } else {
      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
      delete this.autocompleteMap[`header-value-${index}`]
    }
  }

  /**
   * Removes a header from the list.
   * @param name The name of the header to remove
   */
  removeHeader(name: string): void

  /**
   * Removes a header from the list.
   * @param index The position of the header to remove
   */
  removeHeader(index: number): void

  /**
   * Removes a header from the list.
   * @param nameOrIndex Header name or its index.
   */
  removeHeader(nameOrIndex: string | number): void {
    const { internalModel } = this
    let index: number
    if (typeof nameOrIndex === 'number') {
      index = nameOrIndex
    } else {
      const lowerName = nameOrIndex.toLowerCase()
      index = internalModel.findIndex((item) => (item.name || '').toLocaleLowerCase() === lowerName)
    }
    if (index < 0) {
      return
    }
    internalModel.splice(index, 1)
    this.model.splice(index, 1)
    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
    delete this.autocompleteMap[`header-value-${index}`]
    if (this.text) {
      this.textValue = internalModel.toRawString()
    }
    this.propagateModelChange()
    this.requestUpdate()
  }

  /**
   * Adds a header to the list of headers
   */
  addHeader(name = '', value = ''): void {
    const prop = Property.String(name, value)
    this.internalModel.push(prop)
    this.model.push(prop.toJSON())
    // the value hasn't actually changed here so no events
    this.requestUpdate()
  }

  /**
   * Removes all parameters.
   */
  clear(): void {
    this.model.splice(0)
    this.internalModel.splice(0)
    this.propagateModelChange()
    this.autocompleteMap = {}
    if (this.text) {
      this.textValue = ''
    }
    this.requestUpdate()
  }

  /**
   * Toggles enabled state of a header.
   */
  toggleHeader(index: number, enabled: boolean): void {
    this.model[index].enabled = enabled
    this.internalModel[index].enabled = enabled
    if (this.text) {
      this.textValue = this.internalModel.toRawString()
    }
    this.propagateModelChange()
  }

  /**
   * Toggles the state between form and text editors.
   */
  toggle(): void {
    this.text = !this.text
  }

  async copyToClipboard(): Promise<void> {
    if (!navigator.clipboard) {
      throw new Error(`Clipboard API not supported.`)
    }
    await navigator.clipboard.writeText(this.internalModel.toString())
    SnackNotifications.notify('Headers copied to clipboard.', 2000)
    CoreEvents.Telemetry.event(
      {
        category: 'Usage',
        action: 'Click',
        label: 'Headers editor clipboard copy',
      },
      this
    )
  }

  protected handleGlobalContentType(e: CustomEvent): void {
    const { value } = e.detail
    this.updateHeaderValue('content-type', value)
  }

  /**
   * Updates the `value` from the current model and dispatches the value change event
   */
  protected propagateModelChange(): void {
    this.notifyChange()
  }

  protected async focusLast(): Promise<void> {
    await this.updateComplete
    const row = this.shadowRoot?.querySelector('.param-row:last-of-type')
    if (!row) {
      return
    }
    try {
      const node = row.querySelector('.name-input input') as HTMLInputElement | undefined
      if (node) {
        node.focus()
      }
    } catch {
      // ...
    }
  }

  protected handleAdd(): void {
    this.addHeader()
  }

  protected handleOption(e: CustomEvent): void {
    const element = e.detail.item as HTMLElement
    const { option } = element.dataset
    if (!option) {
      return
    }
    if (option === 'clear') {
      this.clear()
    } else if (option === 'text-editor') {
      this.toggle()
    }
  }

  protected handleNameChange(e: Event): void {
    const input = e.target as Input
    const index = Number(input.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.updateHeaderName(index, input.value)
    input.checkValidity()
  }

  protected handleValueChange(e: Event): void {
    const input = e.target as Input
    const index = Number(input.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.updateHeaderValue(index, input.value)
    input.checkValidity()
  }

  protected handleRemove(e: Event): void {
    const node = e.currentTarget as HTMLElement
    const index = Number(node.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.removeHeader(index)
  }

  protected handleToggle(e: Event): void {
    const node = e.currentTarget as SwitchElement
    const index = Number(node.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.toggleHeader(index, !!node.checked)
  }

  protected handleText(e: Event): void {
    const input = e.target as HTMLTextAreaElement
    const { value } = input
    this.setupHeaders(value)
    this.textValue = value
    this.notifyChange()
  }

  protected handleCopy(): void {
    this.copyToClipboard()
  }

  protected override render(): TemplateResult {
    return html`
      ${this.renderActionButtons()} ${this.renderEditor()} ${this.renderNamesList()} ${this.renderAutocompleteLists()}
    `
  }

  protected renderActionButtons(): TemplateResult {
    return html`
      <div class="editor-actions">
        <ui-button color="tonal" @click="${this.handleAdd}">Add</ui-button>
        <ui-button color="tonal" @click="${this.handleCopy}">Copy</ui-button>
        <ui-dropdown-list @select="${this.handleOption}" closeOnOutsideClick class="dropdown">
          <ui-icon-button aria-label="Additional options"
            ><ui-icon icon="moreVert" role="presentation"></ui-icon
          ></ui-icon-button>
          <ui-list slot="dropdown" role="menu">
            <ui-list-item
              role="menuitem"
              data-option="text-editor"
              image="icon"
              title="Toggles between a text editor and a form"
            >
              <ui-icon role="presentation" slot="start" icon="${this.text ? 'checkBox' : 'checkBoxBlank'}"></ui-icon>
              Text editor
            </ui-list-item>
            <ui-divider type="inset"></ui-divider>
            <ui-list-item
              role="menuitem"
              data-option="clear"
              image="icon"
              title="Removes all parameters from the editor"
            >
              <ui-icon role="presentation" slot="start" icon="deleteOutline"></ui-icon>
              Clear form
            </ui-list-item>
          </ui-list>
        </ui-dropdown-list>
      </div>
    `
  }

  protected renderEditor(): TemplateResult {
    if (this.text) {
      return this.renderText()
    }
    return this.renderList()
  }

  protected renderText(): TemplateResult {
    const { textValue = '' } = this
    return html`
      <div class="text-input">
        <div class="error" role="presentation">Invalid character</div>
        <div class="state"></div>
        <textarea .value="${live(textValue)}" data-enable-grammarly="false" @change="${this.handleText}"></textarea>
      </div>
    `
  }

  protected renderList(): TemplateResult {
    const items = this.internalModel
    if (!Array.isArray(items) || !items.length) {
      return this.renderEmptyMessage()
    }
    return this.renderParams(items)
  }

  protected renderEmptyMessage(): TemplateResult {
    return html` <div class="empty-message">No parameters added</div> `
  }

  protected renderParams(items: HeadersArray): TemplateResult {
    return html` ${items.map((item, index) => this.renderParam(item, index))} `
  }

  protected renderParam(item: Property, index: number): TemplateResult {
    return html`
      <div class="param-row">
        ${this.renderToggleButton(item, index)} ${this.renderNameField(item, index)}
        ${this.renderValueField(item, index)} ${this.renderDeleteParameter(index)}
      </div>
    `
  }

  protected renderToggleButton(item: Property, index: number): TemplateResult {
    return html`
      <ui-switch
        .checked="${!!item.enabled}"
        @change="${this.handleToggle}"
        aria-label="Toggle this parameter"
        data-index="${index}"
      ></ui-switch>
    `
  }

  protected renderDeleteParameter(index: number): TemplateResult {
    return html`
      <ui-icon-button aria-label="Delete this parameter" data-index="${index}" @click="${this.handleRemove}">
        <ui-icon icon="deleteOutline"></ui-icon>
      </ui-icon-button>
    `
  }

  protected renderNameField(item: Property, index: number): TemplateResult {
    return html`
      <div class="input-wrapper name-input">
        <div class="error" role="presentation">Invalid character</div>
        <div class="state"></div>
        <input
          type="text"
          name="name"
          autocomplete="off"
          .value="${live(item.name)}"
          data-index="${index}"
          ?readOnly="${this.readOnly}"
          pattern="\\S*"
          aria-label="Parameter name value"
          @change="${this.handleNameChange}"
          list="names-list"
        />
      </div>
    `
  }

  protected renderValueField(item: Property, index: number): TemplateResult {
    return html`
      <div class="input-wrapper value-input">
        <div class="error" role="presentation">Invalid character</div>
        <div class="state"></div>
        <input
          type="text"
          name="value"
          .value="${live(item.value as string)}"
          data-index="${index}"
          ?readOnly="${this.readOnly}"
          aria-label="Parameter value"
          @change="${this.handleValueChange}"
          list="header-value-${index}"
        />
      </div>
    `
  }

  protected renderNamesList(): TemplateResult {
    return html`
      <datalist id="names-list">${nameSuggestions.map((i) => html`<option value="${i}"></option>`)}</datalist>
    `
  }

  protected renderAutocompleteLists(): TemplateResult[] {
    const { autocompleteMap } = this
    return Object.keys(autocompleteMap).map(
      (key) => html`
        <datalist id="${key}">${autocompleteMap[key].map((i) => html`<option value="${i}"></option>`)}</datalist>
      `
    )
  }
}
