import { html, TemplateResult } from 'lit'
import { property, queryAll } from 'lit/decorators.js'
import type {
  IApiParametrizedSecurityScheme,
  IApiSecurityHttpSettings,
  IApiSecurityRequirement,
} from '@api-client/core/amf/definitions/Api.js'
import type { IRequestAuthorization } from '@api-client/core/models/RequestAuthorization.js'
import { Oauth2Credentials } from '../authorization/types.js'
import ApiElement from '../ApiElement.js'
import { Authorization } from '../authorization/ui/Authorization.js'
import '../../define/amf/authorization/amf-basic-authorization.js'
import '../../define/amf/authorization/amf-passthrough-authorization.js'
import '../../define/amf/authorization/amf-bearer-authorization.js'
import '../../define/amf/authorization/amf-apikey-authorization.js'
import '../../define/amf/authorization/amf-ramlcustom-authorization.js'
import '../../define/amf/authorization/amf-oauth2-authorization.js'
import '../../define/amf/authorization/amf-oidc-authorization.js'

export interface SecurityMethods {
  types: string[]
  schemes: IApiParametrizedSecurityScheme[]
}

export default class ApiAuthorizationEditor extends ApiElement {
  protected securityInternal: IApiSecurityRequirement | undefined

  protected methodsInternal?: SecurityMethods

  get security(): IApiSecurityRequirement | undefined {
    return this.securityInternal
  }

  set security(value: IApiSecurityRequirement | undefined) {
    const old = this.securityInternal
    if (old === value) {
      return
    }
    this.securityInternal = value
    this.processModel()
    this.requestUpdate()
  }

  get methods(): SecurityMethods | undefined {
    return this.methodsInternal
  }

  /**
   * Whether or not the element is invalid. The validation state changes
   * when settings change or when the `validate()` function is called.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor invalid: boolean | undefined

  /**
   * List of credentials source
   */
  @property({ type: Array }) accessor credentialsSource: Oauth2Credentials[] | undefined

  /**
   * Redirect URL for the OAuth2 authorization.
   * @attribute
   */
  @property({ type: String }) accessor oauth2RedirectUri: string | undefined

  /**
   * When set it overrides the `authorizationUri` in the authorization editor,
   * regardless to the authorization scheme applied to the request.
   * This is to be used with the mocking service.
   * @attribute
   */
  @property({ type: String }) accessor oauth2AuthorizationUri: string | undefined

  /**
   * When set it overrides the `authorizationUri` in the authorization editor,
   * regardless to the authorization scheme applied to the request.
   * This is to be used with the mocking service.
   * @attribute
   */
  @property({ type: String }) accessor oauth2AccessTokenUri: string | undefined

  @queryAll('.auth-editor') accessor editors!: NodeListOf<Authorization>

  /**
   * Reads list of authorization methods from the model.
   */
  protected processModel(): void {
    const security = this.securityInternal
    if (!security) {
      return
    }
    this.methodsInternal = this.computeMethods(security.schemes)
  }

  /**
   * Computes list of security schemes that can be applied to the element.
   *
   * @param schemes A list of security schemes to process.
   * @returns A list of authorization methods that can be applied to
   * the current endpoint. Each object describes the list of security types
   * that can be applied to the editor. In OAS an auth method may be an union
   * of methods.
   */
  protected computeMethods(schemes: IApiParametrizedSecurityScheme[]): SecurityMethods {
    const result: SecurityMethods = {
      types: [],
      schemes: [],
    }
    schemes.forEach((security) => {
      const type = this.listSchemeLabels(security)
      if (!type) {
        return
      }
      result.types.push(type)
      result.schemes.push(security)
    })
    return result
  }

  /**
   * Reads authorization scheme's name and type from the AMF model.
   */
  protected listSchemeLabels(security: IApiParametrizedSecurityScheme): string | undefined {
    const { name, scheme } = security
    if (name === 'null') {
      // RAML allows to define a "null" scheme. This means that the authorization
      // for this endpoint is optional.
      return 'No authorization'
    }
    if (!scheme) {
      return undefined
    }
    let { type } = scheme
    if (type === 'http') {
      // HTTP type can be `basic` or `bearer`.
      const config = scheme.settings as IApiSecurityHttpSettings
      if (!config) {
        // this happens when AMF doesn't properly read graph model back to the store.
        // AMF team promised to fix this...
        type = undefined
      } else {
        type = config.scheme
      }
    }
    return type
  }

  /**
   * A function called each time anything change in the editor.
   * Revalidates the component and dispatches the `change` event.
   */
  protected changeHandler(): void {
    this.validate()
    this.notifyChange()
  }

  /**
   * Validates state of the editor. It sets `invalid` property when called.
   *
   * Exception: OAuth 2 form reports valid even when the `accessToken` is not
   * set. This adjust for this and reports invalid when `accessToken` for OAuth 2
   * is missing.
   *
   * @returns True when the form has valid data.
   */
  validate(): boolean {
    const { editors } = this
    let valid = true
    for (let i = 0, len = editors.length; i < len; i++) {
      const node = editors[i]
      const methodValid = node.checkValidity()
      if (!methodValid) {
        valid = methodValid
        break
      }
      // if (['amf-oauth2-authorization', 'amf-oidc-authorization'].includes(node.localName)) {
      //   const oauth = node as OAuth2Authorization;
      //   const { config } = oauth;
      //   if (!config.accessToken) {
      //     valid = false;
      //     break;
      //   }
      // }
    }
    this.invalid = !valid
    return valid
  }

