import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../game/enums.dart';
import '../game/models.dart';
import '../game/config.dart';
import '../providers/battle_provider.dart';
import '../providers/settings_provider.dart';
import '../widgets/battle/effect_sprite_widget.dart';
import '../widgets/battle/floating_battle_texts.dart';
import '../widgets/battle/battle_animation_widget.dart';
import '../widgets/battle/shake_widget.dart';
import '../widgets/battle/explosion_widget.dart';

enum AnimationPhase { none, start, middle, end, hurt, block }

mixin BattleVisualHandler<T extends StatefulWidget> on State<T> {
  // State variables for animations
  final List<DamageTextData> floatingDamageTexts = [];
  final List<FloatingEffectData> floatingEffects = [];
  final List<FeedbackTextData> floatingFeedbackTexts = [];

  final GlobalKey playerKey = GlobalKey();
  final GlobalKey enemyKey = GlobalKey();
  final GlobalKey stackKey = GlobalKey();
  final GlobalKey<ShakeWidgetState> shakeKey = GlobalKey<ShakeWidgetState>();
  final GlobalKey<BattleAnimationWidgetState> playerAnimKey = GlobalKey();
  final GlobalKey<BattleAnimationWidgetState> enemyAnimKey = GlobalKey();
  final GlobalKey<ExplosionWidgetState> explosionKey = GlobalKey();
  final GlobalKey<EffectSpriteWidgetState> effectSpriteKey = GlobalKey();
  final GlobalKey rootStackKey = GlobalKey();

  AnimationPhase playerAnimPhase = AnimationPhase.none;
  RiskLevel? activeRiskLevel;
  bool isAttackSuccess = true;
  bool isPlayerAttacking = false;
  bool isEnemyAttacking = false;

  StreamSubscription<DamageEvent>? damageSubscription;
  StreamSubscription<EffectEvent>? effectSubscription;
  StreamSubscription<HealEvent>? healSubscription;

  DateTime? lastFeedbackTime;

  void setupVisualListeners(BattleProvider battleProvider) {
    damageSubscription = battleProvider.damageStream.listen(_onDamageEvent);
    effectSubscription = battleProvider.effectStream.listen(onEffectEvent);
    healSubscription = battleProvider.healStream.listen(onHealEvent);
  }

  void disposeVisualListeners() {
    damageSubscription?.cancel();
    effectSubscription?.cancel();
    healSubscription?.cancel();
  }

  // --- Animation Core Logic ---

  void _onDamageEvent(DamageEvent event) {
    if (!mounted) return;

    if (event.target == DamageTarget.player) {
      if (event.armorDamage > 0) {
        _triggerPlayerBlock();
      } else if (event.damage > 0) {
        _triggerPlayerHurt();
      }
    }

    addFloatingDamageText(event);
  }

  void _triggerPlayerHurt() {
    if (playerAnimPhase != AnimationPhase.none &&
        playerAnimPhase != AnimationPhase.hurt) {
      return;
    }
    setState(() => playerAnimPhase = AnimationPhase.hurt);
    Future.delayed(const Duration(milliseconds: 800), () {
      if (mounted && playerAnimPhase == AnimationPhase.hurt) {
        setState(() => playerAnimPhase = AnimationPhase.none);
      }
    });
  }

  void _triggerPlayerBlock() {
    if (playerAnimPhase != AnimationPhase.none &&
        playerAnimPhase != AnimationPhase.block) {
      return;
    }
    setState(() => playerAnimPhase = AnimationPhase.block);
    Future.delayed(const Duration(milliseconds: 800), () {
      if (mounted && playerAnimPhase == AnimationPhase.block) {
        setState(() => playerAnimPhase = AnimationPhase.none);
      }
    });
  }

  void onHealEvent(HealEvent event) {
    if (!mounted) return;

    RenderBox? rootBox =
        rootStackKey.currentContext?.findRenderObject() as RenderBox?;
    if (rootBox == null) return;
    Offset rootOffset = rootBox.localToGlobal(Offset.zero);

    Offset position = Offset(rootBox.size.width / 2, rootBox.size.height / 2);

    if (event.target == HealTarget.player && playerAnimKey.currentContext != null) {
      RenderBox? imageBox =
          playerAnimKey.currentContext!.findRenderObject() as RenderBox?;
      if (imageBox != null) {
        Offset globalCenter = imageBox.localToGlobal(
          Offset(imageBox.size.width / 2, imageBox.size.height / 2),
        );
        position = globalCenter - rootOffset;
      }
    }

    effectSpriteKey.currentState?.playEffect(
      position: position,
      assetPath: 'assets/images/effects/heal.png',
      frameCount: 4,
      tileWidth: 100.0,
      tileHeight: 100.0,
      scale: 6.0,
    );

    final String id = UniqueKey().toString();
    setState(() {
      floatingDamageTexts.add(
        DamageTextData(
          id: id,
          widget: Positioned(
            key: ValueKey('pos_$id'),
            left: position.dx + BattleConfig.damageTextOffsetX,
            top: position.dy + BattleConfig.damageTextOffsetY,
            child: FloatingDamageText(
              key: ValueKey(id),
              damage: "+${event.amount}",
              color: ThemeConfig.statHpPlayerColor,
              onRemove: () {
                if (mounted) {
                  setState(() => floatingDamageTexts.removeWhere((e) => e.id == id));
                }
              },
            ),
          ),
        ),
      );
    });
  }

  void onEffectEvent(EffectEvent event) {
    if (!mounted) return;

    void showEffect() {
      if (event.feedbackType != null) {
        addFloatingFeedbackText(event);
        return;
      }

      IconData icon = BattleConfig.getIcon(event.type);
      Color color = BattleConfig.getColor(event.type, event.risk);
      double size = BattleConfig.getSize(event.risk);
      addFloatingEffect(event, icon, color, size);
    }

    if (event.isVisualOnly) {
      showEffect();
      context.read<BattleProvider>().handleImpact(event);
    } else if (event.type == ActionType.attack && event.target == EffectTarget.enemy) {
      final RenderBox? pBox = playerKey.currentContext?.findRenderObject() as RenderBox?;
      final RenderBox? eBox = enemyKey.currentContext?.findRenderObject() as RenderBox?;

      if (pBox != null && eBox != null) {
        final offset = eBox.localToGlobal(Offset.zero) - pBox.localToGlobal(Offset.zero);

        setState(() {
          isPlayerAttacking = true;
          activeRiskLevel = event.risk;
          isAttackSuccess = event.isSuccess == true && event.feedbackType == null;
        });

        final animRisk = event.feedbackType != null ? RiskLevel.safe : event.risk;

        playerAnimKey.currentState?.animateAttack(
          offset,
          () {
            showEffect();
            context.read<BattleProvider>().handleImpact(event);
            if (event.risk == RiskLevel.risky && event.feedbackType == null) {
              shakeKey.currentState?.shake();
              RenderBox? sBox = stackKey.currentContext?.findRenderObject() as RenderBox?;
              if (sBox != null) {
                Offset localEnemyPos = sBox.globalToLocal(eBox.localToGlobal(Offset.zero)) +
                    Offset(eBox.size.width / 2, eBox.size.height / 2);
                explosionKey.currentState?.explode(localEnemyPos);
              }
            }
          },
          animRisk,
          onAnimationStart: () => setState(() => playerAnimPhase = AnimationPhase.start),
          onAnimationMiddle: () => setState(() => playerAnimPhase = AnimationPhase.middle),
          onAnimationEnd: () => setState(() => playerAnimPhase = AnimationPhase.end),
        ).then((_) {
          if (mounted) {
            setState(() {
              isPlayerAttacking = false;
              playerAnimPhase = AnimationPhase.none;
              activeRiskLevel = null;
              isAttackSuccess = true;
            });
          }
        });
      }
    } else if (event.type == ActionType.attack && event.target == EffectTarget.player) {
      bool enableAnim = context.read<SettingsProvider>().enableEnemyAnimations;
      if (!enableAnim) {
        showEffect();
        context.read<BattleProvider>().handleImpact(event);
        return;
      }

      final RenderBox? pBox = playerKey.currentContext?.findRenderObject() as RenderBox?;
      final RenderBox? eBox = enemyKey.currentContext?.findRenderObject() as RenderBox?;

      if (pBox != null && eBox != null) {
        final offset = pBox.localToGlobal(Offset.zero) - eBox.localToGlobal(Offset.zero);
        setState(() => isEnemyAttacking = true);

        final animRisk = event.feedbackType != null ? RiskLevel.safe : event.risk;

        enemyAnimKey.currentState?.animateAttack(offset, () {
          showEffect();
          context.read<BattleProvider>().handleImpact(event);
          if (event.risk == RiskLevel.risky && event.feedbackType == null) {
            shakeKey.currentState?.shake();
            RenderBox? sBox = stackKey.currentContext?.findRenderObject() as RenderBox?;
            if (sBox != null) {
              Offset localPlayerPos = sBox.globalToLocal(pBox.localToGlobal(Offset.zero)) +
                  Offset(pBox.size.width / 2, pBox.size.height / 2);
              explosionKey.currentState?.explode(localPlayerPos);
            }
          }
        }, animRisk).then((_) {
          if (mounted) setState(() => isEnemyAttacking = false);
        });
      }
    } else if (event.type == ActionType.defend) {
      if (event.target == EffectTarget.player) {
        setState(() => isPlayerAttacking = true);
        playerAnimKey.currentState?.animateDefense(() {
          showEffect();
          context.read<BattleProvider>().handleImpact(event);
        }).then((_) {
          if (mounted) setState(() => isPlayerAttacking = false);
        });
      } else {
        showEffect();
        context.read<BattleProvider>().handleImpact(event);
      }
    } else {
      showEffect();
      context.read<BattleProvider>().handleImpact(event);
    }
  }

  void addFloatingDamageText(DamageEvent event) {
    if (!mounted) return;
    final String id = UniqueKey().toString();
    final targetKey = event.target == DamageTarget.player ? playerKey : enemyKey;
    final RenderBox? box = targetKey.currentContext?.findRenderObject() as RenderBox?;
    if (box == null) return;

    Offset position = box.localToGlobal(Offset(box.size.width / 2, box.size.height / 3));
    RenderBox? rootBox = rootStackKey.currentContext?.findRenderObject() as RenderBox?;
    if (rootBox != null) position = rootBox.globalToLocal(position);

    setState(() {
      floatingDamageTexts.add(
        DamageTextData(
          id: id,
          widget: Positioned(
            key: ValueKey('pos_$id'),
            left: position.dx,
            top: position.dy,
            child: FloatingDamageText(
              key: ValueKey(id),
              damage: event.damage.toString(),
              color: event.color,
              onRemove: () {
                if (mounted) {
                  setState(() => floatingDamageTexts.removeWhere((e) => e.id == id));
                }
              },
            ),
          ),
        ),
      );
    });
  }

  void addFloatingEffect(EffectEvent event, IconData icon, Color color, double size) {
    if (!mounted) return;
    final String id = UniqueKey().toString();
    final targetKey = event.target == EffectTarget.player ? playerKey : enemyKey;
    final RenderBox? box = targetKey.currentContext?.findRenderObject() as RenderBox?;
    if (box == null) return;

    Offset position = box.localToGlobal(Offset.zero);
    RenderBox? rootBox = rootStackKey.currentContext?.findRenderObject() as RenderBox?;
    if (rootBox != null) {
      position = rootBox.globalToLocal(position);
    }

    double offsetX = 0;
    double offsetY = 0;
    if (event.target == EffectTarget.enemy) {
      offsetX = box.size.width * BattleConfig.effectEnemyOffsetX;
      offsetY = box.size.height * BattleConfig.effectEnemyOffsetY;
    } else {
      offsetX = box.size.width * BattleConfig.effectPlayerOffsetX;
      offsetY = box.size.height * BattleConfig.effectPlayerOffsetY;
    }
    position = position + Offset(offsetX, offsetY);

    setState(() {
      floatingEffects.add(
        FloatingEffectData(
          id: id,
          widget: Positioned(
            key: ValueKey('pos_$id'),
            left: position.dx,
            top: position.dy,
            child: FloatingEffect(
              key: ValueKey(id),
              icon: icon,
              color: color,
              size: size,
              onRemove: () {
                if (mounted) {
                  setState(() => floatingEffects.removeWhere((e) => e.id == id));
                }
              },
            ),
          ),
        ),
      );
    });
  }

  void addFloatingFeedbackText(EffectEvent event) {
    if (!mounted || event.feedbackType == null) return;

    final now = DateTime.now();
    if (lastFeedbackTime != null && now.difference(lastFeedbackTime!) < const Duration(milliseconds: 300)) {
      return;
    }
    lastFeedbackTime = now;

    final String id = UniqueKey().toString();
    final targetKey = event.target == EffectTarget.player ? playerKey : enemyKey;
    final RenderBox? box = targetKey.currentContext?.findRenderObject() as RenderBox?;
    if (box == null) return;

    Offset position = box.localToGlobal(Offset.zero);
    RenderBox? rootBox = rootStackKey.currentContext?.findRenderObject() as RenderBox?;
    if (rootBox != null) {
      position = rootBox.globalToLocal(position);
    }

    double offsetX = 0;
    double offsetY = 0;
    if (event.target == EffectTarget.enemy) {
      offsetX = box.size.width * BattleConfig.effectEnemyOffsetX;
      offsetY = box.size.height * BattleConfig.effectEnemyOffsetY;
    } else {
      offsetX = box.size.width * BattleConfig.effectPlayerOffsetX;
      offsetY = box.size.height * BattleConfig.effectPlayerOffsetY;
    }
    position = position + Offset(offsetX, offsetY);

    final feedbackText = BattleConfig.getFeedbackText(event.feedbackType!);
    final feedbackColor = BattleConfig.getFeedbackColor(event.feedbackType!);

    setState(() {
      floatingFeedbackTexts.add(
        FeedbackTextData(
          id: id,
          eventId: event.id,
          widget: Positioned(
            key: ValueKey('pos_$id'),
            left: position.dx,
            top: position.dy,
            child: FloatingFeedbackText(
              key: ValueKey(id),
              feedback: feedbackText,
              color: feedbackColor,
              onRemove: () {
                if (mounted) {
                  setState(() => floatingFeedbackTexts.removeWhere((e) => e.id == id));
                }
              },
            ),
          ),
        ),
      );
    });
  }

  bool get hasPendingBattleAnimations {
    return isPlayerAttacking ||
        isEnemyAttacking ||
        floatingDamageTexts.isNotEmpty ||
        floatingEffects.isNotEmpty ||
        floatingFeedbackTexts.isNotEmpty ||
        (explosionKey.currentState?.isAnimating ?? false);
  }

  Future<void> waitForBattleAnimationsToSettle() async {
    final deadline = DateTime.now().add(
      AnimationConfig.attackRiskyTotal +
          AnimationConfig.floatingTextDuration +
          const Duration(milliseconds: 400),
    );

    while (mounted && hasPendingBattleAnimations && DateTime.now().isBefore(deadline)) {
      await Future<void>.delayed(const Duration(milliseconds: 50));
    }
  }
}
