/**
 * Extract display text and metadata from native/original errors
 * (Video.js, Shaka, EME, Error, xhr, etc.).
 */

export const ERROR_MESSAGE_KEYS = {
  shakaElementCreateFailed: "errorShakaElementCreateFailed",
  shakaInitFailed: "errorShakaInitFailed",
  drmPluginMissing: "errorDrmPluginMissing",
  playerNotInitialized: "errorPlayerNotInitialized",
  // DRM failure classification. Keep these distinct: a browser/CDM that
  // cannot use the requested key system (6001) is a different problem from a
  // license server rejecting a request (6007/6008), which is different again
  // from a license that has simply expired (6014) or content that is being
  // blocked by an output-protection requirement (HDCP, key status
  // "output-restricted"). Collapsing these into one generic message makes it
  // impossible for QA/support to tell "browser unsupported" apart from
  // "backend/license problem".
  drmKeySystemUnsupported: "errorDrmKeySystemUnsupported",
  drmLicenseRequestFailed: "errorDrmLicenseRequestFailed",
  drmLicenseResponseRejected: "errorDrmLicenseResponseRejected",
  drmLicenseExpired: "errorDrmLicenseExpired",
  drmOutputRestricted: "errorDrmOutputRestricted",
  drmPlaybackFailed: "errorDrmPlaybackFailed",
  // isDRM=true but drmConfig had no entry (widevine/fairplay/playready)
  // this browser/tech could actually use — e.g. the caller only supplied
  // "fairplay" while playback is happening on a non-WebKit engine. Without
  // this check, playback proceeds with zero configured license servers and
  // fails silently (content just never decodes, no error is ever raised).
  drmConfigMissing: "errorDrmConfigMissing",
  // A certificate/license request that never received *any* response
  // (server hung, connection dropped silently, etc.) rather than an
  // explicit HTTP error. Distinct from drmLicenseRequestFailed so QA can
  // tell "server responded with an error" apart from "server never
  // responded at all".
  drmRequestTimeout: "errorDrmRequestTimeout",
};

/**
 * Shaka error categories (shaka.util.Error.Category). Duplicated as plain
 * numbers so this module has no hard dependency on the shaka-player package.
 */
const SHAKA_ERROR_CATEGORY_NETWORK = 1;
const SHAKA_ERROR_CATEGORY_DRM = 6;

/**
 * Shaka NETWORK-category error codes (shaka.util.Error.Code) and Shaka's
 * request-type enum (shaka.net.NetworkingEngine.RequestType), duplicated as
 * plain maps for the same reason as above. Used only for human-readable
 * logging, never for control flow.
 */
const SHAKA_NETWORK_ERROR_CODE_NAMES = {
  1001: "UNSUPPORTED_SCHEME",
  1002: "BAD_HTTP_STATUS",
  1003: "HTTP_ERROR",
  1004: "TIMEOUT",
  1005: "MALFORMED_DATA_URI",
  1006: "UNKNOWN_DATA_URI_ENCODING",
  1007: "REQUEST_FILTER_ERROR",
  1008: "RESPONSE_FILTER_ERROR",
  1009: "MALFORMED_TEST_URI",
  1010: "UNEXPECTED_TEST_REQUEST",
  1011: "MAX_SEGMENTS_INFO_TIMEOUT",
};

const SHAKA_REQUEST_TYPE_NAMES = {
  0: "MANIFEST",
  1: "SEGMENT",
  2: "LICENSE",
  3: "APP",
  4: "TIMING",
  5: "SERVER_CERTIFICATE",
  6: "KEY",
  7: "ADS",
};

