/**
 * PlayerPrivacyManager — Manages player privacy settings for automation opt-ins.
 *
 * Owns: privacy settings retrieval, validation, persistence, and change notifications.
 * Settings are stored as user flags on the user document.
 * No socket broadcasting — each client reads its own user's flags.
 *
 * 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/PlayerPrivacyManager
 */

import {
  PRIVACY_SETTINGS_DEFAULT,
  PRIVACY_SETTING_KEYS,
  FEATURE_NAME_MAP,
  MAX_PORTRAIT_SIZE,
  validateSettingKey,
  validateSettingValue,
  validateFeatureName,
  validatePortraitDataURL,
} from "../contracts/privacy-settings.js";

/**
 * Manages player privacy settings for automation opt-ins.
 *
 * Settings are stored as world-level user flags:
 * - game.user.setFlag('scrying-pool', 'reactionCamEnabled', boolean)
 *
 * Players can only edit their own settings.
 * GM can read (but not edit) all players' settings.
 *
 * No socket broadcasting — privacy is client-local.
 */
export class PlayerPrivacyManager {
  /**
   * @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
   *   Injected FoundryAdapter surface for user flag access.
   * @throws {TypeError} If adapter is invalid or lacks required user methods.
   */
  constructor(adapter) {
    // Validate adapter
    if (!adapter || typeof adapter !== "object") {
      throw new TypeError(
        "PlayerPrivacyManager: adapter argument is required and must be an object"
      );
    }

    // Validate adapter.users
    if (!adapter.users || typeof adapter.users !== "object") {
      throw new TypeError(
        "PlayerPrivacyManager: adapter.users must be an object"
      );
    }

    // Validate adapter.users.get
    if (
      !adapter.users.get ||
      typeof adapter.users.get !== "function"
    ) {
      throw new TypeError(
        "PlayerPrivacyManager: adapter.users.get must be a function"
      );
    }

    // Validate adapter.users.all
    if (
      !adapter.users.all ||
      typeof adapter.users.all !== "function"
    ) {
      throw new TypeError(
        "PlayerPrivacyManager: adapter.users.all must be a function"
      );
    }

    this._adapter = adapter;

    /**
     * @type {Set<function(userId: string, key: string, value: boolean, previousValue: boolean): void>}
     * Subscribers for setting change events.
     */
    this._subscribers = new Set();
  }

  /**
   * Retrieves privacy settings for a specific user.
   *
   * Reads from user flags with module scope 'scrying-pool'.
   * Missing settings are merged with defaults (all false).
   *
   * @param {string} userId - The user ID to retrieve settings for.
   * @returns {import('../contracts/privacy-settings.js').PrivacySettings} The user's privacy settings.
   */
  getSettings(userId) {
    const user = this._adapter.users.get(userId);

    // Return defaults if user doesn't exist or has no getFlag
    if (!user || typeof user.getFlag !== "function") {
      return { ...PRIVACY_SETTINGS_DEFAULT };
    }

    const settings = { ...PRIVACY_SETTINGS_DEFAULT };

    for (const key of PRIVACY_SETTING_KEYS) {
      const value = user.getFlag("scrying-pool", key);
      if (value !== undefined && value !== null) {
        settings[key] = value;
      }
    }

    return settings;
  }

  /**
   * Updates a single privacy setting for a user.
   *
   * Validates key and value before persistence.
   * Emits change event to subscribers after successful update.
   *
   * @param {string} userId - The user ID to update settings for.
   * @param {string} key - The setting key (must be in PRIVACY_SETTING_KEYS and not customPortraitFallback).
   * @param {boolean} value - The new setting value.
   * @returns {Promise<void>} Resolves when the setting is persisted.
   * @throws {TypeError} If key is invalid, value is not boolean, or user doesn't exist.
   */
  async setSetting(userId, key, value) {
    // Reject customPortraitFallback - use dedicated method instead
    if (key === "customPortraitFallback") {
      throw new TypeError(
        "PlayerPrivacyManager: customPortraitFallback must use setPortraitFallback() method"
      );
    }

    // Validate key
    validateSettingKey(key);

    // Validate value
    validateSettingValue(value);

    // Get user
    const user = this._adapter.users.get(userId);
    if (!user) {
      throw new TypeError(`PlayerPrivacyManager: User '${userId}' not found`);
    }

    // Validate user has setFlag method
    if (typeof user.setFlag !== "function") {
      throw new TypeError(
        `PlayerPrivacyManager: User '${userId}' does not support setFlag`
      );
    }

    // Get previous value for change event
    const previousValue = this.getSettings(userId)[key];

    // Persist the setting via user flag
    // Note: FoundryVTT user.setFlag returns a Promise
    await user.setFlag("scrying-pool", key, value);

    // Notify subscribers
    this._notifySubscribers(userId, key, value, previousValue);
  }

