import 'dart:developer';
import 'dart:math' as math;

import 'package:wolf_3d_dart/src/entities/entities/enemies/dog.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/guard.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/mutant.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/officer.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/ss.dart';
import 'package:wolf_3d_dart/src/entities/entity.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

/// The base class for all computer-controlled opponents in the game.
///
/// This class encapsulates the shared AI behaviors, including movement physics,
/// difficulty-based spawning, and the signature Wolfenstein 3D sight-detection system.
abstract class Enemy extends Entity {
  static int _nextDebugId = 0;

  /// A sequential ID assigned at spawn for debug identification.
  final int debugId = ++_nextDebugId;

  Enemy({
    required super.x,
    required super.y,
    required super.spriteIndex,
    super.angle,
    super.state,
    super.mapId,
    super.lastActionTime,
  });

  /// The current "Tic" count remaining for the active animation frame.
  int _ticCount = 0;

  /// Accumulates milliseconds to determine when a Tic has passed (14.28ms per Tic).
  double _ticAccumulator = 0;

  /// The current animation frame index (used to progress through states).
  int currentFrame = 0;

  /// The amount of damage the enemy can take before dying.
  int health = 25;

  /// The potential damage dealt to the player per successful attack.
  int damage = 10;

  /// Set to true when the enemy enters the [EntityState.dead] transition.
  bool isDying = false;

  /// Returns the metadata for this specific enemy type (Guard, SS, etc.).
  ///
  /// This allows the engine to access [EnemyAnimationMap] data without
  /// knowing the specific subclass.
  EnemyType get type;

  /// The sound played when this enemy notices the player or hears combat.
  SoundEffect get alertSound => type.alertSound;

  /// The sound played when this enemy performs its attack animation.
  SoundEffect get attackSound => type.attackSound;

  /// The score awarded when this enemy is killed.
  int get scoreValue => type.scoreValue;

  /// The sound played once when this enemy starts dying.
  SoundEffect get deathSound => type.deathSound;

  /// Ensures enemies drop only one item (like ammo or a key) upon death.
  bool hasDroppedItem = false;

  /// Prevents duplicate death screams while death frames continue animating.
  bool hasPlayedDeathSound = false;

  /// When true, the enemy has spotted the player and is actively pursuing or attacking.
  bool isAlerted = false;

  /// Used to simulate a "reaction delay" before an alerted enemy begins to move.
  /// This replaces the `ob->temp2` variable found in the original `WL_ACT2.C`.
  int reactionTimeMs = 0;

  ({int x, int y})? _patrolTargetTile;
  int _patrolDirX = 0;
  int _patrolDirY = 0;

  /// Processes elapsed time and returns true if the enemy's animation frame has completed.
  ///
  /// Movement is applied continuously during the frame, but state changes (like deciding
  /// to shoot) only happen when [processTics] returns true.
  bool processTics(int elapsedDeltaMs, {required double moveSpeed}) {
    // 1 Tic = 1/70th of a second (~14.28ms)
    _ticAccumulator += elapsedDeltaMs;

    // Calculate how many Tics passed this frame
    int ticsPassed = (_ticAccumulator / 14.28).floor();

    if (ticsPassed > 0) {
      _ticAccumulator -= (ticsPassed * 14.28);
      _ticCount -= ticsPassed;
    }

    return _ticCount <= 0;
  }

  /// Sets the duration for the next animation frame.
  void setTics(int tics) {
    _ticCount = tics;
  }

  /// Canonical Wolf3D ranged damage buckets by tile distance.
  int rollRangedDamage(double distance, {bool isSharpShooter = false}) {
    final adjustedDistance = isSharpShooter
        ? (distance * (2.0 / 3.0))
        : distance;

    if (adjustedDistance < 2.0) {
      return math.Random().nextInt(64); // 0..63
    }
    if (adjustedDistance < 4.0) {
      return math.Random().nextInt(32); // 0..31
    }
    return math.Random().nextInt(16); // 0..15
  }

