/* eslint-disable @typescript-eslint/no-extraneous-class */
import {
  IApiKeyAuthorization,
  IBasicAuthorization,
  IBearerAuthorization,
  ICCAuthorization,
  IOAuth2Authorization,
  IOidcAuthorization,
  IOidcTokenInfo,
  IPassThroughAuthorization,
  IRamlCustomAuthorization,
} from '../../models/Authorization.js'
import { IRequestAuthorization, RequestAuthorization } from '../../models/RequestAuthorization.js'
import { IHttpRequest } from '../../models/HttpRequest.js'
import { IProperty } from '../../models/Property.js'
import { Headers } from '../../lib/headers/Headers.js'
import { HttpCertificate } from '../../models/ClientCertificate.js'
import { UrlProcessor } from '../../lib/parsers/UrlProcessor.js'
import { UrlEncoder } from '../../lib/parsers/UrlEncoder.js'
import { hasBuffer } from '../../Platform.js'
import {
  normalizeType,
  METHOD_BASIC,
  METHOD_BEARER,
  METHOD_OAUTH2,
  METHOD_OIDC,
  METHOD_CUSTOM,
  METHOD_API_KEY,
  METHOD_PASS_THROUGH,
} from './Utils.js'

export interface IAuthApplyOptions {
  /**
   * When set it won't change the originating authorization objects.
   * By default it sets the authorization's `enabled` property to `false` after applying the
   * value to the request.
   */
  immutable?: boolean
}

