/**
 * @typedef {import("./types.js").MissionDefinition} MissionDefinition
 * @typedef {import("./types.js").MissionSaveData} MissionSaveData
 * @typedef {import("./types.js").Objective} Objective
 * @typedef {import("./types.js").GameEventData} GameEventData
 */

import { narrativeManager } from "./NarrativeManager.js";
import { MissionGenerator } from "../systems/MissionGenerator.js";

/**
 * MissionManager.js
 * Manages campaign progression, mission selection, narrative triggers, and objective tracking.
 * @class
 */
export class MissionManager {
  /**
   * @param {import("../core/Persistence.js").Persistence} [persistence] - Persistence manager (optional)
   */
  constructor(persistence = null) {
    /** @type {import("../core/Persistence.js").Persistence | null} */
    this.persistence = persistence;

    // Campaign State
    /** @type {string | null} */
    this.activeMissionId = null;
    /** @type {Set<string>} */
    this.completedMissions = new Set();
    /** @type {Set<string>} */
    this.unlockedMissions = new Set(); // Track unlocked missions
    this.unlockedMissions.add("MISSION_ACT1_01"); // Default unlock
    this.proceduralMissionsUnlocked = false; // Flag for procedural generation
    /** @type {Map<string, MissionDefinition>} */
    this.missionRegistry = new Map();
    /** @type {Map<string, MissionDefinition>} */
    this.completedMissionDetails = new Map();

    // Active Run State
    /** @type {MissionDefinition | null} */
    this.currentMissionDef = null;
    /** @type {Objective[]} */
    this.currentObjectives = [];
    /** @type {Objective[]} */
    this.secondaryObjectives = [];
    /** @type {Array<{type: string; [key: string]: unknown}>} */
    this.failureConditions = [];
    /** @type {UnitManager | null} */
    this.unitManager = null;
    /** @type {TurnSystem | null} */
    this.turnSystem = null;
    /** @type {import("../grid/VoxelGrid.js").VoxelGrid | null} */
    this.grid = null;
    /** @type {import("../systems/MovementSystem.js").MovementSystem | null} */
    this.movementSystem = null;
    /** @type {boolean} */
    this._hadEnemies = false; // Track if we had enemies for ELIMINATE_ALL check
    /** @type {number} */
    this.currentTurn = 0;

    /** @type {Promise<void> | null} */
    this._missionsLoadPromise = null;
  }

  /**
   * Lazy-loads all mission definitions if not already loaded.
   * @returns {Promise<void>}
   */
  async _ensureMissionsLoaded() {
    if (this._missionsLoadPromise) {
      return this._missionsLoadPromise;
    }

    this._missionsLoadPromise = this._loadMissions();
    return this._missionsLoadPromise;
  }

  /**
   * Loads all mission definitions.
   * @private
   * @returns {Promise<void>}
   */
  async _loadMissions() {
    // Only load if registry is empty (first time)
    if (this.missionRegistry.size > 0) {
      return;
    }

    try {
      // Step 1: Load Initial Mission (Act 1, Mission 1)
      const mission01 = await import(
        "../assets/data/missions/mission_act1_01.json",
        {
          with: { type: "json" },
        }
      ).then((m) => m.default);

      this.registerMission(mission01);

      // Step 2: Load unlock state
      await this.loadUnlockState();

      // Step 3: Load any other unlocked missions
      if (this.unlockedMissions.size > 0) {
        for (const missionId of this.unlockedMissions) {
          if (
            missionId !== "MISSION_ACT1_01" &&
            !this.missionRegistry.has(missionId)
          ) {
            await this.loadMissionFile(missionId);
          }
        }
      }

      console.log("MissionManager: Missions loaded successfully.");
    } catch (err) {
      console.error("MissionManager: Failed to load missions:", err);
    }
  }

  /**
   * Dynamically loads a mission file by ID.
   * Uses simple heuristic: MISSION_XYZ -> mission_xyz.json
   * @param {string} missionId
   */
  async loadMissionFile(missionId) {
    try {
      const filename = missionId.toLowerCase() + ".json";
      let path = `../assets/data/missions/${filename}`;

      const mission = await import(/* @vite-ignore */ path, {
        with: { type: "json" },
      }).then((m) => m.default);

      this.registerMission(mission);
      console.log(`Dynamically loaded unlocked mission: ${missionId}`);
    } catch (e) {
      console.warn(
        `Failed to dynamically load mission ${missionId} from ${filename}`,
        e
      );
      // Fallback: Try with underscore
      try {
        const pathWithUnderscore = `../assets/data/missions/_${missionId.toLowerCase()}.json`;
        const mission = await import(/* @vite-ignore */ pathWithUnderscore, {
          with: { type: "json" },
        }).then((m) => m.default);
        this.registerMission(mission);
        console.log(
          `Dynamically loaded unlocked mission (with underscore): ${missionId}`
        );
      } catch (e2) {
        console.error(
          `Could not load mission ${missionId} with either naming convention.`
        );
      }
    }
  }

  /**
   * Registers a mission definition.
   * @param {MissionDefinition} missionDef - Mission definition to register
   */
  registerMission(missionDef) {
    this.missionRegistry.set(missionDef.id, missionDef);
    // Dispatch event to notify UI components (like MissionBoard) that missions have been updated
    window.dispatchEvent(
      new CustomEvent("missions-updated", {
        detail: { missionId: missionDef.id, action: "registered" },
      })
    );
  }