  /// Approximate whether this enemy is on-screen from the player's perspective.
  bool isVisibleFromPlayer({
    required Coordinate2D playerPosition,
    required double playerAngle,
  }) {
    final double angleToEnemy = playerPosition.angleTo(position);
    double diff = playerAngle - angleToEnemy;

    while (diff <= -math.pi) {
      diff += 2 * math.pi;
    }
    while (diff > math.pi) {
      diff -= 2 * math.pi;
    }

    // Wolf3D visibility in logic is tied to rendered visibility; use an 80-degree
    // front arc as an approximation for dodge-capable shots.
    return diff.abs() <= (math.pi * (80.0 / 180.0));
  }

  /// Canonical enemy gun hit chance from `T_Shoot`.
  bool tryRollRangedHit({
    required double distance,
    required Coordinate2D playerPosition,
    required double playerAngle,
    required bool isPlayerRunning,
    bool isSharpShooter = false,
  }) {
    final adjustedDistance = isSharpShooter
        ? (distance * (2.0 / 3.0))
        : distance;
    final dist = adjustedDistance.floor();
    final isVisible = isVisibleFromPlayer(
      playerPosition: playerPosition,
      playerAngle: playerAngle,
    );

    int hitchance;
    if (isPlayerRunning) {
      hitchance = isVisible ? 160 - dist * 16 : 160 - dist * 8;
    } else {
      hitchance = isVisible ? 256 - dist * 16 : 256 - dist * 8;
    }

    hitchance = hitchance.clamp(0, 255);
    return math.Random().nextInt(256) < hitchance;
  }

  /// Reduces health and handles state transitions for pain or death.
  ///
  /// Alerts the enemy automatically upon taking damage. There is a 50% chance
  /// to trigger a "pain" flinch animation if the enemy survives the hit.
  void takeDamage(int amount, int currentTime) {
    if (state == EntityState.dead) return;

    health -= amount;
    lastActionTime = currentTime;
    isAlerted = true;

    if (health <= 0) {
      state = EntityState.dead;
      isDying = true;
      log(
        '[DEBUG] Enemy #$debugId (${type.name}) killed at tile (${x.toInt()}, ${y.toInt()})',
      );
    } else if (math.Random().nextDouble() < 0.5) {
      state = EntityState.pain;
      setTics(5); // Add this so they actually pause to flinch
    } else {
      state = EntityState.patrolling;
      setTics(10); // Add this to reset their movement rhythm
    }
  }

