// OQ-1 Spike Result: css-fallback — FoundryVTT v14 — 2026-05-21
//
// Investigation findings (Story 1.2 spike):
//   - game.webrtc is foundry.av.AVMaster — no getConnection(userId) method exists.
//   - Remote stream access: game.webrtc.client.getMediaStreamForUser(userId)
//     via the abstract AVClient public interface.
//   - track.enabled = false on remote inbound tracks does NOT stop WebRTC bandwidth.
//     RTP packets continue arriving from the remote peer regardless.
//   - True bandwidth elimination requires SDP renegotiation (not in AVMaster public API).
//   - LiveKit backend (CONFIG.WebRTC.clientClass override) removes peers/remoteStreams;
//     only getMediaStreamForUser() remains safe across backends.
//
// Conclusion: this.webrtc = null. scrying-pool.webrtcMode = 'css-fallback' (world default).
// CSS/DOM cosmetic hiding is the only honest implementation path for Story 1.3+.

/**
 * Sole gateway to game.* APIs for the Scrying Pool module.
 *
 * Exposes typed surfaces for: settings, socket, users, scenes, notifications, hooks, webrtc.
 * All surfaces are built in the constructor — construction is side-effect-free.
 *
 * The `webrtc` property starts as null and must be set externally at
 * Hooks.once('ready') via {@link FoundryAdapter.probeCapability} and
 * {@link FoundryAdapter.buildWebRTCSurface}.
 */
export class FoundryAdapter {
  /** Settings namespace for all scrying-pool world settings. */
  static SETTINGS_NS = 'scrying-pool';

  /**
   * World setting key for the resolved WebRTC capability mode.
   * Full identifier: `scrying-pool.webrtcMode`.
   * Value is one of: `'track-disable'` | `'css-fallback'` | `'unsupported'`
   */
  static SETTING_WEBRTC_MODE = 'webrtcMode';

  /** Flag scope/namespace for module-specific user flags. */
  static FLAG_SCOPE = 'scrying-pool';