  /**
   * Checks if procedural missions are unlocked.
   * @returns {boolean}
   */
  areProceduralMissionsUnlocked() {
    return this.proceduralMissionsUnlocked;
  }

  /**
   * Refreshes procedural missions on the board.
   * Generates new side ops and removes expired ones.
   * Only generates missions if they are unlocked.
   * @param {boolean} isDailyReset - If true, decrements expiresIn for all missions
   */
  refreshProceduralMissions(isDailyReset = false) {
    // Only generate procedural missions if they're unlocked
    if (!this.areProceduralMissionsUnlocked()) {
      return;
    }
    // Get current campaign tier (default to 1, can be calculated from completed missions later)
    const tier = this._calculateCampaignTier();

    // Get unlocked regions (default to at least Rusting Wastes)
    const unlockedRegions = this._getUnlockedRegions();

    // Get mission history for series generation
    const history = this._getMissionHistory();

    // Separate static missions from procedural missions
    const staticMissions = [];
    const proceduralMissions = [];
    const completedProceduralMissions = [];

    for (const mission of this.missionRegistry.values()) {
      if (mission.type === "SIDE_QUEST" && mission.id?.startsWith("SIDE_OP_")) {
        // Keep completed procedural missions in the registry so they show in completed tab
        if (this.completedMissions.has(mission.id)) {
          completedProceduralMissions.push(mission);
        } else {
          proceduralMissions.push(mission);
        }
      } else {
        staticMissions.push(mission);
      }
    }

    // Refresh procedural missions (refreshBoard fills up to 5 missions)
    // Only pass non-completed procedural missions to refresh
    const refreshedProcedural = MissionGenerator.refreshBoard(
      proceduralMissions,
      tier,
      unlockedRegions,
      history,
      isDailyReset
    );

    // Remove old non-completed procedural missions from registry
    proceduralMissions.forEach((mission) => {
      this.missionRegistry.delete(mission.id);
    });

    // Register refreshed procedural missions (batch register to avoid multiple events)
    refreshedProcedural.forEach((mission) => {
      // Register without dispatching event for each mission
      this.missionRegistry.set(mission.id, mission);
    });

    console.log(
      `Refreshed procedural missions: ${refreshedProcedural.length} available`
    );

    // Dispatch single event after all missions are registered
    window.dispatchEvent(
      new CustomEvent("missions-updated", {
        detail: { action: "refreshed", count: refreshedProcedural.length },
      })
    );
  }

  /**
   * Calculates the current campaign tier based on completed missions.
   * @private
   * @returns {number} Campaign tier (1-5)
   */
  _calculateCampaignTier() {
    // Simple tier calculation: 1 + floor(completed missions / 2)
    // This can be refined later based on specific story mission completion
    const completedCount = this.completedMissions.size;
    const tier = Math.min(5, Math.max(1, 1 + Math.floor(completedCount / 2)));
    return tier;
  }

  /**
   * Gets the list of unlocked biome regions.
   * @private
   * @returns {Array<string>} Array of biome type IDs
   */
  _getUnlockedRegions() {
    // Default: Rusting Wastes is always unlocked
    const defaultRegions = ["BIOME_RUSTING_WASTES"];

    // Unlock additional regions based on completed missions
    // This is a simple implementation - can be enhanced with actual unlock logic
    if (this.completedMissions.has("MISSION_ACT1_01")) {
      // After tutorial, unlock Crystal Spires
      if (!defaultRegions.includes("BIOME_CRYSTAL_SPIRES")) {
        defaultRegions.push("BIOME_CRYSTAL_SPIRES");
      }
    }

    // Add more biome unlocks as story missions are completed
    // For now, return at least the default region
    return defaultRegions;
  }

  /**
   * Gets mission history for series generation.
   * @private
   * @returns {Array<string>} Array of completed mission titles or IDs
   */
  _getMissionHistory() {
    const history = [];

    // Get titles of completed missions from registry
    for (const missionId of this.completedMissions) {
      const mission = this.missionRegistry.get(missionId);
      if (mission?.config?.title) {
        history.push(mission.config.title);
      }
    }

    return history;
  }

  // --- PERSISTENCE (Campaign) ---

  /**
   * Loads campaign save data.
   * @param {MissionSaveData} saveData - Save data to load
   */
  load(saveData) {
    this.completedMissions = new Set(saveData.completedMissions || []);
    // Restore completed mission details (especially important for procedural missions)
    this.completedMissionDetails = new Map();
    if (saveData.completedMissionDetails) {
      for (const [missionId, missionDef] of Object.entries(
        saveData.completedMissionDetails
      )) {
        this.completedMissionDetails.set(missionId, missionDef);
      }
    }
    // Default to Tutorial if history is empty
    if (this.completedMissions.size === 0) {
      this.activeMissionId = "MISSION_ACT1_01";
    }
  }

