/**
 * @typedef {import("./types.js").ClassMastery} ClassMastery
 * @typedef {import("./types.js").Equipment} Equipment
 */

import { Unit } from "./Unit.js";

/**
 * Explorer.js
 * Player character class supporting Multi-Class Mastery and Persistent Progression.
 * @class
 */
export class Explorer extends Unit {
  /**
   * @param {string} id - Unique unit identifier
   * @param {string} name - Explorer name
   * @param {string} startingClassId - Starting class ID
   * @param {Record<string, unknown>} classDefinition - Class definition data
   */
  constructor(id, name, startingClassId, classDefinition) {
    super(id, name, "EXPLORER", `${startingClassId}_MODEL`);

    /** @type {string} */
    this.activeClassId = startingClassId;

    // Persistent Mastery: Tracks progress for EVERY class this character has played
    // Key: ClassID, Value: { level, xp, skillPoints, unlockedNodes[] }
    /** @type {Record<string, ClassMastery>} */
    this.classMastery = {};

    // Initialize the starting class entry
    this.initializeMastery(startingClassId);

    // Hydrate stats based on the provided definition
    if (classDefinition) {
      this.recalculateBaseStats(classDefinition);
      this.currentHealth = this.baseStats.health;
    }

    // Inventory
    /** @type {Equipment} */
    this.equipment = {
      weapon: null,
      armor: null,
      utility: null,
      relic: null,
    };

    // Loadout (New inventory system per spec)
    /** @type {Object} */
    this.loadout = {
      mainHand: null,
      offHand: null,
      body: null,
      accessory: null,
      belt: [null, null], // Fixed 2 slots
    };

    // Active Skills (Populated by Skill Tree)
    /** @type {unknown[]} */
    this.actions = [];
    /** @type {unknown[]} */
    this.passives = [];
  }

  /**
   * Initializes mastery data for a class.
   * @param {string} classId - Class ID to initialize
   */
  initializeMastery(classId) {
    if (!this.classMastery[classId]) {
      this.classMastery[classId] = {
        level: 1,
        xp: 0,
        skillPoints: 0,
        unlockedNodes: [],
      };
    }
  }

  /**
   * Updates base stats based on the active class's base + growth rates * level.
   * @param {Record<string, unknown>} classDef - The JSON definition of the class stats.
   */
  recalculateBaseStats(classDef) {
    if (classDef.id !== this.activeClassId) {
      console.warn(
        `Mismatch: Recalculating stats for ${this.activeClassId} using definition for ${classDef.id}`
      );
    }

    const mastery = this.classMastery[this.activeClassId];

    // 1. Start with Class Defaults
    let stats = { ...classDef.base_stats };

    // 2. Add Level Growth
    // (Level 1 is base, so growth applies for levels 2+)
    const levelsGained = mastery.level - 1;
    if (levelsGained > 0) {
      for (let stat in classDef.growth_rates) {
        if (stats[stat] !== undefined) {
          stats[stat] += classDef.growth_rates[stat] * levelsGained;
        }
      }
    }

    this.baseStats = stats;
    this.maxHealth = stats.health; // Update MaxHP cap
  }

  /**
   * Swaps the active class logic.
   * NOTE: Does NOT check unlock requirements (handled by UI/MetaSystem).
   * @param {string} newClassId - New class ID
   * @param {Record<string, unknown>} newClassDef - New class definition
   */
  changeClass(newClassId, newClassDef) {
    // 1. Ensure mastery record exists
    this.initializeMastery(newClassId);

    // 2. Switch ID
    this.activeClassId = newClassId;

    // 3. Update Model ID (Visuals)
    this.voxelModelID = `${newClassId}_MODEL`;

    // 4. Recalculate Stats for the new job
    this.recalculateBaseStats(newClassDef);

    // 5. Reset Current HP to new Max (or keep percentage? Standard is reset)
    this.currentHealth = this.baseStats.health;
  }

  /**
   * Adds XP to the *current* class.
   * @param {number} amount - XP amount to add
   */
  gainExperience(amount) {
    const mastery = this.classMastery[this.activeClassId];
    mastery.xp += amount;
    // Level up logic would be handled by a system checking XP curves
  }

  /**
   * Gets the current level of the active class.
   * @returns {number} - Current level
   */
  getLevel() {
    return this.classMastery[this.activeClassId].level;
  }