  /// Handles the standard 3-phase attack progression (Aim -> Shoot -> Cooldown).
  ///
  /// Calculates hit chance and damage based on the [distance] to the player.
  void handleAttackState({
    required int elapsedDeltaMs,
    required void Function(int damage) onDamagePlayer,
    required void Function() onAttack,
    required int shootTics,
    required int cooldownTics,
    required int postAttackTics,
    required double distance,
  }) {
    if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
      currentFrame++;

      if (currentFrame == 1) {
        log(
          '[DEBUG] Enemy #$debugId (${type.name}) is shooting at player at distance ${distance.toStringAsFixed(2)}',
        );
        // Phase 1: Bang! Calculate hit chance based on distance.
        // Drops by ~5% per tile distance, capped at a minimum 10% chance to hit.
        onAttack();
        double hitChance = (1.0 - (distance * 0.05)).clamp(0.1, 1.0);

        if (math.Random().nextDouble() <= hitChance) {
          // Hit! Roll for damage between 1 and the enemy's max damage stat.
          int actualDamage = math.Random().nextInt(damage) + 1;
          onDamagePlayer(actualDamage);
        }

        setTics(shootTics);
      } else if (currentFrame == 2) {
        // Phase 2: Cooldown. Gun lowered.
        setTics(cooldownTics);
      } else {
        // Phase 3: Attack complete. Return to patrol logic.
        state = EntityState.patrolling;
        currentFrame = 0;
        setTics(postAttackTics);
      }
    }
  }

  /// Periodically checks if the enemy should spot the player.
  ///
  /// Includes a randomized delay based on [baseReactionMs] and [reactionVarianceMs]
  /// to prevent all enemies in a room from reacting on the exact same frame.
  bool checkWakeUp({
    required int elapsedMs,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    required int Function(int x, int y) areaAt,
    required bool Function(int area) isAreaConnectedToPlayer,
    int baseReactionMs = 200,
    int reactionVarianceMs = 600,
  }) {
    bool didAlert = false;

    final int area = areaAt(x.toInt(), y.toInt());
    if (area >= 0 && !isAreaConnectedToPlayer(area)) {
      return false;
    }

    if (!isAlerted && hasLineOfSight(playerPosition, isWalkable)) {
      if (reactionTimeMs == 0) {
        // First frame of spotting: calculate how long until they "wake up"
        reactionTimeMs =
            elapsedMs +
            baseReactionMs +
            math.Random().nextInt(reactionVarianceMs);
        log(
          '[DEBUG] Enemy #$debugId (${type.name}) spotted player but is reacting... '
          'enemy=(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}) '
          'player=(${playerPosition.x.toStringAsFixed(2)}, ${playerPosition.y.toStringAsFixed(2)}) '
          'reactionTimeMs=$reactionTimeMs',
        );
      } else if (elapsedMs >= reactionTimeMs) {
        // Reaction delay has passed
        isAlerted = true;
        didAlert = true;

        log(
          '[DEBUG] Enemy #$debugId (${type.name}) alerted by sight '
          'enemy=(${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}) '
          'player=(${playerPosition.x.toStringAsFixed(2)}, ${playerPosition.y.toStringAsFixed(2)})',
        );

        if (state == EntityState.idle || state == EntityState.ambushing) {
          state = EntityState.patrolling;
          setTics(10);
        }

        lastActionTime = elapsedMs;
        reactionTimeMs = 0;
      }
    }

    return didAlert;
  }

  /// Determines if there is a clear, unobstructed path between the enemy and the player.
  ///
  /// This logic matches the sight rules of the original engine:
  /// 1. **Proximity**: If the player is within ~1.2 tiles, sight is automatic.
  /// 2. **Field of View**: The player must be within a 180-degree arc in front of the enemy.
  /// 3. **Raycast**: Uses an Integer Bresenham traversal to check for wall obstructions.
  bool hasLineOfSight(
    Coordinate2D playerPosition,
    bool Function(int x, int y) isWalkable,
  ) {
    // 1. Proximity Check (Matches 'MINSIGHT' in original C)
    if (position.distanceTo(playerPosition) < 1.2) {
      return true;
    }

    // 2. FOV Check (Matches original 180-degree sight angles)
    double angleToPlayer = position.angleTo(playerPosition);
    double diff = angle - angleToPlayer;

    // Normalize angle difference to (-pi, pi]
    while (diff <= -math.pi) {
      diff += 2 * math.pi;
    }
    while (diff > math.pi) {
      diff -= 2 * math.pi;
    }

    // If the player is behind the enemy (more than 90 degrees left or right), return false
    if (diff.abs() > math.pi / 2) return false;

    // 3. Map Check (Integer Bresenham Traversal)
    int currentX = position.x.toInt();
    int currentY = position.y.toInt();
    int targetX = playerPosition.x.toInt();
    int targetY = playerPosition.y.toInt();

    int dx = (targetX - currentX).abs();
    int dy = -(targetY - currentY).abs();
    int sx = currentX < targetX ? 1 : -1;
    int sy = currentY < targetY ? 1 : -1;
    int err = dx + dy;

    while (true) {
      if (!isWalkable(currentX, currentY)) return false; // Hit a wall
      if (currentX == targetX && currentY == targetY) break; // Reached player

      int e2 = 2 * err;
      if (e2 >= dy) {
        err += dy;
        currentX += sx;
      }
      if (e2 <= dx) {
        err += dx;
        currentY += sy;
      }
    }
    return true;
  }

  /// Resolves movement by checking for collisions and attempting to interact with doors.
  ///
  /// The logic performs separate X and Y collision checks to allow "sliding" along
  /// walls. If a movement is blocked, it calls [tryOpenDoor] to simulate the
  /// enemy's ability to navigate through the level.
  Coordinate2D getValidMovement({
    required Coordinate2D intendedMovement,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    required void Function(int x, int y) tryOpenDoor,
  }) {
    Coordinate2D normalizeTiny(Coordinate2D movement) {
      const epsilon = 1e-6;
      final x = movement.x.abs() < epsilon ? 0.0 : movement.x;
      final y = movement.y.abs() < epsilon ? 0.0 : movement.y;
      return Coordinate2D(x, y);
    }

    final double distToPlayer = position.distanceTo(playerPosition);
    const double minDistance = 0.9;

    if (distToPlayer < minDistance) {
      // If already too close, only allow movement if it increases distance
      Coordinate2D nextPos = position + intendedMovement;
      if (nextPos.distanceTo(playerPosition) < distToPlayer) {
        return const Coordinate2D(0, 0);
      }
    }

    double newX = position.x + intendedMovement.x;
    double newY = position.y + intendedMovement.y;

    int currentTileX = position.x.toInt();
    int currentTileY = position.y.toInt();
    int targetTileX = newX.toInt();
    int targetTileY = newY.toInt();

    bool movedX = currentTileX != targetTileX;
    bool movedY = currentTileY != targetTileY;

    // 1. Check Diagonal Movement
    if (movedX && movedY) {
      bool canMoveX = isWalkable(targetTileX, currentTileY);
      bool canMoveY = isWalkable(currentTileX, targetTileY);
      bool canMoveDiag = isWalkable(targetTileX, targetTileY);

      if (!canMoveX || !canMoveY || !canMoveDiag) {
        // Trigger door logic if a wall is blocking the path
        if (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
        if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
        if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY);

        // Slide in the primary direction of travel. This prevents the entity from
        // being deflected sideways when navigating through a doorway: if the dog
        // is moving mostly north and clips a wall tile beside an open door, the
        // Y (northward) slide is preferred over the X (sideways) slide.
        if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
          if (canMoveY) {
            return normalizeTiny(Coordinate2D(0, intendedMovement.y));
          }
          if (canMoveX) {
            return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
          }
        } else {
          if (canMoveX) {
            return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
          }
          if (canMoveY) {
            return normalizeTiny(Coordinate2D(0, intendedMovement.y));
          }
        }
        return const Coordinate2D(0, 0);
      }
    }

    // 2. Check Cardinal (Straight) Movement
    if (movedX && !movedY) {
      if (!isWalkable(targetTileX, currentTileY)) {
        tryOpenDoor(targetTileX, currentTileY);
        return const Coordinate2D(0, 0);
      }
    }
    if (movedY && !movedX) {
      if (!isWalkable(currentTileX, targetTileY)) {
        tryOpenDoor(currentTileX, targetTileY);
        return const Coordinate2D(0, 0);
      }
    }

    return normalizeTiny(intendedMovement);
  }

  void resetPatrolPathState() {
    _patrolTargetTile = null;
    _patrolDirX = 0;
    _patrolDirY = 0;
  }

  Coordinate2D getPatrolPathMovement({
    required double moveSpeed,
    required Coordinate2D playerPosition,
    required bool Function(int x, int y) isWalkable,
    required void Function(int x, int y) tryOpenDoor,
  }) {
    ({int x, int y}) directionFromAngle8(double angle) {
      int dx = math.cos(angle).round();
      int dy = math.sin(angle).round();

      if (dx == 0 && dy == 0) {
        dx = math.cos(angle) >= 0 ? 1 : -1;
      }

      return (x: dx.clamp(-1, 1), y: dy.clamp(-1, 1));
    }

    final currentTileX = x.toInt();
    final currentTileY = y.toInt();
    final dir = directionFromAngle8(angle);

    if (dir.x != _patrolDirX || dir.y != _patrolDirY) {
      _patrolDirX = dir.x;
      _patrolDirY = dir.y;
      _patrolTargetTile = null;
    }

    final reachedCurrentTarget =
        _patrolTargetTile != null &&
        _patrolTargetTile!.x == currentTileX &&
        _patrolTargetTile!.y == currentTileY;

    if (_patrolTargetTile == null || reachedCurrentTarget) {
      _patrolTargetTile = (
        x: currentTileX + _patrolDirX,
        y: currentTileY + _patrolDirY,
      );
    }

    final target = _patrolTargetTile!;
    if (!isWalkable(target.x, target.y)) {
      tryOpenDoor(target.x, target.y);
      return const Coordinate2D(0, 0);
    }

    final targetCenter = Coordinate2D(target.x + 0.5, target.y + 0.5);
    final toTarget = targetCenter - position;
    final distance = math.sqrt(
      toTarget.x * toTarget.x + toTarget.y * toTarget.y,
    );

    if (distance <= 0.0001) {
      return const Coordinate2D(0, 0);
    }

    if (distance <= moveSpeed) {
      return getValidMovement(
        intendedMovement: toTarget,
        playerPosition: playerPosition,
        isWalkable: isWalkable,
        tryOpenDoor: tryOpenDoor,
      );
    }

    final step = Coordinate2D(
      (toTarget.x / distance) * moveSpeed,
      (toTarget.y / distance) * moveSpeed,
    );

    return getValidMovement(
      intendedMovement: step,
      playerPosition: playerPosition,
      isWalkable: isWalkable,
      tryOpenDoor: tryOpenDoor,
    );
  }

  /// The per-frame update logic to be implemented by specific enemy types.
  ({Coordinate2D movement, double newAngle}) update({
    required int elapsedMs,
    required int elapsedDeltaMs,
    required Coordinate2D playerPosition,
    required double playerAngle,
    required bool isPlayerRunning,
    required bool Function(int x, int y) isWalkable,
    required int Function(int x, int y) areaAt,
    required bool Function(int area) isAreaConnectedToPlayer,
    required void Function(int x, int y) tryOpenDoor,
    required void Function(int damage) onDamagePlayer,
    required void Function(SoundEffect effect) onPlaySound,
  });

  /// Factory method to spawn the correct [Enemy] subclass based on a Map ID.
  ///
  /// This validates [difficulty] requirements, filters out non-enemy objects,
  /// and ensures [isSharewareMode] restrictions are respected (e.g., no Mutants).
  static Enemy? spawn(
    int objId,
    double x,
    double y,
    Difficulty difficulty, {
    bool isSharewareMode = false,
  }) {
    // Filter out decorative bodies or player starts
    if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
      return null;
    }
    if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
      return null;
    }

    // Prevent bosses from spawning via standard enemy logic
    if (objId >= MapObject.bossHansGrosse &&
        objId <= MapObject.bossFettgesicht) {
      return null;
    }

    EnemyType? matchedType;
    int? normalizedId;

    // Find which enemy type claims this ID for the current difficulty
    for (final type in EnemyType.values) {
      if (isSharewareMode && !type.existsInShareware) continue;

      normalizedId = type.mapData.getNormalizedId(objId, difficulty);
      if (normalizedId != null) {
        matchedType = type;
        break;
      }
    }

    // If no type claimed it, or the difficulty was too low, abort spawn
    if (matchedType == null || normalizedId == null) return null;

    // Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base)
    // This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions.
    double spawnAngle = CardinalDirection.fromEnemyIndex(
      normalizedId - matchedType.mapData.baseId,
    ).radians;

    EntityState spawnState;

    if (matchedType.mapData.isPatrol(normalizedId)) {
      spawnState = EntityState.patrolling;
    } else if (matchedType.mapData.isStatic(normalizedId)) {
      // Standing map placements should hold position in an ambush state until
      // their wake-up logic transitions them to active behavior.
      spawnState = EntityState.ambushing;
    } else {
      return null;
    }

    // Return the specific instance
    return switch (matchedType) {
      EnemyType.guard => Guard(
        x: x,
        y: y,
        angle: spawnAngle,
        mapId: objId,
        difficulty: difficulty,
      ),
      EnemyType.dog => Dog(
        x: x,
        y: y,
        angle: spawnAngle,
        mapId: objId,
        difficulty: difficulty,
      ),
      EnemyType.ss => SS(
        x: x,
        y: y,
        angle: spawnAngle,
        mapId: objId,
        difficulty: difficulty,
      ),
      EnemyType.mutant => Mutant(
        x: x,
        y: y,
        angle: spawnAngle,
        mapId: objId,
        difficulty: difficulty,
      ),
      EnemyType.officer => Officer(
        x: x,
        y: y,
        angle: spawnAngle,
        mapId: objId,
        difficulty: difficulty,
      ),
    }..state = spawnState;
  }
}