  /**
   * Saves campaign data.
   * @returns {MissionSaveData} - Serialized campaign data
   */
  save() {
    // Convert completed mission details Map to object for serialization
    const completedMissionDetailsObj = {};
    for (const [
      missionId,
      missionDef,
    ] of this.completedMissionDetails.entries()) {
      completedMissionDetailsObj[missionId] = missionDef;
    }

    return {
      completedMissions: Array.from(this.completedMissions),
      completedMissionDetails: completedMissionDetailsObj,
    };
  }

  // --- MISSION SETUP & NARRATIVE ---

  /**
   * Gets the configuration for the currently selected mission.
   * Ensures missions are loaded before accessing.
   * @returns {Promise<MissionDefinition | undefined>} - Active mission definition
   */
  async getActiveMission() {
    await this._ensureMissionsLoaded();
    if (!this.activeMissionId)
      return this.missionRegistry.get("MISSION_ACT1_01");
    return this.missionRegistry.get(this.activeMissionId);
  }

  /**
   * Sets the unit manager reference for objective checking.
   * @param {UnitManager} unitManager - Unit manager instance
   */
  setUnitManager(unitManager) {
    this.unitManager = unitManager;
  }

  /**
   * Sets the turn system reference for turn-based objectives.
   * @param {TurnSystem} turnSystem - Turn system instance
   */
  setTurnSystem(turnSystem) {
    this.turnSystem = turnSystem;
  }

  /**
   * Sets the grid and movement system references for zone coordinate generation.
   * @param {import("../grid/VoxelGrid.js").VoxelGrid} grid - Voxel grid instance
   * @param {import("../systems/MovementSystem.js").MovementSystem} movementSystem - Movement system instance
   */
  setGridContext(grid, movementSystem) {
    this.grid = grid;
    this.movementSystem = movementSystem;
  }

  /**
   * Populates zone coordinates for REACH_ZONE objectives that don't have them.
   * Generates random walkable positions on the map.
   */
  populateZoneCoordinates() {
    if (!this.grid || !this.movementSystem) {
      console.warn(
        "Cannot populate zone coordinates: grid or movementSystem not set"
      );
      return;
    }

    // Find all REACH_ZONE objectives that need zone_coords
    const reachZoneObjectives = [
      ...this.currentObjectives,
      ...this.secondaryObjectives,
    ].filter(
      (obj) =>
        obj.type === "REACH_ZONE" &&
        (!obj.zone_coords || obj.zone_coords.length === 0)
    );

    for (const obj of reachZoneObjectives) {
      const targetCount = obj.target_count || 3;
      const zones = [];

      // Generate random walkable positions
      const attempts = 100;
      for (let i = 0; i < attempts && zones.length < targetCount; i++) {
        const x = Math.floor(Math.random() * this.grid.size.x);
        const z = Math.floor(Math.random() * this.grid.size.z);
        const y = Math.floor(this.grid.size.y / 2); // Start from middle height

        const walkableY = this.movementSystem.findWalkableY(x, z, y);
        if (
          walkableY !== null &&
          !this.grid.isOccupied({ x, y: walkableY, z })
        ) {
          // Check if we already have a zone at this position (avoid duplicates)
          const isDuplicate = zones.some(
            (zone) => zone.x === x && zone.y === walkableY && zone.z === z
          );
          if (!isDuplicate) {
            zones.push({ x, y: walkableY, z });
          }
        }
      }

      if (zones.length > 0) {
        obj.zone_coords = zones;
        console.log(
          `Populated ${zones.length} zone coordinates for objective ${obj.id}:`,
          zones
        );
      } else {
        console.warn(
          `Failed to generate zone coordinates for objective ${obj.id}`
        );
      }
    }
  }

  /**
   * Resolves positions for mission objects based on placement strategies.
   * Modifies the mission_objects array in-place with calculated positions.
   */
  resolveMissionObjectPositions() {
    if (!this.grid || !this.movementSystem) {
      console.warn(
        "Cannot resolve mission object positions: grid or movementSystem not set"
      );
      return;
    }

    if (!this.currentMissionDef || !this.currentMissionDef.mission_objects) {
      return;
    }

    for (const obj of this.currentMissionDef.mission_objects) {
      if (obj.position) continue; // Already has position

      let position = null;
      const strategy = obj.placement_strategy || "random_walkable";

      if (strategy === "random_walkable") {
        const attempts = 50;
        for (let i = 0; i < attempts; i++) {
          const x = Math.floor(Math.random() * this.grid.size.x);
          const z = Math.floor(Math.random() * this.grid.size.z);
          const y = Math.floor(this.grid.size.y / 2); // Start middle

          const walkableY = this.movementSystem.findWalkableY(x, z, y);
          if (
            walkableY !== null &&
            !this.grid.isOccupied({ x, y: walkableY, z })
          ) {
            position = { x, y: walkableY, z };
            break;
          }
        }
      } else if (strategy === "center_of_enemy_room") {
        // Placeholder: For now, treating as random if logic missing
        // Ideally this would query a RoomManager or similar
        // Falling back to random for now to ensure it spawns somewhere
        console.warn(
          "Placement strategy 'center_of_enemy_room' not fully implemented, falling back to random."
        );
        position = this._findRandomWalkablePosition();
      } else if (strategy === "center_of_player_room") {
        // Placeholder
        position = this._findRandomWalkablePosition();
      }

      if (position) {
        obj.position = position;
        console.log(`Resolved position for object ${obj.object_id}:`, position);
      } else {
        console.warn(
          `Failed to resolve position for object ${obj.object_id} with strategy ${strategy}`
        );
      }
    }
  }

