/**
 * PresetImportExportManager — Handles import and export of scene presets.
 *
 * Provides: JSON export of all presets, file download, JSON import with validation,
 * merge/replace modes, and comprehensive error handling.
 *
 * Import rule: may only import from src/contracts/.
 * Constructors are side-effect free — call init() from module.js Hooks.once('ready').
 *
 * @module core/PresetImportExportManager
 */

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

/**
 * Result of an import operation.
 * @typedef {Object} ImportResult
 * @property {boolean} success - Whether the import operation succeeded.
 * @property {string} message - Human-readable summary message.
 * @property {number} added - Number of presets added (merge mode).
 * @property {number} replaced - Number of presets replaced (replace mode).
 * @property {number} skipped - Number of presets skipped (duplicates in merge mode).
 * @property {string[]} errors - Array of error messages for failed validations.
 */

/**
 * Export data structure.
 * @typedef {Object} ExportData
 * @property {number} _version - Export schema version.
 * @property {string} [worldName] - Name of the world being exported from (optional).
 * @property {number} [exportedAt] - Timestamp when export was created (optional).
 * @property {Object.<string, import('../contracts/scene-preset.js').ScenePreset>} presets - All presets by name.
 */

/**
 * Manages import and export of scene presets.
 * Provides JSON export, file download, and import with merge/replace modes.
 */
export class PresetImportExportManager {
  /**
   * @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
   *   Injected FoundryAdapter surface.
   * @param {import('./ScenePresetManager.js').ScenePresetManager} scenePresetManager
   *   Injected ScenePresetManager for preset operations.
   */
  constructor(adapter, scenePresetManager) {
    if (!adapter || typeof adapter !== 'object') {
      throw new TypeError('PresetImportExportManager: adapter argument is required and must be an object');
    }
    if (!scenePresetManager || typeof scenePresetManager !== 'object') {
      throw new TypeError('PresetImportExportManager: scenePresetManager argument is required and must be an object');
    }

    // Validate adapter has required methods
    if (typeof adapter.scenes?.current !== 'function') {
      throw new TypeError('PresetImportExportManager: adapter.scenes.current must be a function');
    }

    // Validate scenePresetManager has required methods
    const requiredMethods = ['list', 'save', 'delete', 'get'];
    for (const method of requiredMethods) {
      if (typeof scenePresetManager[method] !== 'function') {
        throw new TypeError(`PresetImportExportManager: scenePresetManager.${method} must be a function`);
      }
    }

    this._adapter = adapter;
    this._scenePresetManager = scenePresetManager;
  }

  /**
   * Exports all presets from the current scene as a JSON string.
   *
   * @returns {Promise<string>} JSON string containing all presets.
   * @throws {TypeError} If no scene is active.
   */
  async exportAllPresets() {
    const currentScene = this._adapter.scenes.current?.();
    if (!currentScene) {
      throw new TypeError('PresetImportExportManager.exportAllPresets: no active scene');
    }

    const presets = this._scenePresetManager.list();
    const worldName = this._adapter.scenes.current?.()?.parent?.name ?? currentScene.name ?? 'world';

    /** @type {ExportData} */
    const exportData = {
      _version: SCENE_PRESET_VERSION,
      worldName,
      exportedAt: Date.now(),
      presets: {},
    };

    for (const preset of presets) {
      exportData.presets[preset.name] = preset;
    }

    return JSON.stringify(exportData, null, 2);
  }

  /**
   * Generates a filename for the export file.
   *
   * @param {string} [worldName] - Name of the world. Defaults to current scene name or 'world'.
   * @param {boolean} [includeTimestamp=true] - Whether to include timestamp in filename.
   * @returns {string} Generated filename.
   */
  generateExportFilename(worldName, includeTimestamp = true) {
    const currentScene = this._adapter.scenes.current?.();
    const name = worldName ?? currentScene?.parent?.name ?? currentScene?.name ?? 'world';
    // Sanitize name: replace any non-alphanumeric, dash, or underscore with underscore
    // Also handle empty string by using 'world' as fallback
    const safeName = name.replace(/[^a-zA-Z0-9\-_]/g, '_').toLowerCase() || 'world';
    const timestamp = includeTimestamp ? `_${Date.now()}` : '';
    return `scrying-pool-presets-${safeName}${timestamp}.json`;
  }