  /**
   * Creates a FoundryAdapter. Side-effect-free — no hooks or listeners registered.
   * @param {object} [game] - The FoundryVTT `game` global. Optional for legacy/test
   *   compatibility; surfaces will be no-ops if omitted.
   */
  constructor(game) {
    this._game = game ?? {};

    /**
     * WebRTC track-disabling surface, or null on the css-fallback/unsupported path.
     * Set externally at ready time via probeCapability + buildWebRTCSurface.
     * @type {{ disableTrack(userId: string): void, enableTrack(userId: string): void } | null}
     */
    this.webrtc = null;

    const g = /** @type {any} */ (this._game);
    const ns = FoundryAdapter.SETTINGS_NS;

    /**
     * Settings surface — wraps game.settings, pre-namespaced to 'scrying-pool'.
     * Keys passed to these methods are the SHORT key (e.g. 'visibilityMatrix'),
     * NOT the fully-qualified key ('scrying-pool.visibilityMatrix').
     */
    this.settings = {
      /**
       * Register a setting.
       * @param {string} key - The setting key.
       * @param {object} config - The setting configuration.
       * @returns {void|undefined}
       */
      register: (key, config) => g.settings?.register(ns, key, config),
      /**
       * Get a setting value.
       * @param {string} key - The setting key.
       * @returns {unknown}
       */
      get: (key) => g.settings?.get(ns, key) ?? null,
      /**
       * Set a setting value.
       * @param {string} key - The setting key.
       * @param {unknown} value - The value to set.
       * @returns {Promise<unknown>}
       */
      set: (key, value) => /** @type {Promise<unknown>} */ (g.settings?.set(ns, key, value) ?? Promise.resolve()),
      /**
       * Register a settings menu.
       * @param {string} namespace - The namespace.
       * @param {string} menuKey - The menu key.
       * @param {object} config - The menu configuration.
       * @returns {void|undefined}
       */
      registerMenu: (namespace, menuKey, config) => g.settings?.registerMenu(namespace, menuKey, config),
    };

    /** Socket surface — wraps game.socket. */
    this.socket = {
      /**
       * Emit a socket event.
       * @param {string} event - The event name.
       * @param {object} payload - The event payload.
       * @returns {void|undefined}
       */
      emit: (event, payload) => {
        if (g.socket) return g.socket.emit(event, payload);
      },
      /**
       * Subscribe to a socket event.
       * @param {string} event - The event name.
       * @param {Function} handler - The event handler.
       * @returns {void|undefined}
       */
      on: (event, handler) => {
        if (g.socket) return g.socket.on(event, handler);
      },
      /**
       * Unsubscribe from a socket event.
       * @param {string} event - The event name.
       * @param {Function} handler - The event handler.
       * @returns {void|undefined}
       */
      off: (event, handler) => {
        if (g.socket) return g.socket.off(event, handler);
      },
    };

    /** Users surface — wraps game.users / game.user. */
    this.users = {
      /**
       * Get a user by ID.
       * @param {string} userId - The user ID.
       * @returns {object|null} The user object or null.
       */
      get: (userId) => g.users?.get(userId) ?? null,
      /**
       * Get all users.
       * @returns {object[]} Array of all users.
       */
      all: () => Array.from(g.users ?? []),
      /**
       * @param {string} [userId] - If omitted or null/undefined, checks current user.
       * @returns {boolean}
       */
      isGM: (userId) => {
        if (userId == null) {
          // null or undefined: check current user
          return (g.user?.isGM ?? false);
        }
        // Explicit userId (including "0" or ""): look up in users map
        return (g.users?.get(userId)?.isGM ?? false);
      },
      /** @returns {object|null} */
      current: () => g.user ?? null,
      /**
       * Gets a user flag for a specific user.
       * @param {string} userId - The user ID to get the flag for.
       * @param {string} scope - The flag scope/namespace.
       * @param {string} key - The flag key.
       * @returns {unknown|null} The flag value, or null if not found.
       */
      getFlag: (userId, scope, key) => {
        const user = g.users?.get(userId);
        if (user && typeof user.getFlag === 'function') {
          return user.getFlag(scope, key) ?? null;
        }
        return null;
      },
      /**
       * Sets a user flag for a specific user.
       * Note: In FoundryVTT, users can only set their own flags. GM can set flags for any user.
       * This method wraps the native user.setFlag() which returns a Promise.
       * @param {string} userId - The user ID to set the flag for.
       * @param {string} scope - The flag scope/namespace.
       * @param {string} key - The flag key.
       * @param {unknown} value - The flag value to set.
       * @returns {Promise<unknown>|null} The promise from user.setFlag(), or null if user not found.
       */
      setFlag: (userId, scope, key, value) => {
        const user = g.users?.get(userId);
        if (user && typeof user.setFlag === 'function') {
          return /** @type {Promise<unknown>} */ (user.setFlag(scope, key, value));
        }
        return null;
      },
      /**
       * Convenience method to get a module-scoped flag.
       * @param {string} userId - The user ID to get the flag for.
       * @param {string} key - The flag key (will be scoped to FoundryAdapter.FLAG_SCOPE).
       * @returns {unknown|null} The flag value, or null if not found.
       */
      getFlagModule: (userId, key) => {
        const user = g.users?.get(userId);
        if (user && typeof user.getFlag === 'function') {
          return user.getFlag(FoundryAdapter.FLAG_SCOPE, key) ?? null;
        }
        return null;
      },
      /**
       * Convenience method to set a module-scoped flag.
       * @param {string} userId - The user ID to set the flag for.
       * @param {string} key - The flag key (will be scoped to FoundryAdapter.FLAG_SCOPE).
       * @param {unknown} value - The flag value to set.
       * @returns {Promise<unknown>|null} The promise from user.setFlag(), or null if user not found.
       */
      setFlagModule: (userId, key, value) => {
        const user = g.users?.get(userId);
        if (user && typeof user.setFlag === 'function') {
          return /** @type {Promise<unknown>} */ (user.setFlag(FoundryAdapter.FLAG_SCOPE, key, value));
        }
        return null;
      },
    };

    /** Actors surface — wraps game.actors. */
    this.actors = {
      all: () => Array.from(g.actors ?? []),
      get: (id) => g.actors?.get(id) ?? null,
    };

    /** Scenes surface — wraps game.scenes. */
    this.scenes = {
      /**
       * Get the currently active scene.
       * @returns {object|null} The active scene or null.
       */
      current: () => g.scenes?.active ?? null,
      /**
       * Get a scene by ID.
       * @param {string} id - The scene ID.
       * @returns {object|null} The scene object or null.
       */
      get: (id) => g.scenes?.get(id) ?? null,
    };

    /**
     * Notifications surface — wraps ui.notifications.
     * Errors are caught silently — notification failures must never crash visibility logic.
     */
    this.notifications = {
      /**
       * Show an info notification.
       * @param {string} msg - The message to display.
       * @returns {void}
       */
      info: (msg) => { try { ui.notifications?.info(msg); } catch (err) { console.warn('[ScryingPool] notifications.info:', err); } },
      /**
       * Show a warning notification.
       * @param {string} msg - The message to display.
       * @returns {void}
       */
      warn: (msg) => { try { ui.notifications?.warn(msg); } catch (err) { console.warn('[ScryingPool] notifications.warn:', err); } },
      /**
       * Show an error notification.
       * @param {string} msg - The message to display.
       * @returns {void}
       */
      error: (msg) => { try { ui.notifications?.error(msg); } catch (err) { console.warn('[ScryingPool] notifications.error:', err); } },
    };

    /** Hooks surface — wraps FoundryVTT Hooks global. */
    this.hooks = {
      /**
       * Subscribe to a hook event.
       * @param {string} event - The event name.
       * @param {(...args: unknown[]) => void} handler - The event handler.
       * @returns {void|undefined}
       */
      on: (event, handler) => {
        if (typeof Hooks !== 'undefined') return Hooks.on(event, handler);
      },
      /**
       * Subscribe to a hook event once.
       * @param {string} event - The event name.
       * @param {(...args: unknown[]) => void} handler - The event handler.
       * @returns {void|undefined}
       */
      once: (event, handler) => {
        if (typeof Hooks !== 'undefined') return Hooks.once(event, handler);
      },
      /**
       * Unsubscribe from a hook event.
       * @param {string} event - The event name.
       * @param {(...args: unknown[]) => void} handler - The event handler.
       * @returns {void|undefined}
       */
      off: (event, handler) => {
        if (typeof Hooks !== 'undefined') return Hooks.off(event, handler);
      },
      /**
       * Call all handlers for a hook event.
       * @param {string} event - The event name.
       * @param {...unknown[]} args - The event arguments.
       * @returns {boolean|undefined}
       */
      callAll: (event, ...args) => {
        if (typeof Hooks !== 'undefined') return Hooks.callAll(event, ...args);
        return false;
      },
    };

    /** i18n surface — wraps game.i18n for localization. */
    this.i18n = {
      /**
       * Localize a string using the module's translation keys.
       * @param {string} key - The translation key (e.g., 'scrying-pool.notifications.gmHid')
       * @param {object} [data] - Optional data for string interpolation
       * @returns {string} The localized string
       */
      localize: (key, data) => {
        if (g.i18n && typeof g.i18n.localize === 'function') {
          return g.i18n.localize(key, data);
        }
        return key; // Fallback: return the key if i18n not available
      },
    };
  }