/**
 * Shaka error codes (shaka.util.Error.Code) that we classify explicitly.
 * See https://shaka-project.github.io/shaka-player/docs/api/shaka.util.Error.html
 *
 * 6001 REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE — the browser/CDM cannot
 *      satisfy the requested key system configuration. This is a
 *      browser/device compatibility problem, NOT a license server failure,
 *      and must never be reported to users/QA as "the license server is
 *      down". It is also the expected (benign) result when running in a
 *      Chromium build with no Widevine CDM, e.g. Playwright's bundled
 *      Chromium.
 * 6007 LICENSE_REQUEST_FAILED — the network request for a license failed
 *      (network error / non-2xx / CORS). See error.data for HTTP status.
 * 6008 LICENSE_RESPONSE_REJECTED — the CDM rejected the license response
 *      (bad format, bad signature, key mismatch, etc.).
 * 6014 EXPIRED — every key in the key status map is "expired". Do not
 *      confuse this with 6007/6008: the request/response succeeded at some
 *      point, the license has simply run out.
 * 6018 MIN_HDCP_VERSION_NOT_MATCH / 6019 ERROR_CHECKING_HDCP_VERSION — the
 *      output-protection (HDCP) requirement could not be satisfied. This is
 *      the Shaka-level equivalent of an EME key status of
 *      "output-restricted" (see classifyKeyStatuses below), and is a
 *      display/output-path issue, not a license failure.
 */
/**
 * Best-effort mapping from Shaka error category (non-DRM) to Video.js
 * MediaError codes (1 ABORTED, 2 NETWORK, 3 DECODE, 4 SRC_NOT_SUPPORTED).
 * DRM-category (6) errors are handled separately via
 * SHAKA_DRM_ERROR_CODE_MAP/classifyShakaError, which takes priority.
 */
const SHAKA_CATEGORY_TO_MEDIA_ERROR_CODE = {
  1: 2, // NETWORK
  2: 3, // TEXT
  3: 3, // MEDIA
  4: 3, // MANIFEST
  5: 1, // STREAMING
  6: 5, // DRM (overridden by classifyShakaError when code is recognized)
  7: 0, // PLAYER
  8: 0, // CAST
  9: 0, // STORAGE
};

const SHAKA_DRM_ERROR_CODE_MAP = {
  6001: ERROR_MESSAGE_KEYS.drmKeySystemUnsupported,
  6007: ERROR_MESSAGE_KEYS.drmLicenseRequestFailed,
  6008: ERROR_MESSAGE_KEYS.drmLicenseResponseRejected,
  6014: ERROR_MESSAGE_KEYS.drmLicenseExpired,
  6018: ERROR_MESSAGE_KEYS.drmOutputRestricted,
  6019: ERROR_MESSAGE_KEYS.drmOutputRestricted,
};

/**
 * EME MediaKeyStatus values that indicate playback is actively being
 * blocked (as opposed to "usable" or benign transient states like
 * "status-pending"/"released").
 * https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus
 */
const RESTRICTED_KEY_STATUSES = new Set([
  "output-restricted",
  "output-downscaled",
  "internal-error",
]);

export function createLocalizedError(messageKey, options = {}) {
  return {
    code: options.code ?? 0,
    messageKey,
    messageTokens: options.messageTokens,
    message: messageKey,
    ...(options.drmContext ? { drmContext: options.drmContext } : {}),
  };
}

/**
 * Classify a Shaka Player error (from the `error` event or `load().catch()`)
 * into one of our DRM message keys. Returns null for non-DRM-category
 * errors so callers can fall back to their existing (non-DRM) handling.
 *
 * @param {*} error - shaka.util.Error-like object ({ code, category, severity, ... })
 * @returns {{messageKey: string, code: number|null, category: number, severity: number|null}|null}
 */
export function classifyShakaError(error) {
  if (!error || typeof error !== "object" || error.category == null) {
    return null;
  }

  if (error.category !== SHAKA_ERROR_CATEGORY_DRM) {
    return null;
  }

  const messageKey =
    SHAKA_DRM_ERROR_CODE_MAP[error.code] ?? ERROR_MESSAGE_KEYS.drmPlaybackFailed;

  return {
    messageKey,
    code: error.code ?? null,
    category: error.category,
    severity: error.severity ?? null,
  };
}