  /**
   * Triggers a browser download of the export file.
   *
   * @param {string} jsonString - JSON string to download.
   * @param {string} filename - Name for the downloaded file.
   * @throws {TypeError} If arguments are invalid.
   */
  downloadExportFile(jsonString, filename) {
    if (typeof jsonString !== 'string') {
      throw new TypeError('PresetImportExportManager.downloadExportFile: jsonString must be a string');
    }
    if (typeof filename !== 'string' || filename.length === 0) {
      throw new TypeError('PresetImportExportManager.downloadExportFile: filename must be a non-empty string');
    }

    const blob = new Blob([jsonString], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    
    try {
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
    } finally {
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  }

  /**
   * Validates imported JSON data structure.
   *
   * @param {unknown} data - Parsed JSON data to validate.
   * @returns {ExportData} Validated export data object.
   * @throws {TypeError} If data structure is invalid.
   */
  validateImportData(data) {
    if (data === null || typeof data !== 'object') {
      throw new TypeError('Import data must be a non-null object');
    }

    const obj = /** @type {Record<string, unknown>} */ (data);

    // Check for _version
    if (typeof obj._version !== 'number') {
      throw new TypeError(`Import data: missing or invalid _version, got ${typeof obj._version}`);
    }

    // Check schema version
    if (obj._version !== SCENE_PRESET_VERSION) {
      throw new TypeError(`Import data: unsupported schema version ${obj._version}, expected ${SCENE_PRESET_VERSION}`);
    }

    // Check for presets
    if (!obj.presets || typeof obj.presets !== 'object' || Array.isArray(obj.presets)) {
      throw new TypeError('Import data: missing or invalid presets object');
    }

    // Validate optional worldName - must be non-empty string if present
    if (obj.worldName !== undefined) {
      if (typeof obj.worldName !== 'string') {
        throw new TypeError('Import data: worldName must be a string if present');
      }
      if (obj.worldName.length === 0) {
        throw new TypeError('Import data: worldName must be a non-empty string if present');
      }
      if (obj.worldName.length > 250) {
        throw new TypeError('Import data: worldName exceeds maximum length of 250 characters');
      }
    }

    // Validate optional exportedAt - must be non-negative finite number if present
    if (obj.exportedAt !== undefined) {
      if (typeof obj.exportedAt !== 'number' || !Number.isFinite(obj.exportedAt)) {
        throw new TypeError('Import data: exportedAt must be a finite number if present');
      }
      if (obj.exportedAt < 0) {
        throw new TypeError('Import data: exportedAt must be a non-negative number if present');
      }
    }

    return /** @type {ExportData} */ (obj);
  }

  /**
   * Extracts and validates individual presets from import data.
   *
   * @param {ExportData} data - Import data (may be partially validated if skipValidation was used).
   * @returns {Array<{name: string, preset: import('../contracts/scene-preset.js').ScenePreset|null, error: string|null}>} Array of extraction results.
   * @private
   */
  _extractAndValidatePresets(data) {
    const results = [];
    const seenNames = new Set();

    // Handle missing or invalid presets
    if (!data.presets || typeof data.presets !== 'object' || Array.isArray(data.presets)) {
      return results; // Return empty array - errors will be caught by validation
    }

    for (const [name, presetData] of Object.entries(data.presets)) {
      // Check for duplicate names in the import file
      if (seenNames.has(name)) {
        results.push({ name, preset: null, error: `Preset "${name}": duplicate name in import file` });
        continue;
      }
      seenNames.add(name);

      // Validate preset name
      if (typeof name !== 'string' || name.length === 0) {
        results.push({ name: String(name), preset: null, error: `Preset: name must be a non-empty string, got "${String(name)}"` });
        continue;
      }

      if (name.length > 100) {
        results.push({ name, preset: null, error: `Preset "${name}": name exceeds maximum length of 100 characters` });
        continue;
      }

      // Validate preset name characters (alphanumeric, dash, underscore, space, dot)
      if (!/^[a-zA-Z0-9 ._-]+$/.test(name)) {
        results.push({ name, preset: null, error: `Preset "${name}": name contains invalid characters (only alphanumeric, space, dot, dash, underscore allowed)` });
        continue;
      }

      try {
        const validated = isValidScenePreset(presetData);
        results.push({ name, preset: validated, error: null });
      } catch (err) {
        const errorMsg = err instanceof Error ? err.message : String(err);
        results.push({ name, preset: null, error: `Preset "${name}": ${errorMsg}` });
      }
    }

    return results;
  }

  /**
   * Imports presets from JSON string with specified mode.
   *
   * @param {string} jsonString - JSON string containing export data.
   * @param {'merge'|'replace'} mode - Import mode: 'merge' adds new presets, 'replace' overwrites all.
   * @param {object} [options] - Import options.
   * @param {boolean} [options.skipValidation=false] - Skip JSON structure validation (for testing).
   * @returns {Promise<ImportResult>} Result of the import operation.
   * @throws {TypeError} If JSON parsing fails or mode is invalid.
   */
  async importPresets(jsonString, mode, options = {}) {
    if (!options || typeof options !== 'object') {
      throw new TypeError('PresetImportExportManager.importPresets: options must be an object');
    }
    const { skipValidation = false } = options;

    // Validate mode
    if (mode !== 'merge' && mode !== 'replace') {
      throw new TypeError('PresetImportExportManager.importPresets: mode must be "merge" or "replace"');
    }

    // Parse JSON
    let parsedData;
    try {
      parsedData = JSON.parse(jsonString);
    } catch {
      throw new TypeError('Import failed: invalid JSON format');
    }

    // Validate structure
    if (!skipValidation) {
      try {
        this.validateImportData(parsedData);
      } catch (err) {
        throw new TypeError(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    /** @type {ExportData} */
    const data = parsedData;

    // Extract and validate presets
    const extractionResults = this._extractAndValidatePresets(data);
    const validPresets = extractionResults.filter(r => r.error === null);
    const errors = extractionResults.filter(r => r.error !== null).map(r => r.error);

    // Get existing preset names for duplicate detection and limit checking
    const existingPresetNames = new Set(this._scenePresetManager.list().map(p => p.name));
    const existingCount = existingPresetNames.size;
    
    // Count how many presets would actually be added (excluding duplicates in merge mode)
    let netNewCount = validPresets.length;
    if (mode === 'merge') {
      // Count only presets that don't already exist
      netNewCount = validPresets.filter(r => !existingPresetNames.has(r.name)).length;
    }

    // Check preset limits
    if (mode === 'merge' && existingCount + netNewCount > MAX_PRESETS_PER_WORLD) {
      errors.push(`Import would exceed maximum of ${MAX_PRESETS_PER_WORLD} presets (currently ${existingCount}, adding ${netNewCount} new)`);
      return {
        success: false,
        message: 'Import cancelled: would exceed preset limit',
        added: 0,
        replaced: 0,
        skipped: 0,
        errors,
      };
    }

    if (mode === 'replace' && validPresets.length > MAX_PRESETS_PER_WORLD) {
      errors.push(`Import file contains ${validPresets.length} presets, exceeding maximum of ${MAX_PRESETS_PER_WORLD}`);
      return {
        success: false,
        message: 'Import cancelled: file exceeds preset limit',
        added: 0,
        replaced: 0,
        skipped: 0,
        errors,
      };
    }

    // Process based on mode
    if (mode === 'replace') {
      const result = await this._replacePresets(data, validPresets, existingCount);
      // Merge extraction errors with replace errors
      result.errors = [...errors, ...result.errors];
      result.success = result.errors.length === 0;
      return result;
    }
    const result = await this._mergePresets(data, validPresets, existingPresetNames);
    // Merge extraction errors with merge errors
    result.errors = [...errors, ...result.errors];
    result.success = result.errors.length === 0;
    return result;
  }

  /**
   * Merges presets into current scene - adds new presets, skips duplicates.
   * Uses public API to ensure proper validation and persistence.
   *
   * @param {ExportData} data - Validated import data.
   * @param {Array<{name: string, preset: import('../contracts/scene-preset.js').ScenePreset|null, error: string|null}>} validPresets - Validated presets to import.
   * @param {Set<string>} existingPresetNames - Set of existing preset names for duplicate detection.
   * @returns {Promise<ImportResult>} Result of the merge operation.
   * @private
   */
  async _mergePresets(data, validPresets, existingPresetNames) {
    let added = 0;
    let skipped = 0;
    const errors = [];

    for (const { name, preset } of validPresets) {
      if (existingPresetNames.has(name)) {
        skipped++;
      } else {
        try {
          // Use public save method instead of direct cache manipulation
          await this._scenePresetManager.save(name, preset);
          added++;
        } catch (err) {
          errors.push(`Failed to add preset "${name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    const message = `Imported ${validPresets.length} presets (${added} new, ${skipped} skipped as duplicates)`;
    return {
      success: errors.length === 0,
      message,
      added,
      replaced: 0,
      skipped,
      errors: errors.length > 0 ? errors : [],
    };
  }

  /**
   * Replaces all existing presets with imported ones.
   * Uses a rollback mechanism: if any add fails after deletions, attempts to restore.
   * Uses public API to ensure proper validation and persistence.
   *
   * @param {ExportData} data - Validated import data.
   * @param {Array<{name: string, preset: import('../contracts/scene-preset.js').ScenePreset|null, error: string|null}>} validPresets - Validated presets to import.
   * @param {number} existingCount - Number of existing presets before replacement.
   * @returns {Promise<ImportResult>} Result of the replace operation.
   * @private
   */
  async _replacePresets(data, validPresets, existingCount) {
    let added = 0;
    const errors = [];
    /** @type {Array<{name: string, preset: import('../contracts/scene-preset.js').ScenePreset}>} */
    const deletedPresets = [];

    // Step 1: Delete all existing presets, saving them for potential rollback
    const existingNames = this._scenePresetManager.list().map(p => p.name);
    for (const name of existingNames) {
      try {
        const preset = this._scenePresetManager.get(name);
        if (preset) {
          deletedPresets.push({ name, preset });
        }
        await this._scenePresetManager.delete(name);
      } catch (err) {
        errors.push(`Failed to delete existing preset "${name}": ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // Step 2: Add all imported presets
    const successfullyAdded = [];
    for (const { name, preset } of validPresets) {
      try {
        await this._scenePresetManager.save(name, preset);
        successfullyAdded.push({ name, preset });
        added++;
      } catch (err) {
        errors.push(`Failed to add preset "${name}": ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // Step 3: If there were errors, attempt rollback
    if (errors.length > 0 && deletedPresets.length > 0) {
      const rollbackErrors = [];
      for (const { name, preset } of deletedPresets) {
        try {
          await this._scenePresetManager.save(name, preset);
        } catch (err) {
          rollbackErrors.push(`Failed to rollback preset "${name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
      if (rollbackErrors.length > 0) {
        errors.push(...rollbackErrors);
      } else {
        errors.push('Partial import failure - rolled back to previous state');
      }
    }

    const message = errors.length === 0 
      ? `Replaced all presets with ${added} from import`
      : `Partial import: added ${added} of ${validPresets.length} (rolled back)`;
    
    return {
      success: errors.length === 0,
      message,
      added,
      replaced: existingCount,
      skipped: 0,
      errors: errors.length > 0 ? errors : [],
    };
  }
}
