const ConcurrencyLimiter = require('@dougal/concurrency');

/** Make an API request
 *
 * @a resource {String} is the target URL
 * @a init {Object} are the Fetch options
 * @a cb {Function} is a callback function: (res, err) => {}
 * @a opts {Object} are other optional parameters:
 *    opts.silent {Boolean} controls whether snack messages are shown on failure
 *    opts.cache {Object} controls whether Cache API is used
 *    opts.cache.name {String} is the name of the cache to use. Defaults to "dougal"
 *
 * If Cache API is used, this function looks for a matching request in the cache
 * first, and returns it if found. If not found, it makes the request over the API
 * and then stores it in the cache.
 *
 * `opts.cache` may also be `true` (defaults to using the "dougal" cache),
 * a cache name (equivalent to {name: "…"}) or even an empty object (equivalent
 * to `true`).
 */
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb, opts = {}]) {

  const limiter = api.limiter || (api.limiter = new ConcurrencyLimiter(state.maxConcurrent));

  try {
    commit("queueRequest");
    if (init && init.hasOwnProperty("body")) {
      if (!init.headers) {
        // As a convenience, we implicitly add the default content type
        init.headers = {
          "Content-Type": "application/json"
        }
      }
    }
    if (!init.headers) {
      init.headers = {};
    }
    // We also send Authorization: Bearer …
    if (getters.jwt) {
      init.credentials = "include";
      init.headers["Authorization"] = "Bearer "+getters.jwt;
    }
    if (typeof init.body != "string") {
      init.body = JSON.stringify(init.body);
    }
    const url = /^https?:\/\//i.test(resource) ? resource : (state.apiUrl + resource);

    let res; // The response
    let cache; // Potentially, a Cache API cache name
    let isCached;

    if (opts?.cache === true) {
      opts.cache = { name: "dougal" };
    } else if (typeof opts?.cache === "string") {
      opts.cache = { name: opts.cache };
    } else if (opts?.cache) {
      if (!(opts.cache instanceof Object)) {
        opts.cache = { name: "dougal" }
      } else if (!(opts.cache.name)) {
        opts.cache.name = "dougal";
      }
    }

    if (opts?.cache && window.caches) {
      cache = await caches.open(opts.cache.name);
      res = await cache.match(url);
      isCached = !!res;
    }

    if (!res) {
      res = await limiter.enqueue(async () => await fetch(url, init));
    }

    if (cache && !isCached && res.ok) { // Only cache successful responses
      cache.put(url, res.clone());
    }

    if (typeof cb === 'function') {
      await cb(null, res.clone());
    }

    if (res.headers.has("x-dougal-server")) {
      const header = res.headers.get("x-dougal-server")
      const entries = header
        .split(";")
        .map(part => part.trim())
        .filter(part => part.length > 0)
        .map(part => {
          const idx = part.indexOf('=');
          if (idx === -1) {
            return [part, true];
          }
          const key = part.slice(0, idx).trim();
          const value = part.slice(idx + 1).trim();
          return [key, value];
        });
        state.serverInfo = entries.length ? Object.fromEntries(entries) : {};

        if (state.serverInfo["remote-frontend"]) {
          state.isGatewayReliable = ![ 502, 503, 504 ].includes(res.status);
        } else {
          state.isGatewayReliable = null;
        }
    }

    if (res.ok) {

      if (!isCached) {
        if (res.headers.has("x-jwt")) {
          await dispatch('setCredentials', { response: res });
        }
      }

      try {
        if (!res.bodyUsed) { // It may have been consumed by a callback
          const validFormats = [ "arrayBuffer", "blob", "formData", "json", "text" ];
          if (opts.format && validFormats.includes(opts.format)) {
            return await res[opts.format]();
          } else {
            return await res.json();
          }
        }
      } catch (err) {
        if (err instanceof SyntaxError) {
          if (Number(res.headers.get("Content-Length")) === 0) {
            // res.json() cannot parse an empty response and we
            // have run into this case. We just return undefined.
            return;
          }
        }
        throw err;
      }
    } else {
      let message = res.statusText;
      if (res.headers.get("Content-Type").match(/^application\/json/i)) {
        const body = await res.json();
        if (body.message) {
          message = body.message;
        }
      }
      if (!opts?.silent) {
        await dispatch('showSnack', [message, "warning"]);
      }
    }
  } catch (err) {
    if (err && err.name == "AbortError") return;
    await dispatch('showSnack', [err, "error"]);
    if (typeof cb === 'function') {
      cb(err);
    }
  } finally {
    commit("dequeueRequest");
  }
}

export default { api };
