/**
 * VisibilityManager — WebRTC strategy applier and SocketHandler revert handler.
 *
 * Listens to `scrying-pool:stateChanged` hook events (emitted by StateStore) and
 * applies the appropriate webrtcMode strategy:
 *   - 'track-disable' + non-null adapter.webrtc → call disableTrack / enableTrack
 *   - 'css-fallback' / 'unsupported' / null webrtc → no-op (CSS handled by RoleRenderer)
 *
 * Also implements onRevert(pendingOp) for SocketHandler timeout callbacks.
 *
 * Import rule: may only import from src/contracts/ and src/utils/.
 * Constructors are side-effect free — call init() from module.js Hooks.once('ready').
 *
 * @module core/VisibilityManager
 */

/**
 * Applies webrtcMode strategy on state changes and reverts failed operations.
 */
export class VisibilityManager {
  /**
   * @param {import('./StateStore.js').StateStore} stateStore
   * @param {{ settings: { get(key: string): unknown }, webrtc: { disableTrack(userId: string): void, enableTrack(userId: string): void } | null, notifications: { warn(msg: string): void }, hooks: { on(event: string, handler: (...args: unknown[]) => void): void } }} adapter
   */
  constructor(stateStore, adapter) {
    this._stateStore = stateStore;
    this._adapter = adapter;
  }

  /**
   * Registers the Hooks.on('scrying-pool:stateChanged') listener.
   * Called from module.js Hooks.once('ready') — NOT from constructor.
   */
  init() {
    this._stateChangedHookId = this._adapter.hooks.on(
      'scrying-pool:stateChanged',
      (data) => this._onStateChanged(/** @type {any} */ (data))
    );
  }

  /**
   * Unregisters the state-changed listener.
   * Safe to call before init() or multiple times.
   */
  teardown() {
    if (this._stateChangedHookId != null) {
      this._adapter.hooks.off('scrying-pool:stateChanged', this._stateChangedHookId);
      this._stateChangedHookId = null;
    }
  }

  /**
   * Handles a state change by applying the webrtcMode strategy.
   * css-fallback / unsupported → no-op (CSS applied by RoleRenderer in Story 1.5).
   * track-disable + non-null webrtc → disable/enable the participant's track.
   * Always safe with null adapter.webrtc (OQ-1 spike result for v14).
   *
   * @private
   * @param {{ userId: string, state: string }} data
   */
  _onStateChanged(data) {
    const { userId, state } = data;
    // Input validation
    if (!userId || typeof userId !== 'string') {
      console.warn('[ScryingPool]', 'VisibilityManager._onStateChanged: invalid userId');
      return;
    }
    if (!state || typeof state !== 'string') {
      console.warn('[ScryingPool]', 'VisibilityManager._onStateChanged: invalid state');
      return;
    }

    // T-11: Validate mode is a string
    const mode = this._adapter.settings.get('webrtcMode');
    if (typeof mode !== 'string') {
      console.warn('[ScryingPool]', 'VisibilityManager._onStateChanged: webrtcMode is not a string');
      return;
    }
    
    if (mode !== 'track-disable' || !this._adapter.webrtc) return;
    
    // T-10: Validate webrtc methods exist before calling
    if (typeof this._adapter.webrtc.disableTrack !== 'function' || 
        typeof this._adapter.webrtc.enableTrack !== 'function') {
      console.warn('[ScryingPool]', 'VisibilityManager._onStateChanged: webrtc missing disableTrack/enableTrack methods');
      return;
    }
    
    // T-09: Handle all visibility states properly, not just binary hidden/active
    if (state === 'hidden' || state === 'offline' || state === 'cam-lost' || state === 'ghost') {
      this._adapter.webrtc.disableTrack(userId);
    } else {
      this._adapter.webrtc.enableTrack(userId);
    }
  }

  /**
   * Called by SocketHandler after retry exhaustion — reverts the optimistic state
   * and notifies the GM that the operation could not be confirmed.
   *
   * @param {{ userId: string, previousState: string, opId: string }} pendingOp
   */
  onRevert(pendingOp) {
    // Input validation
    if (!pendingOp || typeof pendingOp !== 'object') {
      console.warn('[ScryingPool]', 'VisibilityManager.onRevert: invalid pendingOp');
      return;
    }
    const { userId, previousState } = pendingOp;
    if (!userId || typeof userId !== 'string') {
      console.warn('[ScryingPool]', 'VisibilityManager.onRevert: invalid userId in pendingOp');
      return;
    }
    if (!previousState || typeof previousState !== 'string') {
      console.warn('[ScryingPool]', 'VisibilityManager.onRevert: invalid previousState in pendingOp');
      return;
    }

    try {
      this._stateStore.setVisibility(userId, previousState);
    } catch (err) {
      console.error('[ScryingPool] VisibilityManager.onRevert: setVisibility failed', err);
    }

    try {
      this._adapter.notifications.warn(
        `[ScryingPool] Visibility change for ${userId} could not be confirmed — reverting to ${previousState}`
      );
    } catch (err) {
      console.error('[ScryingPool] VisibilityManager.onRevert: notification failed', err);
    }
  }

  /**
   * Applies a full visibility matrix to all participants.
   * Story 3.2: Added for preset auto-apply support.
   *
   * @param {object} matrix - The visibility matrix to apply.
   * @param {object} matrix.matrix - The matrix object with userId → state mappings.
   * @returns {Promise<void>}
   */
  async applyMatrix(matrix) {
    // Validate matrix
    if (!matrix || typeof matrix !== 'object') {
      throw new TypeError('VisibilityManager.applyMatrix: matrix argument is required and must be an object');
    }
    if (!matrix.matrix || typeof matrix.matrix !== 'object') {
      throw new TypeError('VisibilityManager.applyMatrix: matrix.matrix is required and must be an object');
    }

    // Apply each participant's state from the matrix
    for (const [userId, state] of Object.entries(matrix.matrix)) {
      try {
        await this._stateStore.setVisibility(userId, state);
      } catch (err) {
        console.error(
          `[ScryingPool] VisibilityManager.applyMatrix: failed to set visibility for ${userId}:`,
          err instanceof Error ? err.message : String(err)
        );
      }
    }
  }
}