  /**
   * Checks if a user has opted in to a specific automation feature.
   *
   * @param {string} userId - The user ID to check.
   * @param {string} feature - The feature name ('reactionCam').
   * @returns {boolean} True if the user has opted in, false otherwise.
   * @throws {TypeError} If feature name is invalid.
   */
  isOptedIn(userId, feature) {
    // Validate feature name
    validateFeatureName(feature);

    // Get the setting key for this feature
    const key = FEATURE_NAME_MAP[feature];

    // Get user settings
    const settings = this.getSettings(userId);

    // Return the setting value (defaults to false if not found)
    return settings[key] ?? false;
  }

  /**
   * Retrieves privacy settings for all connected users.
   *
   * Only includes users that have the getFlag method (supports privacy settings).
   * Primarily for GM use to view all players' settings.
   *
   * @returns {Map<string, import('../contracts/privacy-settings.js').PrivacySettings>}
   *   Map of userId → PrivacySettings for all users with valid settings.
   */
  getAllSettings() {
    const allUsers = this._adapter.users.all();
    const result = new Map();

    for (const user of allUsers) {
      // Skip users without proper ID
      if (!user || !user.id) continue;

      // Skip users without getFlag method
      if (typeof user.getFlag !== "function") continue;

      const settings = this.getSettings(user.id);
      result.set(user.id, settings);
    }

    return result;
  }

  /**
   * Subscribes to privacy setting change events.
   *
   * Subscribers are called with (userId, key, newValue, previousValue).
   * Returns an unsubscribe function.
   *
   * @param {function(userId: string, key: string, value: boolean, previousValue: boolean): void} callback
   *   Callback function to invoke on setting changes.
   * @returns {function(): void} Function to unsubscribe.
   */
  onChange(callback) {
    this._subscribers.add(callback);

    // Return unsubscribe function
    return () => {
      this._subscribers.delete(callback);
    };
  }

  /**
   * Notifies all subscribers of a setting change.
   *
   * @private
   * @param {string} userId - The user ID whose setting changed.
   * @param {string} key - The setting key that changed.
   * @param {boolean} newValue - The new setting value.
   * @param {boolean} previousValue - The previous setting value.
   */
  _notifySubscribers(userId, key, newValue, previousValue) {
    for (const callback of this._subscribers) {
      try {
        callback(userId, key, newValue, previousValue);
      } catch (err) {
        // Swallow subscriber errors to prevent one bad subscriber from breaking others
        console.error(
          `[ScryingPool] PlayerPrivacyManager subscriber error:`,
          err
        );
      }
    }
  }

  /**
   * Sets a custom portrait fallback DataURL for a user.
   *
   * Validates the DataURL format and MIME type before persistence.
   * Emits change event with type 'portrait' to subscribers after successful update.
   *
   * @param {string} userId - The user ID to set portrait for.
   * @param {string} dataURL - The DataURL string for the portrait image.
   * @returns {Promise<void>} Resolves when the setting is persisted.
   * @throws {TypeError} If dataURL is invalid, user doesn't exist, or user doesn't support setFlag.
   */
  async setPortraitFallback(userId, dataURL) {
    // Reject null/undefined/empty — use removePortraitFallback instead
    if (dataURL === null || dataURL === undefined || dataURL === "") {
      throw new TypeError(
        "PlayerPrivacyManager: setPortraitFallback requires a non-empty DataURL string. Use removePortraitFallback() to remove the custom portrait."
      );
    }

    // Validate DataURL format
    validatePortraitDataURL(dataURL);

    // Check DataURL size before storage
    // Measure decoded binary size (base64 ~33% overhead)
    const commaIndex = dataURL.indexOf(",");
    const base64Data = commaIndex !== -1 ? dataURL.substring(commaIndex + 1) : dataURL;
    const decodedBytes = Math.ceil(base64Data.length * 3 / 4);
    if (decodedBytes > MAX_PORTRAIT_SIZE) {
      throw new TypeError(
        `PlayerPrivacyManager: DataURL decoded size (${decodedBytes} bytes) exceeds MAX_PORTRAIT_SIZE (${MAX_PORTRAIT_SIZE} bytes)`
      );
    }

    // Get user
    const user = this._adapter.users.get(userId);
    if (!user) {
      throw new TypeError(
        `PlayerPrivacyManager: User '${userId}' not found`
      );
    }

    // Validate user has setFlag method
    if (typeof user.setFlag !== "function") {
      throw new TypeError(
        `PlayerPrivacyManager: User '${userId}' does not support setFlag`
      );
    }

    // Get previous value for change event
    const previousValue = this.getPortraitFallback(userId);

    // Persist the setting via user flag
    await user.setFlag("scrying-pool", "customPortraitFallback", dataURL);

    // Notify subscribers with special portrait type
    this._notifyPortraitChange(userId, dataURL, previousValue);
  }