  /**
   * Creates a list of configuration by calling the `serialize()` function on each
   * currently rendered authorization form.
   *
   * @returns List of authorization settings.
   */
  serialize(): IRequestAuthorization[] {
    const { editors } = this
    return Array.from(editors).map((editor) => {
      const auth = editor.authorization as IRequestAuthorization
      auth.valid = editor.checkValidity()
      return auth
    })
  }

  /**
   * Calls the `authorize()` function on each rendered authorization methods.
   * Currently only `OAuth 1.0` and `OAuth 2.0` actually perform the authorization.
   *
   * Each method is called in order to make sure the user is not overwhelmed with
   * dialogs or other UI elements.
   *
   * The function rejects when at least one authorization method rejects.
   */
  async authorize(): Promise<void> {
    const { editors } = this
    const list = Array.from(editors)
    while (list.length) {
      const auth = list.shift()
      if (!auth) {
        break
      }
      await auth.authorize()
    }
  }

  override render(): TemplateResult {
    const methods = this.methodsInternal
    if (!methods || !methods.schemes.length) {
      return html``
    }
    return html`
      <section class="authorization-union">
        ${methods.schemes.map((scheme, index) => this.methodTemplate(scheme, methods.types[index]))}
      </section>
    `
  }

  protected methodTemplate(scheme: IApiParametrizedSecurityScheme, type: string): TemplateResult | string {
    switch (type) {
      case 'Basic Authentication':
      case 'basic':
        return this.basicAuthTemplate(scheme)
      case 'Digest Authentication':
        return this.digestAuthTemplate()
      case 'Pass Through':
        return this.passThroughAuthTemplate(scheme)
      case 'OAuth 2.0':
        return this.oa2AuthTemplate(scheme)
      case 'OAuth 1.0':
        return this.oa1AuthTemplate()
      case 'bearer':
        return this.bearerAuthTemplate(scheme)
      case 'Api Key':
        return this.apiKeyTemplate(scheme)
      case 'openIdConnect':
        return this.openIdConnectTemplate(scheme)
      default:
        if (String(type).indexOf('x-') === 0) {
          return this.ramlCustomAuthTemplate(scheme)
        }
    }
    return ''
  }

  /**
   * Renders a template for Basic authorization.
   */
  protected basicAuthTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    return html` <amf-basic-authorization
      .security="${security}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-basic-authorization>`
  }

  /**
   * Renders a template for Digest authorization.
   */
  protected digestAuthTemplate(): TemplateResult {
    return html` <p class="no-support digest">The Digest authorization is not longer supported.</p> `
  }

  /**
   * Renders a template for Pass through authorization.
   */
  protected passThroughAuthTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    return html` <amf-passthrough-authorization
      .security="${security}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-passthrough-authorization>`
  }

  /**
   * Renders a template for RAML custom authorization.
   */
  protected ramlCustomAuthTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    return html`<amf-ramlcustom-authorization
      .security="${security}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-ramlcustom-authorization>`
  }

  /**
   * Renders a template for Bearer authorization (OAS).
   */
  protected bearerAuthTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    return html` <amf-bearer-authorization
      .security="${security}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-bearer-authorization>`
  }

  /**
   * Renders a template for OAuth 1 authorization.
   */
  protected oa1AuthTemplate(): TemplateResult {
    return html` <p class="no-support oauth1">The OAuth1 authorization is not longer supported.</p> `
  }

  /**
   * Renders a template for OAuth 2 authorization.
   */
  protected oa2AuthTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    const { oauth2RedirectUri, credentialsSource, oauth2AuthorizationUri, oauth2AccessTokenUri } = this
    return html` <amf-oauth2-authorization
      .defaultRedirectUri="${oauth2RedirectUri}"
      .overrideAuthorizationUri="${oauth2AuthorizationUri}"
      .overrideAccessTokenUri="${oauth2AccessTokenUri}"
      .security="${security}"
      .credentialsSource="${credentialsSource}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-oauth2-authorization>`
  }

  /**
   * Renders a template for OAuth 2 authorization.
   */
  protected openIdConnectTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    const { oauth2RedirectUri, credentialsSource } = this
    return html` <amf-oidc-authorization
      .defaultRedirectUri="${oauth2RedirectUri}"
      .security="${security}"
      .credentialsSource="${credentialsSource}"
      @change="${this.changeHandler}"
      class="auth-editor"
    ></amf-oidc-authorization>`
  }

  /**
   * Renders a template for Api Keys authorization.
   */
  protected apiKeyTemplate(security: IApiParametrizedSecurityScheme): TemplateResult {
    return html`
      <amf-apikey-authorization
        .security="${security}"
        @change="${this.changeHandler}"
        class="auth-editor"
      ></amf-apikey-authorization>
    `
  }
}
