import type { IHttpAction, IHttpFlow } from '@api-client/core/models/http-flows/HttpFlows.js'
import { type IHttpStep } from '@api-client/core/models/http-flows/HttpStep.js'
import { html, nothing, PropertyValues, TemplateResult } from 'lit'
import { property, query } from 'lit/decorators.js'
import type { UiDropdownListSelection } from '../../md/dropdown-list/internals/UiDropdownList.js'
import { SnackNotifications } from '../../md/notification/SnackNotifications.js'
import type Input from '../../md/input/Input.js'
import type UiDialog from '../../md/dialog/internals/Dialog.js'
import type { UiDialogClosingReason } from '../../md/dialog/internals/Dialog.js'
import { HttpFlowsUi, StepsTarget } from './HttpFlowsUi.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/button/ui-button.js'
import '../../md/divider/ui-divider.js'
import '../../md/dialog/ui-dialog.js'
import '../dialog/rename-dialog.js'

type FlowTrigger = 'request' | 'response'

export default class HttpFlows extends HttpFlowsUi {
  /**
   * The list of HTTP flows.
   * This is a live list. All changes are propagated in the model.
   */
  @property({ type: Array }) accessor model: IHttpFlow[] = []

  /**
   * The setting that renders flows for the given trigger (request or response).
   */
  @property({ type: String }) accessor trigger: FlowTrigger = 'response'

  /**
   * The index of the currently selected flow. This is the index on the `model` property.
   * @attribute
   */
  @property({ type: Number }) accessor selectedFlow: number | undefined

  @query('#createFlowDialog') accessor flowAddDialog: UiDialog | null = null

