import { html, nothing, PropertyValues, TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js'
import type { IApiParameter, IApiParametrizedSecurityScheme } from '@api-client/core/amf/definitions/Api.js'
import {
  type IRequestAuthorization,
  Kind as RequestAuthorizationKind,
} from '@api-client/core/models/RequestAuthorization.js'
import { Authorization } from '../../authorization/ui/Authorization.js'
import { AmfParameters } from '../../../amf/lib/AmfParameters.js'
import * as InputCache from '../../../amf/lib/InputCache.js'
import { OperationParameter } from '../../../amf/types.js'
import type { IRamlCustomAuthorization } from '@api-client/core/models/Authorization.js'
import type { IApiNodeShape, IApiPropertyShape, IShapeUnion } from '@api-client/core/amf/definitions/Shapes.js'

export default class AmfRamlCustom extends Authorization {
  /**
   * The in-memory key where the values are stored.
   */
  @property({ type: Object }) accessor security: IApiParametrizedSecurityScheme | undefined

  protected amfParameters: AmfParameters

  @state() accessor schemeName: string | undefined

  constructor() {
    super()
    this.amfParameters = new AmfParameters(this)
    this.amfParameters.globalCache = true
    this.amfParameters.addEventListener('render', () => this.requestUpdate())
    this.amfParameters.addEventListener('change', () => this.notifyChange())
    this.amfParameters.addEventListener('paramchange', () => this.handleParamChange())
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('security')) {
      this.restoreValues()
    }
    super.willUpdate(cp)
  }

  protected handleParamChange(): void {
    const auth = this.ensureConfig()
    auth.config = this.serialize()
  }

  protected ensureConfig(): IRequestAuthorization {
    if (!this.authorization) {
      this.authorization = {
        enabled: true,
        kind: RequestAuthorizationKind,
        type: 'raml custom',
        valid: false,
        config: {} as IRamlCustomAuthorization,
      }
    }
    return this.authorization
  }

  protected restoreValues(): void {
    const { security } = this
    const auth = this.ensureConfig()
    const source = 'settings'
    const list = this.amfParameters.parametersValue
    this.amfParameters.parametersValue = list.filter((item) => item.source !== source)
    if (!security || !security.types.includes(AmfNamespace.aml.vocabularies.security.ParametrizedSecurityScheme)) {
      return
    }
    const { scheme } = security
    if (!scheme) {
      return
    }
    const { type } = scheme
    if (!type || !type.startsWith('x-')) {
      return
    }
    const params = this.amfParameters.parametersValue
    const { headers, queryParameters, queryString } = scheme
    if (Array.isArray(headers)) {
      headers.forEach((p) => {
        const param = { ...p, required: true }
        params.push({
          binding: param.binding || '',
          paramId: param.id,
          parameter: param,
          source,
          schemaId: param.schema && param.schema.id,
          schema: param.schema,
        })
      })
    }
    let addedParameters = false
    if (Array.isArray(queryParameters)) {
      queryParameters.forEach((p) => {
        addedParameters = true
        const param = { ...p, required: true }
        params.push({
          binding: param.binding || '',
          paramId: param.id,
          parameter: param,
          source,
          schemaId: param.schema && param.schema.id,
          schema: param.schema,
        })
      })
    }
    if (!addedParameters && queryString) {
      const shape = queryString as IApiNodeShape
      const { properties } = shape
      const binding = 'query'
      if (!properties) {
        params.push(this.createParameterFromSchema(shape, binding, source))
      } else {
        properties.forEach((prop) => {
          params.push(this.createParameterFromProperty(prop, binding, source))
        })
      }
    }
    this.schemeName = security.name || scheme.name

    this.notifyChange()
    this.requestUpdate()
    auth.config = this.serialize()
  }

  createParameterFromSchema(shape: IShapeUnion, binding: string, source: string): OperationParameter {
    const { id, name } = shape
    const constructed: IApiParameter = {
      id,
      binding,
      schema: shape,
      name,
      examples: [],
      payloads: [],
      types: [AmfNamespace.aml.vocabularies.apiContract.Parameter],
      required: false,
      customDomainProperties: [],
    }
    return {
      binding,
      paramId: id,
      parameter: constructed,
      source,
      schemaId: id,
      schema: shape,
    }
  }

  createParameterFromProperty(prop: IApiPropertyShape, binding: string, source: string): OperationParameter {
    const { id, range, name, minCount = 0 } = prop
    const constructed: IApiParameter = {
      id,
      binding,
      schema: range,
      name,
      examples: [],
      payloads: [],
      types: [AmfNamespace.aml.vocabularies.apiContract.Parameter],
      required: minCount > 0,
      customDomainProperties: [],
    }
    return {
      binding,
      paramId: id,
      parameter: constructed,
      source,
      schemaId: prop.id,
      schema: prop as IShapeUnion,
    }
  }

  /**
   * Updates, if applicable, query parameter value.
   *
   * This does nothing if the query parameter has not been defined for the current
   * scheme.
   *
   * @param name The name of the changed parameter
   * @param newValue A value to apply. May be empty but must be defined.
   */
  updateQueryParameter(name: string, newValue: string): void {
    const list = this.amfParameters.parametersValue
    const param = list.find((i) => i.binding === 'query' && i.parameter.name === name)
    if (param) {
      InputCache.set(this, param.paramId, newValue, true)
      this.requestUpdate()
    }
  }

  /**
   * Updates, if applicable, header value.
   * This is supported for RAML custom scheme and Pass Through
   * that operates on headers model which is only an internal model.
   *
   * This does nothing if the header has not been defined for current
   * scheme.
   *
   * @param name The name of the changed header
   * @param newValue A value to apply. May be empty but must be defined.
   */
  updateHeader(name: string, newValue: string): void {
    const list = this.amfParameters.parametersValue
    const param = list.find((i) => i.binding === 'header' && i.parameter.name === name)
    if (param) {
      InputCache.set(this, param.paramId, newValue, true)
      this.requestUpdate()
    }
  }

  serialize(): IRamlCustomAuthorization {
    const result: IRamlCustomAuthorization = {}
    const params = this.amfParameters.parametersValue
    params.forEach((param) => {
      if (!result[param.binding as keyof IRamlCustomAuthorization]) {
        result[param.binding as keyof IRamlCustomAuthorization] = {}
      }
      let value = InputCache.get(this, param.paramId, true) as string | boolean
      if (value === '' || value === undefined) {
        if (param.parameter.required === false) {
          return
        }
        value = ''
      }
      if (value === false && param.parameter.required === false) {
        return
      }
      if (value === null) {
        value = ''
      }
      ;(result[param.binding as keyof IRamlCustomAuthorization] as Record<string, string>)[param.parameter.name || ''] =
        value as string
    })
    return result
  }

  override checkValidity(): boolean {
    const { nilValues, parametersValue } = this.amfParameters
    return !parametersValue.some((param) => {
      if (nilValues.includes(param.paramId)) {
        return false
      }
      const value = InputCache.get(this, param.paramId, true)
      if (!value && !param.parameter.required) {
        return false
      }
      return !value
    })
  }

  override render(): TemplateResult {
    return html` ${this.renderHeader()} ${this.renderEditor()} `
  }

  protected renderEditor(): TemplateResult | typeof nothing {
    return html`
      <form autocomplete="on" class="auth-form custom-auth">${this.headersTemplate()} ${this.queryTemplate()}</form>
      ${this.renderAuthInfo()}
    `
  }

  headersTemplate(): TemplateResult | typeof nothing {
    const headers = this.amfParameters.parametersValue.filter((item) => item.binding === 'header')
    if (!headers.length) {
      return nothing
    }
    return html`
      <div class="section-title"><span class="label">Headers</span></div>
      ${headers.map((param) => this.amfParameters.parameterTemplate(param))}
    `
  }

  queryTemplate(): TemplateResult | typeof nothing {
    const params = this.amfParameters.parametersValue.filter((item) => item.binding === 'query')
    if (!params.length) {
      return nothing
    }
    return html`
      <div class="section-title"><span class="label">Query parameters</span></div>
      ${params.map((param) => this.amfParameters.parameterTemplate(param))}
    `
  }

  protected override renderHeader(): TemplateResult | typeof nothing {
    const { schemeName = 'Pass Through' } = this
    return html`
      <div class="auth-header">
        <span class="auth-title">Scheme: ${schemeName}</span>
      </div>
    `
  }
}
