/**
 * PortraitFallbackHandler — Manages portrait fallback image resolution and file handling.
 *
 * Owns: portrait fallback URL resolution, file validation, DataURL conversion, and change notifications.
 * No direct game.* access — all Foundry API access via injected adapter.
 * No socket broadcasting — portrait settings are client-local (user 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/PortraitFallbackHandler
 */

import {
  MAX_PORTRAIT_SIZE,
  VALID_PORTRAIT_FORMATS,
} from "../contracts/privacy-settings.js";

/**
 * Default system placeholder avatar URL.
 * Falls back to FoundryVTT's default avatar if available.
 * @type {string}
 */
const DEFAULT_PLACEHOLDER_URL = "icons/svg/mystery-man.svg";

/**
 * Manages portrait fallback image resolution for participants.
 *
 * Resolves the portrait to display when a participant's camera is unavailable
 * (never-connected or cam-lost states) using the priority:
 * 1. Custom Portrait Fallback (from user flag)
 * 2. FoundryVTT user avatar
 * 3. System placeholder
 *
 * Handles file upload, validation, and conversion to DataURL.
 * Emits change events for UI updates.
 */
export class PortraitFallbackHandler {
  /**
   * @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
   *   Injected FoundryAdapter surface for user/avatar access.
   * @param {import('./PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
   *   Injected PlayerPrivacyManager for custom portrait access.
   * @throws {TypeError} If adapter or playerPrivacyManager is invalid.
   */
  constructor(adapter, playerPrivacyManager) {
    // Validate adapter
    if (!adapter || typeof adapter !== "object") {
      throw new TypeError(
        "PortraitFallbackHandler: adapter argument is required and must be an object"
      );
    }

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

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

    // Validate playerPrivacyManager
    if (!playerPrivacyManager || typeof playerPrivacyManager !== "object") {
      throw new TypeError(
        "PortraitFallbackHandler: playerPrivacyManager argument is required and must be an object"
      );
    }

    this._adapter = adapter;
    this._playerPrivacyManager = playerPrivacyManager;
    this._subscribers = new Set();
    this._initBound = false;
  }

  /**
   * Initializes change forwarding from PlayerPrivacyManager to this handler's subscribers.
   * Must be called once during module initialization (side-effect-free constructor pattern).
   */
  init() {
    if (this._initBound) return;
    this._initBound = true;

    this._playerPrivacyManager.onChange((userId, key, newValue, previousValue) => {
      if (key === "customPortraitFallback") {
        this._notifyPortraitChange(userId, newValue, previousValue);
      }
    });
  }

  /**
   * Resolves the fallback image URL for a participant.
   *
   * Priority order:
   * 1. Custom Portrait Fallback (from user flag) - if set
   * 2. FoundryVTT user avatar - if available
   * 3. System placeholder - always available
   *
   * @param {string} userId - The user ID to resolve portrait for.
   * @returns {string|null} The resolved image URL, or null if user doesn't exist.
   */
  getFallbackImageURL(userId) {
    // Check if user exists
    const user = this._adapter.users.get(userId);
    if (!user) {
      return null;
    }

    // Check custom portrait fallback first
    const custom = this._playerPrivacyManager.getPortraitFallbackDataURL(userId);
    if (custom) {
      return custom;
    }

    // Check FoundryVTT user avatar
    if (user.avatar) {
      return user.avatar;
    }

    // Return system placeholder
    return DEFAULT_PLACEHOLDER_URL;
  }

  /**
   * Creates an img element for the fallback portrait.
   *
   * @param {string} userId - The user ID to create portrait element for.
   * @returns {HTMLImageElement} The img element ready for mounting.
   */
  getFallbackImageElement(userId) {
    const url = this.getFallbackImageURL(userId);
    const user = this._adapter.users.get(userId);

    const img = document.createElement("img");
    img.src = url ?? DEFAULT_PLACEHOLDER_URL;
    img.className = "sp-portrait-fallback";
    img.dataset.spRole = "portrait-fallback";

    // Set alt text based on user name
    if (user && user.name) {
      img.alt = `${user.name}'s portrait`;
    } else {
      img.alt = "Participant portrait";
    }

    // Set dimensions to match AV tile
    img.style.width = "100%";
    img.style.height = "100%";
    img.style.objectFit = "cover";

    return img;
  }

  /**
   * Validates a file from the file picker for portrait upload.
   *
   * @param {File} file - The file object from the file picker.
   * @returns {{ valid: boolean, error?: string, dataURL?: string }} Validation result.
   */
  static validatePortraitFile(file) {
    // Validate file object
    if (!(file && typeof file === "object")) {
      return { valid: false, error: "Invalid file object" };
    }

    if (!file.type || typeof file.type !== "string") {
      return { valid: false, error: "File has no detectable MIME type" };
    }

    if (file.size === undefined || typeof file.size !== "number") {
      return { valid: false, error: "File has no detectable size" };
    }

    // Validate MIME type
    if (!VALID_PORTRAIT_FORMATS.includes(file.type.toLowerCase())) {
      return {
        valid: false,
        error: `Unsupported format: ${file.type}. Supported: ${VALID_PORTRAIT_FORMATS.join(", ")}`,
      };
    }

    // Validate file size
    if (file.size > MAX_PORTRAIT_SIZE) {
      return {
        valid: false,
        error: `File is too large. Maximum size: ${MAX_PORTRAIT_SIZE / (1024 * 1024)}MB`,
      };
    }

    return { valid: true };
  }

  /**
   * Converts a File object to a DataURL string.
   *
   * @param {File} file - The file to convert.
   * @returns {Promise<string>} The DataURL string.
   */
  static fileToDataURL(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => {
        const result = reader.result;
        if (typeof result === "string") {
          resolve(result);
        } else {
          reject(new TypeError("FileReader produced non-string result"));
        }
      };

      reader.onerror = () => {
        reject(new TypeError("FileReader error: failed to read file"));
      };

      reader.onabort = () => {
        reject(new TypeError("FileReader error: read aborted"));
      };

      reader.readAsDataURL(file);
    });
  }

  /**
   * Subscribes to portrait change events.
   *
   * @param {function(userId: string, newValue: string|null, previousValue: string|null): void} callback
   *   Callback function invoked on portrait changes.
   * @returns {function(): void} Unsubscribe function.
   */
  onPortraitChange(callback) {
    this._subscribers.add(callback);
    return () => {
      this._subscribers.delete(callback);
    };
  }

  /**
   * 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.
   * @param {string|null} previousValue - The previous portrait DataURL or null.
   */
  _notifyPortraitChange(userId, newValue, previousValue) {
    for (const callback of this._subscribers) {
      try {
        callback(userId, newValue, previousValue);
      } catch (err) {
        console.error(
          "[ScryingPool] PortraitFallbackHandler subscriber error:",
          err
        );
      }
    }
  }

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