/**
 * Classify an EME key status map (from Shaka's `getKeyStatuses()` or
 * contrib-eme's `eme-keystatuschange` event) into a DRM message key.
 * Returns null when nothing is actively restricted (e.g. all "usable", or
 * only transient statuses like "status-pending").
 *
 * @param {Object.<string,string>} keyStatusMap - keyId(hex) -> MediaKeyStatus
 * @returns {{messageKey: string, statuses: Object.<string,string>}|null}
 */
export function classifyKeyStatuses(keyStatusMap) {
  if (!keyStatusMap || typeof keyStatusMap !== "object") {
    return null;
  }

  const statuses = Object.values(keyStatusMap);
  if (!statuses.length) {
    return null;
  }

  // Mirrors Shaka's own EXPIRED (6014) condition: ALL keys expired, not just one.
  if (statuses.every((status) => status === "expired")) {
    return { messageKey: ERROR_MESSAGE_KEYS.drmLicenseExpired, statuses: keyStatusMap };
  }

  if (statuses.some((status) => RESTRICTED_KEY_STATUSES.has(status))) {
    return { messageKey: ERROR_MESSAGE_KEYS.drmOutputRestricted, statuses: keyStatusMap };
  }

  return null;
}

/**
 * Extract safe diagnostic context from a Shaka NETWORK-category error
 * (category 1: manifest/segment/license/key HTTP failures that are NOT
 * DRM-license-classification errors — e.g. 1002 BAD_HTTP_STATUS, 1003
 * HTTP_ERROR, 1004 TIMEOUT). Returns null for anything else.
 *
 * A bare "1003 - Shaka Error 1003" tells QA/support nothing about *which*
 * request failed (manifest? a specific segment? the license endpoint?) or
 * *why* the underlying request failed. Shaka puts that detail in
 * `error.data[]`, but the array layout differs per code — see
 * https://shaka-project.github.io/shaka-player/docs/api/shaka.util.Error.html#.Code
 * This normalizes the common codes into one shape, and always strips query
 * strings from the URI (license/segment URLs can carry auth tokens/signed
 * params) before it is logged or attached to player.error().
 *
 * @param {*} error - shaka.util.Error-like object ({ code, category, data })
 * @returns {{
 *   code: number|null,
 *   codeName: string|undefined,
 *   requestUri: string|null,
 *   requestType: number|undefined,
 *   requestTypeName: string|undefined,
 *   httpStatus?: number,
 *   nestedErrorMessage?: string,
 * }|null}
 */
export function describeShakaNetworkError(error) {
  if (!error || typeof error !== "object" || error.category !== SHAKA_ERROR_CATEGORY_NETWORK) {
    return null;
  }

  const data = Array.isArray(error.data) ? error.data : [];
  const context = {
    code: error.code ?? null,
    codeName: SHAKA_NETWORK_ERROR_CODE_NAMES[error.code],
    requestUri: typeof data[0] === "string" ? buildSafeEndpoint(data[0]) : null,
  };

  switch (error.code) {
    case 1002: // BAD_HTTP_STATUS: data = [uri, status, responseText, headers, requestType, responseURL]
      context.httpStatus = typeof data[1] === "number" ? data[1] : undefined;
      context.requestType = data[4];
      break;
    case 1003: { // HTTP_ERROR: data = [uri, error, requestType]
      const nested = data[1];
      context.nestedErrorMessage =
        nested && typeof nested === "object"
          ? nested.message || nested.name || undefined
          : nested != null
          ? String(nested)
          : undefined;
      context.requestType = data[2];
      break;
    }
    case 1004: // TIMEOUT: data = [uri, requestType]
      context.requestType = data[1];
      break;
    default:
      context.requestType = data[1];
      break;
  }

  if (typeof context.requestType === "number") {
    context.requestTypeName = SHAKA_REQUEST_TYPE_NAMES[context.requestType];
  }

  return context;
}

/**
 * Reduce a license/certificate headers object to boolean presence flags for
 * logging. Never log the actual header values: they carry DRMtoday
 * authorization/entitlement data (x-dt-auth-token, x-dt-custom-data).
 *
 * @param {Object.<string,string>} headers
 * @returns {{hasAuthToken: boolean, hasCustomData: boolean}}
 */
