/**
 * ScryingPoolCameraViews — replaces Foundry's CameraViews as CONFIG.ui.webrtc.
 *
 * Three responsibilities:
 *   1. Redirect the "configure" camera action to the Scrying Pool Directors Board
 *      instead of Foundry's native AVConfig dialog.
 *   2. Inject the Scrying Pool visibility state (sp-cam-hidden) into each user's
 *      camera context so the dock reflects the same hidden/active state as the
 *      module's state machine.
 *   3. Monitor local video streams and auto-recover from browser-level mute/ended
 *      events that cause gray/black video in the player's self-view while other
 *      peers remain unaffected. (Bug fix: stream health monitoring)
 *
 * Set as CONFIG.ui.webrtc in the 'init' hook (before Foundry instantiates ui.webrtc).
 * Dependencies are injected after 'ready' via initScryingPoolCameraViews().
 */

// Lazy base class — avoids ReferenceError at module load time in tests where
// foundry globals are not defined.
function _getCameraViewsBase() {
  if (typeof foundry !== 'undefined') {
    const cls = foundry.applications?.apps?.av?.CameraViews;
    if (cls) return cls;
  }
  // Minimal test-environment fallback — mirrors the pattern in DirectorsBoard.js
  return class _FallbackCameraViews {
    static DEFAULT_OPTIONS = {};
    static PARTS = {};
    constructor(options = {}) {
      this.options = options;
      this._healthCheckInterval = null;
    }
    async render() {}
    async close() {}
    _prepareUserContext(_id) { return {}; }
    _onConfigure() {}
    _onRender() {}
    getUserVideoElement(_userId) { return null; }
  };
}

/** @type {object|null} DirectorsBoard instance — set via initScryingPoolCameraViews */
let _directorsBoard = null;

/** @type {object|null} StateStore instance — set via initScryingPoolCameraViews */
let _stateStore = null;

/** @type {object|null} FoundryAdapter instance — set via initScryingPoolCameraViews */
let _adapter = null;

/**
 * Inject module dependencies. Called from module.js after 'ready' resolves.
 * @param {object|null} directorsBoard - The singleton DirectorsBoard (GM only, else null)
 * @param {object} stateStore - The module StateStore
 * @param {object|null} adapter - The FoundryAdapter instance (for webrtc surface)
 */
export function initScryingPoolCameraViews(directorsBoard, stateStore, adapter) {
  _directorsBoard = directorsBoard;
  _stateStore = stateStore;
  _adapter = adapter;
}

export class ScryingPoolCameraViews extends _getCameraViewsBase() {
  /** @type {number|null} */
  _healthCheckInterval = null;

  /**
   * Intercept the configure camera button.
   * Opens the Scrying Pool Directors Board instead of Foundry's AVConfig dialog.
   * For non-GM players, shows an informational notification since A/V config
   * is GM-controlled in this module.
   * @override
   */
  _onConfigure(event, target) {
    if (_directorsBoard) {
      _directorsBoard.render({ force: true });
    } else if (typeof ui !== 'undefined') {
      ui.notifications?.info(
        game?.i18n?.localize('scrying-pool.notifications.avConfigGMOnly') ??
        'A/V settings are managed by the GM.'
      );
    }
  }

  /**
   * Inject Scrying Pool visibility state into each user's camera tile context.
   * Marks hidden users with `spHidden: true` rather than returning null, so
   * Foundry's _configureRenderParts does not crash on a null part context.
   * @override
   * @param {string} id - User ID
   * @returns {object|undefined}
   */
  _prepareUserContext(id) {
    const ctx = super._prepareUserContext(id);
    const spState = _stateStore?.getState?.(id) ?? 'active';
    if (ctx && spState === 'hidden') ctx.spHidden = true;
    return ctx;
  }

  /**
   * Start periodic stream health monitoring after the dock is first rendered.
   * Only starts once — guarded by _healthCheckInterval.
   * @override
   */
  _onRender(context, options) {
    super._onRender?.(context, options);
    if (_adapter?.webrtc && this._healthCheckInterval === null) {
      this._healthCheckInterval = setInterval(
        () => this._checkVideoStreamHealth(),
        30000
      );
    }
  }

  /**
   * Clean up health check interval when the view is closed.
   * @override
   */
  _onClose() {
    if (this._healthCheckInterval !== null) {
      clearInterval(this._healthCheckInterval);
      this._healthCheckInterval = null;
    }
    super._onClose?.();
  }

  /**
   * Periodic health check on all video tiles in the camera dock.
   * Detects muted/ended tracks and re-acquires the MediaStream to
   * recover from gray/black video without requiring a page reload.
   * Same pattern as ScryingPoolStrip._checkVideoStreamHealth().
   */
  _checkVideoStreamHealth() {
    if (!this.element) return;

    try {
      const userIds = _adapter?.webrtc?.getConnectedUsers?.() ?? [];
      for (const userId of userIds) {
        const videoEl = this.getUserVideoElement(userId);
        if (!videoEl) continue;

        const stream = videoEl.srcObject;
        if (!(stream instanceof MediaStream)) continue;

        const videoTracks = stream.getVideoTracks();

        // Track permanently ended → re-acquire stream
        if (videoTracks.some(t => t.readyState === 'ended')) {
          this._refreshUserVideo(userId);
          continue;
        }

        // Track muted at browser level (tab backgrounded, camera contention)
        if (videoTracks.some(t => t.muted)) {
          this._scheduleMuteCheck(userId, stream);
          continue;
        }

        // Video element not producing frames despite having a stream
        if (videoEl.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
          this._refreshUserVideo(userId);
        }
      }
    } catch (err) {
      console.error('[ScryingPool] CameraViews stream health check failed:', err);
    }
  }

  /**
   * Debounced mute check: waits 3s then refreshes if the track remains muted.
   * Avoids unnecessary re-attach on transient browser-level mutes.
   * @param {string} userId
   * @param {MediaStream} stream - Original stream to detect replacement
   */
  _scheduleMuteCheck(userId, stream) {
    const key = `_muteTimer_${userId}`;
    if (this[key] !== undefined) return;

    this[key] = setTimeout(() => {
      this[key] = undefined;
      try {
        const videoEl = this.getUserVideoElement(userId);
        if (!videoEl || videoEl.srcObject !== stream) return;

        if (stream.getVideoTracks().some(t => t.muted)) {
          this._refreshUserVideo(userId);
        }
      } catch (err) {
        console.error('[ScryingPool] CameraViews mute check failed:', err);
      }
    }, 3000);
  }

  /**
   * Re-acquires and re-attaches a user's MediaStream on the video element.
   * Resetting srcObject forces the browser to restart the media pipeline.
   * @param {string} userId
   */
  _refreshUserVideo(userId) {
    try {
      const stream = _adapter?.webrtc?.getMediaStreamForUser?.(userId);
      if (!(stream instanceof MediaStream)) return;

      const videoEl = this.getUserVideoElement(userId);
      if (!videoEl) return;

      videoEl.srcObject = null;
      videoEl.srcObject = stream;
    } catch (err) {
      console.error('[ScryingPool] CameraViews refresh video failed:', err);
    }
  }
}
