var urlDecodeB64 = function(data) {
  return Buffer.from(data, 'base64').toString('utf8');
}

/**
 * Decodes a string token into the 3 parts, throws if the format is invalid
 * @param token
 */
var decode = function(token) {
  var parts = token.split('.');

  if (parts.length !== 3) {
    throw new Error('ID token could not be decoded')
  }

  return {
    _raw: token,
    header: JSON.parse(urlDecodeB64(parts[0])),
    payload: JSON.parse(urlDecodeB64(parts[1])),
    signature: parts[2]
  };
}

var DEFAULT_LEEWAY = 60; //default clock-skew, in seconds

/**
 * Verifier for ID Tokens following OIDC spec.
 * @param token the string token to verify
 * @param options the options required to run this verification
 * @returns A promise containing the decoded token payload, or throws an exception if validation failed
 */
var verify = (token, options) => {

  if (!token) {
    throw new Error('ID token is required but missing');
  }

  var decodedToken = decode(token);

  // Check algorithm
  var header = decodedToken.header;
  if (header.alg !== 'RS256' && header.alg !== 'HS256') {
    throw new Error(
      `Signature algorithm of "${header.alg}" is not supported. Expected the ID token to be signed with "RS256" or "HS256".`
    );
  }

  var payload = decodedToken.payload;

  // Issuer
  if (!payload.iss || typeof payload.iss !== 'string') {
    throw new Error('Issuer (iss) claim must be a string present in the ID token');
  }
  if (payload.iss !== options.iss) {
    throw new Error(`Issuer (iss) claim mismatch in the ID token; expected "${options.iss}", found "${payload.iss}"`);
  }

  // Subject
  if (!payload.sub || typeof payload.sub !== 'string') {
    throw new Error('Subject (sub) claim must be a string present in the ID token');
  }

  // Audience
  if (!payload.aud || !(typeof payload.aud === 'string' || Array.isArray(payload.aud))) {
    throw new Error('Audience (aud) claim must be a string or array of strings present in the ID token');
  }
  if (Array.isArray(payload.aud) && !payload.aud.includes(options.aud)) {
    throw new Error(`Audience (aud) claim mismatch in the ID token; expected "${options.aud}" but was not one of "${payload.aud.join(', ')}"`);
  }
  else if (typeof payload.aud === 'string' && payload.aud !== options.aud) {
    throw new Error(`Audience (aud) claim mismatch in the ID token; expected "${options.aud}" but found "${payload.aud}"`);
  }

  // --Time validation (epoch)--
  var now = Math.floor(Date.now() / 1000);
  var leeway = options.leeway || DEFAULT_LEEWAY;

  // Expires at
  if (!payload.exp || typeof payload.exp !== 'number') {
    throw new Error('Expiration Time (exp) claim must be a number present in the ID token');
  }
  var expTime = payload.exp + leeway;

  if (now > expTime) {
    throw new Error(`Expiration Time (exp) claim error in the ID token; current time (${now}) is after expiration time (${expTime})`);
  }

  // Issued at
  if (!payload.iat || typeof payload.iat !== 'number') {
    throw new Error('Issued At (iat) claim must be a number present in the ID token');
  }

  // Nonce
  if (options.nonce) {
    if (!payload.nonce || typeof payload.nonce !== 'string') {
      throw new Error('Nonce (nonce) claim must be a string present in the ID token');
    }
    if (payload.nonce !== options.nonce) {
      throw new Error(`Nonce (nonce) claim mismatch in the ID token; expected "${options.nonce}", found "${payload.nonce}"`);
    }
  }

  // Authorized party
  if (Array.isArray(payload.aud) && payload.aud.length > 1) {
    if (!payload.azp || typeof payload.azp !== 'string') {
      throw new Error('Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values');
    }
    if (payload.azp !== options.aud) {
      throw new Error(`Authorized Party (azp) claim mismatch in the ID token; expected "${options.aud}", found "${payload.azp}"`);
    }
  }

  // Authentication time
  if (options.maxAge) {
    if (!payload.auth_time || typeof payload.auth_time !== 'number') {
      throw new Error('Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified');
    }

    var authValidUntil = payload.auth_time + options.maxAge + leeway;
    if (now > authValidUntil) {
      throw new Error(
        `Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Currrent time (${now}) is after last auth at ${authValidUntil}`
      );
    }
  }

  return decodedToken;
};

module.exports = {
  decode: decode,
  verify: verify
};
