import videojs from "video.js";
import shaka from "shaka-player";
import { setupShakaDRM } from "./shaka-drm-config";
import setupQualityTracks from "./setup-quality-tracks";
import setupTextTracks from "./setup-text-tracks";
import setupAudioTracks from "./setup-audio-tracks";
import { DEFAULT_BANDWIDTH_ESTIMATE } from "../constants/bandwidth.js";
import { Logger } from "../utils/logger.js";
import {
  classifyKeyStatuses,
  classifyShakaError,
  createLocalizedError,
  describeShakaNetworkError,
  ERROR_MESSAGE_KEYS,
  reportPlayerError,
} from "../helpers/errorMessages.js";

const Html5 = videojs.getTech("Html5");

// Expose Shaka library globally (only once)
if (typeof window !== "undefined" && !window.__shaka) {
  window.__shaka = shaka;
  const initLogger = new Logger("Shaka-Init", "shaka");
  initLogger.info("Exposed Shaka library to window.__shaka");
  initLogger.info("Version:", shaka.Player.version);
}

class Shaka extends Html5 {
  constructor(options, ready) {
    super(options, ready);

    this.vjsPlayer = videojs(options.playerId);
    this.shaka_ = null;
    // Create logger instance for this tech
    this.logger = new Logger(`Shaka-${this.getContainerTag() || 'Tech'}`, "shaka");

    this.player_.ready(() => {
      this.player_.addClass("vjs-shaka");
    });
  }

  static isSupported() {
    return !!window.MediaSource && shaka.Player.isBrowserSupported();
  }

  static canPlaySource(sourceObj, type) {
    const dashTypeRE = /^(application\/dash\+xml|application\/x-mpegURL)/i;

    if (dashTypeRE.test(sourceObj.type)) {
      return "probably";
    }

    return "";
  }

  createEl() {
    this.el_ = Html5.prototype.createEl.apply(this, arguments);

    // Install built-in polyfills to patch browser incompatibilities.
    shaka.polyfill.installAll();

    // Set debug log level
    if (shaka.log) {
      if (this.options_.debug) {
        shaka.log.setLevel(shaka.log.Level.DEBUG);
      } else {
        shaka.log.setLevel(shaka.log.Level.ERROR);
      }
    }

    this.el_.tech = this;

    return this.el_;
  }

  // Helper method to get container tag for logging
  getContainerTag() {
    return this.containerId_ ? `[${this.containerId_}]` : "";
  }

  async initializeShaka() {
    if (!this.shaka_ && this.el_) {
      this.logger.info("Initializing Shaka Player...");
      try {
        this.shaka_ = new shaka.Player();

        this.shaka_.getNetworkingEngine().registerResponseFilter((type, response) => {
          if (type === shaka.net.NetworkingEngine.RequestType.MANIFEST) {
            const decoder = new TextDecoder('utf-8');
            let manifestText = decoder.decode(response.data);
            manifestText = manifestText.replace(/GROUP-ID="audio-[^"]*"/g, 'GROUP-ID="audio-main"');
            manifestText = manifestText.replace(/AUDIO="audio-[^"]*"/g, 'AUDIO="audio-main"');

            const encoder = new TextEncoder();
            response.data = encoder.encode(manifestText);
          }
        });
        await this.shaka_.attach(this.el_);
        this.shaka_.configure('streaming.maxDisabledTime', 0);

        this.logger.info('Shaka configuration:', this.shaka_.getConfiguration());

        // Log ABR config + restrictions after configuration
        this.logger.info('Effective config abr:', this.shaka_.getConfiguration().abr);
        this.logger.info('Effective config restrictions:', this.shaka_.getConfiguration().restrictions);

        this.logger.info('Player initialized');
        this._bindShakaErrorEvents();
        this._bindShakaKeyStatusEvents();
      } catch (error) {
        this.logger.error('Error initializing Shaka Player:', error);
        this._lastShakaInitError = error;
        if (this.vjsPlayer) {
          reportPlayerError(this.vjsPlayer, error);
        }
      }
    } else if (!this.el_) {
      this.logger.warn('Element not created, cannot initialize Shaka Player');
    }
  }

