import { html, TemplateResult, PropertyValues, nothing } from 'lit'
import { property } from 'lit/decorators.js'
import { live } from 'lit/directives/live.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { type IProperty, Property } from '@api-client/core/models/Property.js'
import { decodeUrlEncoded, encodeUrlEncoded, createViewModel, formArrayToString } from './UrlEncodeUtils.js'
import type SwitchElement from '../../md/switch/internals/SwitchElement.js'
import type Input from '../../md/input/Input.js'
import ApiElement from '../ApiElement.js'
import '../../md/icons/ui-icon.js'
import '../../md/icon-button/ui-icon-button.js'
import '../../md/switch/ui-switch.js'

/**
 * A body editor specializing in URL encoded data.
 */
export default class BodyUrlEncodedEditor extends ApiElement {
  /**
   * When set the editor is in read only mode.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor readOnly = false

  /**
   * When set all controls are disabled in the form
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor disabled = false

  /**
   * When set it automatically encodes and decodes values.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor autoEncode = false

  /**
   * @attribute
   */
  @property({ type: Boolean }) accessor allowFormInfo: boolean | undefined

  protected valueInternal = ''

  /**
   * The HTTP body.
   */
  get value(): string | undefined {
    return this.valueInternal
  }

  @property({ type: String })
  set value(value: string | undefined) {
    const old = this.valueInternal
    if (old === value) {
      return
    }
    this.valueInternal = value || ''
    this.setupModel(value || '')
  }

  protected modelInternal: IProperty[] = []

  /**
   * Computed data model for the view.
   * Don't set both `value` and `model`. If the model exists then
   * set only it or otherwise the `value` setter override the model.
   */
  get model(): IProperty[] {
    return this.modelInternal
  }

  set model(value: IProperty[]) {
    const old = this.modelInternal
    if (old === value) {
      return
    }
    if (Array.isArray(value)) {
      this.modelInternal = value
    } else {
      this.modelInternal = []
    }
    this.valueInternal = this.modelToValue()
    this.requestUpdate()
  }

  protected override update(cp: PropertyValues<this>): void {
    if (cp.has('autoEncode')) {
      this.updateValue()
    }
    super.update(cp)
  }

  /**
   * Removes all parameters.
   */
  clear(): void {
    this.model = []
    this.updateValue()
    this.requestUpdate()
  }

  /**
   * Removes a parameter from an index.
   */
  removeProperty(index: number): void {
    const items = this.model
    items.splice(index, 1)
    this.updateValue()
    this.requestUpdate()
  }

  /**
   * Toggles enabled state of a parameter.
   */
  toggleProperty(index: number, enabled: boolean): void {
    const items = this.model
    const item = items[index]
    item.enabled = enabled
    this.updateValue()
  }

  /**
   * Updates name/value of a parameter on a specific index.
   */
  updateProperty(index: number, field: 'name' | 'value', value: string): void {
    const item = this.model[index]
    const old = item[field]
    if (old === value) {
      return
    }
    item[field] = value as string
    this.updateValue()
  }

  addParameter(name = '', value = ''): void {
    if (this.readOnly || this.disabled) {
      return
    }
    const prop = Property.String(name, value, true)
    const items = this.model
    items[items.length] = prop.toJSON()
    this.model = items
    this.requestUpdate()
    this.focusLast()
  }

  protected updateValue(): void {
    if (this.readOnly) {
      return
    }
    this.valueInternal = this.modelToValue()
    this.notifyChange()
  }

  protected modelToValue(): string {
    const { model, autoEncode } = this
    if (autoEncode) {
      const encoded = encodeUrlEncoded(model) as IProperty[]
      return formArrayToString(encoded)
    }
    return formArrayToString(this.model)
  }

  /**
   * Called when the `value` property change. It generates the view model
   * for the editor.
   */
  protected setupModel(value: string): void {
    if (!value) {
      this.model = []
      return
    }
    const model = createViewModel(value) as IProperty[]
    if (this.autoEncode) {
      this.modelInternal = decodeUrlEncoded(model) as IProperty[]
    } else {
      this.modelInternal = model
    }
    this.requestUpdate()
  }

  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 {
      // ...
    }
  }

  /**
   * Encodes current parameters in the model, updated the value, and notifies the change
   */
  encodeParameters(): void {
    const encoded = encodeUrlEncoded(this.model) as IProperty[]
    this.modelInternal = encoded
    this.valueInternal = formArrayToString(encoded)
    this.notifyChange()
    this.requestUpdate()
  }

  /**
   * Encodes current parameters in the model, updated the value, and notifies the change
   */
  decodeParameters(): void {
    const decoded = decodeUrlEncoded(this.model) as IProperty[]
    this.modelInternal = decoded
    this.valueInternal = formArrayToString(decoded)
    this.notifyChange()
    this.requestUpdate()
  }

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

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

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

  protected handleNameChange(e: Event): void {
    const input = e.target as Input
    const index = Number(input.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.updateProperty(index, 'name', 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.updateProperty(index, 'value', input.value)
    input.checkValidity()
  }

  override render(): TemplateResult {
    const items = this.model
    if (!Array.isArray(items) || !items.length) {
      return html`${this.renderEmptyMessage()}${this.renderFormInfo()}`
    }
    return this.renderParams(items)
  }

  protected renderFormInfo(): TemplateResult | typeof nothing {
    if (!this.allowFormInfo) {
      return nothing
    }
    return html`
      <p class="form-info">
        Note that the content-type header will be added when the request is sent. We encourage you to use variables to
        insert sensitive data.
      </p>
    `
  }

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

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

  protected renderParam(item: IProperty, 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: IProperty, index: number): TemplateResult {
    return html`
      <ui-switch
        .checked="${!!item.enabled}"
        @change="${this.handleToggle}"
        title="Enable / disable parameter"
        aria-label="Activate to toggle enabled state of this parameter"
        data-index="${index}"
        ?disabled="${this.readOnly}"
      ></ui-switch>
    `
  }

  protected renderDeleteParameter(index: number): TemplateResult {
    return html`
      <ui-icon-button
        title="Removes this parameter"
        aria-label="Deletes this parameter"
        data-index="${index}"
        @click="${this.handleRemove}"
        ?disabled="${this.readOnly}"
      >
        <ui-icon icon="deleteOutline" role="presentation"></ui-icon>
      </ui-icon-button>
    `
  }

  protected renderNameField(item: IProperty, index: number): TemplateResult {
    const { autoEncode } = this
    const pattern = !autoEncode ? '\\S*' : undefined
    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 as string)}"
          data-index="${index}"
          ?readOnly="${this.readOnly}"
          pattern="${ifDefined(pattern)}"
          aria-label="Parameter name value"
          @change="${this.handleNameChange}"
        />
      </div>
    `
  }

  protected renderValueField(item: IProperty, index: number): TemplateResult {
    const { autoEncode } = this
    const pattern = !autoEncode ? '\\S*' : undefined
    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}"
          pattern="${ifDefined(pattern)}"
          aria-label="Parameter value"
          @change="${this.handleValueChange}"
        />
      </div>
    `
  }
}
