class ResilientHlsPlayer {
  constructor(video, url, options = {}) {
    this.video = video;
    this.baseUrl = url;

    this.options = Object.assign({
      checkInterval: 2000,
      anomalyThreshold: 3,
      maxRetries: 4,
      addTimeStamp: false,
      debug: true,
      autoPlay: true
    }, options);

    this.hls = null;
    this.retryCount = 0;
    this.anomalyScore = 0;
    this.lastTime = 0;
    this.recoverStats = {};

    this._startupIgnoreCount = 0;
    this._lastWallTime = 0;
    this._lastMediaTime = 0;
    this._readyStateBadCount = 0;

    this._isRecovering = false;
    this._lastRecoverTime = 0;

    this._init();
  }

  log(...args) {
    if (this.options.debug) {
      console.log("[ResilientHls]", ...args);
    }
  }

  _init() {
    this._createHls();
    this._startMonitor();
  }

  _createHls() {
    if (this.hls) {
      this.hls.destroy();
    }

    const url = this._buildUrl();

    this.hls = new Hls({
      enableWorker: true,
      lowLatencyMode: false
    });

    this.hls.loadSource(url);
    this.hls.attachMedia(this.video);

    this._bindEvents();
  }

  _buildUrl() {
    if (!this.options.addTimeStamp) return this.baseUrl;
    return this.baseUrl + (this.baseUrl.includes("?") ? "&" : "?") + "t=" + Date.now();
  }

  _bindEvents() {
    const hls = this.hls;
    const v = this.video;

    hls.on(Hls.Events.MEDIA_ATTACHED, () => {
      this.log("MEDIA_ATTACHED");
      if (this.options.autoPlay) v.play();
    });

    hls.on(Hls.Events.ERROR, (event, data) => {
      this.log("HLS ERROR:", data);

      if (data.details === 'bufferSeekOverHole') {
        this._recover('hole');
        return;
      }

      if (data.details === "keyLoadError" && data.response?.code === 403) {
        this.log("KEY 403 detected");
        return;
      }

      if (data.details === "fragLoadError") {
        this.log("FRAG LOAD ERROR skipped");

        const t = this.video.currentTime;
        this.video.currentTime = t + 1.0;

        this._resetScore();
        return;
      }

      if (data.fatal) {
        if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
          this._recover("network_fatal");
        }
        else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
          this._recover("media_fatal");
        }
        else {
          this._recover("unknown_fatal");
        }
      } else {
        if (data.details === "bufferStalledError") {
          this._increaseScore("bufferStall");
        }
      }
    });

    v.addEventListener("error", () => {
      this.log("VIDEO ERROR");
      this._increaseScore("video_error");
    });

    v.addEventListener("stalled", () => {
      this.log("stalled");
    });

    v.addEventListener("waiting", () => {
      this.log("waiting");
    });
  }

  _startMonitor() {
    this.timer = setInterval(() => this._check(), this.options.checkInterval);
  }

  _check() {
    const v = this.video;

    if (v.paused) return;

    const now = performance.now();
    const current = v.currentTime;

    // ===== startup ignore =====
    if (this._startupIgnoreCount < 10) {
      this._startupIgnoreCount++;
      this._lastWallTime = now;
      this._lastMediaTime = current;
      return;
    }

    const wallDelta = (now - this._lastWallTime) / 1000;
    const mediaDelta = current - this._lastMediaTime;

    let ratio = 1;

    if (wallDelta > 0) {
      ratio = mediaDelta / wallDelta;

      this.log(`ratio: ${ratio.toFixed(3)}`);

      if (ratio < 0.6 || ratio > 1.6) {
        // record only
        this._increaseScore("ratio_spike");
      }
    }

    this._lastWallTime = now;
    this._lastMediaTime = current;

    // ===== readyState =====
    if (v.readyState < 3) {
      this._readyStateBadCount++;
    } else {
      this._readyStateBadCount = 0;
    }

    if (this._readyStateBadCount >= 3) {
      this._increaseScore("readyState");
    }

    // ===== freeze =====
    if (!v.paused && current === this.lastTime) {
      this._increaseScore("freeze");
    }

    this.lastTime = current;

    // ===== gentle decay =====
    this.anomalyScore = Math.max(0, this.anomalyScore - 0.3);

    // ===== recovery trigger =====
    if (this.anomalyScore >= this.options.anomalyThreshold) {
      this.log("Too many anomaly detected to recover");
      this._recover("anomaly");
    }
  }

  _increaseScore(reason) {
    this.anomalyScore++;
    this.log("Anomaly +1:", reason, this.anomalyScore);
  }

  _resetScore() {
    this.anomalyScore = 0;
  }

  _recover(reason) {
    const now = performance.now();

    this.video.playbackRate = 1.0;

    this.recoverStats[reason] = (this.recoverStats[reason] || 0) + 1;

    if (this._isRecovering) {
      this.log("SKIP recover (already recovering)");
      return;
    }

    if (now - this._lastRecoverTime < 2000) {
      this.log("SKIP recover (cooldown)");
      return;
    }

    this._isRecovering = true;
    this._lastRecoverTime = now;

    this.log("RECOVER:", reason, "retry=", this.retryCount, this.recoverStats);

    this._resetScore();

    try {
      if (this.retryCount === 0) {
        this.video.pause();
        this.video.play();

      } else if (this.retryCount === 1) {
        const t = this.video.currentTime;

        this.hls.stopLoad();
        this.hls.startLoad();

        this.video.currentTime = t;

      } else if (this.retryCount === 2) {
        this.log("FORCE media recover");
        this.hls.recoverMediaError();

      } else if (this.retryCount === 3) {
        this._createHls();

      } else {
        this.log("FULL RESET");

        const wasPlaying = !this.video.paused;

        this.video.pause();
        this.video.src = "";
        this.video.load();

        setTimeout(() => {
          this._createHls();
          if (wasPlaying) this.video.play();
        }, 500);

        this.retryCount = 0;

        setTimeout(() => {
          this._isRecovering = false;
        }, 1000);

        return;
      }

      this.retryCount++;

    } catch (e) {
      this.log("Recovery exception:", e);
    }

    setTimeout(() => {
      this._isRecovering = false;
    }, 1000);
  }

  destroy() {
    clearInterval(this.timer);
    if (this.hls) this.hls.destroy();
  }

  setSource(newUrl, autoPlay = true) {
    this.log("SET SOURCE:", newUrl);

    this.baseUrl = newUrl;
    this.retryCount = 0;
    this.anomalyScore = 0;

    const wasPlaying = !this.video.paused;

    try {
      this.video.pause();

      if (this.hls) {
        this.hls.destroy();
        this.hls = null;
      }

      this.video.src = "";
      this.video.load();

      setTimeout(() => {
        this._createHls();

        if (autoPlay && wasPlaying) {
          this.video.play();
        }
      }, 300);

    } catch (e) {
      this.log("setSource error:", e);
    }
  }
}

function HlsStart() {
  const urlNew = document.getElementById('m3u8URLSave').value;
  const video = document.getElementById('oVideo');

  if (!video._player) {
    video._player = new ResilientHlsPlayer(video, urlNew);
  } else {
    video._player.setSource(urlNew);
  }
}