  _findRandomWalkablePosition() {
    const attempts = 50;
    for (let i = 0; i < attempts; i++) {
      const x = Math.floor(Math.random() * this.grid.size.x);
      const z = Math.floor(Math.random() * this.grid.size.z);
      const y = Math.floor(this.grid.size.y / 2);

      const walkableY = this.movementSystem.findWalkableY(x, z, y);
      if (walkableY !== null && !this.grid.isOccupied({ x, y: walkableY, z })) {
        return { x, y: walkableY, z };
      }
    }
    return null;
  }

  /**
   * Prepares the manager for a new run.
   * Resets objectives and prepares narrative hooks.
   * @returns {Promise<void>}
   */
  async setupActiveMission() {
    await this._ensureMissionsLoaded();
    const mission = await this.getActiveMission();
    this.currentMissionDef = mission;
    this.currentTurn = 0;

    // Hydrate primary objectives state
    this.currentObjectives = (mission.objectives.primary || []).map((obj) => ({
      ...obj,
      current: 0,
      complete: false,
    }));

    // Hydrate secondary objectives state
    this.secondaryObjectives = (mission.objectives.secondary || []).map(
      (obj) => ({
        ...obj,
        current: 0,
        complete: false,
      })
    );

    // Store failure conditions
    this.failureConditions = mission.objectives.failure_conditions || [];

    console.log(
      `Mission Setup: ${mission.config.title} - Primary Objectives:`,
      this.currentObjectives
    );
    if (this.secondaryObjectives.length > 0) {
      console.log("Secondary Objectives:", this.secondaryObjectives);
    }
    if (this.failureConditions.length > 0) {
      console.log("Failure Conditions:", this.failureConditions);
    }
  }

  /**
   * Plays the intro narrative if one exists.
   * Returns a Promise that resolves when the game should start.
   */
  async playIntro() {
    if (
      !this.currentMissionDef ||
      !this.currentMissionDef.narrative ||
      !this.currentMissionDef.narrative.intro_sequence
    ) {
      return Promise.resolve();
    }

    return new Promise(async (resolve) => {
      const introId = this.currentMissionDef.narrative.intro_sequence;

      try {
        let narrativeData;

        // Check for dynamic data first
        if (
          this.currentMissionDef.narrative._dynamic_data &&
          this.currentMissionDef.narrative._dynamic_data[introId]
        ) {
          console.log(`Using dynamic narrative data for ${introId}`);
          narrativeData =
            this.currentMissionDef.narrative._dynamic_data[introId];
        } else {
          // Fallback to loading from file
          const narrativeFileName = this._mapNarrativeIdToFileName(introId);
          const response = await fetch(
            `assets/data/narrative/${narrativeFileName}.json`
          );
          if (!response.ok) {
            console.error(`Failed to load narrative: ${narrativeFileName}`);
            resolve();
            return;
          }
          narrativeData = await response.json();
        }

        // Set up listener for narrative end
        const onEnd = () => {
          narrativeManager.removeEventListener("narrative-end", onEnd);
          resolve();
        };
        narrativeManager.addEventListener("narrative-end", onEnd);

        // Start the narrative sequence
        console.log(`Playing Narrative Intro: ${introId}`);
        narrativeManager.startSequence(narrativeData);
      } catch (error) {
        console.error(`Error loading/playing narrative ${introId}:`, error);
        resolve(); // Resolve anyway to not block game start
      }
    });
  }

  /**
   * Maps narrative sequence ID to filename.
   * @param {string} narrativeId - The narrative ID from mission config
   * @returns {string} The filename (without .json extension)
   */
  _mapNarrativeIdToFileName(narrativeId) {
    // Convert NARRATIVE_TUTORIAL_INTRO -> tutorial_intro
    // Remove NARRATIVE_ prefix and convert to lowercase with underscores
    const mapping = {
      NARRATIVE_TUTORIAL_INTRO: "tutorial_intro",
      NARRATIVE_TUTORIAL_SUCCESS: "tutorial_success",
      NARRATIVE_ACT1_FINAL_WIN: "act1_final_win",
      NARRATIVE_ACT1_FINAL_LOSE: "act1_final_lose",
    };

    if (mapping[narrativeId]) {
      return mapping[narrativeId];
    }

    // For unmapped IDs, convert NARRATIVE_XYZ -> narrative_xyz
    // Keep the "narrative_" prefix but lowercase everything
    return narrativeId.toLowerCase();
  }

  // --- GAMEPLAY LOGIC (Objectives) ---

