import { html, nothing, PropertyValues, TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { type HttpProject, Kind as HttpProjectKind } from '@api-client/core/models/HttpProject.js'
import type { IApplication } from '@api-client/core/models/Application.js'
import type { HttpRequest } from '@api-client/core/models/HttpRequest.js'
import type { ProjectRequest } from '@api-client/core/models/ProjectRequest.js'
import type { Environment } from '@api-client/core/models/Environment.js'
import type { IProperty } from '@api-client/core/models/Property.js'
import { HeadersArray } from '@api-client/core/models/HeadersArray.js'
import type { IHttpAssertion } from '@api-client/core/models/http-flows/HttpAssertion.js'
import type { IHttpFlow } from '@api-client/core/models/http-flows/HttpFlows.js'
import { type IRequestConfig, RequestConfig } from '@api-client/core/models/RequestConfig.js'
import type { DeserializedPayload } from '@api-client/core/lib/transformers/PayloadSerializer.js'
import type { AuthorizationType } from '@api-client/core/models/Authorization.js'
import { type IRequestAuthorization, RequestAuthorization } from '@api-client/core/models/RequestAuthorization.js'
import type { IHttpProjectProxyInit, IHttpProjectStoreProxyInit } from '@api-client/core/proxy/HttpProjectProxy.js'
import type { IProjectRunnerOptions } from '@api-client/core/runtime/node/InteropInterfaces.js'
import type { IProjectExecutionLog } from '@api-client/core/models/ProjectExecution.js'
import type { IProxyResult } from '@api-client/core/proxy/Proxy.js'
import type { IRequestLog } from '@api-client/core/models/RequestLog.js'
import { type IHttpHistory, Kind as HttpHistoryKind } from '@api-client/core/models/HttpHistory.js'
import { Events as CoreEvents } from '@api-client/core/events/Events.js'
import type { Operation } from '@api-client/json/types.js'
import RequestEditor from '../http/RequestEditor.js'
// import { Events } from '../../events/Events.js'
import { EventTypes } from '../../events/EventTypes.js'
import { midnightTimestamp } from '../../lib/time/Conversion.js'

import '../../md/icons/ui-icon.js'
import '../../md/button/ui-button.js'
import '../../md/dropdown-list/ui-dropdown-list.js'
import '../../md/list/ui-list.js'
import '../../md/list/ui-list-item.js'

function stopEvent(e: Event): void {
  e.stopPropagation()
}

/**
 * An element that specializes in rendering an HTTP request that is defined on an HttpProject.
 *
 * When using this element set the `project` property to the currently opened
 * project and the `key` to the request to render.
 * The element will hook-up to the events to support project mutations.
 */
export default class HttpProjectRequest extends RequestEditor {
  /**
   * A reference to the global shared variables.
   * Used with the communication with the execution server.
   */
  @property({ type: Object }) accessor sharedVariables: Record<string, string> | undefined

  /**
   * The source project
   */
  @property({ type: Object }) accessor project: HttpProject | undefined

  /**
   * The key of the request being processed.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor key: string | undefined

  /**
   * This property is required for the API access to work.
   * Set it to the current application information.
   */
  @property({ type: Object }) accessor appInfo: IApplication | undefined

  /**
   * The read `expects` value from the project request.
   */
  protected expects?: HttpRequest

  /**
   * The read project request object from the project.
   */
  protected projectRequest?: ProjectRequest

  /**
   * The list of environments that apply to the current request.
   */
  @state() protected accessor environments: Environment[] | undefined

  /**
   * Computed value of the environment to render in the environment selector.
   * Because of the complexity of finding the name, it is computed on `environment` change only.
   */
  @state() protected accessor environmentLabel: string | undefined

  /**
   * When set the requests sent from the editor will contain the entire project serialized with the request
   * rather just the id of the project. This is used in HTTP Client that doesn't use the net-store
   * and uses local proxy.
   * @attribute
   */
  @property({ type: Boolean }) accessor useNetStore: boolean | undefined

  constructor() {
    super()
    // do not propagate HTTP request internal events outside as multiple requests are rendered in the same document.
    this.eventsTarget = this
    this.addEventListener(EventTypes.Http.Request.State.urlChange, stopEvent)
    this.addEventListener(EventTypes.Http.Request.State.contentTypeChange, stopEvent)
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('project') || cp.has('key')) {
      this.setupRequest()
    }
    super.willUpdate(cp)
  }

  applyPatch(info: Operation): void {
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    if (info.path.startsWith('/definitions/environments')) {
      // environments changed.
      this.setupRequestEnvironments(projectRequest)
      return
    }
    // "/definitions/requests/\d+/..."
    const parts = info.path.split('/').slice(4)
    const [root] = parts
    if (root === 'expects' || root === 'log') {
      this.setupRequest()
    }
  }

  protected cleanup(): void {
    this.expects = undefined
    this.environments = undefined
    this.ui = undefined
  }

  protected setupRequest(): void {
    const { project, key } = this
    if (!project || !key) {
      this.cleanup()
      return
    }
    const request = project.findRequest(key)
    if (!request) {
      this.cleanup()
      return
    }
    this.projectRequest = request
    const expects = request.getExpects()
    this.expects = expects
    this.request = expects.toJSON()
    this.authorization = request.authorization?.map((i) => i.toJSON())
    this.assertions = request.assertions
    this.flows = request.flows
    if (request.config) {
      this.config = request.config.toJSON()
    }

    this.setupRequestEnvironments(request)
    this.computeSnippetsRequest()
    this.requestUpdate()
  }

  /**
   * Reads environments to be applies to the request and sets up local variables.
   */
  protected setupRequestEnvironments(request: ProjectRequest): void {
    this.environments = request.readEnvironments()
    this.computeEnvironmentLabel()
    this.computeSnippetsRequest()
  }

  override selectVerb(verb: string): void {
    const { expects } = this
    if (!expects) {
      return
    }
    expects.method = verb
    super.selectVerb(verb)
  }

  override selectUrl(value: string): void {
    const { expects } = this
    if (!expects) {
      return
    }
    expects.url = value
    super.selectUrl(value)
  }

  override selectHeaders(model: IProperty[]): void {
    const { expects } = this
    if (!expects) {
      return
    }
    expects.headersModel = HeadersArray.from(model)
    super.selectHeaders(model)
  }

  override selectAssertions(assertions?: IHttpAssertion[]): void {
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    projectRequest.assertions = assertions
    super.selectAssertions(assertions)
  }

  override selectFlows(flows?: IHttpFlow[]): void {
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    projectRequest.flows = flows
    super.selectFlows(flows)
  }

  override selectRequestConfig(config: IRequestConfig): void {
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    projectRequest.config = new RequestConfig(config)
    super.selectRequestConfig(config)
  }

  protected override async updatePayload(value: DeserializedPayload, mime?: string): Promise<void> {
    const { expects } = this
    if (!expects) {
      return
    }
    await expects.writePayload(value)
    this.setPayloadMimeOption(expects, mime)
    await super.updatePayload(value, mime)
  }

  override addAuthorization(type: AuthorizationType): RequestAuthorization {
    const schema = super.addAuthorization(type)
    if (this.projectRequest) {
      if (!this.projectRequest.authorization) {
        this.projectRequest.authorization = []
      }
      this.projectRequest.authorization.push(schema)
      this.notifyChange()
    }
    return schema
  }

  override updateAuthorization(schema: IRequestAuthorization, isValid: boolean): void {
    super.updateAuthorization(schema, isValid)
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    if (!projectRequest.authorization) {
      // somehow invalid state. These should be in sync...
      return
    }
    const index = projectRequest.authorization.findIndex((i) => i.type === schema.type)
    if (index === -1) {
      return
    }
    projectRequest.authorization[index] = new RequestAuthorization(schema)
    this.notifyChange()
  }

  override removeAuthorization(type: string): void {
    super.removeAuthorization(type)
    const { projectRequest } = this
    if (!projectRequest) {
      return
    }
    if (!projectRequest.authorization) {
      return
    }
    const index = projectRequest.authorization.findIndex((i) => i.type === type)
    if (index === -1) {
      return
    }
    projectRequest.authorization.splice(index, 1)
    this.notifyChange()
  }

  protected override notifySend(): void {
    this.execute()
  }

  protected prepareProxyInit(
    project: HttpProject,
    key: string,
    request: ProjectRequest
  ): IHttpProjectProxyInit | IHttpProjectStoreProxyInit {
    const { sharedVariables } = this
    const options: IProjectRunnerOptions = {
      environment: request.environment,
      request: [key],
      recursive: true,
    }
    if (sharedVariables) {
      options.variables = sharedVariables
    }
    if (this.useNetStore) {
      return {
        kind: HttpProjectKind,
        pid: project.key,
        options,
      } as IHttpProjectStoreProxyInit
    }
    return {
      project: project.toJSON(),
      kind: HttpProjectKind,
      options,
    }
  }

  protected async proxyRequest(
    init: IHttpProjectProxyInit | IHttpProjectStoreProxyInit
  ): Promise<IProxyResult<IProjectExecutionLog> | undefined> {
    let result: IProxyResult<IProjectExecutionLog> | undefined
    try {
      result = await CoreEvents.Transport.Core.httpProject(init, this)
    } catch {
      // ...
    }
    return result
  }

  protected async execute(): Promise<void> {
    const { project, key, projectRequest } = this
    if (!project || !key || !projectRequest) {
      throw new Error(`The project request is not initialized.`)
    }
    const init = this.prepareProxyInit(project, key, projectRequest)
    this.loading = true
    const result = await this.proxyRequest(init)
    if (result && result.result) {
      const [iteration] = result.result.iterations
      if (iteration) {
        const { executed, variables } = iteration
        if (variables) {
          this.mergeExecutedVariables(variables)
        }
        const [report] = executed
        if (report) {
          this.addHistory(report)
          this.requestUpdate()
        }
      }
    }
    this.loading = false
  }

  mergeExecutedVariables(vars: Record<string, string>): void {
    const { sharedVariables } = this
    if (!sharedVariables) {
      return
    }
    Object.keys(vars).forEach((key) => {
      sharedVariables[key] = vars[key]
    })
  }

  protected async addHistory(report: IRequestLog): Promise<void> {
    const { appInfo, project, key } = this
    if (!appInfo) {
      throw new Error(`The appInfo is not set on ${this.localName}`)
    }
    if (!project || !key) {
      return
    }
    const item: IHttpHistory = {
      created: Date.now(),
      kind: HttpHistoryKind,
      log: report,
      project: project.key,
      app: appInfo.code,
      request: key,
      user: '', // added by the bindings
      midnight: midnightTimestamp(),
      key: '',
      organization: '', // will be filled by the bindings.
    }
    // Events.Store.History.create(item)
    throw new Error(`Not yet implemented: ${item}`)
  }

  protected handleEnvironmentSelection(e: CustomEvent): void {
    const { projectRequest } = this
    const target = e.detail.item as HTMLElement | undefined
    if (!target || !projectRequest) {
      return
    }
    const { key } = target.dataset
    if (projectRequest.environment === key) {
      return
    }
    projectRequest.environment = key
    this.computeEnvironmentLabel()
    projectRequest.environment = key
    this.computeSnippetsRequest()
    this.notifyChange()
    this.notifyPropertyChanged('environment', key)
  }

  protected computeEnvironmentLabel(): void {
    const { projectRequest } = this
    if (!projectRequest) {
      this.environmentLabel = undefined
      return
    }
    const url = projectRequest.readUrl({ baseUrl: true, noVariables: true })
    this.environmentLabel = url || 'No server defined'
  }

  protected override async computeSnippetsRequest(): Promise<void> {
    await super.computeSnippetsRequest()
    if (this.snippets) {
      const url = this.projectRequest?.readUrl()
      if (url) {
        this.snippets.url = url
      }
    }
  }

  protected override renderUrlMeta(): TemplateResult {
    return html` ${this.renderHttpMethodSelector()} ${this.renderEnvironmentSelector()} ${this.renderUrlEditor()} `
  }

  protected renderEnvironmentSelector(): TemplateResult | typeof nothing {
    const { environmentLabel: currentEnvironmentLabel, environments } = this
    if (!environments?.length) {
      return nothing
    }
    return html`
      <ui-dropdown-list
        verticalAlign="top"
        @select="${this.handleEnvironmentSelection}"
        class="environment-dropdown"
        closeOnOutsideClick
      >
        <ui-button color="text" class="button">
          ${currentEnvironmentLabel || 'No environment'}
          <ui-icon icon="arrowDropDown"></ui-icon>
        </ui-button>
        <ui-list slot="dropdown" role="menu">
          <ui-list-item
            role="menuitem"
            title="Selects the environment in the request's folder or inherited from a parent"
            >Inherited environment</ui-list-item
          >
          ${this.renderEnvironmentOptions(environments)}
        </ui-list>
      </ui-dropdown-list>
    `
  }

  protected renderEnvironmentOptions(environments: Environment[]): TemplateResult[] {
    return environments.map((env) => this.renderEnvironmentItem(env))
  }

  protected renderEnvironmentItem(env: Environment): TemplateResult {
    const srv = env.getServer()
    const srvUri = srv?.readUri()
    const label = srvUri || 'Server not defined'
    // note, we don't disable items without server because it may still apply variables
    // from it.
    return html`
      <ui-list-item role="menuitem" data-key="${env.key}" lines="two">
        ${env.info.renderLabel}
        <p slot="supporting-text">${label}</p>
      </ui-list-item>
    `
  }

  protected override renderEmptyAuthorization(): TemplateResult {
    return html`
      <div class="body-medium centered">
        This request will inherit authorization defined in an environment (if any).<br />
        Add an authorization to this request to override.
      </div>
    `
  }
}