  async setSrc(src) {
    const me = this;

    // Ensure element and Shaka Player are initialized
    if (!this.el_) {
      this.logger.info("Element has not been created, waiting...");
      // Add retry up to 3 times when waiting for this.el_
      if (typeof this._elRetryCount === "undefined") {
        this._elRetryCount = 0;
      }
      this._elRetryCount++;
      if (this._elRetryCount > 3) {
        this.logger.error(
          "Cannot create element for Shaka Player after 3 retries."
        );
        reportPlayerError(
          this.vjsPlayer,
          createLocalizedError(ERROR_MESSAGE_KEYS.shakaElementCreateFailed)
        );
        return;
      }
      setTimeout(() => {
        me.setSrc(src);
      }, 100);
      return;
    }

    if (!this.shaka_) {
      await this.initializeShaka();
    }

    if (!this.shaka_) {
      this.logger.warn("Cannot initialize Shaka Player");
      reportPlayerError(
        this.vjsPlayer,
        this._lastShakaInitError ||
          createLocalizedError(ERROR_MESSAGE_KEYS.shakaInitFailed)
      );
      return;
    }

    const shakaOptions = this.options_.configuration || {};
    const defaultBandwidthEstimate =
      this.options_.defaultBandwidthEstimate ?? DEFAULT_BANDWIDTH_ESTIMATE;

    if (!shakaOptions.abr) {
      shakaOptions.abr = {
        enabled: true,
      };
    }

    shakaOptions.abr.defaultBandwidthEstimate =
      shakaOptions.abr.defaultBandwidthEstimate ?? defaultBandwidthEstimate;

    this.shaka_.configure({
      abr: shakaOptions.abr,
    });

    // useMMS (Shaka path): play FairPlay via MSE/EME on desktop Safari
    // instead of Safari's built-in HLS player ("src=" mode). By default
    // Shaka sets streaming.useNativeHlsForFairPlay=true; in that mode Shaka
    // never sees the manifest, so getVariantTracks() returns a single
    // synthesized track and the quality menu shows only "Auto" + one entry.
    // Setting useNativeHlsForFairPlay=false enables the full quality list.
    // No effect on iPhone (Shaka tech is not used there) or non-Safari
    // browsers (FairPlay is Safari-only). Same public option also drives
    // VHS Managed Media Source on the HTML5 path in index.js.
    if (this.options_.useMMS) {
      try {
        this.shaka_.configure('streaming.useNativeHlsForFairPlay', false);
        this.logger.info(
          'useMMS: streaming.useNativeHlsForFairPlay=false (FairPlay via MSE/EME on desktop Safari)'
        );
      } catch (error) {
        this.logger.warn('useMMS: failed to configure useNativeHlsForFairPlay:', error);
      }
    }

    if (this.options_.drmConfig) {
      try {
        await setupShakaDRM(this.shaka_, this.options_.drmConfig, this.logger);
      } catch (error) {
        this.logger.error("Error setting up Shaka DRM:", error);
        reportPlayerError(this.vjsPlayer, error, {
          drmStage: "shaka-drm-setup",
          activeTech: "shaka",
        });
      }
    }

    // Log load request
    this.logger.info("Load request:", {
      uri: src,
      isHls: src.includes('.m3u8')
    });

    // Log ABR config + restrictions before load
    this.logger.info("Effective config abr:", this.shaka_.getConfiguration().abr);
    this.logger.info("Effective config restrictions:", this.shaka_.getConfiguration().restrictions);

    this.shaka_.addEventListener("buffering", function (event) {
      if (event.buffering) {
        me.vjsPlayer.trigger("waiting");
      } else {
        me.vjsPlayer.trigger("playing");
      }
    });

    this._bindShakaErrorEvents();
    this._bindShakaKeyStatusEvents();

    // Adaptation event logging
    this.shaka_.addEventListener("adaptation", function () {
      me.logger.info("adaptation event fired");
      me.logVariants(me.shaka_, '(after adaptation)');
    });

    this.shaka_
      .load(src)
      .then(function () {
        me.initShakaMenus();

        // Log variants at startup
        me.logVariants(me.shaka_, '(at startup)');

        // Start stats logging (first 30 seconds)
        me.startStatsLogging(me.shaka_);

        // Expose instances after source is loaded
        if (me.containerId_) {
          me.exposeShakaInstances();
        }
      })
      .catch(function (error) {
        me.logger.error("Error loading source:", error);
        const networkContext = describeShakaNetworkError(error);
        if (networkContext) {
          me.logger.error("Shaka network error details:", networkContext);
        }
        try {
          me.retriggerError(error);
        } catch (retriggerError) {
          me.logger.error("Error in retriggerError:", retriggerError);
        }
      });
  }

