/**
 * Shaka Player DRM Configuration
 */
import shaka from "shaka-player";
import { Logger } from "../utils/logger.js";
import {
  buildSafeEndpoint,
  buildSafeHeaderFlags,
  createLocalizedError,
  ERROR_MESSAGE_KEYS,
} from "../helpers/errorMessages.js";
import {
  loadFairplayCertificate,
  peekFairplayCertificate,
} from "../helpers/fairplayCertificateCache.js";

// A certificate request that never receives *any* response would otherwise
// leave this fetch() pending forever. Only used from the on-demand FairPlay
// SERVER_CERTIFICATE path (after Shaka has selected FairPlay).
const DRM_REQUEST_TIMEOUT_MS = 15000;

async function fetchWithTimeout(url, timeoutMs = DRM_REQUEST_TIMEOUT_MS) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { signal: controller.signal });
  } catch (error) {
    if (error?.name === "AbortError") {
      const timeoutError = new Error(`Request timed out after ${timeoutMs}ms`);
      timeoutError.isTimeout = true;
      throw timeoutError;
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

function certificateToDataUri(certificate) {
  let binary = "";
  for (let i = 0; i < certificate.length; i++) {
    binary += String.fromCharCode(certificate[i]);
  }
  return `data:application/octet-stream;base64,${btoa(binary)}`;
}

export async function setupShakaDRM(shakaPlayer, drmConfig = {}, logger = null) {
  // Keep the caller's prefix but force the "drm" debug category so all DRM
  // logs are toggled together.
  const log = new Logger(logger?.getPrefix?.() || "Shaka-DRM", "drm");

  if (!shakaPlayer || !drmConfig) {
    log.warn("Shaka DRM setup requires player and drmConfig");
    return;
  }

  // setupShakaDRM() runs again on every loadSource() call that reuses the
  // same underlying shaka.Player instance (see shaka.js setSrc()). Without
  // clearing previous filters first, each call would ADD another copy of
  // the request/response filters below on top of the ones already
  // registered by the previous load, so headers get injected N times and
  // (for FairPlay) the base64 response filter runs N times, corrupting the
  // response after the second load. Filters/config are fully rebuilt below,
  // so clearing first is always safe.
  try {
    shakaPlayer.getNetworkingEngine()?.clearAllRequestFilters();
    shakaPlayer.getNetworkingEngine()?.clearAllResponseFilters();
  } catch (error) {
    log.warn("Could not clear previous Shaka networking filters:", error);
  }

  const config = {
    drm: {
      servers: {},
      // Cap DRM networking (license + FairPlay SERVER_CERTIFICATE) at 15s.
      // Matches the HTML5/contrib-eme DRM_REQUEST_TIMEOUT_MS and the
      // abort timeout used by the on-demand FairPlay cert request filter
      // below. Does not change manifest/segment networking timeouts.
      retryParameters: {
        timeout: DRM_REQUEST_TIMEOUT_MS,
      },
    },
  };

  // Widevine
  if (
    drmConfig.widevine &&
    drmConfig.widevine.url &&
    drmConfig.widevine.licenseHeaders
  ) {
    config.drm.servers["com.widevine.alpha"] = drmConfig.widevine.url;
    log.info("Widevine license server configured", {
      endpoint: buildSafeEndpoint(drmConfig.widevine.url),
      ...buildSafeHeaderFlags(drmConfig.widevine.licenseHeaders),
    });
  }

  // FairPlay — do NOT eagerly fetch the certificate here. Shaka only issues
  // a SERVER_CERTIFICATE request after FairPlay is selected as the active
  // key system. If the shared URL-cache already has bytes (e.g. another
  // player fetched them), inject serverCertificate so no network call is
  // made; otherwise register serverCertificateUri and intercept that
  // on-demand request through the shared cache/coalesce helper.
  const requestFairplay = Boolean(
    drmConfig.fairplay &&
      drmConfig.fairplay.certificateUrl &&
      drmConfig.fairplay.url &&
      drmConfig.fairplay.licenseHeaders
  );

  let certificateUrl = null;
  let fairplayCertFromCache = null;

  if (requestFairplay) {
    certificateUrl =
      drmConfig.fairplay.certificateUrl ??
      "https://lic.staging.drmtoday.com/license-server-fairplay/cert/orca";
    fairplayCertFromCache = peekFairplayCertificate(certificateUrl);

    config.drm.servers["com.apple.fps"] = drmConfig.fairplay.url;
    config.drm.advanced = {
      "com.apple.fps": fairplayCertFromCache
        ? { serverCertificate: fairplayCertFromCache }
        : { serverCertificateUri: certificateUrl },
    };
    log.info("FairPlay license server configured", {
      endpoint: buildSafeEndpoint(drmConfig.fairplay.url),
      certificateEndpoint: buildSafeEndpoint(certificateUrl),
      certificateFromCache: Boolean(fairplayCertFromCache),
      ...buildSafeHeaderFlags(drmConfig.fairplay.licenseHeaders),
    });
  }

  // playready
  if (
    drmConfig.playready &&
    drmConfig.playready.url &&
    drmConfig.playready.licenseHeaders
  ) {
    config.drm.servers["com.microsoft.playready"] = drmConfig.playready.url;
    log.info("PlayReady license server configured", {
      endpoint: buildSafeEndpoint(drmConfig.playready.url),
      ...buildSafeHeaderFlags(drmConfig.playready.licenseHeaders),
    });
  }

  // isDRM=true but none of widevine/fairplay/playready in drmConfig had the
  // required fields (e.g. the caller only supplied "fairplay" while playback
  // is happening on a non-WebKit engine that can't use it — see the Windows
  // FairPlay-misconfiguration hypothesis investigated for this stream).
  // Without this check, Shaka loads with zero configured license servers:
  // EME finds no usable key system for the manifest's DRM info, and either
  // fails with a much-less-obvious error deep inside Shaka, or (if content
  // has mixed clear/encrypted segments) simply never decodes anything with
  // no error at all.
  if (Object.keys(config.drm.servers).length === 0) {
    log.error("No usable DRM key systems were built from drmConfig; refusing to configure Shaka without DRM servers.");
    throw createLocalizedError(ERROR_MESSAGE_KEYS.drmConfigMissing, {
      drmContext: { drmStage: "config-missing" },
    });
  }

  // Apply configuration
  shakaPlayer.configure(config);

  const netEngine = shakaPlayer.getNetworkingEngine();

  netEngine.registerRequestFilter(function (type, request) {
    if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
      const upfrontToken =
        drmConfig?.widevine?.licenseHeaders["x-dt-auth-token"] ??
        drmConfig?.fairplay?.licenseHeaders["x-dt-auth-token"] ??
        drmConfig?.playready?.licenseHeaders["x-dt-auth-token"];
      if (upfrontToken) {
        request.headers["x-dt-auth-token"] = upfrontToken;
      }
      const customData =
        drmConfig?.widevine?.licenseHeaders["x-dt-custom-data"] ??
        drmConfig?.fairplay?.licenseHeaders["x-dt-custom-data"] ??
        drmConfig?.playready?.licenseHeaders["x-dt-custom-data"];
      if (customData) {
        request.headers["x-dt-custom-data"] = customData;
      }
    }
  });

  // On-demand FairPlay cert: coalesce with the shared module cache (including
  // HTML5 getCertificate callers). Only registered when we still need a URI
  // fetch — cache-hit setups use serverCertificate and never hit this path.
  if (requestFairplay && !fairplayCertFromCache && certificateUrl) {
    const certContext = {
      drmStage: "fairplay-certificate",
      licenseEndpoint: buildSafeEndpoint(certificateUrl),
    };

    netEngine.registerRequestFilter(async (type, request) => {
      if (type !== shaka.net.NetworkingEngine.RequestType.SERVER_CERTIFICATE) {
        return;
      }

      const requestUrl = request.uris?.[0] || certificateUrl;

      await new Promise((resolve, reject) => {
        loadFairplayCertificate(
          requestUrl,
          (settle) => {
            fetchWithTimeout(requestUrl)
              .then(async (res) => {
                log.info("FairPlay certificate request completed", {
                  ...certContext,
                  status: res.status,
                });
                if (!res.ok) {
                  settle(
                    createLocalizedError(ERROR_MESSAGE_KEYS.drmLicenseRequestFailed, {
                      drmContext: certContext,
                    })
                  );
                  return;
                }
                settle(null, new Uint8Array(await res.arrayBuffer()));
              })
              .catch((error) => {
                log.error("FairPlay certificate request failed", {
                  ...certContext,
                  error: error?.message,
                });
                settle(
                  createLocalizedError(
                    error?.isTimeout
                      ? ERROR_MESSAGE_KEYS.drmRequestTimeout
                      : ERROR_MESSAGE_KEYS.drmLicenseRequestFailed,
                    { drmContext: certContext }
                  )
                );
              });
          },
          (error, certificate) => {
            if (error) {
              reject(error);
              return;
            }
            // Rewrite to a data URI so NetworkingEngine finishes without a
            // second trip to the FairPlay cert host. Cache already holds
            // the bytes for HTML5 / other players.
            request.uris = [certificateToDataUri(certificate)];
            log.info("FairPlay certificate ready (shared cache)", certContext);
            resolve();
          }
        );
      });
    });
  }

  if (requestFairplay) {
    netEngine.registerResponseFilter((type, response) => {
      if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
        if (shakaPlayer.keySystem() === "com.apple.fps") {
          let responseText = shaka.util.StringUtils.fromUTF8(response.data);
          response.data =
            shaka.util.Uint8ArrayUtils.fromBase64(responseText).buffer;
        }
      }
    });
  }

  // Error events are already handled by Shaka.prototype._bindShakaErrorEvents()
  // in shaka.js (which also classifies DRM errors and calls
  // reportPlayerError()); no separate listener is registered here to avoid
  // duplicate "error" listeners piling up across repeated loadSource() calls
  // on the same shaka.Player instance.

  log.info("Shaka DRM configured successfully");
}

export default {
  setupShakaDRM,
};
