// DRM Configuration for DRMtoday integration (HTML5 + videojs-contrib-eme path)
import videojs from "video.js";
import { Logger } from "./utils/logger.js";
import {
  buildSafeEndpoint,
  buildSafeHeaderFlags,
  classifyKeyStatuses,
  createLocalizedError,
  ERROR_MESSAGE_KEYS,
  reportPlayerError,
} from "./helpers/errorMessages.js";
import { loadFairplayCertificate } from "./helpers/fairplayCertificateCache.js";

export const DRM_TYPES = {
  WIDEVINE: "com.widevine.alpha",
  FAIRPLAY: "com.apple.fps.1_0",
  PLAYREADY: "com.microsoft.playready",
};

// videojs.xhr (the "xhr" package) treats this as a client-side abort once
// elapsed, calling back with an Error rather than leaving the request
// pending forever.
const DRM_REQUEST_TIMEOUT_MS = 15000;

function isTimeoutError(err) {
  if (!err) {
    return false;
  }
  return err.code === "ETIMEDOUT" || /timeout/i.test(err.message || "");
}

// contrib-eme's FairPlay support (src: node_modules/videojs-contrib-eme/src/fairplay.js)
// only ever uses the legacy prefixed WebKit EME API (video.webkitSetMediaKeys()
// / session.createSession()), never the modern unprefixed EME API. That legacy
// API requires the app to explicitly pass a "contentId" string, which gets
// embedded directly into the SPC the OS/CDM sends to the license server
// (see concatInitDataIdAndCertificate() in that file). contrib-eme's
// defaultGetContentId() extracts only the *hostname* of the "skd://..." URI
// (e.g. "drmtoday" for DRMtoday's "skd://drmtoday?keyId=<uuid>" convention),
// discarding the "keyId=<uuid>" query string that actually identifies which
// key to serve. This was confirmed as the real root cause on a real iPhone
// Safari device: DRMtoday's license-server-fairplay responded HTTP 400 with
// header "x-dt-resp-code: 10003" (content_keys_not_found) — i.e. it received
// a syntactically valid request but could not resolve any key from the
// (hostname-only) content id embedded in the SPC.
//
// DRMtoday's own integration manual ("all-DRMtoday.pdf", FairPlay chapter)
// confirms exactly this, in a note directly under the license request spec:
// "Fairplay example code provided by Apple uses only the host component of
// the URI while DRMtoday requires the complete SKD URI including protocol
// and query parameters." Their own Video.js + videojs-contrib-eme sample in
// the same manual implements getContentId as a pure identity function
// (`return initData;`, no stripping at all) — so the fix is to *not*
// transform the string at all, not to extract just the query string.
//
// That same official sample also sends getLicense's body as the raw
// keyMessage (SPC) bytes with no extra headers beyond the configured
// dt-custom-data/dt-auth-token — matching a real network capture of a
// *working* Mac Safari session (via the separate Shaka DRM path in
// shaka-drm-config.js) and ruling out an earlier, incorrect theory that
// license-server-fairplay needed a "spc=<base64>&<contentId>"
// form-urlencoded body (that documented HTTP-level parameter name is for
// implementers building requests from scratch, not for this library).
function fairplayGetContentId(initDataString) {
  return initDataString;
}

/**
 * Build the FairPlay entry for contrib-eme's keySystems map.
 *
 * Certificate fetch runs only from getCertificate() — i.e. after the
 * browser/EME has selected FairPlay — never just because fairplay fields
 * were present in drmConfig. The URL-keyed module cache (see
 * fairplayCertificateCache.js) reuses the bytes across key sessions,
 * loadSource(), and other players on the page.
 */
