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

/**
 * SkillTargetingSystem.js
 * Handles skill targeting validation, AoE calculation, and unit selection.
 * Implements the specifications from Combat_Skill_Usage.spec.md
 * @class
 */
export class SkillTargetingSystem {
  /**
   * @param {VoxelGrid} grid - Voxel grid instance
   * @param {UnitManager} unitManager - Unit manager instance
   * @param {Map<string, Object> | Object} skillRegistry - Skill definitions registry
   */
  constructor(grid, unitManager, skillRegistry) {
    /** @type {VoxelGrid} */
    this.grid = grid;
    /** @type {UnitManager} */
    this.unitManager = unitManager;
    /** @type {Map<string, Object> | Object} */
    this.skillRegistry = skillRegistry;
  }

  /**
   * Gets a skill definition from the registry and normalizes it.
   * Maps the actual skill data structure to the format expected by the system.
   * @param {string} skillId - Skill ID
   * @returns {Object | null} - Normalized skill definition or null
   * @private
   */
  getSkillDef(skillId) {
    let skillDef;
    if (this.skillRegistry instanceof Map) {
      skillDef = this.skillRegistry.get(skillId);
    } else if (this.skillRegistry.get) {
      // SkillRegistry instance
      skillDef = this.skillRegistry.get(skillId);
    } else {
      skillDef = this.skillRegistry[skillId];
    }

    if (!skillDef) {
      return null;
    }

    // Normalize the skill definition from nested structure (from JSON files)
    const targeting = skillDef.targeting || {};
    const aoe = targeting.area_of_effect || {};

    // Handle SELF target type (no range needed, targets the caster)
    const targetType = targeting.type || "ENEMY";
    // For range, check if it exists (could be 0, so use != null instead of ||)
    const range =
      targetType === "SELF"
        ? 0
        : targeting.range != null
        ? Number(targeting.range)
        : 0;

    return {
      id: skillDef.id,
      name: skillDef.name,
      range: range,
      target_type: targetType,
      ignore_cover: targeting.line_of_sight === false, // If line_of_sight is false, ignore cover
      aoe_type: aoe.shape || "SINGLE",
      aoe_radius: aoe.shape === "CIRCLE" ? aoe.size || 1 : undefined,
      aoe_length: aoe.shape === "LINE" ? aoe.size || 1 : undefined,
      costAP: skillDef.costs?.ap || 0,
      cooldown: skillDef.cooldown_turns || 0,
      effects: skillDef.effects || [],
    };
  }

  /**
   * Calculates Manhattan distance between two positions.
   * @param {Position} pos1 - First position
   * @param {Position} pos2 - Second position
   * @returns {number} - Manhattan distance
   * @private
   */
  manhattanDistance(pos1, pos2) {
    return (
      Math.abs(pos1.x - pos2.x) +
      Math.abs(pos1.y - pos2.y) +
      Math.abs(pos1.z - pos2.z)
    );
  }

