const { Issuer, custom } = require('openid-client');
const url = require('url');
const urlJoin = require('url-join');
const pkg = require('../package.json');
const debug = require('./debug')('client');
const { JWK } = require('jose');

const telemetryHeader = {
  name: 'express-oidc',
  version: pkg.version,
  env: {
    node: process.version,
  },
};

function sortSpaceDelimitedString(string) {
  return string.split(' ').sort().join(' ');
}

async function get(config) {
  const defaultHttpOptions = (options) => {
    options.headers = {
      ...options.headers,
      'User-Agent': config.httpUserAgent || `${pkg.name}/${pkg.version}`,
      ...(config.enableTelemetry
        ? {
            'Auth0-Client': Buffer.from(
              JSON.stringify(telemetryHeader)
            ).toString('base64'),
          }
        : undefined),
    };
    options.timeout = config.httpTimeout;
    options.agent = config.httpAgent;
    return options;
  };

  const applyHttpOptionsCustom = (entity) =>
    (entity[custom.http_options] = defaultHttpOptions);

  applyHttpOptionsCustom(Issuer);
  const issuer = await Issuer.discover(config.issuerBaseURL);
  applyHttpOptionsCustom(issuer);

  const issuerTokenAlgs = Array.isArray(
    issuer.id_token_signing_alg_values_supported
  )
    ? issuer.id_token_signing_alg_values_supported
    : [];
  if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) {
    debug(
      'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.',
      config.idTokenSigningAlg,
      issuerTokenAlgs
    );
  }

  const configRespType = sortSpaceDelimitedString(
    config.authorizationParams.response_type
  );
  const issuerRespTypes = Array.isArray(issuer.response_types_supported)
    ? issuer.response_types_supported
    : [];
  issuerRespTypes.map(sortSpaceDelimitedString);
  if (!issuerRespTypes.includes(configRespType)) {
    debug(
      'Response type %o is not supported by the issuer. ' +
        'Supported response types are: %o.',
      configRespType,
      issuerRespTypes
    );
  }

  const configRespMode = config.authorizationParams.response_mode;
  const issuerRespModes = Array.isArray(issuer.response_modes_supported)
    ? issuer.response_modes_supported
    : [];
  if (configRespMode && !issuerRespModes.includes(configRespMode)) {
    debug(
      'Response mode %o is not supported by the issuer. ' +
        'Supported response modes are %o.',
      configRespMode,
      issuerRespModes
    );
  }

  if (
    config.pushedAuthorizationRequests &&
    !issuer.pushed_authorization_request_endpoint
  ) {
    throw new TypeError(
      'pushed_authorization_request_endpoint must be configured on the issuer to use pushedAuthorizationRequests'
    );
  }

  let jwks;
  if (config.clientAssertionSigningKey) {
    const jwk = JWK.asKey(config.clientAssertionSigningKey).toJWK(true);
    jwks = { keys: [jwk] };
  }

  const client = new issuer.Client(
    {
      client_id: config.clientID,
      client_secret: config.clientSecret,
      id_token_signed_response_alg: config.idTokenSigningAlg,
      token_endpoint_auth_method: config.clientAuthMethod,
      ...(config.clientAssertionSigningAlg && {
        token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg,
      }),
    },
    jwks
  );
  applyHttpOptionsCustom(client);
  client[custom.clock_tolerance] = config.clockTolerance;

  if (config.idpLogout) {
    if (
      config.auth0Logout ||
      (url.parse(issuer.issuer).hostname.match('\\.auth0\\.com$') &&
        config.auth0Logout !== false)
    ) {
      Object.defineProperty(client, 'endSessionUrl', {
        value(params) {
          const { id_token_hint, post_logout_redirect_uri, ...extraParams } =
            params;
          const parsedUrl = url.parse(urlJoin(issuer.issuer, '/v2/logout'));
          parsedUrl.query = {
            ...extraParams,
            returnTo: post_logout_redirect_uri,
            client_id: client.client_id,
          };

          Object.entries(parsedUrl.query).forEach(([key, value]) => {
            if (value === null || value === undefined) {
              delete parsedUrl.query[key];
            }
          });

          return url.format(parsedUrl);
        },
      });
    } else if (!issuer.end_session_endpoint) {
      debug('the issuer does not support RP-Initiated Logout');
    }
  }

  return { client, issuer };
}

const cache = new Map();
let timestamp = 0;

exports.get = (config) => {
  const { discoveryCacheMaxAge: cacheMaxAge } = config;
  const now = Date.now();
  if (cache.has(config) && now < timestamp + cacheMaxAge) {
    return cache.get(config);
  }
  timestamp = now;
  const promise = get(config).catch((e) => {
    cache.delete(config);
    throw e;
  });
  cache.set(config, promise);
  return promise;
};
