import {
  CUSTOM_CREDENTIALS,
  generateState,
  readUrlValue,
  validateRedirectUri,
} from '@api-client/core/authorization/lib/Utils.js'
import * as KnownGrants from '@api-client/core/authorization/lib/KnownGrants.js'
import type { IOAuth2Authorization, IOauth2GrantType, ITokenInfo } from '@api-client/core/models/Authorization.js'
import type { OAuth2DeliveryMethod } from '@api-client/core/models/Authorization.js'
import { Events as CoreEvents } from '@api-client/core/events/Events.js'
import { html, nothing, TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { live } from 'lit/directives/live.js'
import { CredentialsInfo, Oauth2Credentials } from '../types.js'
import { Authorization } from './Authorization.js'
import Input from '../../../md/input/Input.js'
import { SupportedInputTypes } from '../../../types/input.js'
import CheckboxElement from '../../../md/checkbox/internals/CheckboxElement.js'
import { selectNode } from '../Utils.js'
import { SnackNotifications } from '../../../md/notification/SnackNotifications.js'
import '../../../md/button/ui-button.js'
import '../../../md/text-field/ui-filled-text-field.js'
import '../../../md/list/ui-list.js'
import '../../../md/list/ui-list-item.js'
import '../../../md/dropdown-list/ui-dropdown-list.js'
import '../../../md/icon-button/ui-icon-button.js'
import '../../../md/checkbox/ui-checkbox.js'
import '../../../md/icons/ui-icon.js'

/**
 * List of OAuth 2.0 default response types.
 * This list can be extended by custom grants
 *
 * @returns List of objects with `type` and `label` properties.
 */
export const oauth2GrantTypes: IOauth2GrantType[] = [
  {
    type: KnownGrants.implicit,
    label: 'Implicit',
  },
  {
    type: KnownGrants.code,
    label: 'Authorization code',
  },
  {
    type: KnownGrants.clientCredentials,
    label: 'Client credentials',
  },
  {
    type: KnownGrants.password,
    label: 'Password',
  },
  {
    type: KnownGrants.deviceCode,
    label: 'Device code',
  },
  {
    type: KnownGrants.jwtBearer,
    label: 'JWT Bearer',
  },
]

const LocationLabels: Record<OAuth2DeliveryMethod, string> = {
  body: 'Message body',
  header: 'Authorization header',
  query: 'Query string',
}

export default class OAuth2Authorization extends Authorization {
  get config(): IOAuth2Authorization {
    const { authorization } = this
    if (!authorization) {
      return {}
    }
    if (!authorization.config) {
      authorization.config = {} as IOAuth2Authorization
    }
    return authorization.config as IOAuth2Authorization
  }

  /**
   * @return Computed value, true if the response type is a custom definition.
   */
  get isCustomGrantType(): boolean {
    const { grantType } = this.config
    return (
      !!grantType &&
      ![
        KnownGrants.implicit,
        KnownGrants.code,
        KnownGrants.clientCredentials,
        KnownGrants.password,
        KnownGrants.jwtBearer,
        KnownGrants.deviceCode,
      ].includes(grantType)
    )
  }

  /**
   * @returns true when the client id field is required.
   */
  get clientIdRequired(): boolean {
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return ![KnownGrants.clientCredentials, KnownGrants.password, KnownGrants.deviceCode].includes(grantType)
  }

  /**
   * @returns true when the client id field is rendered.
   */
  get hasClientId(): boolean {
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return ![KnownGrants.jwtBearer].includes(grantType)
  }

  /**
   * @returns true when the client secret field is rendered.
   */
  get hasClientSecret(): boolean {
    const { isCustomGrantType } = this
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    if (isCustomGrantType) {
      return isCustomGrantType
    }
    return [KnownGrants.code, KnownGrants.clientCredentials, KnownGrants.password, KnownGrants.deviceCode].includes(
      grantType
    )
  }

  /**
   * @returns true when the client secret field is required.
   */
  get clientSecretRequired(): boolean {
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return [KnownGrants.code].includes(grantType)
  }

  /**
   * @returns true when the authorization URI field is rendered.
   */
  get authorizationUriRendered(): boolean {
    const { isCustomGrantType } = this
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return isCustomGrantType || [KnownGrants.implicit, KnownGrants.code].includes(grantType)
  }

  /**
   * @returns true when the token URI field is rendered.
   */
  get accessTokenUriRendered(): boolean {
    const { isCustomGrantType } = this
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return (
      isCustomGrantType ||
      [
        KnownGrants.clientCredentials,
        KnownGrants.code,
        KnownGrants.password,
        KnownGrants.jwtBearer,
        KnownGrants.deviceCode,
      ].includes(grantType)
    )
  }

  /**
   * @returns true when the username and password fields are rendered.
   */
  get passwordRendered(): boolean {
    const { isCustomGrantType } = this
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return isCustomGrantType || [KnownGrants.password].includes(grantType)
  }

  /**
   * @returns True when the current `grantType` can support redirect URI.
   */
  get hasRedirectUri(): boolean {
    const { grantType } = this.config
    if (!grantType) {
      return false
    }
    return [KnownGrants.implicit, KnownGrants.code].includes(grantType)
  }

  /**
   * Currently available grant types.
   */
  @state() accessor grantTypes: IOauth2GrantType[] | undefined = oauth2GrantTypes

  /**
   * The definition of client credentials to be rendered for a given grant type.
   * When set on the editor it renders a drop down where the user can choose from predefined
   * credentials (client id & secret).
   *
   */
  @state() accessor credentialsSource: Oauth2Credentials[] | undefined

  /**
   * Selected credential source
   */
  @state() accessor credentialSource: string | undefined

  /**
   * The default redirect URI to use if not define din the config.
   * @attribute
   */
  @property({ type: String }) accessor defaultRedirectUri: string | undefined

  @property({ type: Boolean }) accessor credentialsDisabled = this.disabled

  /**
   * The base URI to use to construct the correct URLs to the authorization endpoints.
   *
   * When the paths are relative then base URI is added to the path.
   * Relative paths must start with '/'.
   *
   * Note, URL processing is happening internally in the component. The produced authorize event
   * will have base URI already applied.
   */
  @property({ type: String }) accessor baseUri: string | undefined

  /**
   * The error message returned by the authorization library.
   * It renders error dialog when an error ocurred.
   * It is automatically cleared when the user request the token again.
   */
  @state() accessor lastErrorMessage: string | undefined

  /**
   * When this property is set then the PKCE option is not rendered for the
   * `authorization_code`. This is mainly meant to be used by the `api-authorization-method`
   * to keep this control disabled and override generated settings when the API spec
   * says that the PKCE is supported.
   * @attribute
   */
  @property({ type: Boolean }) accessor noPkce = false

  /**
   * If set, the response type selector is hidden from the UI.
   * @attribute
   */
  @property({ type: Boolean }) accessor noGrantType = false

  /**
   * @returns The list of client credentials to render in the credentials selector.
   */
  listCredentials(): CredentialsInfo[] {
    const { credentialsSource, config } = this
    const { grantType } = config
    let credentials: CredentialsInfo[] = []
    if (credentialsSource && credentialsSource.length > 0) {
      const grantTypeCredentials = credentialsSource.find((s) => s.grantType === grantType)
      if (grantTypeCredentials) {
        const customCredential = { name: CUSTOM_CREDENTIALS }
        credentials = [customCredential].concat(grantTypeCredentials.credentials)
      }
    }
    return credentials
  }

  /**
   * Sets the client credentials after updating them from the credentials source selector.
   * @param clientId The client id to set on the editor.
   * @param clientSecret The client secret to set on the editor.
   * @param disabled Whether the credentials input is disabled.
   */
  updateCredentials(clientId: string, clientSecret: string, disabled: boolean): void {
    const { config } = this
    config.clientId = clientId
    config.clientSecret = clientSecret
    this.credentialsDisabled = disabled
    this.requestUpdate()
  }

  /**
   * This triggers change in the client id & secret of the editor after selecting
   * a credentials source by the user.
   *
   * @param selectedSource The name of the selected credentials source to select.
   */
  updateClientCredentials(selectedSource: string): void {
    const { credentialsSource } = this
    if (!credentialsSource) {
      return
    }
    if (selectedSource) {
      const credentials = this.listCredentials()
      const credential = credentials.find((c) => c.name === selectedSource)
      if (credential) {
        this.updateCredentials(
          credential.clientId || '',
          credential.clientSecret || '',
          credential.name !== CUSTOM_CREDENTIALS
        )
      }
    } else {
      this.updateCredentials('', '', true)
    }
  }

  /**
   * @returns true when a credentials source is being selected.
   */
  isSourceSelected(): boolean {
    const { credentialSource } = this
    const credentials = this.listCredentials()
    if (credentials.length > 0) {
      if (!credentialSource) {
        return false
      }
    }
    return true
  }

  /**
   * Adds a scope value. If the value has a comma then it is split.
   * @param scope The scope string to add.
   */
  addScope(scope: string): void {
    const { config } = this
    if (!Array.isArray(config.scopes)) {
      config.scopes = []
    }
    const values = scope.split(',').filter((i) => !!i.trim())
    config.scopes = config.scopes.concat(values)
    this.requestUpdate()
    this.notifyChange()
  }

  removeScope(index: number): void {
    const { config } = this
    if (!Array.isArray(config.scopes)) {
      return
    }
    if (config.scopes[index] === undefined) {
      return
    }
    config.scopes.splice(index, 1)
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * Selects a grant type. It has no effect when the type is already selected.
   * @param value The grant type value to use.
   */
  updateGrantType(value: string): void {
    const { config, credentialSource } = this
    const { grantType } = config
    if (config.grantType === value) {
      return
    }
    if (grantType !== value && credentialSource) {
      this.credentialSource = undefined
      this.credentialsDisabled = this.disabled
    }
    config.grantType = value
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * It does nothing when the value is already set.
   * @param value The credentials source to use
   */
  updateCredentialSource(value: string): void {
    if (this.credentialSource === value) {
      return
    }
    this.updateClientCredentials(value)
    this.credentialSource = value
    this.notifyChange()
    this.requestUpdate()
  }

  updateClientId(value?: string): void {
    const { config } = this
    if (config.clientId === value) {
      return
    }
    config.clientId = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateClientSecret(value?: string): void {
    const { config } = this
    if (config.clientSecret === value) {
      return
    }
    config.clientSecret = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateAuthorizationUri(value?: string): void {
    const { config } = this
    if (config.authorizationUri === value) {
      return
    }
    config.authorizationUri = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateAccessTokenUri(value?: string): void {
    const { config } = this
    if (config.accessTokenUri === value) {
      return
    }
    config.accessTokenUri = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateUsername(value?: string): void {
    const { config } = this
    if (config.username === value) {
      return
    }
    config.username = value
    this.requestUpdate()
    this.notifyChange()
  }

  updatePassword(value?: string): void {
    const { config } = this
    if (config.password === value) {
      return
    }
    config.password = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateDeliveryMethod(value: OAuth2DeliveryMethod): void {
    const { config } = this
    if (config.deliveryMethod === value) {
      return
    }
    config.deliveryMethod = value
    this.requestUpdate()
    this.notifyChange()
  }

  updatePkce(enabled: boolean): void {
    const { config } = this
    if (config.pkce !== enabled) {
      config.pkce = enabled
      this.notifyChange()
    }
  }

  updateAssertion(value?: string): void {
    const { config } = this
    if (config.assertion === value) {
      return
    }
    config.assertion = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateDeviceCode(value?: string): void {
    const { config } = this
    if (config.deviceCode === value) {
      return
    }
    config.deviceCode = value
    this.requestUpdate()
    this.notifyChange()
  }

  updateRedirectUri(value: string): void {
    const { defaultRedirectUri = '', config } = this
    const { redirectUri = defaultRedirectUri } = config

    const isValid = validateRedirectUri(value)
    if (!isValid) {
      if (config.redirectUri !== defaultRedirectUri) {
        config.redirectUri = defaultRedirectUri
        this.requestUpdate()
        this.notifyChange()
      }
      return
    }
    if (value === redirectUri) {
      delete config.redirectUri
    } else {
      config.redirectUri = value
    }
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * Serialized input values
   * @returns An object with user input
   */
  serialize(): IOAuth2Authorization {
    const { config, baseUri } = this
    const { grantType, tokenType = 'Bearer' } = config
    const detail: IOAuth2Authorization = {
      grantType,
      tokenType,
      clientId: config.clientId,
      accessToken: config.accessToken || '',
      scopes: config.scopes,
      deliveryMethod: config.deliveryMethod || 'header',
      deliveryName: config.deliveryName || 'authorization',
    }

    switch (grantType) {
      case KnownGrants.implicit:
        // The browser flow.
        detail.authorizationUri = readUrlValue(config.authorizationUri, baseUri)
        detail.redirectUri = readUrlValue(config.redirectUri, baseUri)
        break
      case KnownGrants.code:
        // The server flow.
        detail.authorizationUri = readUrlValue(config.authorizationUri, baseUri)
        detail.clientSecret = config.clientSecret
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        detail.redirectUri = readUrlValue(config.redirectUri, baseUri)
        detail.pkce = config.pkce
        break
      case 'application':
      case KnownGrants.clientCredentials:
        // The server flow.
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        detail.clientSecret = config.clientSecret
        break
      case KnownGrants.password:
        // The server flow.
        detail.username = config.username
        detail.password = config.password
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        detail.clientSecret = config.clientSecret
        break
      case KnownGrants.jwtBearer:
        // https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
        detail.assertion = config.assertion
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        delete detail.clientId
        break
      case KnownGrants.deviceCode:
        // https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
        detail.deviceCode = config.deviceCode
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        detail.clientSecret = config.clientSecret
        break
      default:
        // Custom response type.
        detail.authorizationUri = readUrlValue(config.authorizationUri, baseUri)
        detail.clientSecret = config.clientSecret
        detail.accessTokenUri = readUrlValue(config.accessTokenUri, baseUri)
        detail.redirectUri = readUrlValue(config.redirectUri, baseUri)
        detail.username = config.username
        detail.password = config.password
        break
    }
    return detail
  }

  override checkValidity(): boolean {
    const { config } = this
    const {
      grantType,
      clientId,
      authorizationUri,
      clientSecret,
      accessTokenUri,
      password,
      username,
      assertion,
      deviceCode,
    } = config
    if (!grantType) {
      return false
    }
    switch (grantType) {
      case KnownGrants.implicit:
        return !!clientId && !!authorizationUri
      case KnownGrants.code:
        return !!clientId && !!authorizationUri && !!clientSecret && !!accessTokenUri
      case KnownGrants.clientCredentials:
        return !!accessTokenUri
      case KnownGrants.password:
        return !!accessTokenUri && !!password && !!username
      case KnownGrants.jwtBearer:
        return !!accessTokenUri && !!assertion
      case KnownGrants.deviceCode:
        return !!accessTokenUri && !!deviceCode
      default:
        return !!accessTokenUri
    }
  }

  override async authorize(): Promise<ITokenInfo | null> {
    if (this.lastErrorMessage) {
      this.lastErrorMessage = undefined
    }

    const validationResult = this.checkValidity()
    if (!validationResult) {
      return null
    }

    this.authorizing = true
    const detail = this.serialize()
    const authState = generateState()
    detail.state = authState
    let tokenInfo: ITokenInfo | undefined
    try {
      tokenInfo = await CoreEvents.Authorization.OAuth2.authorize(detail, this)
      this.authorizing = false
      if (!tokenInfo) {
        return null
      }
      if (detail.grantType === KnownGrants.implicit && tokenInfo.state !== authState) {
        return null
      }
      if (tokenInfo.accessToken && tokenInfo.accessToken !== this.config.accessToken) {
        if (tokenInfo.tokenType && tokenInfo.tokenType !== this.config.tokenType) {
          this.config.tokenType = tokenInfo.tokenType
        } else if (!tokenInfo.tokenType && this.config.tokenType !== 'Bearer') {
          this.config.tokenType = 'Bearer'
        }
        this.config.accessToken = tokenInfo.accessToken
        this.requestUpdate()
        this.notifyChange()
      }
    } catch (e) {
      const { message = 'Unknown error' } = e as Error
      this.lastErrorMessage = message
      this.authorizing = false
      throw e
    }
    return tokenInfo
  }

  protected handleGrantTypeSelect(e: CustomEvent): void {
    const target = e.detail && (e.detail.item as HTMLElement | undefined)
    if (!target) {
      return
    }
    const selected = target.dataset.value as string
    if (!selected) {
      return
    }
    this.updateGrantType(selected)
  }

  protected handleCredentialSource(e: CustomEvent): void {
    const target = e.detail && (e.detail.item as HTMLElement | undefined)
    if (!target) {
      return
    }
    const selected = target.dataset.value as string
    if (selected) {
      this.updateCredentialSource(selected)
    }
  }

  protected handleClientId(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateClientId(value)
  }

  protected handleClientSecret(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateClientSecret(value)
  }

  protected handleAuthorizationUri(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateAuthorizationUri(value)
  }

  protected handleAccessTokenUri(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateAccessTokenUri(value)
  }

  protected handleUsername(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateUsername(value)
  }

  protected handlePassword(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updatePassword(value)
  }

  protected handleDeliveryMethod(e: CustomEvent): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const selected = target.dataset.value as OAuth2DeliveryMethod | undefined
    if (selected) {
      this.updateDeliveryMethod(selected)
    }
  }

  protected handlePkceChange(e: Event): void {
    const input = e.target as CheckboxElement
    this.updatePkce(!!input.checked)
  }

  protected handleAssertion(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateAssertion(value)
  }

  protected handleDeviceCode(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateDeviceCode(value)
  }

  /**
   * A handler for `focus` event on a label that contains text and
   * should be copied to clipboard when user is interacting with it.
   */
  protected handleClickCopy(e: Event): void {
    const node = e.target as HTMLElement
    this.copyFromNode(node)
  }

  protected handlerCopyKeydown(e: KeyboardEvent): void {
    if (e.code !== 'Space') {
      return
    }
    const node = e.target as HTMLElement
    this.copyFromNode(node)
  }

  /**
   * Copies the content of the node to clipboard.
   */
  protected async copyFromNode(node: HTMLElement): Promise<void> {
    await navigator.clipboard.writeText(node.innerText)
    selectNode(node)
    SnackNotifications.notify('Copied to clipboard', 2000)
  }

  protected handleRedirectUri(e: Event): void {
    const input = e.target as Input
    const { value } = input
    this.updateRedirectUri(value)
  }

  protected handleScopeAdd(e: Event): void {
    const input = (e.currentTarget as HTMLElement).parentElement as Input
    const { value } = input
    if (!value) {
      return
    }
    this.addScope(value)
    input.value = ''
  }

  protected handleScopeRemove(e: Event): void {
    const target = e.currentTarget as HTMLElement
    const index = Number(target.dataset.index)
    if (Number.isNaN(index)) {
      return
    }
    this.removeScope(index)
  }

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

  protected renderEditor(): TemplateResult | typeof nothing {
    const { authorization } = this
    if (!authorization) {
      return nothing
    }
    return html`
      <form autocomplete="on" class="auth-form oauth2-auth">${this.renderFormContent()}</form>
      ${this.renderAuthAction()} ${this.renderError()} ${this.renderAuthInfo()}
    `
  }

  protected renderAuthAction(): TemplateResult | typeof nothing {
    const { config } = this
    const { accessToken } = config
    return accessToken ? this.renderOAuth2Token() : this.renderOAth2Authorize()
  }

  protected renderError(): TemplateResult | typeof nothing {
    const { lastErrorMessage } = this
    if (!lastErrorMessage) {
      return nothing
    }
    return html`<p class="error-message">⚠ ${lastErrorMessage}</p>`
  }

  /**
   * @returns The template for the OAuth 2 token value
   */
  protected renderOAuth2Token(): TemplateResult | typeof nothing {
    const { config, authorizing } = this
    const { accessToken } = config
    return html` <div class="current-token">
      <label class="token-label label-large">Current token</label>
      <p class="token-value body-small">
        <span @click="${this.handleClickCopy}" @keydown="${this.handlerCopyKeydown}">${accessToken}</span>
      </p>
      <div class="authorize-actions base-level">
        <ui-button
          color="filled"
          ?disabled="${authorizing}"
          class="auth-button"
          data-type="refresh-token"
          @click="${this.authorize}"
          >Refresh access token</ui-button
        >
      </div>
    </div>`
  }

  /**
   * @returns The template for the "authorize" button.
   */
  protected renderOAth2Authorize(): TemplateResult | typeof nothing {
    const { authorizing } = this
    return html` <div class="authorize-actions base-level">
      <ui-button
        color="filled"
        ?disabled="${authorizing}"
        class="auth-button"
        data-type="get-token"
        @click="${this.authorize}"
        >Request access token</ui-button
      >
    </div>`
  }

  /**
   * @returns The template for the <form> content.
   */
  protected renderFormContent(): (TemplateResult | typeof nothing)[] {
    const { baseUri } = this
    const urlType = (baseUri ? 'string' : 'url') as SupportedInputTypes
    const result: (TemplateResult | typeof nothing)[] = [
      this.renderGrantType(),
      this.renderCredentialsSource(),
      this.renderClientId(),
      this.renderClientSecret(),
      this.renderAssertion(),
      this.renderDeviceCode(),
      this.renderAuthorizationUri(urlType),
      this.renderAccessTokenUri(urlType),
      this.renderRedirect(),
      this.renderUsernameTemplate(),
      this.renderPassword(),
      this.renderScopes(),
      this.renderParamsLocation(),
      this.renderCustomProperties(),
    ]
    return result
  }

  /**
   * @return The template for API custom properties (annotations)
   */
  protected renderCustomProperties(): TemplateResult | typeof nothing {
    return nothing
  }

  /**
   * @returns The template for the OAuth 2 response type selector
   */
  protected renderGrantType(): TemplateResult | typeof nothing {
    const { config, readOnly, disabled, noGrantType, isCustomGrantType } = this
    const { grantType } = config
    const items = this.grantTypes || []
    const item = items.find((i) => i.type === grantType)
    const value = item && item.label
    return html`
      <div class="full-input grant-line">
        <ui-dropdown-list verticalAlign="top" @select="${this.handleGrantTypeSelect}" closeOnOutsideClick>
          <ui-filled-text-field
            readOnly
            name="grantType"
            .value="${value || ''}"
            label="Grant type"
            ?required="${!noGrantType && !isCustomGrantType}"
            ?hidden="${noGrantType}"
            ?disabled="${disabled || readOnly}"
          >
            <ui-icon slot="suffix" icon="arrowDropDown" role="presentation"></ui-icon>
          </ui-filled-text-field>
          <ui-list slot="dropdown" role="menu">
            ${items.map(
              (current) =>
                html`<ui-list-item role="menuitem" data-value="${current.type}">${current.label}</ui-list-item>`
            )}
          </ui-list>
        </ui-dropdown-list>

        ${this.renderPkce()}
      </div>
    `
  }

  /**
   * @return The template for the client credentials source.
   */
  protected renderCredentialsSource(): TemplateResult | typeof nothing {
    const { credentialSource } = this
    const credentials = this.listCredentials()
    if (credentials.length === 0) {
      return nothing
    }
    return html` <div class="full-input credential-source-list">
      <ui-dropdown-list verticalAlign="top" @select="${this.handleCredentialSource}" closeOnOutsideClick>
        <ui-filled-text-field readOnly name="credentialSource" .value="${credentialSource || ''}" label="Grant type">
          <ui-icon slot="suffix" icon="arrowDropDown" role="presentation"></ui-icon>
        </ui-filled-text-field>
        <ui-list slot="dropdown" role="menu">
          ${credentials.map(
            (current) => html`<ui-list-item role="menuitem" data-value="${current.name}">${current.name}</ui-list-item>`
          )}
        </ui-list>
      </ui-dropdown-list>
    </div>`
  }

  /**
   * @returns The template for the OAuth 2 client id input.
   */
  protected renderClientId(): TemplateResult | typeof nothing {
    if (!this.hasClientId) {
      return nothing
    }
    const { config, credentialsDisabled, clientIdRequired } = this
    const { clientId } = config
    const sourceSelected = this.isSourceSelected()
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Client id"
          name="clientId"
          type="password"
          ?required="${clientIdRequired}"
          invalidText="Client ID is required for this response type"
          .value="${live(clientId || '')}"
          ?disabled="${sourceSelected ? credentialsDisabled : true}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleClientId}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @returns The template for the OAuth 2 client secret input.
   */
  protected renderClientSecret(): TemplateResult | typeof nothing {
    const { hasClientSecret } = this
    if (!hasClientSecret) {
      return nothing
    }
    const { config, credentialsDisabled, clientSecretRequired } = this
    const { clientSecret } = config
    const sourceSelected = this.isSourceSelected()
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Client secret"
          name="clientSecret"
          type="password"
          ?required="${clientSecretRequired}"
          invalidText="Client secret is required for this response type"
          .value="${live(clientSecret || '')}"
          ?disabled="${sourceSelected ? credentialsDisabled : true}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleClientSecret}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @returns The template for the assertion (JWT) input, when needed.
   */
  protected renderAssertion(): TemplateResult | typeof nothing {
    const { config } = this
    if (config.grantType !== KnownGrants.jwtBearer) {
      return nothing
    }
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Assertion (JWT)"
          name="assertion"
          .value="${live(config.assertion || '')}"
          required
          invalidText="Assertion is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleAssertion}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @return The template for the device code input, when needed.
   */
  protected renderDeviceCode(): TemplateResult | typeof nothing {
    const { config } = this
    if (config.grantType !== KnownGrants.deviceCode) {
      return nothing
    }
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Device code"
          name="deviceCode"
          .value="${live(config.deviceCode || '')}"
          required
          invalidText="Device code is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleDeviceCode}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @param urlType The input type to render
   * @returns The template for the authorization URI input
   */
  protected renderAuthorizationUri(urlType: SupportedInputTypes): TemplateResult | typeof nothing {
    if (!this.authorizationUriRendered) {
      return nothing
    }
    const { config, isCustomGrantType } = this
    const { authorizationUri } = config
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Authorization URI"
          name="authorizationUri"
          .value="${live(authorizationUri || '')}"
          type=${urlType}
          ?required="${!isCustomGrantType}"
          invalidText="Authorization URI is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleAuthorizationUri}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @param urlType The input type to render
   * @returns The template for the access token URI input
   */
  protected renderAccessTokenUri(urlType: SupportedInputTypes): TemplateResult | typeof nothing {
    if (!this.accessTokenUriRendered) {
      return nothing
    }
    const { config, isCustomGrantType } = this
    const { accessTokenUri } = config
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Access token URI"
          name="accessTokenUri"
          .value="${live(accessTokenUri || '')}"
          type=${urlType}
          ?required="${!isCustomGrantType}"
          invalidText="Access token URI is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleAccessTokenUri}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @returns The template for the user name input
   */
  protected renderUsernameTemplate(): TemplateResult | typeof nothing {
    if (!this.passwordRendered) {
      return nothing
    }
    const { config, isCustomGrantType } = this
    const { username } = config
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Username"
          name="username"
          .value="${live(username || '')}"
          ?required="${!isCustomGrantType}"
          invalidText="User name is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleUsername}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * @returns The template for the user password input
   */
  protected renderPassword(): TemplateResult | typeof nothing {
    if (!this.passwordRendered) {
      return nothing
    }
    const { config, isCustomGrantType } = this
    const { password } = config
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Password"
          name="password"
          .value="${live(password || '')}"
          type="password"
          ?required="${!isCustomGrantType}"
          invalidText="Password is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handlePassword}"
        ></ui-filled-text-field>
      </div>
    `
  }

  /**
   * For client_credentials grant this renders the dropdown with an option to select
   * where the credentials should be used. Current values:
   * - authorization header
   * - message body
   */
  protected renderParamsLocation(): TemplateResult | typeof nothing {
    const { config } = this
    const { grantType, deliveryMethod } = config
    if (grantType !== KnownGrants.clientCredentials) {
      return nothing
    }
    const { disabled, readOnly } = this
    return html` <div class="full-input delivery-method-line">
      <ui-dropdown-list verticalAlign="top" @select="${this.handleDeliveryMethod}" closeOnOutsideClick>
        <ui-filled-text-field
          readOnly
          name="deliveryMethod"
          .value="${LocationLabels[deliveryMethod as OAuth2DeliveryMethod] || ''}"
          label="Credentials location"
          ?disabled="${disabled || readOnly}"
          data-selected="${deliveryMethod || ''}"
        >
          <ui-icon slot="suffix" icon="arrowDropDown" role="presentation"></ui-icon>
        </ui-filled-text-field>
        <ui-list slot="dropdown" role="menu">
          <ui-list-item role="menuitem" data-value="header">${LocationLabels.header}</ui-list-item>
          <ui-list-item role="menuitem" data-value="body">${LocationLabels.body}</ui-list-item>
        </ui-list>
      </ui-dropdown-list>
    </div>`
  }

  /**
   * @returns The template for the PKCE option of the OAuth 2 extension.
   */
  protected renderPkce(): TemplateResult | typeof nothing {
    const { config, noPkce } = this
    const { grantType, pkce } = config
    if (noPkce || grantType !== KnownGrants.code) {
      return nothing
    }
    return html`
      <label class="label-large pkce">
        <ui-checkbox
          .checked="${!!pkce}"
          @change="${this.handlePkceChange}"
          name="pkce"
          aria-label="Use PKCE extension"
        ></ui-checkbox>
        Use PKCE extension
      </label>
    `
  }

  /**
   * @returns The template for the OAuth 2 redirect URI label
   */
  protected renderRedirect(): TemplateResult | typeof nothing {
    const { hasRedirectUri } = this
    if (!hasRedirectUri) {
      return nothing
    }
    const { defaultRedirectUri = '', config } = this
    const { redirectUri = defaultRedirectUri } = config
    return html`
      <div class="full-input base-level">
        <ui-filled-text-field
          label="Redirect URI"
          name="redirectUri"
          .value="${live(redirectUri)}"
          type="url"
          required
          invalidText="Redirect URI is required for this response type"
          ?disabled="${this.disabled}"
          ?readOnly="${this.readOnly}"
          @change="${this.handleRedirectUri}"
        ></ui-filled-text-field>
      </div>
    `
  }

  protected renderScopes(): TemplateResult | typeof nothing {
    const { config } = this
    const { scopes = [] } = config

    return html`
      <section aria-label="Scopes">
        <div class="label-large scopes-label">
          Scopes
          <span class="badge large-container">${scopes.length}</span>
        </div>

        <div class="full-input base-level">
          <ui-filled-text-field
            label="Scope value"
            name="scope"
            ?disabled="${this.disabled}"
            ?readOnly="${this.readOnly}"
          >
            <ui-icon-button
              type="outlined"
              slot="suffix"
              aria-label="Add value"
              title="Click this button to accept the value"
              @click="${this.handleScopeAdd}"
            >
              <ui-icon icon="add" role="presentation"></ui-icon>
            </ui-icon-button>
          </ui-filled-text-field>
        </div>
        ${this.renderScopesList(scopes)}
      </section>
    `
  }

  protected renderScopesList(scopes: string[]): TemplateResult | typeof nothing {
    if (!scopes || !scopes.length) {
      return nothing
    }
    return html`
      <ui-list class="scopes-list base-level" role="list">
        ${scopes.map(
          (item, index) => html`
            <ui-list-item role="listitem">
              ${item}
              <ui-icon-button
                slot="end"
                aria-label="Remove scope"
                title="Removes the scope from the list"
                @click="${this.handleScopeRemove}"
                data-index="${index}"
              >
                <ui-icon icon="deleteOutline" role="presentation"></ui-icon>
              </ui-icon-button>
            </ui-list-item>
          `
        )}
      </ui-list>
    `
  }
}
