const objectHash = require('object-hash');
const LRU = require('lru-cache');

const { RPError } = require('../errors');

const { assertIssuerConfiguration } = require('./assert');
const KeyStore = require('./keystore');
const { keystores } = require('./weak_cache');
const processResponse = require('./process_response');
const request = require('./request');

const inFlight = new WeakMap();
const caches = new WeakMap();
const lrus = (ctx) => {
  if (!caches.has(ctx)) {
    caches.set(ctx, new LRU({ max: 100 }));
  }
  return caches.get(ctx);
};

async function getKeyStore(reload = false) {
  assertIssuerConfiguration(this, 'jwks_uri');

  const keystore = keystores.get(this);
  const cache = lrus(this);

  if (reload || !keystore) {
    if (inFlight.has(this)) {
      return inFlight.get(this);
    }
    cache.reset();
    inFlight.set(
      this,
      (async () => {
        const response = await request
          .call(this, {
            method: 'GET',
            responseType: 'json',
            url: this.jwks_uri,
            headers: {
              Accept: 'application/json, application/jwk-set+json',
            },
          })
          .finally(() => {
            inFlight.delete(this);
          });
        const jwks = processResponse(response);

        const joseKeyStore = KeyStore.fromJWKS(jwks, { onlyPublic: true });
        cache.set('throttle', true, 60 * 1000);
        keystores.set(this, joseKeyStore);

        return joseKeyStore;
      })(),
    );

    return inFlight.get(this);
  }

  return keystore;
}

async function queryKeyStore({ kid, kty, alg, use }, { allowMulti = false } = {}) {
  const cache = lrus(this);

  const def = {
    kid,
    kty,
    alg,
    use,
  };

  const defHash = objectHash(def, {
    algorithm: 'sha256',
    ignoreUnknown: true,
    unorderedArrays: true,
    unorderedSets: true,
  });

  // refresh keystore on every unknown key but also only upto once every minute
  const freshJwksUri = cache.get(defHash) || cache.get('throttle');

  const keystore = await getKeyStore.call(this, !freshJwksUri);
  const keys = keystore.all(def);

  delete def.use;
  if (keys.length === 0) {
    throw new RPError({
      printf: ["no valid key found in issuer's jwks_uri for key parameters %j", def],
      jwks: keystore,
    });
  }

  if (!allowMulti && keys.length > 1 && !kid) {
    throw new RPError({
      printf: [
        "multiple matching keys found in issuer's jwks_uri for key parameters %j, kid must be provided in this case",
        def,
      ],
      jwks: keystore,
    });
  }

  cache.set(defHash, true);

  return keys;
}

module.exports.queryKeyStore = queryKeyStore;
module.exports.keystore = getKeyStore;
