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

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

  protected amfParameters: AmfParameters

  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: 'api key',
        valid: false,
        config: {} as IApiKeyAuthorization,
      }
    }
    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('Api Key')) {
      return
    }
    const config = scheme.settings as IApiSecurityApiKeySettings | undefined
    if (!config) {
      return
    }

    const { in: binding = '', id } = config
    if (!InputCache.has(this, id, true)) {
      InputCache.set(this, id, '', true)
    }

    const params = this.amfParameters.parametersValue
    params.push({
      binding,
      paramId: id,
      parameter: { ...(config as IApiParameter), binding },
      source: 'settings',
      schemaId: scheme.id,
      schema: scheme as unknown as IShapeUnion,
    })

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

  /**
   * 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()
    }
  }

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

  serialize(): IApiKeyAuthorization {
    const result: IApiKeyAuthorization = {}
    const params = this.amfParameters.parametersValue
    params.forEach((param) => {
      if (!result[param.binding as keyof IApiKeyAuthorization]) {
        result[param.binding as keyof IApiKeyAuthorization] = {}
      }
      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 IApiKeyAuthorization] 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 true
      }
      const value = InputCache.get(this, param.paramId, true)
      return !value
    })
  }

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

  protected override renderHeader(): TemplateResult | typeof nothing {
    return html`
      <div class="auth-header">
        <span class="auth-title">Scheme: Api Key</span>
      </div>
    `
  }

  protected renderEditor(): TemplateResult | typeof nothing {
    return html`
      <form autocomplete="on" class="auth-form apikey-auth">
        ${this.headersTemplate()} ${this.queryTemplate()} ${this.cookieTemplate()}
      </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))}
    `
  }

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