/**
 * ScenePresetManager — Manages scene preset CRUD operations.
 *
 * Owns: preset creation, loading, deletion, renaming, listing.
 * Story 3.2: Added auto-apply logic with per-scene configuration.
 * Persists presets to Scene document flags.
 * Emits socket messages for preset apply operations.
 *
 * 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/ScenePresetManager
 */

import { createScenePreset, isValidScenePreset, MAX_PRESETS_PER_WORLD, SCENE_PRESET_VERSION } from '../contracts/scene-preset.js';
import { SOCKET_EVENTS } from '../contracts/socket-message.js';

// Story 3.2: Auto-apply constants
/** @type {number} Maximum pre-delay in milliseconds */
const MAX_PREDELAY_MS = 5000;
/** @type {number} Minimum pre-delay in milliseconds */
const MIN_PREDELAY_MS = 0;

/**
 * Manages scene preset CRUD operations.
 * Persists presets to Scene document flags.
 * Emits socket messages for preset apply operations.
 * Story 3.2: Extended with auto-apply on scene activation.
 */
export class ScenePresetManager {
  /**
   * @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
   *   Injected FoundryAdapter surface.
   * @param {import('./StateStore.js').StateStore} stateStore
   *   Injected StateStore for visibility matrix access.
   * @param {import('./SocketHandler.js').SocketHandler} socketHandler
   *   Injected SocketHandler for socket message emission.
   * @param {import('./VisibilityManager.js').VisibilityManager|null} visibilityManager
   *   Injected VisibilityManager for applying visibility matrix (Story 3.2).
   */
  constructor(adapter, stateStore, socketHandler, visibilityManager = null) {
    // Validate dependencies
    if (!adapter || typeof adapter !== 'object') {
      throw new TypeError('ScenePresetManager: adapter argument is required and must be an object');
    }
    if (!stateStore || typeof stateStore !== 'object') {
      throw new TypeError('ScenePresetManager: stateStore argument is required and must be an object');
    }
    if (!socketHandler || typeof socketHandler !== 'object') {
      throw new TypeError('ScenePresetManager: socketHandler argument is required and must be an object');
    }

    this._adapter = adapter;
    this._stateStore = stateStore;
    this._socketHandler = socketHandler;
    this._visibilityManager = visibilityManager;
    /** @type {Map<string, import('../contracts/scene-preset.js').ScenePreset>} name → ScenePreset */
    this._presetsCache = new Map();
    /** @type {Map<string, number>} sceneId → timeoutId for active pre-delay timers */
    this._activeTimers = new Map();
  }

  /**
   * Initializes the manager by loading presets from the current scene.
   * No hooks registered here for Story 3.1 — hooks for auto-apply come in Story 3.2.
   * Side-effect: loads presets into _presetsCache.
   */
  init() {
    this._loadCurrentScenePresets();
  }

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

  /**
   * Saves the current Visibility Matrix as a named preset.
   *
   * @param {string} name - Preset name (non-empty string).
   * @returns {Promise<import('../contracts/scene-preset.js').ScenePreset>} The created preset.
   * @throws {TypeError} If name is invalid, duplicate, or max presets reached.
   */
  async save(name) {
    // Validate name
    if (typeof name !== 'string' || name.length === 0) {
      throw new TypeError('ScenePresetManager.save: name must be a non-empty string');
    }

    // Get current scene
    const currentScene = this._adapter.scenes.current?.();
    if (!currentScene) {
      throw new TypeError('ScenePresetManager.save: no active scene');
    }

    // Check max presets limit
    if (this._presetsCache.size >= MAX_PRESETS_PER_WORLD) {
      throw new TypeError(
        `ScenePresetManager.save: maximum of ${MAX_PRESETS_PER_WORLD} presets reached. Delete an existing preset to save a new one.`
      );
    }

    // Check for duplicate name
    if (this._presetsCache.has(name)) {
      throw new TypeError(`ScenePresetManager.save: a preset with name "${name}" already exists`);
    }

    // Get current visibility matrix
    const matrixData = this._stateStore.getMatrix();
    const matrix = matrixData.matrix;

    // Create preset
    const preset = createScenePreset(name, matrix);

    // Add to cache
    this._presetsCache.set(name, preset);

    // Persist to scene flag
    await this._saveScenePresets();

    // Emit notification
    this._adapter.notifications.info(
      this._adapter.i18n.localize('scrying-pool.presets.notifications.saved')
        .replace('{name}', name)
    );

    return preset;
  }

