import crypto from 'crypto'
import defaults from '../defaults.mjs'
import * as base64UrlSafe from './base64urlsafe.mjs'

const defaultOptions = {
  expiresSkew: 0,
  expiresMax: 0,
  nbfIatSkew: 300,
  fixup: null
}

/**
 *
 * @param {string} jwt
 * @param {Object} publicKeys
 * @param {Array<string>} audiences
 * @param {Object} [options]
 * @param {Object} [options.expiresSkew=0]
 * @param {Object} [options.expiresMax=0]
 * @param {Object} [options.nbfIatSkew=300]
 * @param {Function<header,body,void>} [options.fixup]
 */
 export default function decode(jwt, publicKeys, audiences, options = defaultOptions) {
  if (typeof jwt !== 'string') {
    throw new Error('jwt needs to a string')
  }

  if (typeof publicKeys !== 'object' || Array.isArray(publicKeys)) {
    throw new Error(
      'publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }'
    )
  }

  if (!Array.isArray(audiences)) {
    throw new Error('audiences needs to be an array of allowed audiences')
  }

  if (typeof options !== 'object' || Array.isArray(publicKeys)) {
    throw new Error('options needs to a map of { nbfIatSkew: 300, ... }')
  }

  let parts = jwt.split(/\./)
  if (parts.length !== 3) {
    throw new Error('JWT does not contain 3 dots')
  }

  let header
  let body
  try {
    header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
    body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
  } catch (err) {
    throw new Error('Token was invalid')
  }
  if (typeof options.fixup === 'function') {
    options.fixup(header, body)
  }

  let hmacAlgo = null
  switch (header.alg) {
    case 'HS256':
      hmacAlgo = 'sha256'
      break
    case 'HS384':
      hmacAlgo = 'sha384'
      break
    case 'HS512':
      hmacAlgo = 'sha512'
      break
    default:
      throw new Error(
        'Only alg HS256, HS384 and HS512 are supported'
      )
  }

  if (!body.iss) {
    throw new Error('No issuer set')
  }

  let issuer = publicKeys[body.iss]
  if (!issuer) {
    throw new Error(`Unknown issuer '${body.iss}'`)
  }

  // Find public key
  let pubkeyOrSharedKey =
    typeof header.kid === 'string'
      ? issuer[`${header.kid}@${header.alg}`]
      : issuer[`default@${header.alg}`]
  let overrideOptions = {}

  if (!pubkeyOrSharedKey) {
    throw new Error(
      `Unknown pubkey id '${header.kid || 'default'}' on this issuer`
    )
  } else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') {
    throw new Error(
      `Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
    )
  }
  if (typeof(pubkeyOrSharedKey) === 'object') {
    if (typeof(pubkeyOrSharedKey.key) !== 'string') {
      throw new Error(
        `Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
      )
    }
    overrideOptions = pubkeyOrSharedKey
    pubkeyOrSharedKey = pubkeyOrSharedKey.key
  }

  let signatureOrHash = base64UrlSafe.decode(parts[2])
  const hmac = crypto.createHmac(hmacAlgo, pubkeyOrSharedKey)
  hmac.update(`${parts[0]}.${parts[1]}`, 'utf8')
  let signatureBuffer = hmac.digest()

  if (signatureOrHash.length !== signatureBuffer.length || !crypto.timingSafeEqual(signatureOrHash, signatureBuffer)) {
    throw new Error(`Verification failed with alg '${header.alg}'`)
  }

  let unixNow = Math.floor(Date.now() / 1000)

  let validators = defaults(options.validators, {
    aud: validateAudience,
    exp: validateExpires,
    iat: validateIssuedAt,
    nbf: validateNotBefore
  })

  let validationOptions = defaults(overrideOptions, options)

  validators.aud(body, audiences, validationOptions)
  validators.iat(body, unixNow, validationOptions)
  validators.nbf(body, unixNow, validationOptions)
  validators.exp(body, unixNow, validationOptions)

  return body
}

function validateNotBefore(body, unixNow, options) {
  if (body.nbf && body.nbf > unixNow + options.nbfIatSkew) {
    throw new Error(
      `Not before in the future by more than ${options.nbfIatSkew} seconds`
    )
  }
}

function validateIssuedAt(body, unixNow, options) {
  if (body.iat && body.iat > unixNow + options.nbfIatSkew) {
    throw new Error(
      `Issued at in the future by more than ${options.nbfIatSkew} seconds`
    )
  }
}

function validateAudience(body, audiences, options) {
  if (!body.aud && audiences.length === 0) return

  let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
  if (!auds.some(aud => audiences.includes(aud))) {
    throw new Error(`Unknown audience '${auds.join(',')}'`)
  }
}

function validateExpires(body, unixNow, options) {
  if (!body.exp) {
    throw new Error(`No expires set on token`)
  }
  let notBefore = body.iat || body.nbf || unixNow
  if (options.expiresMax && body.exp > notBefore + options.expiresMax) {
    throw new Error(
      `Expires in the future by more than ${options.expiresMax} seconds`
    )
  }
  if (body.exp + (options.expiresSkew || 0) <= unixNow) {
    throw new Error('Token has expired')
  }
}