  /**
   * Retrieves the custom portrait fallback DataURL for a user.
   *
   * @param {string} userId - The user ID to retrieve portrait for.
   * @returns {string|null} The DataURL string, or null if not set.
   */
  getPortraitFallback(userId) {
    const user = this._adapter.users.get(userId);

    // Return null if user doesn't exist or has no getFlag
    if (!user || typeof user.getFlag !== "function") {
      return null;
    }

    const dataURL = user.getFlag("scrying-pool", "customPortraitFallback");

    // Validate the stored DataURL (defensive programming)
    if (dataURL !== null && dataURL !== undefined) {
      try {
        // Only accept string values
        if (typeof dataURL !== "string") {
          return null;
        }
        // Normalize empty string to null
        if (dataURL === "") {
          return null;
        }
        validatePortraitDataURL(dataURL);
        return dataURL;
      } catch (e) {
        // Invalid stored DataURL - treat as null
        console.warn(
          `[ScryingPool] PlayerPrivacyManager: Invalid stored portrait DataURL for user '${userId}': ${e.message}`
        );
        return null;
      }
    }

    return null;
  }

  /**
   * Convenience method to get portrait fallback as DataURL directly.
   * Same as getPortraitFallback but with explicit null return type.
   *
   * @param {string} userId - The user ID to retrieve portrait for.
   * @returns {string|null} The DataURL string, or null if not set.
   */
  getPortraitFallbackDataURL(userId) {
    return this.getPortraitFallback(userId);
  }

  /**
   * Removes the custom portrait fallback for a user.
   *
   * Emits change event with type 'portrait' to subscribers after successful removal.
   *
   * @param {string} userId - The user ID to remove portrait for.
   * @returns {Promise<void>} Resolves when the setting is removed.
   * @throws {TypeError} If user doesn't exist or user doesn't support unsetFlag.
   */
  async removePortraitFallback(userId) {
    const user = this._adapter.users.get(userId);
    if (!user) {
      throw new TypeError(
        `PlayerPrivacyManager: User '${userId}' not found`
      );
    }

    // Validate user has unsetFlag method
    if (typeof user.unsetFlag !== "function") {
      throw new TypeError(
        `PlayerPrivacyManager: User '${userId}' does not support unsetFlag`
      );
    }

    // Get previous value for change event
    const previousValue = this.getPortraitFallback(userId);

    // Remove the setting via user flag
    await user.unsetFlag("scrying-pool", "customPortraitFallback");

    // Notify subscribers with special portrait type
    this._notifyPortraitChange(userId, null, previousValue);
  }

  /**
   * Notifies all subscribers of a portrait change.
   *
   * @private
   * @param {string} userId - The user ID whose portrait changed.
   * @param {string|null} newValue - The new portrait DataURL (or null if removed).
   * @param {string|null} previousValue - The previous portrait DataURL (or null).
   */
  _notifyPortraitChange(userId, newValue, previousValue) {
    for (const callback of this._subscribers) {
      try {
        callback(userId, "customPortraitFallback", newValue, previousValue);
      } catch (err) {
        // Swallow subscriber errors to prevent one bad subscriber from breaking others
        console.error(
          `[ScryingPool] PlayerPrivacyManager portrait subscriber error:`,
          err
        );
      }
    }
  }

  /**
   * Cleans up internal state.
   * Safe to call multiple times.
   */
  teardown() {
    this._subscribers.clear();
  }
}
