/**
 * @typedef {import("../units/Unit.js").Unit} Unit
 * @typedef {import("../grid/VoxelGrid.js").VoxelGrid} VoxelGrid
 * @typedef {import("../managers/UnitManager.js").UnitManager} UnitManager
 * @typedef {import("../grid/types.js").Position} Position
 */

/**
 * MovementSystem.js
 * Manages unit movement, pathfinding, and validation.
 * Implements the specifications from CombatState.spec.md
 * @class
 */
export class MovementSystem {
  /**
   * @param {VoxelGrid} [grid] - Voxel grid instance
   * @param {UnitManager} [unitManager] - Unit manager instance
   */
  constructor(grid = null, unitManager = null) {
    /** @type {VoxelGrid | null} */
    this.grid = grid;
    /** @type {UnitManager | null} */
    this.unitManager = unitManager;
  }

  /**
   * Sets the context (grid and unit manager).
   * @param {VoxelGrid} grid - Voxel grid instance
   * @param {UnitManager} unitManager - Unit manager instance
   */
  setContext(grid, unitManager) {
    this.grid = grid;
    this.unitManager = unitManager;
  }

  /**
   * Finds the walkable Y level for a given X,Z position.
   * @param {number} x - X coordinate
   * @param {number} z - Z coordinate
   * @param {number} referenceY - Reference Y level to check around
   * @returns {number | null} - Walkable Y level or null if not walkable
   * @private
   */
  findWalkableY(x, z, referenceY) {
    if (!this.grid) return null;

    // Check same level, up 1, down 1, down 2 (matching GameLoop logic)
    const yLevels = [
      referenceY,
      referenceY + 1,
      referenceY - 1,
      referenceY - 2,
    ];
    for (const y of yLevels) {
      if (this.isWalkable(x, y, z)) {
        return y;
      }
    }
    return null;
  }

  /**
   * Checks if a position is walkable.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {boolean} - True if walkable
   * @private
   */
  isWalkable(x, y, z) {
    if (!this.grid) return false;

    // Check if cell is air
    if (this.grid.getCell(x, y, z) !== 0) return false;
    // Check if there's a floor below
    if (this.grid.getCell(x, y - 1, z) === 0) return false;
    // Check if there's air above
    if (this.grid.getCell(x, y + 1, z) !== 0) return false;
    return true;
  }

  /**
   * Calculates all reachable positions for a unit using BFS.
   * Takes into account the unit's current AP, not just base movement stat.
   * @param {Unit} unit - The unit to calculate movement for
   * @param {number} maxRange - Maximum movement range (overrides AP calculation if provided)
   * @returns {Position[]} - Array of reachable positions
   */
  getReachableTiles(unit, maxRange = null) {
    if (!this.grid || !unit.position) return [];

    const start = unit.position;
    const baseMovement = unit.baseStats?.movement || 4;
    const currentAP = unit.currentAP || 0;

    // Use the minimum of base movement and current AP as the effective range
    // This ensures we only show tiles the unit can actually reach with their current AP
    const effectiveMaxRange =
      maxRange !== null ? maxRange : Math.min(baseMovement, currentAP);

    const reachable = new Set();
    const queue = [{ x: start.x, z: start.z, y: start.y }];
    const visited = new Set();
    visited.add(`${start.x},${start.z}`); // Track by X,Z only for horizontal movement

    // Horizontal movement directions (4-connected)
    const directions = [
      { x: 1, z: 0 },
      { x: -1, z: 0 },
      { x: 0, z: 1 },
      { x: 0, z: -1 },
    ];

    while (queue.length > 0) {
      const { x, z, y } = queue.shift();

      // Find the walkable Y level for this X,Z position
      const walkableY = this.findWalkableY(x, z, y);
      if (walkableY === null) continue;

      const pos = { x, y: walkableY, z };
      // Use walkableY in the key, not the reference y
      const posKey = `${x},${walkableY},${z}`;

      // Calculate actual AP cost (Manhattan distance from start)
      // This matches the cost calculation in validateMove()
      const horizontalDistance = Math.abs(x - start.x) + Math.abs(z - start.z);
      const movementCost = Math.max(1, horizontalDistance);

      // Only include positions that:
      // 1. Are not occupied (or are the starting position)
      // 2. Cost no more AP than the unit currently has (or is the starting position)
      // 3. Are within the effective movement range
      const isStartPos =
        x === start.x && z === start.z && walkableY === start.y;
      const canAfford = movementCost <= currentAP || isStartPos;
      const inRange = horizontalDistance <= effectiveMaxRange;

      if ((!this.grid.isOccupied(pos) || isStartPos) && canAfford && inRange) {
        reachable.add(posKey);
      }

      // Explore neighbors if we haven't exceeded max range
      // Continue exploring even if current position wasn't reachable (might be blocked but neighbors aren't)
      if (horizontalDistance < effectiveMaxRange) {
        for (const dir of directions) {
          const newX = x + dir.x;
          const newZ = z + dir.z;
          const key = `${newX},${newZ}`;

          if (
            !visited.has(key) &&
            this.grid.isValidBounds({ x: newX, y: 0, z: newZ })
          ) {
            visited.add(key);
            // Use the walkable Y we found as reference for next position
            queue.push({
              x: newX,
              z: newZ,
              y: walkableY,
            });
          }
        }
      }
    }

    // Convert Set to array of Position objects
    return Array.from(reachable).map((key) => {
      const [x, y, z] = key.split(",").map(Number);
      return { x, y, z };
    });
  }