  /**
   * Called by GameLoop whenever a relevant event occurs.
   * @param {string} type - 'ENEMY_DEATH', 'TURN_END', 'PLAYER_DEATH', etc.
   * @param {GameEventData} data - Context data
   */
  onGameEvent(type, data) {
    if (!this.currentMissionDef) return;

    // Check failure conditions first
    this.checkFailureConditions(type, data);

    // Update objectives
    let statusChanged = false;

    // Process primary objectives
    statusChanged =
      this.updateObjectives(this.currentObjectives, type, data) ||
      statusChanged;

    // Process secondary objectives
    this.updateObjectives(this.secondaryObjectives, type, data);

    // Check for ELIMINATE_ALL objective completion (needs active check)
    // Check after enemy death or at turn end
    if (type === "ENEMY_DEATH") {
      console.log(
        `[MissionManager] ENEMY_DEATH event received, checking ELIMINATE_ALL objective`
      );
      // Mark that we had enemies since we received an ENEMY_DEATH event
      this._hadEnemies = true;
      statusChanged = this.checkEliminateAllObjective() || statusChanged;
      console.log(
        `[MissionManager] ELIMINATE_ALL check returned statusChanged: ${statusChanged}`
      );
    } else if (type === "TURN_END") {
      // Also check on turn end in case all enemies died from status effects
      statusChanged = this.checkEliminateAllObjective() || statusChanged;
    }

    if (statusChanged) {
      console.log(
        `[MissionManager] Objective status changed, calling checkVictory()`
      );
      this.checkVictory();
    } else {
      console.log(
        `[MissionManager] No objective status change, not checking victory`
      );
    }
  }

  /**
   * Updates objectives based on game events.
   * @param {Objective[]} objectives - Objectives to update
   * @param {string} eventType - Event type
   * @param {GameEventData} data - Event data
   * @returns {boolean} True if any objective status changed
   */
  updateObjectives(objectives, eventType, data) {
    let statusChanged = false;

    objectives.forEach((obj) => {
      if (obj.complete) return;

      // ELIMINATE_UNIT: Track specific enemy deaths
      if (eventType === "ENEMY_DEATH" && obj.type === "ELIMINATE_UNIT") {
        // Check if the killed enemy matches the target (by defId, not unitId)
        // unitId is the instance ID, defId is the definition ID we're looking for
        if (data.defId === obj.target_def_id) {
          obj.current = (obj.current || 0) + 1;
          const targetCount =
            obj.target_count !== undefined ? obj.target_count : 1; // Default to 1 if not specified
          if (obj.current >= targetCount) {
            obj.complete = true;
            statusChanged = true;
            console.log(
              `[MissionManager] ELIMINATE_UNIT objective completed! Killed ${obj.current}/${targetCount} of ${obj.target_def_id}`
            );
          } else {
            console.log(
              `[MissionManager] ELIMINATE_UNIT progress: ${obj.current}/${targetCount} of ${obj.target_def_id}`
            );
          }
        } else {
          console.log(
            `[MissionManager] Enemy killed (${data.defId}) does not match target (${obj.target_def_id})`
          );
        }
      }

      // SURVIVE: Check turn count
      // SURVIVE: Check turn count
      if (eventType === "TURN_END" && obj.type === "SURVIVE") {
        const limit = obj.turn_limit || obj.turn_count;
        if (limit && this.currentTurn >= limit) {
          obj.complete = true;
          statusChanged = true;
          console.log(
            `[MissionManager] SURVIVE objective complete (Turn ${this.currentTurn} >= ${limit})`
          );
        }
      }

      // REACH_ZONE: Check if unit reached target zone
      if (eventType === "UNIT_MOVE" && obj.type === "REACH_ZONE") {
        if (data.position && obj.zone_coords && obj.zone_coords.length > 0) {
          // Find which zone was reached (match X and Z, Y can vary slightly due to walkable level)
          const reachedZoneIndex = obj.zone_coords.findIndex(
            (coord) =>
              coord.x === data.position.x &&
              coord.z === data.position.z &&
              Math.abs(coord.y - data.position.y) <= 1 // Allow Y to vary by 1 level
          );
          if (reachedZoneIndex !== -1) {
            // Mark this zone as reached (remove it from the list)
            const reachedZone = obj.zone_coords[reachedZoneIndex];
            obj.zone_coords.splice(reachedZoneIndex, 1);
            obj.current = (obj.current || 0) + 1;

            console.log(
              `[MissionManager] Zone reached at (${data.position.x}, ${
                data.position.y
              }, ${data.position.z})! Progress: ${obj.current}/${
                obj.target_count || obj.zone_coords.length + obj.current
              }`
            );

            // Check if we've reached the target count, or if all zones are reached (no target_count)
            if (obj.target_count) {
              // Has target_count: complete when current >= target_count
              if (obj.current >= obj.target_count) {
                obj.complete = true;
                statusChanged = true;
                console.log(`[MissionManager] REACH_ZONE objective completed!`);
              } else {
                statusChanged = true; // Progress updated
              }
            } else {
              // No target_count: complete when all zones are reached
              if (obj.zone_coords.length === 0) {
                obj.complete = true;
                statusChanged = true;
                console.log(
                  `[MissionManager] REACH_ZONE objective completed (all zones reached)!`
                );
              } else {
                statusChanged = true; // Progress updated
              }
            }
          } else {
            // Debug: log when we're checking but not matching
            console.log(
              `[MissionManager] Unit at (${data.position.x}, ${data.position.y}, ${data.position.z}), checking ${obj.zone_coords.length} zones:`,
              obj.zone_coords.map((c) => `(${c.x}, ${c.y}, ${c.z})`)
            );
          }
        } else if (obj.type === "REACH_ZONE") {
          console.warn(
            `[MissionManager] REACH_ZONE objective missing zone_coords or position data:`,
            {
              hasPosition: !!data.position,
              hasZoneCoords: !!obj.zone_coords,
              zoneCoordsLength: obj.zone_coords?.length,
            }
          );
        }
      }

      // INTERACT: Check if unit interacted with target object
      if (eventType === "INTERACT" && obj.type === "INTERACT") {
        if (data.objectId === obj.target_object_id) {
          obj.complete = true;
          statusChanged = true;
        }
      }

      // SQUAD_SURVIVAL: Check if minimum units are alive
      if (eventType === "PLAYER_DEATH" && obj.type === "SQUAD_SURVIVAL") {
        if (this.unitManager) {
          const playerUnits = Array.from(
            this.unitManager.activeUnits.values()
          ).filter((u) => u.team === "PLAYER" && u.currentHealth > 0);
          if (obj.min_alive && playerUnits.length >= obj.min_alive) {
            obj.complete = true;
            statusChanged = true;
          }
        }
      }
    });

    return statusChanged;
  }