  /**
   * The currently selected flow definition.
   *
   * If there's no selection it returns the first flow.
   */
  get currentFlow(): IHttpFlow | undefined {
    const { model = [], selectedFlow, trigger = 'response' } = this
    if (selectedFlow === undefined) {
      return undefined
    }
    const item = model[selectedFlow]
    if (!item || item.trigger !== trigger) {
      return undefined
    }
    return item
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.preSelectFlow()
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('trigger')) {
      this.selectedFlow = undefined
      this.preSelectFlow()
    }
    super.willUpdate(cp)
  }

  preSelectFlow(): void {
    const { model = [], trigger = 'response', selectedFlow } = this
    if (Number.isInteger(selectedFlow)) {
      return
    }
    const index = model.findIndex((i) => i.trigger === trigger)
    if (index >= 0) {
      this.selectedFlow = index
    }
  }

  addFlow(description?: string): void {
    const flow: IHttpFlow = {
      trigger: this.trigger,
      actions: [],
    }
    if (description) {
      flow.description = description
    }
    const len = this.model.push(flow)
    this.selectedFlow = len - 1
    this.notifyChange()
    this.requestUpdate()
  }

  removeFlow(index: number): void {
    const { model } = this
    if (index >= model.length || index < 0) {
      throw RangeError(`Invalid index: ${index}. Expected an index in range from 0 to ${model.length - 1}`)
    }
    const removed = model[index]
    model.splice(index, 1)
    this.requestUpdate()
    this.notifyChange()
    if (!model[index]) {
      this.selectedFlow = 0
    }
    SnackNotifications.notify(`Flow was deleted.`, {
      actionLabel: 'Redo',

      actionCallback: () => {
        model.splice(index, 0, removed)
        this.selectedFlow = index
        this.requestUpdate()
        this.notifyChange()
      },

      cancellable: true,
    })
  }

  removeFlowAction(actionIndex: number): void {
    const { currentFlow } = this
    if (!currentFlow) {
      return
    }
    currentFlow.actions.splice(actionIndex, 1)
    this.notifyChange()
    this.requestUpdate()
  }

  renameFlowAction(actionIndex: number): void {
    const { currentFlow } = this
    if (!currentFlow) {
      return
    }
    const action = currentFlow.actions[actionIndex]
    if (!action) {
      return
    }
    const dialog = document.createElement('rename-dialog')
    dialog.name = action.description
    dialog.modal = true
    document.body.appendChild(dialog)
    dialog.open = true
    dialog.addEventListener('close', (ev: Event) => {
      document.parentElement?.removeChild(dialog)
      const event = ev as CustomEvent<UiDialogClosingReason>
      const { cancelled, value } = event.detail
      if (!cancelled && value) {
        action.description = value as string
        this.notifyChange()
        this.requestUpdate()
      }
    })
  }

  protected handleAdd(): void {
    const { flowAddDialog } = this
    if (!flowAddDialog) {
      return
    }
    flowAddDialog.open = true
  }

  protected handleTriggerSelect(e: CustomEvent<UiDropdownListSelection>): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const { trigger } = target.dataset
    if (!trigger) {
      return
    }
    this.trigger = trigger as FlowTrigger
  }

  protected handleFlowSelect(e: CustomEvent<UiDropdownListSelection>): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const { trigger = 'response', model = [] } = this
    const option = target.dataset.option as string | undefined
    if (option === 'create') {
      this.handleAdd()
    } else if (option === 'delete') {
      const { currentFlow } = this
      if (!currentFlow) {
        return
      }
      const index = model.indexOf(currentFlow)
      if (index >= 0) {
        this.removeFlow(index)
      }
    } else if (option === 'select') {
      const index = Number(target.dataset.index)
      if (Number.isNaN(index)) {
        return
      }
      let current = -1
      for (let i = 0, len = model.length; i < len; i++) {
        const item = model[i]
        if (item.trigger !== trigger) {
          continue
        }
        current += 1
        if (current === index) {
          this.selectedFlow = i
          break
        }
      }
    }
  }

  protected handleFlowCreateDialogResult(e: CustomEvent<UiDialogClosingReason>): void {
    if (e.detail.cancelled) {
      return
    }
    const dialog = e.target as UiDialog
    const input = dialog.querySelector('ui-filled-text-field')
    if (!input) {
      return
    }
    const { value } = input
    input.value = ''
    this.addFlow(value)
  }

  /**
   * On the editor element we only allow "flow blocks" items (conditions and actions).
   * Only one condition can be added so once a condition is set we do not allow to add another condition.
   */
  protected handleEditorDragOver(e: DragEvent): void {
    const { dataTransfer } = e
    if (e.defaultPrevented || !dataTransfer || !this.isValidDropTarget(dataTransfer)) {
      return
    }
    e.preventDefault()
    dataTransfer.dropEffect = 'copy'
    const target = e.currentTarget as HTMLElement
    target.classList.add('dragover')
  }

  protected handleEditorDragLeave(e: DragEvent): void {
    if (e.defaultPrevented || !this.isValidDropTarget(e.dataTransfer)) {
      return
    }
    const target = e.currentTarget as HTMLElement
    target.classList.remove('dragover')
  }

  protected handleEditorDrop(e: DragEvent): void {
    const { dataTransfer } = e
    if (!dataTransfer || e.defaultPrevented || !this.isValidDropTarget(dataTransfer)) {
      return
    }
    e.preventDefault()
    this.resetDragIndices()
    const group = dataTransfer.getData('flow/group')
    if (group !== 'flow-block') {
      SnackNotifications.notify('Only blocks from the "Flow blocks" group can be dropped here.', 2000)
      return
    }
    const kind = dataTransfer.getData('flow/kind')
    if (kind === 'Core#Condition') {
      this.setCondition()
    } else if (kind === 'Core#Action') {
      this.addAction()
    }
  }

  /**
   * Adds a condition to the current flow.
   * Note, when the condition already exists this does nothing.
   */
  setCondition(): void {
    const { currentFlow } = this
    if (!currentFlow) {
      SnackNotifications.notify('Select a flow first.', 2000)
      return
    }
    if (currentFlow.condition) {
      SnackNotifications.notify('This flow already have a condition.', 2000)
      return
    }
    currentFlow.condition = {
      steps: [],
    }
    this.requestUpdate()
    this.notifyChange()
  }

  addAction(): void {
    const { currentFlow } = this
    if (!currentFlow) {
      SnackNotifications.notify('Select a flow first.', 2000)
      return
    }
    if (!Array.isArray(currentFlow.actions)) {
      currentFlow.actions = []
    }
    currentFlow.actions.push({
      steps: [],
    })
    this.requestUpdate()
    this.notifyChange()
  }

  protected stepFromChangeEvent(e: Event): IHttpStep | undefined {
    const input = e.target as Input
    input.checkValidity()
    const { actionIndex: aci, assertionIndex: asi } = input.dataset
    const actionIndex = Number(aci || asi)
    const action = this.getStepsTarget(actionIndex)
    if (!action) {
      return undefined
    }
    const stepIndex = Number(input.dataset.stepIndex)
    if (Number.isNaN(stepIndex)) {
      return undefined
    }
    return action.steps[stepIndex]
  }

  protected getStepsTarget(assertionOrActionIndex: number): StepsTarget | undefined {
    if (Number.isNaN(assertionOrActionIndex)) {
      return undefined
    }
    const { currentFlow } = this
    if (!currentFlow) {
      return undefined
    }
    if (assertionOrActionIndex === -1) {
      return currentFlow.condition
    }
    return currentFlow.actions[assertionOrActionIndex]
  }

  protected handleConditionOption(e: CustomEvent<UiDropdownListSelection>): void {
    const { currentFlow } = this
    if (!currentFlow) {
      return
    }
    const { item } = e.detail
    const { option } = item.dataset
    if (option === 'delete') {
      delete currentFlow.condition
      this.notifyChange()
      this.requestUpdate()
    }
  }

  protected handleActionOption(e: CustomEvent<UiDropdownListSelection>): void {
    const list = e.target as HTMLElement
    const actionIndex = Number(list.dataset.actionIndex)
    if (Number.isNaN(actionIndex)) {
      return
    }
    const { item } = e.detail
    const { option } = item.dataset
    switch (option) {
      case 'delete':
        this.removeFlowAction(actionIndex)
        break
      case 'rename':
        this.renameFlowAction(actionIndex)
        break
      default:
    }
  }

  protected isConditionTarget(e: Event): boolean {
    const path = e.composedPath()
    return path.some((i) => {
      const element = i as Element
      return !!element.classList && element.classList.contains('condition-row')
    })
  }

  /**
   * We override this method tpo handle conditions which can only accept certain
   * blocks and have index of `-1`
   */
  protected override handleTargetDragOver(e: DragEvent): void {
    if (e.defaultPrevented) {
      return
    }
    const dt = e.dataTransfer as DataTransfer
    if (!this.isConditionTarget(e)) {
      if (dt.types.includes('flow/assertion')) {
        // we do not allow assertions in actions.
        e.preventDefault()
        return
      }
      super.handleTargetDragOver(e)
      return
    }
    if (dt.types.includes('flow/action')) {
      e.preventDefault()
      dt.dropEffect = 'none'
      return
    }
    super.handleTargetDragOver(e)
  }

  /**
   * Overrides the method to support conditions.
   */
  protected override handleDrop(e: DragEvent): void {
    if (e.defaultPrevented) {
      return
    }
    const dt = e.dataTransfer as DataTransfer
    if (!this.isConditionTarget(e)) {
      if (dt.types.includes('flow/assertion')) {
        // we do not allow assertions in actions.
        e.preventDefault()
        SnackNotifications.notify(`Assertions cannot be a part of a flow action.`, 3000)
        return
      }
      super.handleDrop(e)
      return
    }
    if (dt.types.includes('flow/action')) {
      e.preventDefault()
      SnackNotifications.notify(`Flow actions cannot be a part of a condition.`, 3000)
      return
    }
    super.handleDrop(e)
  }

  protected override render(): TemplateResult {
    return html`
      ${this.renderActionButtons()}
      <div class="container">
        ${this.renderEditor()}
        <ui-divider vertical></ui-divider>
        ${this.renderBuildingBlocks()}
      </div>
      ${this.renderCreateFlowDialog()}
    `
  }

  protected renderActionButtons(): TemplateResult {
    return html` <div class="editor-actions">${this.renderTriggerSelector()} ${this.renderFlowSelector()}</div> `
  }

  protected renderTriggerSelector(): TemplateResult {
    const { trigger = 'response' } = this
    const label = trigger === 'request' ? 'Pre-request' : 'Post-request'
    return html`
      <ui-dropdown-list @select="${this.handleTriggerSelect}" horizontalAlign="right" closeOnOutsideClick>
        <ui-button color="outlined" class="new-button">
          <ui-icon icon="arrowDropDown" slot="icon"></ui-icon>
          ${label}
        </ui-button>
        <ui-list slot="dropdown" role="menu">
          <ui-list-item data-trigger="request" role="menuitem" title="Select pre-request flows"
            >Pre-request</ui-list-item
          >
          <ui-list-item data-trigger="response" role="menuitem" title="Select post-request flows"
            >Post-request</ui-list-item
          >
        </ui-list>
      </ui-dropdown-list>
    `
  }

  protected renderFlowSelector(): TemplateResult {
    const { trigger = 'response', currentFlow, model } = this
    const label = currentFlow ? currentFlow.description || 'Unnamed flow' : 'Select a flow'
    const items = model.filter((i) => i.trigger === trigger)
    const hasItems = !!items.length
    return html`
      <ui-dropdown-list @select="${this.handleFlowSelect}" horizontalAlign="right" closeOnOutsideClick>
        <ui-button color="outlined" class="new-button">
          <ui-icon icon="arrowDropDown" slot="icon"></ui-icon>
          ${label}
        </ui-button>
        <ui-list slot="dropdown" role="menu">
          ${hasItems
            ? html`
                ${items.map(
                  (item, index) =>
                    html`<ui-list-item
                      data-option="select"
                      data-index="${index}"
                      role="menuitem"
                      title="Select this flow"
                      >${item.description || `Flow # ${index}`}</ui-list-item
                    >`
                )}
                <ui-divider></ui-divider>
              `
            : nothing}
          ${currentFlow
            ? html` <ui-list-item image="icon" data-option="delete" role="menuitem" title="Deletes the current flow">
                <ui-icon icon="deleteOutline" slot="start"></ui-icon>
                Delete
              </ui-list-item>`
            : nothing}
          <ui-list-item image="icon" data-option="create" role="menuitem" title="Adds a new flow">
            <ui-icon icon="add" slot="start"></ui-icon>
            Create
          </ui-list-item>
        </ui-list>
      </ui-dropdown-list>
    `
  }

  protected renderCreateFlowDialog(): TemplateResult {
    return html`
      <ui-dialog
        modal
        id="createFlowDialog"
        class="add-dialog"
        @close="${this.handleFlowCreateDialogResult}"
        dismissLabel="Cancel"
        confirmLabel="Add"
      >
        <span slot="title">New Flow</span>

        <ui-filled-text-field
          class="input"
          label="Flow name"
          name="name"
          required
          invalidText="Flow name is required"
        ></ui-filled-text-field>
      </ui-dialog>
    `
  }

  protected renderEditor(): TemplateResult {
    const { currentFlow, model = [], trigger } = this
    const content: TemplateResult[] = []
    if (!currentFlow && !model.filter((i) => i.trigger === trigger).length) {
      content.push(this.renderEmptyFlowMessage())
    } else if (!currentFlow) {
      content.push(this.renderSelectFlowMessage())
    } else {
      const { condition, actions } = currentFlow
      if (!condition && (!actions || !actions.length)) {
        content.push(this.renderEmptyActionsMessage())
      } else {
        if (condition) {
          content.push(this.renderCondition(condition))
        }
        if (actions && actions.length) {
          content.push(this.renderList(currentFlow.actions))
        }
      }
    }
    return html`
      <section
        aria-label="Flows editor"
        class="editor surface1"
        @dragover="${this.handleEditorDragOver}"
        @dragleave="${this.handleEditorDragLeave}"
        @drop="${this.handleEditorDrop}"
      >
        ${content}
      </section>
    `
  }

  protected renderEmptyFlowMessage(): TemplateResult {
    return html`
      <div class="empty-message">Create a flow to get started.</div>
      <div class="empty-message">
        <ui-button color="outlined" @click="${this.handleAdd}" class="empty-button">Create</ui-button>
      </div>
    `
  }

  protected renderSelectFlowMessage(): TemplateResult {
    return html` <div class="empty-message">Select a flow to get started.</div> `
  }

  protected renderEmptyActionsMessage(): TemplateResult {
    return html` <div class="empty-message">Drop a condition or an action here</div> `
  }

  protected renderCondition(condition: IHttpAction): TemplateResult {
    return html`
      <div class="step-row condition">
        <div class="action-tab">
          <span class="body-small">Condition</span>
          ${this.renderConditionTabActions()}
        </div>
        <div class="param-row condition-row">${this.renderSteps(condition, -1)}</div>
      </div>
    `
  }

  protected renderList(model: IHttpAction[]): TemplateResult {
    return html` ${model.map((item, index) => this.renderAction(item, index))} `
  }

  protected renderAction(item: IHttpAction, index: number): TemplateResult {
    const { description } = item
    const label = description || `Action ${index + 1}`
    return html`
      <div class="step-row action">
        <div class="action-tab">
          <span class="body-small">${label}</span>
          ${this.renderStepTabActions(index)}
        </div>
        <div class="param-row action-row">${this.renderSteps(item, index)}</div>
      </div>
    `
  }

  protected renderBuildingBlocks(): TemplateResult {
    const { currentFlow } = this
    return html`
      <section aria-label="Flow building block" class="options">
        ${this.renderFlowBlocksOptions(currentFlow)} ${this.renderDataSourceOptions(currentFlow)}
        ${this.renderTransformationOptions(currentFlow)} ${this.renderActions(currentFlow)}
        ${this.renderAssertionOptions(currentFlow)}
      </section>
    `
  }

  protected renderFlowBlocksOptions(currentFlow?: IHttpFlow): TemplateResult {
    const allDisabled = !currentFlow
    const conditionDisabled = allDisabled || (!!currentFlow && !!currentFlow.condition)
    return html`
      <details class="action-group" open>
        <summary>Flow Blocks</summary>
        <div class="group-content">
          ${this.renderBlockChip('Condition', 'flow-block', 'Core#Condition', {
            disabled: conditionDisabled,
            title: 'Adds a condition that is checked before the flow is executed. Only one condition per flow.',
          })}
          ${this.renderBlockChip('Action', 'flow-block', 'Core#Action', {
            disabled: allDisabled,
            title: 'Adds an action to this flow.',
          })}
        </div>
      </details>
    `
  }

  protected renderDataSourceOptions(currentFlow?: IHttpFlow): TemplateResult {
    let allDisabled = !currentFlow
    if (currentFlow) {
      const { condition, actions = [] } = currentFlow
      if (!condition && !actions.length) {
        allDisabled = true
      }
    }
    const { trigger = 'response' } = this
    if (trigger === 'request') {
      return this.renderRequestDataSourceSuggestions(allDisabled)
    }
    return this.renderResponseDataSourceSuggestions(allDisabled)
  }

  protected renderTransformationOptions(currentFlow?: IHttpFlow): TemplateResult {
    let allDisabled = !currentFlow
    if (currentFlow) {
      const { condition, actions = [] } = currentFlow
      if (!condition && !actions.length) {
        allDisabled = true
      }
    }
    return this.renderTransformationSuggestions(allDisabled)
  }

  protected renderActions(currentFlow?: IHttpFlow): TemplateResult {
    let allDisabled = !currentFlow
    if (currentFlow) {
      const { condition, actions = [] } = currentFlow
      if (!condition && !actions.length) {
        allDisabled = true
      }
    }
    return this.renderActionSuggestions(allDisabled)
  }

  protected renderAssertionOptions(currentFlow?: IHttpFlow): TemplateResult {
    let allDisabled = !currentFlow
    if (currentFlow) {
      const { condition, actions = [] } = currentFlow
      if (!condition && !actions.length) {
        allDisabled = true
      }
    }
    return this.renderAssertionSuggestions(allDisabled)
  }

  protected renderConditionTabActions(): TemplateResult {
    return html`
      <ui-dropdown-list @select="${this.handleConditionOption}" closeOnOutsideClick>
        <ui-icon-button class="condition-options-trigger">
          <ui-icon icon="moreVert"></ui-icon>
        </ui-icon-button>
        <ui-list slot="dropdown" role="menu">
          <ui-list-item data-option="delete" role="menuitem">Remove</ui-list-item>
        </ui-list>
      </ui-dropdown-list>
    `
  }

  protected renderStepTabActions(actionIndex: number): TemplateResult {
    return html`
      <ui-dropdown-list @select="${this.handleActionOption}" closeOnOutsideClick data-action-index="${actionIndex}">
        <ui-icon-button class="action-options-trigger">
          <ui-icon icon="moreVert"></ui-icon>
        </ui-icon-button>
        <ui-list slot="dropdown" role="menu">
          <ui-list-item data-option="rename" role="menuitem">Rename</ui-list-item>
          <ui-divider></ui-divider>
          <ui-list-item data-option="delete" role="menuitem">Remove</ui-list-item>
        </ui-list>
      </ui-dropdown-list>
    `
  }
}