  /**
   * Validates if a move is possible.
   * CoA 1: Validation - checks blocked/occupied, path exists, sufficient AP
   * @param {Unit} unit - The unit attempting to move
   * @param {Position} targetPos - Target position
   * @returns {{ valid: boolean; cost: number; path: Position[] }} - Validation result
   */
  validateMove(unit, targetPos) {
    if (!this.grid || !unit.position) {
      return { valid: false, cost: 0, path: [] };
    }

    // Find walkable Y level
    const walkableY = this.findWalkableY(targetPos.x, targetPos.z, targetPos.y);
    if (walkableY === null) {
      return { valid: false, cost: 0, path: [] };
    }

    const finalTargetPos = { x: targetPos.x, y: walkableY, z: targetPos.z };

    // Check if target is blocked/occupied
    if (this.grid.isOccupied(finalTargetPos)) {
      return { valid: false, cost: 0, path: [] };
    }

    // Calculate movement cost first (horizontal Manhattan distance)
    const horizontalDistance =
      Math.abs(finalTargetPos.x - unit.position.x) +
      Math.abs(finalTargetPos.z - unit.position.z);
    const movementCost = Math.max(1, horizontalDistance);

    // Check if target is reachable (path exists)
    const reachableTiles = this.getReachableTiles(unit);
    const isReachable = reachableTiles.some(
      (pos) =>
        pos.x === finalTargetPos.x &&
        pos.y === finalTargetPos.y &&
        pos.z === finalTargetPos.z
    );

    // If not reachable, check if it's due to insufficient AP or out of range
    if (!isReachable) {
      // Check if it's within movement range but just not affordable
      const baseMovement = unit.baseStats?.movement || 4;
      const inMovementRange = horizontalDistance <= baseMovement;

      // If within movement range but not reachable, it's likely due to insufficient AP
      if (inMovementRange && unit.currentAP < movementCost) {
        return { valid: false, cost: movementCost, path: [] };
      }

      // Otherwise, it's out of range
      return { valid: false, cost: 0, path: [] };
    }

    // Check if unit has sufficient AP
    if (unit.currentAP < movementCost) {
      return { valid: false, cost: movementCost, path: [] };
    }

    // Build simple path (straight line for now, could use A* later)
    const path = [unit.position, finalTargetPos];

    return { valid: true, cost: movementCost, path };
  }

  /**
   * Executes a move for a unit.
   * CoA 2: Execution - updates position, grid, deducts AP
   * @param {Unit} unit - The unit to move
   * @param {Position} targetPos - Target position
   * @returns {Promise<boolean>} - True if move was successful
   */
  async executeMove(unit, targetPos) {
    // Validate first
    const validation = this.validateMove(unit, targetPos);
    if (!validation.valid) {
      return false;
    }

    // Find walkable Y level
    const walkableY = this.findWalkableY(targetPos.x, targetPos.z, targetPos.y);
    if (walkableY === null) {
      return false;
    }

    const finalTargetPos = { x: targetPos.x, y: walkableY, z: targetPos.z };

    // Update grid occupancy
    if (!this.grid.moveUnit(unit, finalTargetPos)) {
      return false;
    }

    // Deduct AP
    unit.currentAP -= validation.cost;

    // Return immediately (no animation for now)
    return true;
  }

  /**
   * Checks if a move is valid (convenience method).
   * @param {Unit} unit - The unit attempting to move
   * @param {Position} targetPos - Target position
   * @returns {boolean} - True if move is valid
   */
  isValidMove(unit, targetPos) {
    return this.validateMove(unit, targetPos).valid;
  }
}