  /**
   * Checks line of sight from source to target.
   * Raycasts from source head height to target center.
   * @param {Position} sourcePos - Source position
   * @param {Position} targetPos - Target position
   * @param {boolean} ignoreCover - Whether to ignore cover
   * @returns {boolean} - True if line of sight is clear
   * @private
   */
  /**
   * Checks line of sight and calculates obstruction level.
   * @param {Position} sourcePos - Source position
   * @param {Position} targetPos - Target position
   * @param {boolean} ignoreCover - Whether to ignore cover
   * @returns {{ clear: boolean; obstruction: number }} - LOS result with obstruction level (0-1, where 1 = fully blocked)
   */
  hasLineOfSight(sourcePos, targetPos, ignoreCover = false) {
    if (ignoreCover) {
      return { clear: true, obstruction: 0 };
    }

    // Source head height (assuming unit is 1.5 voxels tall, head at y + 1.5)
    const sourceHeadY = sourcePos.y + 1.5;
    // Target center (assuming target is at y + 0.5)
    const targetCenterY = targetPos.y + 0.5;

    // Use 3D DDA (Digital Differential Analyzer) algorithm for accurate voxel traversal
    // This ensures we check every voxel the ray passes through
    let x = Math.floor(sourcePos.x);
    let y = Math.floor(sourceHeadY);
    let z = Math.floor(sourcePos.z);

    const endX = Math.floor(targetPos.x);
    const endY = Math.floor(targetCenterY);
    const endZ = Math.floor(targetPos.z);

    // Track obstruction: count solid voxels and total voxels checked
    let solidVoxels = 0;
    let totalVoxels = 0;

    // If we're already at the target, LOS is clear
    if (x === endX && y === endY && z === endZ) {
      return { clear: true, obstruction: 0 };
    }

    // Calculate direction
    const dx = targetPos.x - sourcePos.x;
    const dy = targetCenterY - sourceHeadY;
    const dz = targetPos.z - sourcePos.z;

    // Step direction for each axis
    const stepX = dx > 0 ? 1 : dx < 0 ? -1 : 0;
    const stepY = dy > 0 ? 1 : dy < 0 ? -1 : 0;
    const stepZ = dz > 0 ? 1 : dz < 0 ? -1 : 0;

    // Calculate delta distances (how far along the ray we must travel to cross one voxel boundary)
    const deltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity;
    const deltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity;
    const deltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity;

    // Calculate initial distances to next voxel boundary
    let nextX = Infinity;
    let nextY = Infinity;
    let nextZ = Infinity;

    if (stepX !== 0) {
      nextX =
        stepX > 0
          ? (Math.floor(sourcePos.x) + 1 - sourcePos.x) / dx
          : (sourcePos.x - Math.floor(sourcePos.x)) / -dx;
      if (isNaN(nextX) || !isFinite(nextX)) nextX = Infinity;
    }

    if (stepY !== 0) {
      nextY =
        stepY > 0
          ? (Math.floor(sourceHeadY) + 1 - sourceHeadY) / dy
          : (sourceHeadY - Math.floor(sourceHeadY)) / -dy;
      if (isNaN(nextY) || !isFinite(nextY)) nextY = Infinity;
    }

    if (stepZ !== 0) {
      nextZ =
        stepZ > 0
          ? (Math.floor(sourcePos.z) + 1 - sourcePos.z) / dz
          : (sourcePos.z - Math.floor(sourcePos.z)) / -dz;
      if (isNaN(nextZ) || !isFinite(nextZ)) nextZ = Infinity;
    }

    // Track previous position to avoid checking the same voxel twice
    let prevX = null;
    let prevY = null;
    let prevZ = null;

    // Maximum iterations to prevent infinite loops
    const maxIterations =
      Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z) + 10;
    let iterations = 0;

    // Traverse voxels along the ray
    while (iterations < maxIterations) {
      iterations++;

      // Skip if we've already checked this voxel
      if (x !== prevX || y !== prevY || z !== prevZ) {
        // Check if we've reached the target
        if (x === endX && y === endY && z === endZ) {
          // Calculate obstruction level (0 = clear, 1 = fully blocked)
          const obstruction = totalVoxels > 0 ? solidVoxels / totalVoxels : 0;
          return {
            clear: obstruction < 1.0,
            obstruction: Math.min(1.0, obstruction),
          };
        }

        // Check if this voxel is solid (but skip source position)
        if (
          !(
            x === Math.floor(sourcePos.x) &&
            y === Math.floor(sourceHeadY) &&
            z === Math.floor(sourcePos.z)
          )
        ) {
          totalVoxels++;
          if (this.grid.isSolid({ x, y, z })) {
            solidVoxels++;
            // If fully blocked, return immediately
            return { clear: false, obstruction: 1.0 };
          }
        }

        prevX = x;
        prevY = y;
        prevZ = z;
      }

      // Determine which axis to step along (the one with the smallest next boundary distance)
      if (nextX < nextY && nextX < nextZ) {
        nextX += deltaX;
        x += stepX;
      } else if (nextY < nextZ) {
        nextY += deltaY;
        y += stepY;
      } else {
        nextZ += deltaZ;
        z += stepZ;
      }
    }