  /**
   * Probes the game.webrtc (AVMaster) instance for WebRTC capability.
   *
   * Probe logic and results (FoundryVTT v14, 2026-05-21):
   * - If game.webrtc is null/falsy → AV disabled or not yet initialised → `'unsupported'`
   * - If game.webrtc is not an object → invalid type → `'unsupported'`
   * - If game.webrtc.client lacks getMediaStreamForUser() → non-standard backend → `'unsupported'`
   * - Otherwise: getMediaStreamForUser() is available for stream access → `'stream-access'`
   *
   * NOTE: For FULL REPLACEMENT architecture (hiding Foundry's AV dock and showing our own):
   * - We use 'stream-access' mode to get actual MediaStream objects via getMediaStreamForUser()
   * - We create our own <video> elements and set srcObject to the stream
   * - We hide Foundry's AV dock with CSS
   *
   * The 'track-disable' and 'css-fallback' modes from the original spike were for OVERLAY
   * architecture (overlaying on Foundry's tiles). For REPLACEMENT, we need 'stream-access'.
   *
   * @param {unknown} gameWebrtc - The game.webrtc value at ready time (may be null/undefined)
   * @returns {'stream-access'|'unsupported'}
   */
  static probeCapability(gameWebrtc) {
    if (!gameWebrtc || typeof gameWebrtc !== 'object') return 'unsupported';
    const client = /** @type {any} */ (gameWebrtc).client;
    if (!client || typeof client.getMediaStreamForUser !== 'function') return 'unsupported';
    // Stream access is available - we can get MediaStream objects for all users
    return 'stream-access';
  }