  /**
   * Checks if ELIMINATE_ALL objective is complete.
   * @returns {boolean} True if status changed
   */
  checkEliminateAllObjective() {
    let statusChanged = false;

    [...this.currentObjectives, ...this.secondaryObjectives].forEach((obj) => {
      if (obj.complete || obj.type !== "ELIMINATE_ALL") return;

      if (this.unitManager) {
        const allUnits = Array.from(this.unitManager.activeUnits.values());
        const allEnemies = allUnits.filter((u) => u.team === "ENEMY");
        const aliveEnemies = allEnemies.filter((u) => u.currentHealth > 0);

        console.log(
          `[MissionManager] ELIMINATE_ALL check: ${aliveEnemies.length} alive enemies remaining (out of ${allEnemies.length} total enemies, ${allUnits.length} total units)`
        );

        // If we have an ELIMINATE_ALL objective and no alive enemies, mission is complete
        // We check aliveEnemies.length === 0 because:
        // - If enemies exist but are all dead (health <= 0), aliveEnemies will be empty
        // - If all enemies were removed, aliveEnemies will also be empty
        // - We only want to complete if we actually had enemies (allEnemies.length > 0 at some point)
        //   But since we're checking after an ENEMY_DEATH event, we know there was at least one enemy
        if (aliveEnemies.length === 0) {
          // Check if we had enemies at some point (either still in list dead, or were just removed)
          // If allEnemies.length > 0, we had enemies (some may be dead but still in list)
          // If allEnemies.length === 0 but we got an ENEMY_DEATH event, the last enemy was just removed
          if (allEnemies.length > 0 || this._hadEnemies) {
            console.log(
              "[MissionManager] ELIMINATE_ALL objective completed! (No alive enemies)"
            );
            obj.complete = true;
            statusChanged = true;
            this._hadEnemies = false; // Reset flag
          } else {
            console.log(
              "[MissionManager] ELIMINATE_ALL: No enemies found in unitManager (may not have spawned yet)"
            );
          }
        } else {
          // Track that we had enemies
          this._hadEnemies = true;
        }
      } else {
        console.warn(
          "[MissionManager] checkEliminateAllObjective: unitManager not set"
        );
      }
    });

    return statusChanged;
  }

  /**
   * Checks failure conditions and triggers failure if met.
   * @param {string} eventType - Event type
   * @param {GameEventData} data - Event data
   */
  checkFailureConditions(eventType, data) {
    if (!this.failureConditions.length) return;

    for (const condition of this.failureConditions) {
      // SQUAD_WIPE: All player units are dead
      if (condition.type === "SQUAD_WIPE") {
        if (this.unitManager) {
          const playerUnits = Array.from(
            this.unitManager.activeUnits.values()
          ).filter((u) => u.team === "PLAYER" && u.currentHealth > 0);
          if (playerUnits.length === 0) {
            this.triggerFailure("SQUAD_WIPE");
            return;
          }
        }
      }

      // VIP_DEATH: VIP unit died
      if (condition.type === "VIP_DEATH" && eventType === "PLAYER_DEATH") {
        if (data.unitId && condition.target_tag) {
          const unit = this.unitManager?.getUnitById(data.unitId);
          if (unit && unit.tags && unit.tags.includes(condition.target_tag)) {
            this.triggerFailure("VIP_DEATH", { unitId: data.unitId });
            return;
          }
        }
      }

      // TURN_LIMIT_EXCEEDED: Mission took too long
      if (
        condition.type === "TURN_LIMIT_EXCEEDED" &&
        eventType === "TURN_END"
      ) {
        if (condition.turn_limit && this.currentTurn > condition.turn_limit) {
          this.triggerFailure("TURN_LIMIT_EXCEEDED", {
            turn: this.currentTurn,
          });
          return;
        }
      }
    }
  }

  /**
   * Triggers mission failure.
   * @param {string} reason - Failure reason
   * @param {GameEventData} [data] - Additional failure data
   */
  triggerFailure(reason, data = {}) {
    console.log(`MISSION FAILED: ${reason}`, data);
    window.dispatchEvent(
      new CustomEvent("mission-failure", {
        detail: {
          missionId: this.activeMissionId,
          reason: reason,
          ...data,
        },
      })
    );
  }