  dispose() {
    // Clear stats logging timer
    if (this._statsTimer) {
      clearInterval(this._statsTimer);
      this._statsTimer = null;
    }

    if (this.shaka_) {
      this.logger.info('Unloading Shaka Player');

      // Detach Shaka player from element BEFORE unloading
      // This ensures the element is still valid for child components
      try {
        this.shaka_.detach();
      } catch (e) {
        // Ignore if already detached or if there is an error
        this.logger.warn('Error detaching Shaka player:', e);
      }

      // Unload and destroy Shaka player
      try {
        this.shaka_.unload();
      } catch (e) {
        this.logger.warn('Error unloading Shaka player:', e);
      }

      try {
        this.shaka_.destroy();
      } catch (e) {
        this.logger.warn('Error destroying Shaka player:', e);
      }

      this.shaka_ = null;
      this._shakaErrorBound = false;
      this._onShakaError = null;
      this._shakaKeyStatusBound = false;
      this._onShakaKeyStatus = null;
      this._tracksChangedBound = false;
      this._onShakaTracksChanged = null;
      // super.dispose();
    }
  }

  _bindShakaErrorEvents() {
    if (!this.shaka_ || this._shakaErrorBound) {
      return;
    }

    this._onShakaError = (event) => {
      this.logger.error("Shaka error event:", event.detail);
      const networkContext = describeShakaNetworkError(event.detail);
      if (networkContext) {
        this.logger.error("Shaka network error details:", networkContext);
      }
      try {
        this.retriggerError(event.detail);
      } catch (error) {
        this.logger.error("Error in retriggerError:", error);
        reportPlayerError(this.vjsPlayer, event.detail || error);
      }
    };

    this.shaka_.addEventListener("error", this._onShakaError);
    this._shakaErrorBound = true;
  }

  /**
   * Monitor EME key statuses so that DRM output-protection/expiration
   * problems are classified and surfaced even when Shaka doesn't (or can't)
   * raise a corresponding error event. Two concrete cases this matters for:
   *   - "output-restricted" (e.g. missing HDCP): Shaka does not treat this
   *     as a fatal error by default — playback can silently stall on a
   *     variant that will never become playable.
   *   - "expired": Shaka only raises EXPIRED (6014) once ALL keys expire;
   *     a single expired key on a multi-key manifest can already break
   *     playback before that fires.
   */
  _bindShakaKeyStatusEvents() {
    if (!this.shaka_ || this._shakaKeyStatusBound) {
      return;
    }

    this._onShakaKeyStatus = () => {
      try {
        const keyStatusMap = this.shaka_.getKeyStatuses();
        const classification = classifyKeyStatuses(keyStatusMap);
        if (!classification) {
          return;
        }

        this.logger.error("Shaka key status indicates blocked/expired playback:", {
          statuses: keyStatusMap,
          messageKey: classification.messageKey,
        });
        reportPlayerError(
          this.vjsPlayer,
          createLocalizedError(classification.messageKey),
          {
            drmStage: "key-status",
            keySystem: this.shaka_.keySystem?.() || undefined,
            activeTech: "shaka",
          }
        );
      } catch (error) {
        this.logger.warn("Error while classifying Shaka key statuses:", error);
      }
    };

    this.shaka_.addEventListener("keystatuschanged", this._onShakaKeyStatus);
    this._shakaKeyStatusBound = true;
  }

  initShakaMenus() {
    // Setup quality tracks, text tracks, audio tracks
    this.logger.info('Setting up Shaka menus...');

    try {
      // Setup quality tracks (renditions)
      setupQualityTracks(this, this.shaka_);
      this.logger.info('Quality tracks setup completed');

      // Setup text tracks (subtitles)
      setupTextTracks(this, this.shaka_);
      this.logger.info('Text tracks setup completed');

      // Setup audio tracks
      setupAudioTracks(this, this.shaka_);
      this.logger.info('Audio tracks setup completed');
    } catch (error) {
      this.logger.error('Error setting up Shaka:', error);
      reportPlayerError(this.vjsPlayer, error);
    }

    // The variant list is not final at load() time: with multi-key DRM,
    // variants can be restricted/unrestricted when key statuses arrive
    // (slightly after load() resolves), and Shaka announces every such
    // change with "trackschanged". Without re-emitting the quality data
    // here, the settings menu keeps whatever list existed at load time —
    // which can be empty (menu shows only "Auto") if keys were still
    // pending at that moment.
    if (!this._tracksChangedBound) {
      const me = this;
      this._onShakaTracksChanged = function () {
        me.logger.info('trackschanged event fired; refreshing quality tracks');
        me.refreshQualityTracks();
      };
      this.shaka_.addEventListener('trackschanged', this._onShakaTracksChanged);
      this._tracksChangedBound = true;
    }
  }

  /**
   * Re-emit "loadedqualitydata" from the current Shaka variant list.
   * Called from the trackschanged handler above, and also usable as a
   * pull-based fallback by UI components (e.g. the settings popup asks for
   * a refresh when its quality list is still empty at menu-open time).
   */
  refreshQualityTracks() {
    if (!this.shaka_) {
      return;
    }
    try {
      setupQualityTracks(this, this.shaka_);
    } catch (error) {
      this.logger.warn('Error refreshing quality tracks:', error);
    }
  }

