text: 'use strict'
  
  const fp = require('fastify-plugin')
  const { createSigner, createDecoder, createVerifier, TokenError } = require('fast-jwt')
  const assert = require('assert')
  const steed = require('steed')
  const { parse } = require('@lukeed/ms')
  const {
    BadRequest,
    Unauthorized
  } = require('http-errors')
  
  const messages = {
    badRequestErrorMessage: 'Format is Authorization: Bearer [token]',
    badCookieRequestErrorMessage: 'Cookie could not be parsed in request',
    noAuthorizationInHeaderMessage: 'No Authorization was found in request.headers',
    noAuthorizationInCookieMessage: 'No Authorization was found in request.cookies',
    authorizationTokenExpiredMessage: 'Authorization token expired',
    authorizationTokenInvalid: (err) => `Authorization token is invalid: ${err.message}`,
    authorizationTokenUntrusted: 'Untrusted authorization token'
  }
  
  function wrapStaticSecretInCallback (secret) {
    return function (request, payload, cb) {
      return cb(null, secret)
    }
  }
  
  function convertToMs (time) {
    // by default if time is number we assume that they are seconds - see README.md
    if (typeof time === 'number') {
      return time * 1000
    }
    return parse(time)
  }
  
  function convertTemporalProps (options, isVerifyOptions) {
    if (options && typeof options !== 'function') {
      if (isVerifyOptions && options.maxAge) {
        options.maxAge = convertToMs(options.maxAge)
      } else if (options.expiresIn || options.notBefore) {
        if (options.expiresIn) {
          options.expiresIn = convertToMs(options.expiresIn)
        }
  
        if (options.notBefore) {
          options.notBefore = convertToMs(options.notBefore)
        }
      }
    }
  
    return options
  }
  
  function fastifyJwt (fastify, options, next) {
    if (!options.secret) {
      return next(new Error('missing secret'))
    }
  
    if (options.options) {
      return next(new Error('options prefix is deprecated'))
    }
  
    const secret = options.secret
    const trusted = options.trusted
    let secretOrPrivateKey
    let secretOrPublicKey
  
    if (typeof secret === 'object' && !Buffer.isBuffer(secret)) {
      if (!secret.private || !secret.public) {
        return next(new Error('missing private key and/or public key'))
      }
      secretOrPrivateKey = secret.private
      secretOrPublicKey = secret.public
    } else {
      secretOrPrivateKey = secretOrPublicKey = secret
    }
  
    let secretCallbackSign = secretOrPrivateKey
    let secretCallbackVerify = secretOrPublicKey
    if (typeof secretCallbackSign !== 'function') {
      secretCallbackSign = wrapStaticSecretInCallback(secretCallbackSign)
    }
    if (typeof secretCallbackVerify !== 'function') {
      secretCallbackVerify = wrapStaticSecretInCallback(secretCallbackVerify)
    }
  
    const cookie = options.cookie
    const formatUser = options.formatUser
  
    const decodeOptions = options.decode || {}
    const signOptions = convertTemporalProps(options.sign) || {}
    const verifyOptions = convertTemporalProps(options.verify, true) || {}
    const messagesOptions = Object.assign({}, messages, options.messages)
    const namespace = typeof options.namespace === 'string' ? options.namespace : undefined
  
    if (
      signOptions &&
      signOptions.algorithm &&
      signOptions.algorithm.includes('RS') &&
      (typeof secret === 'string' ||
        secret instanceof Buffer)
    ) {
      return next(new Error('RSA Signatures set as Algorithm in the options require a private and public key to be set as the secret'))
    }
    if (
      signOptions &&
      signOptions.algorithm &&
      signOptions.algorithm.includes('ES') &&
      (typeof secret === 'string' ||
        secret instanceof Buffer)
    ) {
      return next(new Error('ECDSA Signatures set as Algorithm in the options require a private and public key to be set as the secret'))
    }
  
    const jwtConfig = {
      decode: decode,
      options: {
        decode: decodeOptions,
        sign: signOptions,
        verify: verifyOptions,
        messages: messagesOptions
      },
      cookie: cookie,
      sign: sign,
      verify: verify,
      lookupToken: lookupToken
    }
  
    let jwtDecodeName = 'jwtDecode'
    let jwtVerifyName = 'jwtVerify'
    let jwtSignName = 'jwtSign'
    if (namespace) {
      if (!fastify.jwt) {
        fastify.decorateRequest('user', null)
        fastify.decorate('jwt', Object.create(null))
      }
  
      if (fastify.jwt[namespace]) {
        return next(new Error(`JWT namespace already used "${namespace}"`))
      }
      fastify.jwt[namespace] = jwtConfig
  
      jwtDecodeName = options.jwtDecode ? (typeof options.jwtDecode === 'string' ? options.jwtDecode : 'jwtDecode') : `${namespace}JwtDecode`
      jwtVerifyName = options.jwtVerify || `${namespace}JwtVerify`
      jwtSignName = options.jwtSign || `${namespace}JwtSign`
    } else {
      fastify.decorateRequest('user', null)
      fastify.decorate('jwt', jwtConfig)
    }
  
    // Temporary conditional to prevent breaking changes by exposing `jwtDecode`,
    // which already exists in fastify-auth0-verify.
    // If jwtDecode has been requested, or plugin is configured to use a namespace.
    // TODO Remove conditional when fastify-jwt >=4.x.x
    if (options.jwtDecode || namespace) {
      fastify.decorateRequest(jwtDecodeName, requestDecode)
    }
    fastify.decorateRequest(jwtVerifyName, requestVerify)
    fastify.decorateReply(jwtSignName, replySign)
  
    const signerConfig = checkAndMergeSignOptions()
    const signer = createSigner(signerConfig.options)
    const decoder = createDecoder(decodeOptions)
    const verifierConfig = checkAndMergeVerifyOptions()
    const verifier = createVerifier(verifierConfig.options)
  
    next()
  
    function decode (token, options) {
      assert(token, 'missing token')
  
      if (options && typeof options !== 'function') {
        const localDecoder = createDecoder(options)
        return localDecoder(token)
      }
  
      return decoder(token)
    }
  
    function lookupToken (request, options) {
      assert(request, 'missing request')
  
      options = Object.assign({}, verifyOptions, options)
  
      let token
      const extractToken = options.extractToken
      if (extractToken) {
        token = extractToken(request)
        if (!token) {
          throw new BadRequest(messagesOptions.badRequestErrorMessage)
        }
      } else if (request.headers && request.headers.authorization) {
        const parts = request.headers.authorization.split(' ')
        if (parts.length === 2) {
          const scheme = parts[0]
          token = parts[1]
  
          if (!/^Bearer$/i.test(scheme)) {
            throw new BadRequest(messagesOptions.badRequestErrorMessage)
          }
        } else {
          throw new BadRequest(messagesOptions.badRequestErrorMessage)
        }
      } else if (cookie) {
        if (request.cookies) {
          if (request.cookies[cookie.cookieName]) {
            const tokenValue = request.cookies[cookie.cookieName]
  
            token = cookie.signed ? request.unsignCookie(tokenValue).value : tokenValue
          } else {
            throw new Unauthorized(messagesOptions.noAuthorizationInCookieMessage)
          }
        } else {
          throw new BadRequest(messagesOptions.badCookieRequestErrorMessage)
        }
      } else {
        throw new Unauthorized(messagesOptions.noAuthorizationInHeaderMessage)
      }
  
      return token
    }
  
    function mergeOptionsWithKey (options, useProvidedPrivateKey) {
      if (useProvidedPrivateKey && (typeof useProvidedPrivateKey !== 'boolean')) {
        return Object.assign({}, options, { key: useProvidedPrivateKey })
      } else {
        const key = useProvidedPrivateKey ? secretOrPrivateKey : secretOrPublicKey
        return Object.assign(!options.key ? { key } : {}, options)
      }
    }
  
    function checkAndMergeOptions (options, defaultOptions, usePrivateKey, callback) {
      let mergedOptions
  
      if (typeof options === 'function') {
        callback = options
        mergedOptions = mergeOptionsWithKey(defaultOptions, usePrivateKey)
      } else {
        if (!options) {
          mergedOptions = mergeOptionsWithKey(defaultOptions, usePrivateKey)
        } else {
          mergedOptions = mergeOptionsWithKey(options, usePrivateKey)
        }
      }
  
      return { options: mergedOptions, callback }
    }
  
    function checkAndMergeSignOptions (options, callback) {
      return checkAndMergeOptions(options, signOptions, true, callback)
    }
  
    function checkAndMergeVerifyOptions (options, callback) {
      return checkAndMergeOptions(options, verifyOptions, false, callback)
    }
  
    function sign (payload, options, callback) {
      assert(payload, 'missing payload')
      let localSigner = signer
  
      convertTemporalProps(options)
      const signerConfig = checkAndMergeSignOptions(options, callback)
  
      if (options && typeof options !== 'function') {
        localSigner = createSigner(signerConfig.options)
      }
  
      if (typeof signerConfig.callback === 'function') {
        const token = localSigner(payload)
        signerConfig.callback(null, token)
      } else {
        return localSigner(payload)
      }
    }
  
    function verify (token, options, callback) {
      assert(token, 'missing token')
      assert(secretOrPublicKey, 'missing secret')
  
      let localVerifier = verifier
  
      convertTemporalProps(options, true)
      const veriferConfig = checkAndMergeVerifyOptions(options, callback)
  
      if (options && typeof options !== 'function') {
        localVerifier = createVerifier(veriferConfig.options)
      }
  
      if (typeof veriferConfig.callback === 'function') {
        const result = localVerifier(token)
        veriferConfig.callback(null, result)
      } else {
        return localVerifier(token)
      }
    }
  
    function replySign (payload, options, next) {
      let useLocalSigner = true
      if (typeof options === 'function') {
        next = options
        options = {}
        useLocalSigner = false
      } // support no options
  
      if (!options) {
        options = {}
        useLocalSigner = false
      }
  
      const reply = this
  
      if (next === undefined) {
        return new Promise(function (resolve, reject) {
          reply[jwtSignName](payload, options, function (err, val) {
            err ? reject(err) : resolve(val)
          })
        })
      }
  
      if (options.sign) {
        convertTemporalProps(options.sign)
        // New supported contract, options supports sign and can expand
        options = {
          sign: mergeOptionsWithKey({ ...signOptions, ...options.sign }, true)
        }
      } else {
        convertTemporalProps(options)
        // Original contract, options supports only sign
        options = mergeOptionsWithKey({ ...signOptions, ...options }, true)
      }
  
      if (!payload) {
        return next(new Error('jwtSign requires a payload'))
      }
  
      steed.waterfall([
        function getSecret (callback) {
          const signResult = secretCallbackSign(reply.request, payload, callback)
  
          if (signResult && typeof signResult.then === 'function') {
            signResult.then(result => callback(null, result), callback)
          }
        },
        function sign (secretOrPrivateKey, callback) {
          if (useLocalSigner) {
            const signerOptions = mergeOptionsWithKey(options.sign || options, secretOrPrivateKey)
            const localSigner = createSigner(signerOptions)
            const token = localSigner(payload)
            callback(null, token)
          } else {
            const token = signer(payload)
            callback(null, token)
          }
        }
      ], next)
    }
  
    function requestDecode (options, next) {
      if (typeof options === 'function' && !next) {
        next = options
        options = {}
      } // support no options
  
      if (!options) {
        options = {}
      }
  
      options = {
        decode: Object.assign({}, decodeOptions, options.decode),
        verify: Object.assign({}, verifyOptions, options.verify)
      }
  
      const request = this
  
      if (next === undefined) {
        return new Promise(function (resolve, reject) {
          request[jwtDecodeName](options, function (err, val) {
            err ? reject(err) : resolve(val)
          })
        })
      }
  
      try {
        const token = lookupToken(request, options.verify)
        const decodedToken = decode(token, options.decode)
        return next(null, decodedToken)
      } catch (err) {
        return next(err)
      }
    }
  
    function requestVerify (options, next) {
      let useLocalVerifier = true
      if (typeof options === 'function' && !next) {
        next = options
        options = {}
        useLocalVerifier = false
      } // support no options
  
      if (!options) {
        options = {}
        useLocalVerifier = false
      }
  
      if (options.decode || options.verify) {
        convertTemporalProps(options.verify, true)
        // New supported contract, options supports both decode and verify
        options = {
          decode: Object.assign({}, decodeOptions, options.decode),
          verify: Object.assign({}, verifyOptions, options.verify)
        }
      } else {
        convertTemporalProps(options, true)
        // Original contract, options supports only verify
        options = Object.assign({}, verifyOptions, options)
      }
  
      const request = this
  
      if (next === undefined) {
        return new Promise(function (resolve, reject) {
          request[jwtVerifyName](options, function (err, val) {
            err ? reject(err) : resolve(val)
          })
        })
      }
  
      let token
      try {
        token = lookupToken(request, options.verify || options)
      } catch (err) {
        return next(err)
      }
  
      const decodedToken = decode(token, options.decode || decodeOptions)
  
      steed.waterfall([
        function getSecret (callback) {
          const verifyResult = secretCallbackVerify(request, decodedToken, callback)
          if (verifyResult && typeof verifyResult.then === 'function') {
            verifyResult.then(result => callback(null, result), callback)
          }
        },
        function verify (secretOrPublicKey, callback) {
          try {
            if (useLocalVerifier) {
              const verifierOptions = mergeOptionsWithKey(options.verify || options, secretOrPublicKey)
              const localVerifier = createVerifier(verifierOptions)
              const verifyResult = localVerifier(token)
              callback(null, verifyResult)
            } else {
              const verifyResult = verifier(token)
              callback(null, verifyResult)
            }
          } catch (error) {
            if (error.code === TokenError.codes.expired) {
              return callback(new Unauthorized(messagesOptions.authorizationTokenExpiredMessage))
            }
  
            if (error.code === TokenError.codes.invalidKey ||
                error.code === TokenError.codes.invalidSignature ||
                error.code === TokenError.codes.invalidClaimValue
            ) {
              return callback(new Unauthorized(typeof messagesOptions.authorizationTokenInvalid === 'function' ? messagesOptions.authorizationTokenInvalid(error) : messagesOptions.authorizationTokenInvalid))
            }
  
            return callback(error)
          }
        },
        function checkIfIsTrusted (result, callback) {
          if (!trusted) {
            callback(null, result)
          } else {
            const maybePromise = trusted(request, result)
  
            if (maybePromise && maybePromise.then) {
              maybePromise
                .then(trusted => trusted ? callback(null, result) : callback(new Unauthorized(messagesOptions.authorizationTokenUntrusted)))
            } else if (maybePromise) {
              callback(null, maybePromise)
            } else {
              callback(new Unauthorized(messagesOptions.authorizationTokenUntrusted))
            }
          }
        }
      ], function (err, result) {
        if (err) {
          next(err)
        } else {
          const user = formatUser ? formatUser(result) : result
          request.user = user
          next(null, user)
        }
      })
    }
  }
  
  module.exports = fp(fastifyJwt, {
    fastify: '>=3.0.0',
    name: 'fastify-jwt'
  })
  