  /**
   * Loads a preset by name, applying its matrix to the current visibility state.
   *
   * @param {string} name - Preset name to load.
   * @param {object} options - Options object.
   * @param {boolean} [options.emitSocket=true] - Whether to emit socket message.
   *   Set to false when called in response to a socket message to prevent loops.
   * @returns {Promise<void>}
   * @throws {TypeError} If name is invalid or preset not found.
   */
  async load(name, options = {}) {
    const { emitSocket = true } = options;
    // Validate name
    if (typeof name !== 'string' || name.length === 0) {
      throw new TypeError('ScenePresetManager.load: name must be a non-empty string');
    }

    // Get preset from cache (loaded via init or save)
    const preset = this._presetsCache.get(name);
    if (!preset) {
      throw new TypeError(`ScenePresetManager.load: preset "${name}" not found`);
    }

    // Validate preset structure
    isValidScenePreset(preset);

    // Apply via VisibilityManager if available (Story 3.2), otherwise fall back to StateStore
    if (this._visibilityManager) {
      await this._visibilityManager.applyMatrix(preset.matrix);
    } else {
      await this._stateStore.setMatrix({
        _version: preset._version,
        matrix: { ...preset.matrix },
      });
    }

    // Emit socket message only if requested (prevents loops when called from socket handler)
    if (emitSocket) {
      const now = Date.now();
      this._socketHandler.emit(SOCKET_EVENTS.PRESET_APPLIED, {
        presetName: name,
        timestamp: now,
      });
    }

    // Emit notification
    this._adapter.notifications.info(
      this._adapter.i18n.localize('scrying-pool.presets.notifications.applied')
        .replace('{name}', name)
    );
  }

  /**
   * Deletes a preset by name.
   *
   * @param {string} name - Preset name to delete.
   * @returns {Promise<void>}
   * @throws {TypeError} If name is invalid or preset not found.
   */
  async delete(name) {
    // Validate name
    if (typeof name !== 'string' || name.length === 0) {
      throw new TypeError('ScenePresetManager.delete: name must be a non-empty string');
    }

    // Check if preset exists
    if (!this._presetsCache.has(name)) {
      throw new TypeError(`ScenePresetManager.delete: preset "${name}" not found`);
    }

    // Delete from cache
    this._presetsCache.delete(name);

    // Persist changes
    await this._saveScenePresets();
  }

