/**
 * Shared, URL-keyed FairPlay application-certificate cache.
 *
 * Lives at module scope so one successful fetch is reused across FairPlay key
 * sessions, loadSource() calls, recreated players, and multiple players on the
 * same page (HTML5/contrib-eme and Shaka paths).
 *
 * On-demand only: callers must invoke loadFairplayCertificate() / read the
 * cache only after FairPlay has actually been selected (e.g. contrib-eme
 * getCertificate(), or Shaka's SERVER_CERTIFICATE request). Merely seeing
 * drmConfig.fairplay must never trigger a network fetch.
 *
 * Concurrent callers for the same URL share a single in-flight request
 * (coalescing). Failed attempts delete the cache entry so a later call can
 * retry. Each successful caller receives its own Uint8Array copy.
 */

/** @type {Map<string, { certificate: Uint8Array } | { waiters: Function[] }>} */
const fairplayCertificateCache = new Map();

/**
 * Synchronous peek — no network. Returns a copy, or null on miss / in-flight.
 *
 * @param {string} certificateUrl
 * @returns {Uint8Array | null}
 */
export function peekFairplayCertificate(certificateUrl) {
  if (!certificateUrl) {
    return null;
  }
  const entry = fairplayCertificateCache.get(certificateUrl);
  if (entry?.certificate) {
    return new Uint8Array(entry.certificate);
  }
  return null;
}

/**
 * Store a successful certificate response for later reuse.
 *
 * @param {string} certificateUrl
 * @param {ArrayBuffer | Uint8Array} certificate
 */
export function storeFairplayCertificate(certificateUrl, certificate) {
  if (!certificateUrl || !certificate) {
    return;
  }
  fairplayCertificateCache.set(certificateUrl, {
    certificate: new Uint8Array(certificate),
  });
}

/**
 * Drop a cache entry (used after a failed fetch so the next attempt retries).
 *
 * @param {string} certificateUrl
 */
export function invalidateFairplayCertificate(certificateUrl) {
  if (!certificateUrl) {
    return;
  }
  fairplayCertificateCache.delete(certificateUrl);
}

/**
 * Load a FairPlay certificate by URL, using the shared cache + coalescing.
 *
 * @param {string} certificateUrl
 * @param {(settle: (error: Error|null, bytes?: ArrayBuffer|Uint8Array) => void) => void} doFetch
 *   Performs the network request when no cache entry / in-flight request exists.
 *   Must call settle(error) or settle(null, bytes) exactly once.
 * @param {(error: Error|null, certificate: Uint8Array|null) => void} callback
 */
export function loadFairplayCertificate(certificateUrl, doFetch, callback) {
  if (!certificateUrl) {
    callback(new Error("FairPlay certificateUrl is required"));
    return;
  }

  const cached = fairplayCertificateCache.get(certificateUrl);

  if (cached?.certificate) {
    callback(null, new Uint8Array(cached.certificate));
    return;
  }

  if (cached?.waiters) {
    cached.waiters.push(callback);
    return;
  }

  const entry = { waiters: [callback] };
  fairplayCertificateCache.set(certificateUrl, entry);

  doFetch((error, bytes) => {
    const waiters = entry.waiters || [];
    entry.waiters = null;

    if (error || !bytes) {
      // Drop so a later attempt can retry the fetch.
      fairplayCertificateCache.delete(certificateUrl);
      const failError =
        error || new Error("FairPlay certificate response was empty");
      waiters.forEach((waiter) => waiter(failError, null));
      return;
    }

    let certificate;
    try {
      certificate = new Uint8Array(bytes);
    } catch (parseError) {
      fairplayCertificateCache.delete(certificateUrl);
      waiters.forEach((waiter) => waiter(parseError, null));
      return;
    }

    fairplayCertificateCache.set(certificateUrl, { certificate });
    waiters.forEach((waiter) => waiter(null, new Uint8Array(certificate)));
  });
}