  /**
   * Builds the webrtc surface object for the `'stream-access'` capability path.
   * Provides full access to WebRTC streams for creating custom video tiles.
   *
   * NOTE: This is used for FULL REPLACEMENT architecture where we:
   * 1. Hide Foundry's AV dock
   * 2. Create our own video elements using getMediaStreamForUser()
   * 3. Display actual video feeds in our ScryingPoolStrip
   *
   * @param {{ client: object }} gameWebrtc - The game.webrtc (AVMaster) instance
   * @returns {object} WebRTC surface with stream access methods
   * @throws {TypeError} if gameWebrtc or gameWebrtc.client is invalid
   */
  static buildWebRTCSurface(gameWebrtc) {
    if (!gameWebrtc || typeof gameWebrtc !== 'object' || !gameWebrtc.client) {
      throw new TypeError('FoundryAdapter.buildWebRTCSurface: gameWebrtc must be an object with a client property');
    }
    
    const client = gameWebrtc.client;
    
    return {
      /**
       * Get the MediaStream for a specific user.
       * @param {string} userId - The user ID
       * @returns {MediaStream|null} The MediaStream or null if not available
       */
      getMediaStreamForUser: (userId) => {
        try {
          if (typeof userId !== 'string' || userId.trim() === '') {
            console.warn('[ScryingPool] getMediaStreamForUser: invalid userId:', userId);
            return null;
          }
          return client.getMediaStreamForUser?.(userId) ?? null;
        } catch (err) {
          console.error('[ScryingPool] getMediaStreamForUser failed:', err);
          return null;
        }
      },
      
      /**
       * Get all connected user IDs.
       * @returns {string[]} Array of connected user IDs
       */
      getConnectedUsers: () => {
        try {
          return client.getConnectedUsers?.() ?? [];
        } catch (err) {
          console.error('[ScryingPool] getConnectedUsers failed:', err);
          return [];
        }
      },
      
      /**
       * Get the levels stream for a user (for volume detection).
       * @param {string} userId - The user ID
       * @returns {MediaStream|null} The levels stream or null
       */
      getLevelsStreamForUser: (userId) => {
        try {
          if (typeof userId !== 'string' || userId.trim() === '') {
            console.warn('[ScryingPool] getLevelsStreamForUser: invalid userId:', userId);
            return null;
          }
          return client.getLevelsStreamForUser?.(userId) ?? null;
        } catch (err) {
          console.error('[ScryingPool] getLevelsStreamForUser failed:', err);
          return null;
        }
      },
      
      /**
       * Check if current user's audio is enabled.
       * @returns {boolean}
       */
      isAudioEnabled: () => {
        try {
          return client.isAudioEnabled?.() ?? false;
        } catch (err) {
          console.error('[ScryingPool] isAudioEnabled failed:', err);
          return false;
        }
      },
      
      /**
       * Check if current user's video is enabled.
       * @returns {boolean}
       */
      isVideoEnabled: () => {
        try {
          return client.isVideoEnabled?.() ?? false;
        } catch (err) {
          console.error('[ScryingPool] isVideoEnabled failed:', err);
          return false;
        }
      },
      
      /**
       * Enable or disable current user's audio.
       * @param {boolean} enable - Whether to enable audio
       */
      toggleAudio: (enable) => {
        try {
          if (typeof client.toggleAudio === 'function') {
            client.toggleAudio(enable);
          }
        } catch (err) {
          console.error('[ScryingPool] toggleAudio failed:', err);
        }
      },
      
      /**
       * Enable or disable current user's video.
       * @param {boolean} enable - Whether to enable video
       */
      toggleVideo: (enable) => {
        try {
          if (typeof client.toggleVideo === 'function') {
            client.toggleVideo(enable);
          }
        } catch (err) {
          console.error('[ScryingPool] toggleVideo failed:', err);
        }
      },
      
      /**
       * Enable or disable current user's broadcast.
       * @param {boolean} enable - Whether to enable broadcast
       */
      toggleBroadcast: (enable) => {
        try {
          if (typeof client.toggleBroadcast === 'function') {
            client.toggleBroadcast(enable);
          }
        } catch (err) {
          console.error('[ScryingPool] toggleBroadcast failed:', err);
        }
      },
      
      /**
       * Set a video element for a user with their stream.
       * @param {string} userId - The user ID
       * @param {HTMLVideoElement} videoElement - The video element to set
       * @returns {Promise<void>}
       */
      setUserVideo: async (userId, videoElement) => {
        try {
          if (typeof userId !== 'string' || userId.trim() === '') {
            console.warn('[ScryingPool] setUserVideo: invalid userId:', userId);
            return;
          }
          if (!(videoElement instanceof HTMLVideoElement)) {
            console.warn('[ScryingPool] setUserVideo: videoElement is not a HTMLVideoElement');
            return;
          }
          if (typeof client.setUserVideo === 'function') {
            await client.setUserVideo(userId, videoElement);
          }
        } catch (err) {
          console.error('[ScryingPool] setUserVideo failed:', err);
        }
      },
      
      // Legacy: disable video track (cosmetic only, doesn't reduce bandwidth)
      disableTrack: (userId) => {
        try {
          if (typeof userId !== 'string' || userId.trim() === '') {
            console.warn('[ScryingPool] disableTrack: invalid userId:', userId);
            return;
          }
          const stream = client.getMediaStreamForUser?.(userId);
          const tracks = stream?.getVideoTracks() ?? [];
          for (const track of tracks) track.enabled = false;
          if (tracks.length === 0) {
            console.warn('[ScryingPool] disableTrack: no video tracks found for', userId);
          }
        } catch (err) {
          console.error('[ScryingPool] disableTrack failed:', err);
        }
      },
      
      // Legacy: enable video track
      enableTrack: (userId) => {
        try {
          if (typeof userId !== 'string' || userId.trim() === '') {
            console.warn('[ScryingPool] enableTrack: invalid userId:', userId);
            return;
          }
          const stream = client.getMediaStreamForUser?.(userId);
          const tracks = stream?.getVideoTracks() ?? [];
          for (const track of tracks) track.enabled = true;
          if (tracks.length === 0) {
            console.warn('[ScryingPool] enableTrack: no video tracks found for', userId);
          }
        } catch (err) {
          console.error('[ScryingPool] enableTrack failed:', err);
        }
      },
    };
  }
}