export class SecurityProcessor {
  /**
   * Applies authorization configuration to the request object.
   */
  static applyAuthorization(
    request: IHttpRequest,
    authorization?: (IRequestAuthorization | RequestAuthorization)[],
    opts: IAuthApplyOptions = {}
  ): void {
    if (!Array.isArray(authorization) || !authorization.length) {
      return
    }

    for (const auth of authorization) {
      if (!auth.enabled || !auth.config) {
        continue
      }

      switch (normalizeType(auth.type)) {
        case METHOD_BASIC:
          SecurityProcessor.applyBasicAuth(request, auth.config as IBasicAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_OAUTH2:
          SecurityProcessor.applyOAuth2(request, auth.config as IOAuth2Authorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_OIDC:
          SecurityProcessor.applyOpenId(request, auth.config as IOidcAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_BEARER:
          SecurityProcessor.applyBearer(request, auth.config as IBearerAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_CUSTOM:
          SecurityProcessor.applyCustomAuth(request, auth.config as IRamlCustomAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_API_KEY:
          SecurityProcessor.applyApiKeys(request, auth.config as IApiKeyAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        case METHOD_PASS_THROUGH:
          SecurityProcessor.applyPassThrough(request, auth.config as IPassThroughAuthorization)
          if (!opts.immutable) {
            auth.enabled = false
          }
          break
        default:
      }
    }
  }

  /**
   * Injects basic auth header into the request headers.
   */
  static applyBasicAuth(request: IHttpRequest, config: IBasicAuthorization): void {
    const { username, password = '' } = config
    if (!username) {
      return
    }
    const hash = `${username}:${password}`
    const value = hasBuffer ? Buffer.from(hash).toString('base64') : btoa(hash)
    this.applyHeader(request, 'authorization', `Basic ${value}`)
  }

  /**
   * Injects oauth 2 auth header into the request headers.
   */
  static applyOAuth2(request: IHttpRequest, config: IOAuth2Authorization): void {
    const { accessToken, tokenType = 'Bearer', deliveryMethod = 'header', deliveryName = 'authorization' } = config
    if (!accessToken) {
      return
    }
    const value = `${tokenType} ${accessToken}`
    if (deliveryMethod === 'header') {
      this.applyHeader(request, deliveryName, value)
    } else if (deliveryMethod === 'query') {
      const { url } = request
      try {
        const parser = new UrlProcessor(url)
        parser.search.append(
          UrlEncoder.encodeQueryString(deliveryName, true),
          UrlEncoder.encodeQueryString(value, true)
        )
        request.url = parser.toString()
      } catch {
        // ...
      }
    }
  }

  /**
   * Injects OpenID Connect auth header into the request headers.
   */
  static applyOpenId(request: IHttpRequest, config: IOidcAuthorization): void {
    const { accessToken, tokens, tokenInUse } = config
    if (accessToken) {
      SecurityProcessor.applyOAuth2(request, config)
    } else if (Array.isArray(tokens) && typeof tokenInUse === 'number') {
      const data = tokens[tokenInUse] as IOidcTokenInfo
      if (data && data.accessToken) {
        const copy = { ...config }
        copy.accessToken = data.accessToken
        SecurityProcessor.applyOAuth2(request, copy)
      }
    }
  }

  /**
   * Injects bearer auth header into the request headers.
   */
  static applyBearer(request: IHttpRequest, config: IBearerAuthorization): void {
    const { token } = config
    if (!token) {
      return
    }
    const value = `Bearer ${token}`
    this.applyHeader(request, 'authorization', value)
  }

  /**
   * Injects the RAML custom configuration into the request
   */
  static applyCustomAuth(request: IHttpRequest, config: IRamlCustomAuthorization): void {
    const { header, query } = config
    this.applyQueryParams(request, query)
    this.applyHeaders(request, header)
  }

  /**
   * Injects the ApiKey configuration into the request
   */
  static applyApiKeys(request: IHttpRequest, config: IApiKeyAuthorization): void {
    const { header, query } = config
    this.applyQueryParams(request, query)
    this.applyHeaders(request, header)
  }

  /**
   * Injects the PassThrough configuration into the request
   */
  static applyPassThrough(request: IHttpRequest, config: IPassThroughAuthorization): void {
    const { header, query } = config
    this.applyQueryParams(request, query)
    this.applyHeaders(request, header)
  }

  static applyHeader(request: IHttpRequest, name: string, value: string): void {
    const { headers, headersModel } = request
    if (headersModel && !headers) {
      const disabled = headersModel.filter((i) => !i.enabled)
      const parser = Headers.fromPropertyArray(headersModel.filter((i) => !!i.enabled))
      parser.append(name, value)
      request.headersModel = [...parser.toPropertyArray(), ...disabled]
    } else {
      const parser = new Headers(headers)
      parser.append(name, value)
      request.headers = parser.toString()
    }
  }

  /**
   * Applies a map of query parameters to the request object.
   * @param request The request object
   * @param params A map of query parameters to apply to the request
   */
  static applyQueryParams(request: IHttpRequest, params?: Record<string, string>): void {
    if (typeof params !== 'object') {
      return
    }
    const keys = Object.keys(params)
    if (!keys.length) {
      return
    }

    try {
      const parser = new UrlProcessor(request.url)
      keys.forEach((name) => {
        const value = params[name]
        parser.search.set(UrlEncoder.encodeQueryString(name, true), UrlEncoder.encodeQueryString(value, true))
      })
      request.url = parser.toString()
    } catch {
      // ...
    }
  }

  /**
   * Applies a map of headers to the request object.
   * @param request The request object
   * @param headers A map of headers to apply to the request
   */
  static applyHeaders(request: IHttpRequest, headers?: Record<string, string>): void {
    if (typeof headers !== 'object') {
      return
    }
    const keys = Object.keys(headers)
    if (!keys.length) {
      return
    }

    let parser: Headers
    let disabled: IProperty[] | undefined
    const usesModel = !!request.headersModel && !request.headers
    if (usesModel) {
      const model = request.headersModel as IProperty[]
      disabled = model.filter((i) => !i.enabled)
      parser = Headers.fromPropertyArray(model.filter((i) => !!i.enabled))
    } else {
      parser = new Headers(request.headers)
    }
    keys.forEach((name) => {
      const value = headers[name]
      parser.append(name, value)
    })
    if (usesModel) {
      request.headersModel = [...parser.toPropertyArray(), ...(disabled || [])]
    } else {
      request.headers = parser.toString()
    }
  }

  /**
   * Reads the client certificate from the authorization configuration.
   *
   * @param authorization The HTTP request authorization configuration.
   * @returns The certificate to use with the HTTP request or undefined when not configured.
   */
  static readCertificate(authorization?: IRequestAuthorization[]): HttpCertificate | undefined {
    if (!Array.isArray(authorization) || !authorization.length) {
      return undefined
    }
    const item = authorization.find((i) => i.enabled !== false && i.type === 'client certificate')
    if (!item || !item.config) {
      return undefined
    }
    const init = item.config as ICCAuthorization
    return init.certificate
  }
}