    // If we exit the loop, calculate obstruction and return
    const obstruction = totalVoxels > 0 ? solidVoxels / totalVoxels : 0;
    return {
      clear: obstruction < 1.0,
      obstruction: Math.min(1.0, obstruction),
    };
  }

  /**
   * Validates if a target is valid for a skill.
   * Checks range, line of sight, and target type.
   * @param {Unit} sourceUnit - Source unit casting the skill
   * @param {Position} targetPos - Target position
   * @param {string} skillId - Skill ID
   * @returns {{ valid: boolean; reason: string }} - Validation result
   */
  validateTarget(sourceUnit, targetPos, skillId) {
    const skillDef = this.getSkillDef(skillId);
    if (!skillDef) {
      return { valid: false, reason: "Skill not found" };
    }

    // 1. Range Check
    if (!sourceUnit.position) {
      return { valid: false, reason: "Source unit has no position" };
    }

    // Check if this is a teleport skill (range = -1 means unlimited range, limited by LOS)
    const isTeleportSkill =
      skillDef.effects && skillDef.effects.some((e) => e.type === "TELEPORT");
    const hasUnlimitedRange =
      skillDef.range === -1 || skillDef.range === Infinity;

    // For teleport skills with unlimited range, skip distance check (only LOS matters)
    if (!isTeleportSkill || !hasUnlimitedRange) {
      const distance = this.manhattanDistance(sourceUnit.position, targetPos);
      if (distance > skillDef.range) {
        return { valid: false, reason: "Target out of range" };
      }
    }

    // 2. Line of Sight Check
    const ignoreCover = skillDef.ignore_cover || false;
    // All skills including teleport require line of sight for targeting
    // (Teleport failure chance is handled during execution if LOS becomes obstructed)
    const losResult = this.hasLineOfSight(
      sourceUnit.position,
      targetPos,
      ignoreCover
    );
    if (!losResult.clear) {
      return {
        valid: false,
        reason: "No line of sight",
        obstruction: losResult.obstruction,
      };
    }

    // Store obstruction level for highlighting (even if clear, partial obstruction affects success chance)
    const obstruction = losResult.obstruction || 0;

    // 3. Content Check (Target Type)
    const targetUnit = this.grid.getUnitAt(targetPos);
    const targetType = skillDef.target_type;
    const hasAoE = skillDef.aoe_type && skillDef.aoe_type !== "SINGLE";

    if (targetType === "SELF") {
      // SELF skills target the caster's position
      if (
        targetPos.x !== sourceUnit.position.x ||
        targetPos.y !== sourceUnit.position.y ||
        targetPos.z !== sourceUnit.position.z
      ) {
        return {
          valid: false,
          reason: "Invalid target type: must target self",
        };
      }
    } else if (targetType === "ENEMY") {
      // For AoE skills, allow targeting empty spaces (the AoE will find enemies)
      // For single-target skills, must target an enemy unit
      if (hasAoE) {
        // AoE skills can target empty spaces or enemy units
        if (targetUnit && targetUnit.team === sourceUnit.team) {
          return {
            valid: false,
            reason: "Invalid target type: cannot target ally",
          };
        }
        // Allow empty spaces or enemy units for AoE
      } else {
        // Single-target skills must target an enemy unit
        if (!targetUnit || targetUnit.team === sourceUnit.team) {
          return { valid: false, reason: "Invalid target type: must be enemy" };
        }
      }
    } else if (targetType === "ALLY") {
      // For AoE skills, allow targeting empty spaces (the AoE will find allies)
      if (hasAoE) {
        // AoE skills can target empty spaces or ally units
        if (targetUnit && targetUnit.team !== sourceUnit.team) {
          return {
            valid: false,
            reason: "Invalid target type: cannot target enemy",
          };
        }
        // Allow empty spaces or ally units for AoE
      } else {
        // Single-target skills must target an ally unit
        if (!targetUnit || targetUnit.team !== sourceUnit.team) {
          return { valid: false, reason: "Invalid target type: must be ally" };
        }
      }
    } else if (targetType === "EMPTY") {
      if (targetUnit) {
        return { valid: false, reason: "Invalid target type: must be empty" };
      }
    }

    // 4. Surface Check - ensure the target position is a valid surface (has a floor)
    // This is important for AoE skills and teleport skills that can target empty spaces
    const hasTeleportEffect =
      skillDef.effects && skillDef.effects.some((e) => e.type === "TELEPORT");
    const needsSurfaceCheck = (hasAoE || hasTeleportEffect) && !targetUnit;

    if (needsSurfaceCheck) {
      if (hasTeleportEffect) {
        // For teleport skills, require a walkable position (air + floor + air above)
        // First, check if the target position itself is solid (inside a wall)
        const targetCell = this.grid.getCell(
          targetPos.x,
          targetPos.y,
          targetPos.z
        );
        if (targetCell !== 0) {
          // Target is inside a solid block (wall)
          return {
            valid: false,
            reason: "Invalid target: position is inside a wall",
          };
        }

        // Check a range of Y levels (same as movement system does)
        const yLevelsToCheck = [
          targetPos.y,
          targetPos.y + 1,
          targetPos.y - 1,
          targetPos.y - 2,
        ];

        let foundWalkable = false;
        for (const checkY of yLevelsToCheck) {
          if (checkY < 0) continue;

          // Check if this position is walkable (air at Y, floor below, air above)
          const cellAtY = this.grid.getCell(targetPos.x, checkY, targetPos.z);

          // Skip if this Y level is solid (inside a wall)
          if (cellAtY !== 0) continue;

          const floorY = checkY - 1;
          if (floorY < 0) continue;

          const floorCell = this.grid.getCell(targetPos.x, floorY, targetPos.z);
          const cellAbove = this.grid.getCell(
            targetPos.x,
            checkY + 1,
            targetPos.z
          );

          if (
            floorCell !== 0 && // Floor below
            cellAbove === 0 // Air above
          ) {
            foundWalkable = true;
            break;
          }
        }

        if (!foundWalkable) {
          return {
            valid: false,
            reason: "Invalid target: position is not walkable",
          };
        }
      } else {
        // For AoE skills, just check for a valid surface (floor below)
        let foundValidSurface = false;
        for (let checkY = targetPos.y; checkY >= 0; checkY--) {
          const cellAtY = this.grid.getCell(targetPos.x, checkY, targetPos.z);
          if (cellAtY === 0) {
            const floorY = checkY - 1;
            if (floorY >= 0) {
              const floorCell = this.grid.getCell(
                targetPos.x,
                floorY,
                targetPos.z
              );
              if (floorCell !== 0) {
                foundValidSurface = true;
                break;
              }
            }
          }
        }

        if (!foundValidSurface) {
          return {
            valid: false,
            reason: "Invalid target: no valid surface at this position",
          };
        }
      }
    }

    return { valid: true, reason: "", obstruction: obstruction };
  }

  /**
   * Calculates the Area of Effect tiles for a skill.
   * @param {Position} sourcePos - Source position
   * @param {Position} cursorPos - Cursor/target position
   * @param {string} skillId - Skill ID
   * @returns {Position[]} - Array of affected tile positions
   */
  getAoETiles(sourcePos, cursorPos, skillId) {
    const skillDef = this.getSkillDef(skillId);
    if (!skillDef) {
      return [];
    }

    const aoeType = skillDef.aoe_type || "SINGLE";

    switch (aoeType) {
      case "SINGLE":
        return [cursorPos];

      case "CIRCLE": {
        const radius = skillDef.aoe_radius || 1;
        const tiles = [];

        // Generate all tiles within Manhattan distance radius
        // Only iterate X and Z (horizontal plane), use cursorPos.y for all tiles
        for (let x = cursorPos.x - radius; x <= cursorPos.x + radius; x++) {
          for (let z = cursorPos.z - radius; z <= cursorPos.z + radius; z++) {
            const pos = { x, y: cursorPos.y, z };
            const dist = this.manhattanDistance(cursorPos, pos);
            if (dist <= radius) {
              tiles.push(pos);
            }
          }
        }

        return tiles;
      }

      case "LINE": {
        const length = skillDef.aoe_length || 1;
        const tiles = [];

        // Calculate direction from source to cursor
        const dx = cursorPos.x - sourcePos.x;
        const dz = cursorPos.z - sourcePos.z;

        // Determine cardinal direction
        let dirX = 0;
        let dirZ = 0;
        if (Math.abs(dx) > Math.abs(dz)) {
          dirX = dx > 0 ? 1 : -1;
        } else {
          dirZ = dz > 0 ? 1 : -1;
        }

        // Generate line tiles
        for (let i = 0; i < length; i++) {
          const x = sourcePos.x + dirX * i;
          const z = sourcePos.z + dirZ * i;
          tiles.push({ x, y: cursorPos.y, z });
        }

        return tiles;
      }

      case "CONE": {
        // TODO: Implement cone pattern
        // For now, return single tile
        return [cursorPos];
      }

      default:
        return [cursorPos];
    }
  }

  /**
   * Gets all units within the AoE of a skill.
   * @param {Position} sourcePos - Source position (for LINE AoE calculation)
   * @param {Position} targetPos - Target position (center of AoE)
   * @param {string} skillId - Skill ID
   * @returns {Unit[]} - Array of units in AoE
   */
  getUnitsInAoE(sourcePos, targetPos, skillId) {
    const skillDef = this.getSkillDef(skillId);
    if (!skillDef) {
      return [];
    }

    const aoeTiles = this.getAoETiles(sourcePos, targetPos, skillId);
    const units = [];
    const unitIdsFound = new Set(); // Track found unit IDs to avoid duplicates

    for (const tile of aoeTiles) {
      // Check unit at the exact tile position
      const unit = this.grid.getUnitAt(tile);
      if (unit && !unitIdsFound.has(unit.id)) {
        units.push(unit);
        unitIdsFound.add(unit.id);
      }

      // Also check adjacent Y levels (units might be on different floors)
      // Check one level above and below
      if (tile.y > 0) {
        const tileBelow = { x: tile.x, y: tile.y - 1, z: tile.z };
        const unitBelow = this.grid.getUnitAt(tileBelow);
        if (unitBelow && !unitIdsFound.has(unitBelow.id)) {
          units.push(unitBelow);
          unitIdsFound.add(unitBelow.id);
        }
      }
      const tileAbove = { x: tile.x, y: tile.y + 1, z: tile.z };
      const unitAbove = this.grid.getUnitAt(tileAbove);
      if (unitAbove && !unitIdsFound.has(unitAbove.id)) {
        units.push(unitAbove);
        unitIdsFound.add(unitAbove.id);
      }
    }

    return units;
  }
}