  /**
   * Initializes starting equipment from class definition.
   * Creates ItemInstance objects and equips them to appropriate slots.
   * @param {Object} itemRegistry - Item registry to get item definitions
   * @param {Record<string, unknown>} classDefinition - Class definition with starting_equipment array
   */
  initializeStartingEquipment(itemRegistry, classDefinition) {
    if (!itemRegistry || !classDefinition) {
      return;
    }

    const startingEquipment = classDefinition.starting_equipment;
    if (!Array.isArray(startingEquipment) || startingEquipment.length === 0) {
      return;
    }

    // Map item types to loadout slots
    const typeToSlot = {
      WEAPON: "mainHand",
      ARMOR: "body",
      UTILITY: "offHand", // Default to offHand, but could be belt
      RELIC: "accessory",
      CONSUMABLE: null, // Consumables go to belt or stash
    };

    let beltIndex = 0;

    for (const itemDefId of startingEquipment) {
      const itemDef = itemRegistry.get(itemDefId);
      if (!itemDef) {
        console.warn(`Starting equipment item not found: ${itemDefId}`);
        continue;
      }

      // Create ItemInstance
      const itemInstance = {
        uid: `${itemDefId}_${this.id}_${Date.now()}_${Math.random()
          .toString(36)
          .substr(2, 9)}`,
        defId: itemDefId,
        isNew: false,
        quantity: 1,
      };

      // Determine slot based on item type
      const itemType = itemDef.type;
      let targetSlot = typeToSlot[itemType];

      // Special handling for consumables - put in belt
      if (itemType === "CONSUMABLE" || itemType === "MATERIAL") {
        if (beltIndex < 2) {
          this.loadout.belt[beltIndex] = itemInstance;
          beltIndex++;
          continue;
        } else {
          // Belt is full, skip or add to stash later
          console.warn(`Belt full, cannot equip consumable: ${itemDefId}`);
          continue;
        }
      }

      // Handle UTILITY items - can go to offHand or belt
      if (itemType === "UTILITY") {
        // If offHand is empty, use it; otherwise try belt
        if (!this.loadout.offHand) {
          targetSlot = "offHand";
        } else if (beltIndex < 2) {
          this.loadout.belt[beltIndex] = itemInstance;
          beltIndex++;
          continue;
        } else {
          // Both offHand and belt full, skip
          console.warn(`Cannot equip utility item, slots full: ${itemDefId}`);
          continue;
        }
      }

      // Equip to determined slot
      if (targetSlot && this.loadout[targetSlot] === null) {
        this.loadout[targetSlot] = itemInstance;
      } else if (targetSlot) {
        // Slot occupied, skip or log warning
        console.warn(
          `Starting equipment slot ${targetSlot} already occupied, skipping: ${itemDefId}`
        );
      }
    }

    // Recalculate stats after equipping starting gear
    this.recalculateStats(itemRegistry);
  }

  /**
   * Recalculates effective stats including equipment bonuses and skill tree stat boosts.
   * This method should be called whenever equipment or skill tree nodes change.
   * @param {Object} [itemRegistry] - Optional item registry to get item definitions
   * @param {Object} [treeDef] - Optional skill tree definition to get stat boosts from unlocked nodes
   */
  recalculateStats(itemRegistry = null, treeDef = null) {
    // Start with base stats (already calculated from class + level)
    const effectiveStats = { ...this.baseStats };

    // Apply equipment bonuses if itemRegistry is provided
    if (itemRegistry) {
      // Check mainHand
      if (this.loadout.mainHand) {
        const itemDef = itemRegistry.get(this.loadout.mainHand.defId);
        if (itemDef && itemDef.stats) {
          for (const [stat, value] of Object.entries(itemDef.stats)) {
            effectiveStats[stat] = (effectiveStats[stat] || 0) + value;
          }
        }
      }

      // Check offHand
      if (this.loadout.offHand) {
        const itemDef = itemRegistry.get(this.loadout.offHand.defId);
        if (itemDef && itemDef.stats) {
          for (const [stat, value] of Object.entries(itemDef.stats)) {
            effectiveStats[stat] = (effectiveStats[stat] || 0) + value;
          }
        }
      }

      // Check body
      if (this.loadout.body) {
        const itemDef = itemRegistry.get(this.loadout.body.defId);
        if (itemDef && itemDef.stats) {
          for (const [stat, value] of Object.entries(itemDef.stats)) {
            effectiveStats[stat] = (effectiveStats[stat] || 0) + value;
          }
        }
      }

      // Check accessory
      if (this.loadout.accessory) {
        const itemDef = itemRegistry.get(this.loadout.accessory.defId);
        if (itemDef && itemDef.stats) {
          for (const [stat, value] of Object.entries(itemDef.stats)) {
            effectiveStats[stat] = (effectiveStats[stat] || 0) + value;
          }
        }
      }

      // Belt items don't affect stats (they're consumables)
    }

    // Apply skill tree stat boosts from unlocked nodes
    if (treeDef && this.classMastery) {
      const mastery = this.classMastery[this.activeClassId];
      if (mastery && mastery.unlockedNodes) {
        for (const nodeId of mastery.unlockedNodes) {
          const nodeDef = treeDef.nodes?.[nodeId];
          if (
            nodeDef &&
            nodeDef.type === "STAT_BOOST" &&
            nodeDef.data &&
            nodeDef.data.stat
          ) {
            const statName = nodeDef.data.stat;
            const boostValue = nodeDef.data.value || 0;
            effectiveStats[statName] =
              (effectiveStats[statName] || 0) + boostValue;
          }
        }
      }
    }

    // Update maxHealth if health stat changed
    if (effectiveStats.health !== undefined) {
      const oldMaxHealth = this.maxHealth;
      this.maxHealth = effectiveStats.health;
      // Update currentHealth proportionally using the old maxHealth
      if (oldMaxHealth > 0) {
        const healthRatio = this.currentHealth / oldMaxHealth;
        this.currentHealth = Math.floor(effectiveStats.health * healthRatio);
      } else {
        this.currentHealth = effectiveStats.health;
      }
    }
  }
}