function buildFairplayKeySystemConfig(fairplayConfig, log) {
  const certificateEndpoint = buildSafeEndpoint(fairplayConfig.certificateUrl);
  const licenseEndpoint = buildSafeEndpoint(fairplayConfig.url);

  return {
    // NOTE: this intentionally overrides contrib-eme's
    // defaultGetContentId() (hostname-only, e.g. "drmtoday"). DRMtoday
    // needs the *complete, untouched* "skd://drmtoday?keyId=..." string
    // embedded in the SPC — see the comment above fairplayGetContentId()
    // for details (confirmed via DRMtoday's own integration manual).
    getContentId: function (emeOptions, initDataString) {
      return fairplayGetContentId(initDataString);
    },
    getCertificate: function (emeOptions, callback) {
      const context = {
        drmStage: "fairplay-certificate",
        licenseEndpoint: certificateEndpoint,
      };

      loadFairplayCertificate(
        fairplayConfig.certificateUrl,
        (settle) => {
          videojs.xhr(
            {
              url: fairplayConfig.certificateUrl,
              method: "GET",
              responseType: "arraybuffer",
              // Without a timeout, a server that never responds (as opposed
              // to one that responds with an error) leaves this callback
              // pending forever: no license request happens, no error is
              // ever shown, playback just silently never starts.
              timeout: DRM_REQUEST_TIMEOUT_MS,
            },
            (err, response, responseBody) => {
              const status = response?.statusCode;
              log.info("FairPlay certificate request completed", {
                ...context,
                status,
              });

              if (
                err ||
                !responseBody ||
                (typeof status === "number" && status >= 400)
              ) {
                log.error("FairPlay certificate request failed", {
                  ...context,
                  status,
                  error: err?.message,
                });
                settle(
                  createLocalizedError(
                    isTimeoutError(err)
                      ? ERROR_MESSAGE_KEYS.drmRequestTimeout
                      : ERROR_MESSAGE_KEYS.drmLicenseRequestFailed,
                    { drmContext: context }
                  )
                );
                return;
              }

              settle(null, responseBody);
            }
          );
        },
        (error, certificate) => {
          if (error) {
            // loadFairplayCertificate already dropped the failed entry.
            // Prefer the localized error from doFetch (timeout / HTTP /
            // network). Bare Errors from the shared helper (e.g. empty or
            // unparsable bytes) are treated as an invalid cert response.
            if (error.messageKey) {
              callback(error);
              return;
            }
            callback(
              createLocalizedError(ERROR_MESSAGE_KEYS.drmLicenseResponseRejected, {
                drmContext: context,
              })
            );
            return;
          }

          log.info("FairPlay certificate ready", context);
          callback(null, certificate);
        }
      );
    },
    getLicense: function (emeOptions, contentId, keyMessage, callback) {
      const context = { drmStage: "fairplay-license", licenseEndpoint };
      // Body is the raw SPC (keyMessage) bytes, no extra wrapping — this
      // matches DRMtoday's own official Video.js + videojs-contrib-eme
      // sample (see fairplayGetContentId() comment above) and a real
      // network capture of a working Mac Safari session. The confirmed
      // root cause of the HTTP 400 seen on iPhone was DRMtoday responding
      // with "x-dt-resp-code: 10003" (content_keys_not_found) due to the
      // contentId problem fixed in fairplayGetContentId() above, not the
      // request body shape.
      videojs.xhr(
        {
          url: fairplayConfig.url,
          method: "POST",
          responseType: "text",
          body: keyMessage,
          headers: {
            ...(fairplayConfig.licenseHeaders ?? {}),
          },
          timeout: DRM_REQUEST_TIMEOUT_MS,
        },
        (err, response, responseBody) => {
          const status = response?.statusCode;
          log.info("FairPlay license request completed", { ...context, status });

          if (err || !responseBody || (typeof status === "number" && status >= 400)) {
            log.error("FairPlay license request failed", {
              ...context,
              status,
              error: err?.message,
            });
            callback(
              createLocalizedError(
                isTimeoutError(err)
                  ? ERROR_MESSAGE_KEYS.drmRequestTimeout
                  : ERROR_MESSAGE_KEYS.drmLicenseRequestFailed,
                { drmContext: context }
              )
            );
            return;
          }

          try {
            // Some DRMtoday deployments wrap the base64 CKC in double
            // quotes (a JSON string literal returned with a non-JSON
            // content-type); strip those before decoding.
            const trimmedBody = responseBody.trim();
            const base64Body =
              trimmedBody.startsWith('"') && trimmedBody.endsWith('"')
                ? trimmedBody.slice(1, -1)
                : trimmedBody;
            const rawLicenseString = atob(base64Body);
            const data = new Uint8Array(rawLicenseString.length);
            for (let i = 0; i < rawLicenseString.length; ++i) {
              data[i] = rawLicenseString.charCodeAt(i);
            }
            callback(null, data.buffer);
          } catch (parseError) {
            // Most common cause: responseBody wasn't valid base64 (e.g. an
            // HTML error page or JSON error body returned with a 200
            // status). Surface this as a distinct classification from a
            // network failure so QA doesn't chase the wrong root cause.
            log.error(
              "FairPlay license response could not be decoded as base64:",
              parseError
            );
            callback(
              createLocalizedError(ERROR_MESSAGE_KEYS.drmLicenseResponseRejected, {
                drmContext: context,
              })
            );
          }
        }
      );
    },
  };
}