export function buildSafeHeaderFlags(headers) {
  const safeHeaders = headers && typeof headers === "object" ? headers : {};
  return {
    hasAuthToken: Boolean(safeHeaders["x-dt-auth-token"]),
    hasCustomData: Boolean(safeHeaders["x-dt-custom-data"]),
  };
}

/**
 * Strip query string/fragment from a URL before logging it, so accidental
 * secrets in query params (tokens, signatures) are never written to logs.
 * Origin + path is enough to identify which license/cert endpoint was hit.
 *
 * @param {string} url
 * @returns {string|null}
 */
export function buildSafeEndpoint(url) {
  if (typeof url !== "string" || !url) {
    return null;
  }
  return url.split(/[?#]/)[0];
}

/**
 * @param {*} error
 * @returns {string|null}
 */
export function extractErrorMessage(error) {
  if (error == null) {
    return null;
  }

  if (typeof error === "string") {
    return error;
  }

  // Video.js player "error" event: { detail: ... }
  if (error.detail != null) {
    const fromDetail = extractErrorMessage(error.detail);
    if (fromDetail) {
      return fromDetail;
    }
  }

  // Video.js MediaError / Shaka Util.Error / standard Error
  if (typeof error.message === "string" && error.message.trim()) {
    return error.message.trim();
  }

  // Shaka error without message string but with code
  if (error.code != null && error.category != null) {
    return String(error.code);
  }

  // videojs.xhr style error object
  if (error.statusText) {
    return String(error.statusText);
  }
  if (error.status != null) {
    return `HTTP ${error.status}`;
  }

  if (error instanceof Error) {
    return error.message || error.name || null;
  }

  try {
    const serialized = JSON.stringify(error);
    if (serialized && serialized !== "{}") {
      return serialized;
    }
  } catch {
    // fall through
  }

  return String(error);
}

/**
 * @param {*} error - Original error from player, Shaka, DRM, catch, etc.
 * @returns {{ code: number|string|null, message: string|null, source: * }}
 */
export function normalizePlayerError(error) {
  if (error && typeof error === "object" && error.messageKey) {
    return {
      code: error.code ?? null,
      message: error.messageKey,
      messageKey: error.messageKey,
      messageTokens: error.messageTokens,
      source: error,
    };
  }

  const message = extractErrorMessage(error);
  const code =
    error && typeof error === "object"
      ? error.code ?? error.type ?? error.name ?? null
      : null;

  return {
    code,
    message,
    messageTokens:
      error && typeof error === "object" ? error.messageTokens : undefined,
    source: error,
  };
}

/**
 * Map arbitrary error codes to Video.js MediaError codes (0–5).
 * @param {number|string|null} code
 * @returns {number}
 */
export function toMediaErrorCode(code) {
  const numeric = typeof code === "number" ? code : Number(code);
  if (Number.isInteger(numeric) && numeric >= 0 && numeric <= 5) {
    return numeric;
  }
  return 0;
}

/**
 * Build a readable message from a Shaka Util.Error (or similar).
 * @param {*} shakaError
 * @returns {string|null}
 */
export function formatShakaErrorMessage(shakaError) {
  if (shakaError == null) {
    return null;
  }

  const code = shakaError.code;
  const message = extractErrorMessage(shakaError);

  if (code != null && message) {
    return `${code} - ${message}`;
  }

  return message;
}

/**
 * Resolve user-facing text from the current Video.js MediaError state.
 *
 * @param {import("video.js").Player} player
 * @param {(key: string, tokens?: Array<string|number>) => string} [localize]
 * @returns {string|null}
 */
export function getErrorDisplayMessage(player, localize) {
  const vjsError = player?.error?.();
  if (!vjsError) {
    return null;
  }

  const rawMessage = vjsError.message?.trim();
  if (!rawMessage) {
    return null;
  }

  if (typeof localize === "function") {
    const tokens = Array.isArray(vjsError.messageTokens)
      ? vjsError.messageTokens
      : undefined;
    const localized = tokens ? localize(rawMessage, tokens) : localize(rawMessage);
    if (localized && localized !== rawMessage) {
      return localized;
    }
  }

  return rawMessage;
}

/**
 * Report an error through the native Video.js player.error() API.
 * Triggers the player "error" event for ErrorOverlay and host listeners.
 *
 * Also classifies Shaka DRM errors (6001/6007/6008/6014/6018/6019) into
 * distinct, user-facing message keys instead of a single generic DRM
 * failure message, and attaches a safe DRM diagnostic context (never
 * including token/customData values) to the resulting MediaError so host
 * apps and our own diagnostics can inspect `player.error().drmStage`,
 * `.keySystem`, etc. Video.js's MediaError does `Object.assign(this, code)`
 * when constructed from an object, so any extra enumerable fields we pass
 * here are preserved on `player.error()`.
 *
 * For Shaka NETWORK-category errors (manifest/segment/license HTTP
 * failures, e.g. 1002/1003/1004) that are not DRM classification errors, a
 * `networkContext` object (requestUri without query string, requestType,
 * httpStatus, etc. — see describeShakaNetworkError) is likewise attached as
 * `player.error().networkContext` so a bare "1003 - Shaka Error 1003" can
 * be traced back to which request failed.
 *
 * @param {import("video.js").Player} player
 * @param {*} error
 * @param {{
 *   drmStage?: string,
 *   keySystem?: string,
 *   activeTech?: string,
 *   licenseEndpoint?: string,
 *   certificateStatus?: string,
 *   licenseStatus?: string,
 *   hasAuthToken?: boolean,
 *   hasCustomData?: boolean,
 * }} [drmContext] - safe (non-secret) diagnostic context. Never pass raw
 *   header values, tokens, SPC/CKC payloads, or entitlement data here.
 * @returns {{ code: number|string|null, message: string|null, source: * }}
 */
export function reportPlayerError(player, error, drmContext = {}) {
  const shakaClassification = classifyShakaError(error);
  const networkContext = describeShakaNetworkError(error);
  const payload = normalizePlayerError(error);

  if (shakaClassification) {
    payload.code = shakaClassification.code;
    payload.message = shakaClassification.messageKey;
    payload.messageKey = shakaClassification.messageKey;
  } else if (error && error.category != null) {
    // Non-DRM-category Shaka error (network/media/manifest/etc.): keep the
    // existing best-effort human-readable message, no message classification,
    // but still map to a sensible Video.js MediaError code (Shaka's own
    // numeric error codes, e.g. 1002, are far outside MediaError's 0-5
    // range and would otherwise always collapse to "unknown" (0)).
    payload.message = formatShakaErrorMessage(error) || payload.message;
    payload.code = SHAKA_CATEGORY_TO_MEDIA_ERROR_CODE[error.category] ?? payload.code;
  }

  if (player && typeof player.error === "function") {
    const hasDrmContext = drmContext && Object.keys(drmContext).length > 0;
    player.error({
      code: toMediaErrorCode(payload.code),
      message: payload.message || undefined,
      messageTokens: payload.messageTokens,
      ...(shakaClassification
        ? {
            shakaErrorCode: shakaClassification.code,
            shakaErrorCategory: shakaClassification.category,
            shakaErrorSeverity: shakaClassification.severity,
          }
        : {}),
      ...(networkContext ? { networkContext } : {}),
      ...(hasDrmContext ? { ...drmContext } : {}),
    });
  }

  return payload;
}

/**
 * Clear Video.js error state and hide overlay if present.
 *
 * @param {import("video.js").Player} player
 */
export function clearPlayerError(player) {
  if (player && typeof player.error === "function") {
    player.error(null);
  }

  const overlay = player?.getChild?.("ErrorOverlay");
  if (overlay && typeof overlay.hide === "function") {
    overlay.hide();
  }
}