  /**
   * Renames a preset.
   *
   * @param {string} oldName - Current preset name.
   * @param {string} newName - New preset name.
   * @returns {Promise<import('../contracts/scene-preset.js').ScenePreset>} The renamed preset.
   * @throws {TypeError} If names are invalid, oldName not found, or newName conflicts.
   */
  async rename(oldName, newName) {
    // Validate oldName
    if (typeof oldName !== 'string' || oldName.length === 0) {
      throw new TypeError('ScenePresetManager.rename: oldName must be a non-empty string');
    }

    // Validate newName
    if (typeof newName !== 'string' || newName.length === 0) {
      throw new TypeError('ScenePresetManager.rename: newName must be a non-empty string');
    }

    // Check if oldName exists
    if (!this._presetsCache.has(oldName)) {
      throw new TypeError(`ScenePresetManager.rename: preset "${oldName}" not found`);
    }

    // Check if newName conflicts
    if (oldName !== newName && this._presetsCache.has(newName)) {
      throw new TypeError(
        `ScenePresetManager.rename: a preset with name "${newName}" already exists`
      );
    }

    // Get preset
    const preset = this._presetsCache.get(oldName);

    // Delete old entry
    this._presetsCache.delete(oldName);

    // Update preset name and timestamps
    const now = Date.now();
    const renamedPreset = {
      ...preset,
      name: newName,
      updatedAt: now,
    };
    // Validate the renamed preset structure
    isValidScenePreset(/** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset));

    // Add with new name
    this._presetsCache.set(newName, /** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset));

    // Persist changes
    await this._saveScenePresets();

    return /** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset);
  }

  /**
   * Returns all presets for the current scene.
   *
   * @returns {Array<import('../contracts/scene-preset.js').ScenePreset>} Array of preset objects.
   */
  list() {
    return Array.from(this._presetsCache.values());
  }

  /**
   * Returns a specific preset by name, or null if not found.
   *
   * @param {string} name - Preset name.
   * @returns {import('../contracts/scene-preset.js').ScenePreset|null}
   */
  get(name) {
    return this._presetsCache.get(name) ?? null;
  }

  /**
   * Loads presets from the current scene's flag.
   * @private
   */
  _loadCurrentScenePresets() {
    this._presetsCache.clear();

    const currentScene = this._adapter.scenes.current?.();
    if (!currentScene) {
      return;
    }

    try {
      const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (currentScene).getFlag?.('scrying-pool', 'presets');
      if (!flagData || typeof flagData !== 'object') {
        return; // No presets or invalid format
      }

      // Type assert flagData as having _version and presets
      const flag = /** @type {{ _version: number; presets?: Record<string, unknown> }} */ (flagData);
      if (flag._version !== SCENE_PRESET_VERSION) {
        console.warn(
          `[ScryingPool] ScenePresetManager: unsupported presets schema version ${flag._version}, expected ${SCENE_PRESET_VERSION}`
        );
        return;
      }

      const { presets } = flag;
      if (!presets || typeof presets !== 'object') {
        return;
      }

      for (const [name, presetData] of Object.entries(presets)) {
        try {
          const validated = isValidScenePreset(presetData);
          this._presetsCache.set(name, validated);
        } catch (err) {
          console.warn(
            `[ScryingPool] ScenePresetManager: invalid preset "${name}" in scene flag, skipping: ${err instanceof Error ? err.message : String(err)}`
          );
        }
      }
    } catch (err) {
      console.warn(
        '[ScryingPool] ScenePresetManager: failed to load scene presets',
        err instanceof Error ? err.message : String(err)
      );
    }
  }

  /**
   * Saves presets to the current scene's flag.
   * @private
   * @returns {Promise<void>}
   */
  async _saveScenePresets() {
    const currentScene = this._adapter.scenes.current?.();
    if (!currentScene) {
      throw new TypeError('ScenePresetManager._saveScenePresets: no active scene');
    }

    /** @type {Record<string, import('../contracts/scene-preset.js').ScenePreset>} */
    const presetsObj = {};
    for (const [name, preset] of this._presetsCache) {
      presetsObj[name] = preset;
    }

    // Get existing flag data to preserve autoApply config
    const existingFlag = this._getSceneFlagData(currentScene);
    const autoApply = existingFlag?.autoApply ?? null;

    const flagData = {
      _version: SCENE_PRESET_VERSION,
      presets: presetsObj,
      ...(autoApply && { autoApply }),
    };

    try {
      await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (currentScene).setFlag?.('scrying-pool', 'presets', flagData);
    } catch (err) {
      console.error(
        '[ScryingPool] ScenePresetManager: failed to save scene presets',
        err instanceof Error ? err.message : String(err)
      );
      throw err;
    }
  }

  // ==========================================================================
  // Story 3.2: Auto-Apply Methods
  // ==========================================================================

  /**
   * Handles scene activation for auto-apply functionality.
   * Checks global enable, per-scene config, and applies preset after pre-delay.
   *
   * @param {object} scene - The activated FoundryVTT Scene document.
   * @returns {Promise<void>}
   */
  async onSceneActivate(scene) {
    // Check if auto-apply is globally enabled
    const globalEnabled = this._adapter.settings.get?.('autoApplyEnabled') ?? true;
    if (!globalEnabled) {
      return; // Global disable
    }

    // Get current scene's flag data
    const flagData = this._getSceneFlagData(scene);
    if (!flagData) {
      return; // No preset data for this scene
    }

    // Get auto-apply config (with defaults)
    const autoApplyConfig = this._getAutoApplyConfig(flagData);
    if (!autoApplyConfig.enabled) {
      return; // Per-scene disable
    }

    // Check if preset exists
    const preset = this._presetsCache.get(autoApplyConfig.presetName);
    if (!preset) {
      console.warn(
        `[ScryingPool] ScenePresetManager.onSceneActivate: preset "${autoApplyConfig.presetName}" not found in cache`
      );
      return;
    }

    // Clear ALL pending timers when any scene is activated (prevents old scene timer from firing)
    this._clearAllTimers();

    // Apply preset after pre-delay
    await this._applyWithDelay(scene, autoApplyConfig.presetName, autoApplyConfig.preDelay);
  }

  /**
   * Applies a preset with optional auto-apply flag.
   * Story 3.2: Extended to support auto-applied presets.
   *
   * @param {string} presetName - Name of the preset to apply.
   * @param {object} options - Options object.
   * @param {boolean} options.autoApplied - Whether this is an auto-apply operation.
   * @returns {Promise<void>}
   * @throws {TypeError} If preset not found.
   */
  async applyPreset(presetName, options = {}) {
    const { autoApplied = false } = options;

    // Validate presetName
    if (typeof presetName !== 'string' || presetName.length === 0) {
      throw new TypeError('ScenePresetManager.applyPreset: presetName must be a non-empty string');
    }

    // Get preset from cache
    const preset = this._presetsCache.get(presetName);
    if (!preset) {
      throw new TypeError(`ScenePresetManager.applyPreset: preset "${presetName}" not found`);
    }

    // Validate preset structure
    isValidScenePreset(preset);

    // Apply via VisibilityManager if available (Story 3.2), otherwise fall back to StateStore
    if (this._visibilityManager) {
      await this._visibilityManager.applyMatrix(preset.matrix);
    } else {
      await this._stateStore.setMatrix({
        _version: preset._version,
        matrix: { ...preset.matrix },
      });
    }

    // Emit socket message for preset apply
    const now = Date.now();
    this._socketHandler.emit(SOCKET_EVENTS.PRESET_APPLIED, {
      presetName,
      timestamp: now,
      autoApplied,
    });

    // Also emit hook for local confirmation (ConfirmationBar)
    this._adapter.hooks.callAll('scrying-pool:presetApplied', {
      presetName,
      matrix: preset.matrix,
      autoApplied,
      timestamp: now,
    });
  }

  /**
   * Configures auto-apply settings for a scene.
   *
   * @param {object} scene - The FoundryVTT Scene document.
   * @param {object} config - Auto-apply configuration.
   * @param {boolean} config.enabled - Whether auto-apply is enabled.
   * @param {string|null} config.presetName - Name of preset to auto-apply.
   * @param {number} config.preDelay - Pre-delay in milliseconds (0-5000).
   * @returns {Promise<void>}
   * @throws {TypeError} If validation fails.
   */
  async configureAutoApply(scene, config) {
    // Validate scene
    if (!scene || typeof scene !== 'object') {
      throw new TypeError('ScenePresetManager.configureAutoApply: scene argument is required and must be an object');
    }

    // Validate config
    if (!config || typeof config !== 'object') {
      throw new TypeError('ScenePresetManager.configureAutoApply: config argument is required and must be an object');
    }

    const { enabled, presetName, preDelay } = config;

    // Validate enabled
    if (typeof enabled !== 'boolean') {
      throw new TypeError('ScenePresetManager.configureAutoApply: enabled must be a boolean');
    }

    // Validate presetName
    if (presetName !== null && typeof presetName !== 'string') {
      throw new TypeError('ScenePresetManager.configureAutoApply: presetName must be a string or null');
    }

    // Validate preDelay
    if (typeof preDelay !== 'number' || preDelay < MIN_PREDELAY_MS || preDelay > MAX_PREDELAY_MS) {
      throw new TypeError(
        `ScenePresetManager.configureAutoApply: preDelay must be a number between ${MIN_PREDELAY_MS} and ${MAX_PREDELAY_MS}`
      );
    }

    // Get current flag data
    const currentFlag = this._getSceneFlagData(scene);
    const currentPresets = currentFlag?.presets ?? {};

    // Build new flag data
    const newFlagData = {
      _version: SCENE_PRESET_VERSION,
      presets: currentPresets,
      autoApply: {
        enabled,
        presetName: presetName ?? null,
        preDelay,
      },
    };

    // Persist to scene flag
    try {
      await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (scene).setFlag?.('scrying-pool', 'presets', newFlagData);
    } catch (err) {
      console.error(
        '[ScryingPool] ScenePresetManager: failed to configure auto-apply',
        err instanceof Error ? err.message : String(err)
      );
      throw err;
    }
  }

  /**
   * Gets auto-apply configuration from scene flag data.
   * Returns defaults if autoApply field is missing.
   *
   * @param {object} flagData - The scene flag data.
   * @returns {object} Auto-apply configuration with defaults.
   * @private
   */
  _getAutoApplyConfig(flagData) {
    if (!flagData || typeof flagData !== 'object') {
      return { enabled: false, presetName: null, preDelay: 0 };
    }

    const { autoApply } = flagData;
    if (!autoApply || typeof autoApply !== 'object') {
      return { enabled: false, presetName: null, preDelay: 0 };
    }

    return {
      enabled: Boolean(autoApply.enabled),
      presetName: typeof autoApply.presetName === 'string' ? autoApply.presetName : null,
      preDelay: typeof autoApply.preDelay === 'number' ? Math.max(MIN_PREDELAY_MS, Math.min(autoApply.preDelay, MAX_PREDELAY_MS)) : 0,
    };
  }

  /**
   * Applies a preset after a specified delay.
   *
   * @param {object} scene - The FoundryVTT Scene document.
   * @param {string} presetName - Name of the preset to apply.
   * @param {number} delayMs - Delay in milliseconds.
   * @returns {number} Timeout ID for potential cancellation.
   * @private
   */
  _applyWithDelay(scene, presetName, delayMs) {
    const timerId = setTimeout(async () => {
      try {
        await this.applyPreset(presetName, { autoApplied: true });
        
        // Notify via ui.notifications
        this._adapter.notifications.info(
          this._adapter.i18n.localize('scrying-pool.presets.notifications.scene-applied')
            .replace('{name}', presetName)
        );
      } catch (err) {
        console.error(
          '[ScryingPool] ScenePresetManager: failed to apply preset on scene activation',
          err instanceof Error ? err.message : String(err)
        );
      } finally {
        // Clean up timer reference
        this._activeTimers.delete(scene.id);
      }
    }, delayMs);
    
    // Store timer reference for cancellation
    if (scene?.id) {
      this._activeTimers.set(scene.id, timerId);
    }
    
    return timerId;
  }

  /**
   * Clears the active timer for a scene.
   *
   * @param {object} scene - The FoundryVTT Scene document.
   * @private
   */
  _clearSceneTimer(scene) {
    if (!scene?.id) return;
    
    const timerId = this._activeTimers.get(scene.id);
    if (timerId) {
      clearTimeout(timerId);
      this._activeTimers.delete(scene.id);
    }
  }

  /**
   * Clears ALL active timers.
   * Called when a new scene is activated to prevent old scene timers from firing.
   * @private
   */
  _clearAllTimers() {
    for (const [, timerId] of this._activeTimers) {
      clearTimeout(timerId);
    }
    this._activeTimers.clear();
  }

  /**
   * Gets the complete flag data from a scene.
   *
   * @param {object} scene - The FoundryVTT Scene document.
   * @returns {object|null} The flag data or null.
   * @private
   */
  _getSceneFlagData(scene) {
    try {
      const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (scene).getFlag?.('scrying-pool', 'presets');
      if (!flagData || typeof flagData !== 'object') {
        return null;
      }
      return /** @type {object} */ (flagData);
    } catch (err) {
      console.warn(
        '[ScryingPool] ScenePresetManager: failed to get scene flag data',
        err instanceof Error ? err.message : String(err)
      );
      return null;
    }
  }
}