export function setupDRM(
  player,
  source,
  drmConfig = {},
  logger = null,
  containerId = null
) {
  // Keep the caller's prefix (player/container tag) but force the "drm"
  // debug category so all DRM logs are toggled together.
  const log = new Logger(logger?.getPrefix?.() || "DRM", "drm");

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

  const DRMConfig = {};
  // const drmSupport = detectDRMSupport(); TODO: add detectDRMSupport

  // Default DRMtoday configuration if not provided
  if (drmConfig.widevine) {
    DRMConfig.widevine = {
      url:
        drmConfig.widevine.url ??
        "https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/?specConform=true",
      licenseHeaders: drmConfig.widevine.licenseHeaders ?? {},
    };
  }

  if (drmConfig.fairplay) {
    DRMConfig.fairplay = {
      certificateUrl: drmConfig.fairplay.certificateUrl ?? "https://lic.staging.drmtoday.com/license-server-fairplay/cert/orca",
      url: drmConfig.fairplay.url ?? "https://lic.staging.drmtoday.com/license-server-fairplay/",
      licenseHeaders: drmConfig.fairplay.licenseHeaders ?? {}
    };
  }

  if (drmConfig.playready) {
    DRMConfig.playready = {
      url:
        drmConfig.playready.url ??
        "https://lic.staging.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx",
      licenseHeaders: drmConfig.playready.licenseHeaders ?? {},
    };
  }

  log.info("DRM configuration resolved", {
    hasWidevine: Boolean(DRMConfig.widevine),
    hasFairplay: Boolean(DRMConfig.fairplay),
    hasPlayready: Boolean(DRMConfig.playready),
    widevine: DRMConfig.widevine
      ? {
          endpoint: buildSafeEndpoint(DRMConfig.widevine.url),
          ...buildSafeHeaderFlags(DRMConfig.widevine.licenseHeaders),
        }
      : undefined,
    fairplay: DRMConfig.fairplay
      ? {
          endpoint: buildSafeEndpoint(DRMConfig.fairplay.url),
          certificateEndpoint: buildSafeEndpoint(DRMConfig.fairplay.certificateUrl),
          ...buildSafeHeaderFlags(DRMConfig.fairplay.licenseHeaders),
        }
      : undefined,
    playready: DRMConfig.playready
      ? {
          endpoint: buildSafeEndpoint(DRMConfig.playready.url),
          ...buildSafeHeaderFlags(DRMConfig.playready.licenseHeaders),
        }
      : undefined,
  });

  // Setup EME (Encrypted Media Extensions). `player.eme` is installed by
  // importing "videojs-contrib-eme" (see src/index.js) — if that import is
  // ever removed/externalized without also loading the plugin on the host
  // page, this branch is what protects us from silently playing back
  // unencrypted/failing DRM content.
  if (player.eme) {
    const keySystems = buildKeySystemsConfig(DRMConfig, log);

    player.ready(() => {
      player.eme();

      // Configure key systems
      if (Object.keys(keySystems).length > 0) {
        try {
          player.src({
            src: source.url,
            keySystems,
          });
        } catch (error) {
          log.error("Error setting DRM source:", error);
          reportPlayerError(player, error, { drmStage: "src", activeTech: "html5" });
        }
      } else {
        // isDRM=true but none of widevine/fairplay/playready in drmConfig had
        // the required fields (e.g. only a DRM type this tech can't use was
        // supplied). Without this, playback proceeds with zero configured
        // key systems and fails silently — no license request is ever made,
        // no error is ever shown, content just never decodes.
        log.error("No usable DRM key systems were built from drmConfig; refusing to load without DRM headers.");
        reportPlayerError(
          player,
          createLocalizedError(ERROR_MESSAGE_KEYS.drmConfigMissing),
          { drmStage: "config-missing", activeTech: "html5" }
        );
      }
    });

    addDRMEventListeners(player, log, containerId);
  } else {
    log.error("videojs-contrib-eme plugin is required for DRM support");
    reportPlayerError(
      player,
      createLocalizedError(ERROR_MESSAGE_KEYS.drmPluginMissing),
      { drmStage: "plugin-missing", activeTech: "html5" }
    );
  }
}