  checkVictory() {
    const allPrimaryComplete =
      this.currentObjectives.length > 0 &&
      this.currentObjectives.every((o) => o.complete);

    console.log(
      `[MissionManager] checkVictory: ${this.currentObjectives.length} objectives, all complete: ${allPrimaryComplete}`,
      this.currentObjectives.map((o) => ({
        type: o.type,
        complete: o.complete,
      }))
    );

    if (allPrimaryComplete) {
      console.log("VICTORY! Mission Objectives Complete.");
      this.completeActiveMission();
      // Dispatch event for GameLoop to handle Victory Screen
      window.dispatchEvent(
        new CustomEvent("mission-victory", {
          detail: {
            missionId: this.activeMissionId,
            primaryObjectives: this.currentObjectives,
            secondaryObjectives: this.secondaryObjectives,
          },
        })
      );
    }
  }

  /**
   * Completes the active mission and distributes rewards.
   */
  async completeActiveMission() {
    if (!this.activeMissionId || !this.currentMissionDef) return;

    // Mark mission as completed
    this.completedMissions.add(this.activeMissionId);

    // Save mission details for procedural missions (they get removed from registry on refresh)
    // This ensures we can display them in the completed missions tab
    if (
      this.currentMissionDef.type === "SIDE_QUEST" &&
      this.activeMissionId?.startsWith("SIDE_OP_")
    ) {
      // Deep clone the mission definition to preserve it
      const missionClone = JSON.parse(JSON.stringify(this.currentMissionDef));
      this.completedMissionDetails.set(this.activeMissionId, missionClone);
      console.log(
        "MissionManager: Saved procedural mission details for:",
        this.activeMissionId
      );
    }

    console.log(
      "MissionManager: Mission completed. Active mission ID:",
      this.activeMissionId
    );
    console.log(
      "MissionManager: Completed missions now:",
      Array.from(this.completedMissions)
    );

    // Refresh procedural missions after completing a mission (if unlocked)
    // This replenishes the board as per spec: "Mission Complete: Replenish the board after a run"
    if (this.areProceduralMissionsUnlocked()) {
      this.refreshProceduralMissions();
    }

    // Dispatch event to save campaign data IMMEDIATELY (before outro)
    // This ensures the save happens even if the outro doesn't complete
    console.log("MissionManager: Dispatching campaign-data-changed event");
    window.dispatchEvent(
      new CustomEvent("campaign-data-changed", {
        detail: { missionCompleted: this.activeMissionId },
      })
    );
    console.log("MissionManager: campaign-data-changed event dispatched");

    // Distribute rewards
    this.distributeRewards();

    // Play outro narrative if available (after saving)
    if (this.currentMissionDef.narrative?.outro_success) {
      await this.playOutro(this.currentMissionDef.narrative.outro_success);
    }

    // Signal that the mission sequence (including outro) is complete
    // This tells the GameStateManager to transition back to the Hub
    console.log("MissionManager: Mission sequence complete, requesting exit.");
    window.dispatchEvent(
      new CustomEvent("mission-sequence-complete", {
        detail: { missionId: this.activeMissionId },
      })
    );
  }

  /**
   * Distributes mission rewards (XP, currency, items, unlocks).
   */
  distributeRewards() {
    if (!this.currentMissionDef || !this.currentMissionDef.rewards) return;

    const rewards = this.currentMissionDef.rewards;
    const rewardData = {
      xp: 0,
      currency: {},
      items: [],
      unlocks: [],
      factionReputation: {},
    };

    // Guaranteed rewards
    if (rewards.guaranteed) {
      if (rewards.guaranteed.xp) {
        rewardData.xp += rewards.guaranteed.xp;
      }
      if (rewards.guaranteed.currency) {
        Object.assign(rewardData.currency, rewards.guaranteed.currency);
      }
      if (rewards.guaranteed.items) {
        rewardData.items.push(...rewards.guaranteed.items);
      }
      if (rewards.guaranteed.unlocks) {
        rewardData.unlocks.push(...rewards.guaranteed.unlocks);
      }
    }

    // Conditional rewards (based on secondary objectives)
    if (rewards.conditional) {
      rewards.conditional.forEach((conditional) => {
        const objective = this.secondaryObjectives.find(
          (obj) => obj.id === conditional.objective_id
        );
        if (objective && objective.complete && conditional.reward) {
          if (conditional.reward.xp) {
            rewardData.xp += conditional.reward.xp;
          }
          if (conditional.reward.currency) {
            Object.assign(rewardData.currency, conditional.reward.currency);
          }
          if (conditional.reward.items) {
            rewardData.items.push(...conditional.reward.items);
          }
        }
      });
    }

    // Faction reputation
    if (rewards.faction_reputation) {
      Object.assign(rewardData.factionReputation, rewards.faction_reputation);
    }

    // Dispatch reward event
    window.dispatchEvent(
      new CustomEvent("mission-rewards", {
        detail: rewardData,
      })
    );

    // Handle unlocks
    if (rewardData.unlocks.length > 0) {
      // Separate class unlocks from special unlocks
      const classUnlocks = rewardData.unlocks.filter((unlock) =>
        unlock.startsWith("CLASS_")
      );
      const specialUnlocks = rewardData.unlocks.filter(
        (unlock) => !unlock.startsWith("CLASS_")
      );

      // Handle class unlocks
      if (classUnlocks.length > 0) {
        this.unlockClasses(classUnlocks);
      }

      // Handle special unlocks & Mission unlocks
      specialUnlocks.forEach((unlock) => {
        if (unlock === "UNLOCK_PROCEDURAL_MISSIONS") {
          // Procedural missions are now unlocked
          // They will be generated when the mission board is accessed
          console.log("Procedural missions unlocked!");
          this.proceduralMissionsUnlocked = true;
          // Persist the unlock
          this.unlockClasses(["UNLOCK_PROCEDURAL_MISSIONS"]);
          // Refresh board immediately
          this.refreshProceduralMissions();
        } else if (unlock.startsWith("MISSION_")) {
          this.unlockMission(unlock);
        }
      });
    }

    console.log("Mission Rewards Distributed:", rewardData);
  }