  retriggerError(event) {
    // Classification (DRM code -> message key, category -> MediaError code)
    // and the actual player.error() call both happen inside
    // reportPlayerError(). We only need to supply diagnostic context here.
    reportPlayerError(this.vjsPlayer, event, {
      drmStage: "shaka-error",
      keySystem: this.shaka_?.keySystem?.() || undefined,
      activeTech: "shaka",
    });

    const classification = classifyShakaError(event);
    if (classification) {
      this.logger.error("Shaka DRM error classified:", {
        code: classification.code,
        messageKey: classification.messageKey,
        severity: classification.severity,
      });
    }

    if (event.severity !== shaka.util.Error.Severity.CRITICAL) {
      this.logger.warn("Non-critical Shaka error (player may try to recover):", {
        code: event.code,
        message: event.message,
      });
    }
  }

  getShaka_() {
    return this.shaka_;
  }

  // Log available variants + currently active
  logVariants(player, tag = '') {
    if (!player) return;

    try {
      const variants = player.getVariantTracks();
      const rows = variants
        .map(t => ({
          id: t.id,
          bw: t.bandwidth,
          w: t.width,
          h: t.height,
          codecs: t.codecs,
          active: t.active,
          language: t.language,
        }))
        .sort((a, b) => a.bw - b.bw);

      this.logger.info(`Variants ${tag}`, rows);
    } catch (error) {
      this.logger.warn("Error logging variants:", error);
    }
  }

  // Log ABR stats periodically during playback (initial 30 seconds)
  startStatsLogging(player) {
    if (!player) return;

    // Clear any existing timer
    if (this._statsTimer) {
      clearInterval(this._statsTimer);
      this._statsTimer = null;
    }

    let statsCount = 0;
    const maxStatsLogs = 15; // 30 seconds / 2 seconds = 15 logs

    this._statsTimer = setInterval(() => {
      try {
        const s = player.getStats();
        this.logger.info("Stats", {
          estBw: s.estimatedBandwidth,
          playTime: s.playTime,
          bufferingTime: s.bufferingTime,
          variantBw: s.streamBandwidth,
          droppedFrames: s.droppedFrames,
        });

        statsCount++;
        if (statsCount >= maxStatsLogs) {
          clearInterval(this._statsTimer);
          this._statsTimer = null;
        }
      } catch (error) {
        this.logger.warn("Error getting stats:", error);
        clearInterval(this._statsTimer);
        this._statsTimer = null;
      }
    }, 2000);
  }

  // Set container ID for exposure
  setContainerId(containerId) {
    this.containerId_ = containerId;
    // Update logger prefix to reflect the container ID
    if (this.logger && containerId) {
      this.logger.setPrefix(`Shaka-${containerId}`);
    }
  }

  // Expose Shaka instances to window with container-id as key
  exposeShakaInstances() {
    if (typeof window === "undefined" || !this.containerId_ || !this.shaka_) {
      return;
    }

    const containerId = this.containerId_;

    // Initialize window.__shaka_players if not exists
    if (!window.__shaka_players) {
      window.__shaka_players = {};
    }
    if (!window.__shaka_videos) {
      window.__shaka_videos = {};
    }
    if (!window.__shaka_debug) {
      window.__shaka_debug = {};
    }

    // Expose instances with container-id as key
    window.__shaka_players[containerId] = this.shaka_;
    window.__shaka_videos[containerId] = this.el_;

    // Expose debug helpers
    window.__shaka_debug[containerId] = {
      player: this.shaka_,
      video: this.el_,
      tech: this,
      vjsPlayer: this.vjsPlayer,
      getManifest: () => this.shaka_.getManifest(),
      getConfiguration: () => this.shaka_.getConfiguration(),
      getStats: () => this.shaka_.getStats(),
      getBufferedInfo: () => this.shaka_.getBufferedInfo(),
      getPlaybackRate: () => this.shaka_.getPlaybackRate(),
      getVolume: () => this.shaka_.getVolume(),
      isLive: () => this.shaka_.isLive(),
      seekRange: () => this.shaka_.seekRange(),
    };

    // Log exposure
    this.logger.info(`Exposed instances for container "${containerId}":`, {
      __shaka: !!window.__shaka,
      __shaka_players: !!window.__shaka_players[containerId],
      __shaka_videos: !!window.__shaka_videos[containerId],
      __shaka_debug: !!window.__shaka_debug[containerId],
    });
  }
}

export default Shaka;