function buildKeySystemsConfig(drmConfig, log = new Logger("DRM", "drm")) {
  const keySystems = {};

  // Widevine configuration
  if (drmConfig.widevine && drmConfig.widevine.url && drmConfig.widevine.licenseHeaders) {
    keySystems[DRM_TYPES.WIDEVINE] = {
      // initDataTypes: ['cenc', 'webm'],
      url: drmConfig.widevine.url,
      licenseHeaders: drmConfig.widevine.licenseHeaders ?? {},
      // audioContentType: 'audio/mp4; codecs="mp4a.40.2"',
      // videoContentType: 'video/mp4; codecs="avc1.640028"',
      // audioRobustness: "SW_SECURE_CRYPTO",
      // videoRobustness: "SW_SECURE_CRYPTO",
    };
  }

  // FairPlay configuration — registered for the browser/EME to pick when
  // FairPlay is actually available. Certificate fetch is deferred to
  // getCertificate() inside this config (see buildFairplayKeySystemConfig).
  if (
    drmConfig.fairplay &&
    drmConfig.fairplay.certificateUrl &&
    drmConfig.fairplay.url &&
    drmConfig.fairplay.licenseHeaders
  ) {
    keySystems[DRM_TYPES.FAIRPLAY] = buildFairplayKeySystemConfig(
      drmConfig.fairplay,
      log
    );
  }

  // PlayReady configuration
  if (drmConfig.playready && drmConfig.playready.url) {
    keySystems[DRM_TYPES.PLAYREADY] = {
      url: drmConfig.playready.url,
      licenseHeaders: drmConfig.playready.licenseHeaders || {},
    };
  }

  return keySystems;
}

/**
 * Wire diagnostics for videojs-contrib-eme. Important: contrib-eme fires its
 * custom events ("keystatuschange", "keysessioncreated",
 * "licenserequestattempted") on the *tech* (player.tech(true)), not on the
 * player — there is no "eme-error"/"eme-keystatuschange"/
 * "eme-keysessioncreated" event in contrib-eme's public API. DRM transport
 * errors (cert/license failures, MediaKeys/session failures) are reported
 * exclusively via the standard Video.js `player.error()` call, which our
 * existing `player.on("error", ...)` handling (index.js, ErrorOverlay)
 * already covers — no extra listener needed for that here.
 *
 * @see https://github.com/videojs/videojs-contrib-eme#events
 */
function addDRMEventListeners(player, logger = null, containerId = null) {
  const log = new Logger(logger?.getPrefix?.() || "DRM", "drm");

  const bindToTech = () => {
    const tech = player.tech && player.tech(true);
    if (!tech || typeof tech.on !== "function" || tech._drmDiagnosticsBound) {
      return;
    }
    tech._drmDiagnosticsBound = true;

    // NOTE: FairPlay via initLegacyFairplay() uses the legacy WebKit API
    // (webkitneedkey/webkitkeymessage/webkitkeyadded/webkitkeyerror), which
    // has no MediaKeySession.keyStatuses concept — this event only ever
    // fires for Widevine/PlayReady sessions using the standard EME path.
    tech.on("keystatuschange", (event) => {
      const status = event?.status;
      log.info("EME key status change:", { keyId: event?.keyId, status });

      const classification = classifyKeyStatuses({ [event?.keyId ?? "unknown"]: status });
      if (classification) {
        log.error("EME key status indicates blocked playback:", {
          status,
          messageKey: classification.messageKey,
        });
        reportPlayerError(
          player,
          createLocalizedError(classification.messageKey),
          { drmStage: "key-status", keyStatus: status, activeTech: "html5" }
        );
      }
    });

    tech.on("keysessioncreated", () => {
      log.info("EME key session created");
    });

    tech.on("licenserequestattempted", () => {
      log.info("EME license request attempted");
    });
  };

  // Tech should already exist by the time setupDRM() runs (called from
  // index.js inside player.ready()), but guard for the rare case it hasn't
  // been created yet.
  if (player.tech && player.tech(true)) {
    bindToTech();
  } else {
    player.ready(bindToTech);
  }

  if (containerId) {
    player.on("error", () => {
      const error = player.error();
      if (!error) {
        return;
      }
      const customEvent = new CustomEvent(
        `${containerId}-videojs-drm-player:drm-error`,
        {
          detail: { error, player },
        }
      );
      document.dispatchEvent(customEvent);
    });
  }
}