  /**
   * Unlocks classes and stores them in IndexedDB via Persistence.
   * @param {string[]} classIds - Array of class IDs to unlock
   */
  async unlockClasses(classIds) {
    let unlocks = [];

    try {
      // Load from IndexedDB
      if (this.persistence) {
        unlocks = await this.persistence.loadUnlocks();
      } else {
        // Fallback: try localStorage migration
        const stored = localStorage.getItem("aether_shards_unlocks");
        if (stored) {
          unlocks = JSON.parse(stored);
        }
      }
    } catch (e) {
      console.error("Failed to load unlocks from storage:", e);
    }

    // Add new unlocks
    classIds.forEach((classId) => {
      if (!unlocks.includes(classId)) {
        unlocks.push(classId);
      }
    });

    // Save back to IndexedDB
    try {
      if (this.persistence) {
        await this.persistence.saveUnlocks(unlocks);
        console.log("Unlocked classes:", classIds);

        // Migrate from localStorage if it exists
        if (localStorage.getItem("aether_shards_unlocks")) {
          localStorage.removeItem("aether_shards_unlocks");
          console.log("Migrated unlocks from localStorage to IndexedDB");
        }
      } else {
        // Fallback to localStorage if persistence not available
        localStorage.setItem("aether_shards_unlocks", JSON.stringify(unlocks));
        console.log("Unlocked classes (localStorage fallback):", classIds);
      }

      // Dispatch event so UI components can refresh
      window.dispatchEvent(
        new CustomEvent("classes-unlocked", {
          detail: { unlockedClasses: classIds, allUnlocks: unlocks },
        })
      );
    } catch (e) {
      console.error("Failed to save unlocks to storage:", e);
    }
  }

  /**
   * Plays the outro narrative if one exists.
   * @param {string} outroId - Narrative sequence ID
   * @returns {Promise<void>}
   */
  async playOutro(outroId) {
    return new Promise(async (resolve) => {
      const narrativeFileName = this._mapNarrativeIdToFileName(outroId);

      try {
        const response = await fetch(
          `assets/data/narrative/${narrativeFileName}.json`
        );
        if (!response.ok) {
          console.error(`Failed to load outro narrative: ${narrativeFileName}`);
          resolve();
          return;
        }

        const narrativeData = await response.json();

        const onEnd = () => {
          narrativeManager.removeEventListener("narrative-end", onEnd);
          resolve();
        };
        narrativeManager.addEventListener("narrative-end", onEnd);

        console.log(`Playing Narrative Outro: ${outroId}`);
        narrativeManager.startSequence(narrativeData);
      } catch (error) {
        console.error(
          `Error loading outro narrative ${narrativeFileName}:`,
          error
        );
        resolve();
      }
    });
  }

  /**
   * Updates the current turn count.
   * @param {number} turn - Current turn number
   */
  updateTurn(turn) {
    this.currentTurn = turn;
  }

  /**
   * Unlocks a specific mission ID and loads it.
   * @param {string} missionId
   */
  async unlockMission(missionId) {
    if (this.unlockedMissions.has(missionId)) return;

    console.log(`Unlocking mission: ${missionId}`);
    this.unlockedMissions.add(missionId);

    // Load the file immediately so it's available
    await this.loadMissionFile(missionId);

    // Persist this unlock state
    this.saveUnlockState();
  }

  /**
   * Saves current unlock state (missions) to persistence.
   */
  async saveUnlockState() {
    if (!this.persistence) return;
    try {
      const existing = await this.persistence.loadUnlocks();
      const allUnlocks = new Set([...existing, ...this.unlockedMissions]);
      await this.persistence.saveUnlocks(Array.from(allUnlocks));
    } catch (e) {
      console.error("Failed to save mission unlocks:", e);
    }
  }

  /**
   * Loads unlock state on startup.
   */
  async loadUnlockState() {
    if (!this.persistence) return;
    try {
      const unlocks = await this.persistence.loadUnlocks();
      for (const u of unlocks) {
        if (u.startsWith("MISSION_")) {
          this.unlockedMissions.add(u);
        } else if (u === "UNLOCK_PROCEDURAL_MISSIONS") {
          this.proceduralMissionsUnlocked = true;
        }
      }
    } catch (e) {
      console.warn("Failed to load unlock state:", e);
    }
  }
}
