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

import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
import 'package:wolf_3d_dart/src/entities/entity.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

class Dog extends Enemy {
  /// Original SPDDOG is 1024. 1 Tile is 65536 units.
  /// 1024 / 65536 = ~0.0156 tiles per tic.
  static const double speedPerTic = 0.0156;

  // Used to simulate SelectDodgeDir's zigzag behavior
  double _dodgeAngleOffset = 0.0;
  int _dodgeTicTimer = 0;
  int _stuckFrames = 0;
  bool _wasMoving = false;

  @override
  EnemyType get type => EnemyType.dog;

  Dog({
    required super.x,
    required super.y,
    required super.angle,
    required super.mapId,
    Difficulty difficulty = Difficulty.medium,
  }) : super(
         spriteIndex: EnemyType.dog.animations.walking.start,
         state: EntityState.patrolling,
       ) {
    health = type.hitPointsFor(difficulty);
  }

  @override
  ({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 damage) onDamagePlayer,
    required void Function(int x, int y) tryOpenDoor,
    required void Function(SoundEffect effect) onPlaySound,
  }) {
    final previousState = state;
    Coordinate2D movement = const Coordinate2D(0, 0);
    double newAngle = angle;

    // 1. Perception
    if (state != EntityState.dead && !isDying) {
      if (checkWakeUp(
        elapsedMs: elapsedMs,
        playerPosition: playerPosition,
        isWalkable: isWalkable,
        areaAt: areaAt,
        isAreaConnectedToPlayer: isAreaConnectedToPlayer,
      )) {
        onPlaySound(alertSound);
      }
    }

    bool ticReady = processTics(elapsedDeltaMs, moveSpeed: 0);

    // 2. State-Based Logic Gate
    if (state == EntityState.attacking) {
      if (ticReady) {
        currentFrame++;
        // Phase 2: The actual bite
        if (currentFrame == 1) {
          onPlaySound(attackSound);
          final bool attackSuccessful =
              math.Random().nextDouble() < (180 / 256);

          double dx = (position.x - playerPosition.x).abs() - speedPerTic;
          double dy = (position.y - playerPosition.y).abs() - speedPerTic;
          bool inBiteRange = dx <= 1.0 && dy <= 1.0;

          if (inBiteRange && attackSuccessful) {
            onDamagePlayer(math.Random().nextInt(16)); // Canonical 0..15
          }
        }

        if (currentFrame >= 5) {
          state = EntityState.ambushing;
          currentFrame = 0;
        }
        setTics(10);
      }
      _updateAnimation(elapsedMs, newAngle, playerPosition);
      return (movement: const Coordinate2D(0, 0), newAngle: newAngle);
    } else if (state != EntityState.dead && !isDying) {
      // 3. Chase / Movement Logic
      double ticsThisFrame = elapsedDeltaMs / 14.28;
      double currentMoveSpeed = speedPerTic * ticsThisFrame;

      if (isAlerted) {
        resetPatrolPathState();
        // Only commit to the chase (ambushing) state once we have a valid
        // movement path. While blocked, the dog waits in its current state
        // so it does not visually appear to charge a closed door.
        final bool startedChaseThisFrame = state != EntityState.ambushing;

        if (startedChaseThisFrame) {
          // Reset dodge at chase start so the first pursuit vector is not biased
          // by stale/random strafe offset from previous behavior.
          _dodgeAngleOffset = 0.0;
          _dodgeTicTimer = 0;
        }

        double dx = (position.x - playerPosition.x).abs() - currentMoveSpeed;
        double dy = (position.y - playerPosition.y).abs() - currentMoveSpeed;

        if (dx <= 1.0 && dy <= 1.0) {
          state = EntityState.attacking;
          currentFrame = 0;
          lastActionTime = elapsedMs;
          setTics(10);
          return (
            movement: const Coordinate2D(0, 0),
            newAngle: position.angleTo(playerPosition),
          );
        }

        if (_dodgeTicTimer <= 0) {
          _dodgeAngleOffset =
              (math.Random().nextBool() ? 1 : -1) * (math.pi / 4);
          _dodgeTicTimer = 15;
        } else if (ticReady) {
          _dodgeTicTimer--;
        }

        final baseAngle = position.angleTo(playerPosition);
        final preferredAngle = baseAngle + _dodgeAngleOffset;

        // Build a SelectDodgeDir-inspired candidate list. The first two entries
        // are the dodge-shifted diagonal and the direct diagonal (original
        // behaviour). The next two are the pure cardinal directions toward the
        // player along each axis — these align with doorways and corridors and
        // handle cases where all diagonal variants clip a wall beside the opening.
        final double toDx = playerPosition.x - position.x;
        final double toDy = playerPosition.y - position.y;
        final double primaryCardinal = toDx.abs() >= toDy.abs()
            ? (toDx >= 0 ? 0.0 : math.pi)
            : (toDy >= 0 ? math.pi / 2 : -math.pi / 2);
        final double secondaryCardinal = toDx.abs() < toDy.abs()
            ? (toDx >= 0 ? 0.0 : math.pi)
            : (toDy >= 0 ? math.pi / 2 : -math.pi / 2);

        // Diagonal-toward-player is the primary candidate. In a corridor the
        // diagonal naturally wall-slides into the correct lane (e.g. northwest
        // intent → blocked north → slides west along the corridor floor toward
        // the door). Cardinal directions are kept only as last-resort fallbacks
        // for when the entity is pinned in a corner.
        final candidateAngles = <double>[
          if (startedChaseThisFrame) ...[
            baseAngle, // direct diagonal toward player on first chase frame
            preferredAngle, // dodge-offset diagonal
          ] else ...[
            preferredAngle, // dodge-offset diagonal (established chase)
            baseAngle, // direct diagonal fallback
          ],
          primaryCardinal,
          secondaryCardinal,
          baseAngle + math.pi / 2,
          baseAngle - math.pi / 2,
        ];

        newAngle = preferredAngle;
        movement = const Coordinate2D(0, 0);

        // A movement magnitude threshold prevents accepting near-zero floating-
        // point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement.
        final double minEffective = currentMoveSpeed * 0.5;
        final double currentDistanceToPlayer = position.distanceTo(
          playerPosition,
        );

        int selectedCandidateIndex = -1;
        for (int i = 0; i < candidateAngles.length; i++) {
          final candidateAngle = candidateAngles[i];
          final candidateMovement = getValidMovement(
            intendedMovement: Coordinate2D(
              math.cos(candidateAngle) * currentMoveSpeed,
              math.sin(candidateAngle) * currentMoveSpeed,
            ),
            playerPosition: playerPosition,
            isWalkable: isWalkable,
            // Alerted dogs cannot open doors! Pass an empty closure to treat doors as walls.
            tryOpenDoor: (x, y) {},
          );

          if (candidateMovement.x.abs() + candidateMovement.y.abs() <
              minEffective) {
            continue;
          }

          final candidatePosition = position + candidateMovement;
          final double candidateDistanceToPlayer = candidatePosition.distanceTo(
            playerPosition,
          );

          // Canonical-like chase guard: when actively pursuing, do not pick a
          // vector that moves the dog farther away from the player if a closer
          // candidate exists this frame.
          if (candidateDistanceToPlayer > currentDistanceToPlayer + 0.001) {
            continue;
          }

          newAngle = candidateAngle;
          movement = candidateMovement;
          selectedCandidateIndex = i;
          break;
        }

        if (movement.x == 0 && movement.y == 0) {
          // Blocked — don't visually enter chase state yet.
          // Keep current state (patrolling) until a path opens.
        } else {
          state = EntityState.ambushing;
        }

        if (movement.x == 0 && movement.y == 0) {
          _dodgeTicTimer = 0;
          _stuckFrames++;

          // Emit periodic diagnostics for dogs that appear to keep facing/moving
          // into blocked geometry. This is intentionally sparse to avoid log spam.
          if (_stuckFrames == 1 || _stuckFrames % 20 == 0) {
            final int tileX = x.toInt();
            final int tileY = y.toInt();
            final int playerTileX = playerPosition.x.toInt();
            final int playerTileY = playerPosition.y.toInt();

            log(
              '[DEBUG] Dog #$debugId stuck=$_stuckFrames '
              'at ($tileX,$tileY)->player($playerTileX,$playerTileY) '
              'base=${baseAngle.toStringAsFixed(3)} '
              'pref=${preferredAngle.toStringAsFixed(3)} '
              'cand=$selectedCandidateIndex '
              'walk N=${isWalkable(tileX, tileY - 1)} '
              'E=${isWalkable(tileX + 1, tileY)} '
              'S=${isWalkable(tileX, tileY + 1)} '
              'W=${isWalkable(tileX - 1, tileY)}',
            );
          }
        } else {
          if (_stuckFrames >= 20) {
            log(
              '[DEBUG] Dog #$debugId unstuck after $_stuckFrames frames '
              'using candidate $selectedCandidateIndex '
              'move=(${movement.x.toStringAsFixed(4)},${movement.y.toStringAsFixed(4)})',
            );
          }
          _stuckFrames = 0;
        }
      } else if (state == EntityState.patrolling) {
        movement = getPatrolPathMovement(
          moveSpeed: currentMoveSpeed,
          playerPosition: playerPosition,
          isWalkable: isWalkable,
          // Patrolling dogs using T_Path CAN open doors in the original logic.
          tryOpenDoor: tryOpenDoor,
        );
      }

      if (ticReady) {
        currentFrame = (currentFrame + 1) % 4;
        setTics(10);
      }
    }

    if (state != previousState) {
      log(
        '[DEBUG] Dog #$debugId state ${previousState.name} -> ${state.name} '
        'at (${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)})',
      );
    }

    final bool isMovingNow = movement.x.abs() + movement.y.abs() > 0.0001;
    if (isMovingNow && !_wasMoving) {
      final moveAngle = math.atan2(movement.y, movement.x);
      final cardinalDirection = movement.x.abs() >= movement.y.abs()
          ? (movement.x >= 0 ? 'east' : 'west')
          : (movement.y >= 0 ? 'south' : 'north');
      log(
        '[DEBUG] Dog #$debugId movement start '
        'from (${x.toStringAsFixed(2)}, ${y.toStringAsFixed(2)}) '
        'tile (${x.toInt()}, ${y.toInt()}) '
        'dir=$cardinalDirection angle=${moveAngle.toStringAsFixed(3)} '
        'vec=(${movement.x.toStringAsFixed(4)}, ${movement.y.toStringAsFixed(4)})',
      );
    }
    _wasMoving = isMovingNow;

    _updateAnimation(elapsedMs, newAngle, playerPosition);
    return (movement: movement, newAngle: newAngle);
  }

  void _updateAnimation(
    int elapsedMs,
    double newAngle,
    Coordinate2D playerPosition,
  ) {
    double diff = newAngle - position.angleTo(playerPosition);
    while (diff <= -math.pi) {
      diff += 2 * math.pi;
    }
    while (diff > math.pi) {
      diff -= 2 * math.pi;
    }

    EnemyAnimation currentAnim = switch (state) {
      EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
      EntityState.attacking => EnemyAnimation.attacking,
      EntityState.pain => EnemyAnimation.pain,
      EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
      _ => EnemyAnimation.idle,
    };

    spriteIndex = type.getSpriteFromAnimation(
      animation: currentAnim,
      elapsedMs: elapsedMs,
      lastActionTime: lastActionTime,
      angleDiff: diff,
      walkFrameOverride:
          (state == EntityState.patrolling ||
              state == EntityState.ambushing ||
              isAlerted)
          ? currentFrame
          : null,
    );
  }
}
