package game

import (
	"math/rand"
	"time"

	"github.com/denisovdennis/autohero/internal/model"
	"github.com/denisovdennis/autohero/internal/tuning"
)

// CalculateDamage computes the final damage dealt from attacker stats to a defender,
// applying defense and critical hits.
func CalculateDamage(baseAttack int, defense int, critChance float64) (damage int, isCrit bool) {
	atk := float64(baseAttack)

	// Defense reduces damage (simple formula: damage = atk - def, min 1).
	dmg := atk - float64(defense)
	if dmg < 1 {
		dmg = 1
	}

	// Critical hit check.
	if critChance > 0 && rand.Float64() < critChance {
		dmg *= 2
		isCrit = true
	}

	dmg *= tuning.Get().CombatDamageScale
	if dmg < 1 {
		dmg = 1
	}

	return int(dmg), isCrit
}

// CalculateIncomingDamage applies shield buff reduction to incoming damage.
func CalculateIncomingDamage(rawDamage int, buffs []model.ActiveBuff, now time.Time) int {
	dmg := float64(rawDamage)

	for _, ab := range buffs {
		if ab.IsExpired(now) {
			continue
		}
		if ab.Buff.Type == model.BuffShield {
			dmg *= (1 - ab.Buff.Magnitude)
		}
	}

	if dmg < 1 {
		dmg = 1
	}
	return int(dmg)
}

// ProcessAttack executes a single attack from hero to enemy and returns the combat event.
// It respects dodge ability on enemies and stun debuff on hero.
func ProcessAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) model.CombatEvent {
	// If hero is stunned, skip attack entirely.
	if hero.IsStunned(now) {
		return model.CombatEvent{
			Type:      "attack",
			HeroID:    hero.ID,
			Damage:    0,
			Source:    "hero",
			HeroHP:   hero.HP,
			EnemyHP:  enemy.HP,
			Timestamp: now,
		}
	}

	critChance := 0.0
	if weapon := hero.Gear[model.SlotMainHand]; weapon != nil {
		critChance = weapon.CritChance
	}

	// Check enemy dodge ability.
	if enemy.HasAbility(model.AbilityDodge) {
		if rand.Float64() < tuning.Get().EnemyDodgeChance {
			return model.CombatEvent{
				Type:      "attack",
				HeroID:    hero.ID,
				Damage:    0,
				Source:    "hero",
				HeroHP:   hero.HP,
				EnemyHP:  enemy.HP,
				Timestamp: now,
			}
		}
	}

	dmg, isCrit := CalculateDamage(hero.EffectiveAttackAt(now), enemy.Defense, critChance)
	enemy.HP -= dmg
	if enemy.HP < 0 {
		enemy.HP = 0
	}

	return model.CombatEvent{
		Type:      "attack",
		HeroID:    hero.ID,
		Damage:    dmg,
		Source:    "hero",
		IsCrit:    isCrit,
		HeroHP:   hero.HP,
		EnemyHP:  enemy.HP,
		Timestamp: now,
	}
}

// EnemyAttackDamageMultiplier returns a damage multiplier based on the enemy's
// attack counter and burst/chain abilities. It increments AttackCount.
func EnemyAttackDamageMultiplier(enemy *model.Enemy) float64 {
	enemy.AttackCount++
	mult := 1.0
	cfg := tuning.Get()

	// Orc Warrior: every 3rd attack deals 1.5x damage (spec §4.1).
	if enemy.HasAbility(model.AbilityBurst) && cfg.EnemyBurstEveryN > 0 && enemy.AttackCount%int(cfg.EnemyBurstEveryN) == 0 {
		mult *= cfg.EnemyBurstMultiplier
	}

	// Lightning Titan: after 5 attacks, next attack deals 3x damage (spec §4.2).
	if enemy.HasAbility(model.AbilityChainLightning) && cfg.EnemyChainEveryN > 0 && enemy.AttackCount%int(cfg.EnemyChainEveryN) == 0 {
		mult *= cfg.EnemyChainMultiplier
	}

	return mult
}

// ProcessEnemyAttack executes a single attack from enemy to hero, including
// debuff application and burst/chain abilities based on the enemy's type.
func ProcessEnemyAttack(hero *model.Hero, enemy *model.Enemy, now time.Time) model.CombatEvent {
	critChance := enemy.CritChance
	if enemy.HasAbility(model.AbilityCritical) && critChance < tuning.Get().EnemyCriticalMinChance {
		critChance = tuning.Get().EnemyCriticalMinChance
	}

	rawDmg, isCrit := CalculateDamage(enemy.Attack, hero.EffectiveDefenseAt(now), critChance)

	// Apply burst/chain ability multiplier.
	burstMult := EnemyAttackDamageMultiplier(enemy)
	if burstMult > 1.0 {
		rawDmg = int(float64(rawDmg) * burstMult)
	}

	dmg := CalculateIncomingDamage(rawDmg, hero.Buffs, now)

	hero.HP -= dmg
	if hero.HP < 0 {
		hero.HP = 0
	}

	debuffApplied := tryApplyDebuff(hero, enemy, now)

	return model.CombatEvent{
		Type:          "attack",
		HeroID:        hero.ID,
		Damage:        dmg,
		Source:        "enemy",
		IsCrit:        isCrit,
		DebuffApplied: debuffApplied,
		HeroHP:       hero.HP,
		EnemyHP:      enemy.HP,
		Timestamp:     now,
	}
}

// tryApplyDebuff checks enemy abilities and rolls to apply debuffs to the hero.
// Returns the debuff type string if one was applied, empty string otherwise.
func tryApplyDebuff(hero *model.Hero, enemy *model.Enemy, now time.Time) string {
	type debuffRule struct {
		ability model.SpecialAbility
		debuff  model.DebuffType
		chance  float64
	}

	rules := []debuffRule{
		{model.AbilityBurn, model.DebuffBurn, tuning.Get().DebuffProcBurn},
		{model.AbilityPoison, model.DebuffPoison, tuning.Get().DebuffProcPoison},
		{model.AbilitySlow, model.DebuffSlow, tuning.Get().DebuffProcSlow},
		{model.AbilityStun, model.DebuffStun, tuning.Get().DebuffProcStun},
		{model.AbilityFreeze, model.DebuffFreeze, tuning.Get().DebuffProcFreeze},
		{model.AbilityIceSlow, model.DebuffIceSlow, tuning.Get().DebuffProcIceSlow},
	}

	for _, rule := range rules {
		if !enemy.HasAbility(rule.ability) {
			continue
		}
		if rand.Float64() >= rule.chance {
			continue
		}

		applyDebuff(hero, rule.debuff, now)
		return string(rule.debuff)
	}

	return ""
}

// applyDebuff adds a debuff to the hero. If the same debuff type is already active, it refreshes.
func applyDebuff(hero *model.Hero, debuffType model.DebuffType, now time.Time) {
	def, ok := model.DebuffDefinition(debuffType)
	if !ok {
		return
	}

	// Refresh if already active.
	for i, ad := range hero.Debuffs {
		if ad.Debuff.Type == debuffType && !ad.IsExpired(now) {
			hero.Debuffs[i].AppliedAt = now
			hero.Debuffs[i].ExpiresAt = now.Add(def.Duration)
			return
		}
	}

	// Remove expired debuffs.
	active := hero.Debuffs[:0]
	for _, ad := range hero.Debuffs {
		if !ad.IsExpired(now) {
			active = append(active, ad)
		}
	}

	ad := model.ActiveDebuff{
		Debuff:    def,
		AppliedAt: now,
		ExpiresAt: now.Add(def.Duration),
	}
	hero.Debuffs = append(active, ad)
}

// ProcessDebuffDamage applies periodic damage from active debuffs (poison, burn).
// Should be called each combat tick. Returns total damage dealt by debuffs this tick.
func ProcessDebuffDamage(hero *model.Hero, tickDuration time.Duration, now time.Time) int {
	totalDmg := 0

	for _, ad := range hero.Debuffs {
		if ad.IsExpired(now) {
			continue
		}

		switch ad.Debuff.Type {
		case model.DebuffPoison:
			// -2% HP/sec, scaled by tick duration.
			dmg := int(float64(hero.MaxHP) * ad.Debuff.Magnitude * tickDuration.Seconds())
			if dmg < 1 {
				dmg = 1
			}
			hero.HP -= dmg
			totalDmg += dmg

		case model.DebuffBurn:
			// -3% HP/sec, scaled by tick duration.
			dmg := int(float64(hero.MaxHP) * ad.Debuff.Magnitude * tickDuration.Seconds())
			if dmg < 1 {
				dmg = 1
			}
			hero.HP -= dmg
			totalDmg += dmg
		}
	}

	if hero.HP < 0 {
		hero.HP = 0
	}

	return totalDmg
}

// ProcessEnemyRegen handles HP regeneration for enemies with the regen ability.
// Should be called each combat tick.
func ProcessEnemyRegen(enemy *model.Enemy, tickDuration time.Duration) int {
	if !enemy.HasAbility(model.AbilityRegen) {
		return 0
	}

	// Regen rates vary by enemy type.
	cfg := tuning.Get()
	regenRate := cfg.EnemyRegenDefault
	switch enemy.Type {
	case model.EnemySkeletonKing:
		regenRate = cfg.EnemyRegenSkeletonKing
	case model.EnemyForestWarden:
		regenRate = cfg.EnemyRegenForestWarden
	case model.EnemyBattleLizard:
		regenRate = cfg.EnemyRegenBattleLizard
	}

	healed := int(float64(enemy.MaxHP) * regenRate * tickDuration.Seconds())
	if healed < 1 {
		healed = 1
	}
	enemy.HP += healed
	if enemy.HP > enemy.MaxHP {
		enemy.HP = enemy.MaxHP
	}

	return healed
}

// CheckDeath checks if the hero is dead and attempts resurrection if a buff is active.
// Returns true if the hero is dead (no resurrection available).
func CheckDeath(hero *model.Hero, now time.Time) bool {
	if hero.IsAlive() {
		return false
	}

	// Check for resurrection buff.
	for i, ab := range hero.Buffs {
		if ab.IsExpired(now) {
			continue
		}
		if ab.Buff.Type == model.BuffResurrection {
			// Revive with magnitude % of max HP.
			hero.HP = int(float64(hero.MaxHP) * ab.Buff.Magnitude)
			if hero.HP < 1 {
				hero.HP = 1
			}
			// Consume the buff by expiring it immediately.
			hero.Buffs[i].ExpiresAt = now
			return false
		}
	}

	hero.State = model.StateDead
	return true
}

// ApplyBuff adds a buff to the hero. If the same buff type is already active, it refreshes.
func ApplyBuff(hero *model.Hero, buffType model.BuffType, now time.Time) *model.ActiveBuff {
	def, ok := model.BuffDefinition(buffType)
	if !ok {
		return nil
	}

	// Heal buff is applied instantly.
	if buffType == model.BuffHeal {
		healAmount := int(float64(hero.MaxHP) * def.Magnitude)
		hero.HP += healAmount
		if hero.HP > hero.MaxHP {
			hero.HP = hero.MaxHP
		}
	}

	// Check if already active and refresh.
	for i, ab := range hero.Buffs {
		if ab.Buff.Type == buffType && !ab.IsExpired(now) {
			hero.Buffs[i].AppliedAt = now
			hero.Buffs[i].ExpiresAt = now.Add(def.Duration)
			return &hero.Buffs[i]
		}
	}

	// Remove expired buffs while we're here.
	active := hero.Buffs[:0]
	for _, ab := range hero.Buffs {
		if !ab.IsExpired(now) {
			active = append(active, ab)
		}
	}

	ab := model.ActiveBuff{
		Buff:      def,
		AppliedAt: now,
		ExpiresAt: now.Add(def.Duration),
	}
	hero.Buffs = append(active, ab)
	return &ab
}

// HasLuckBuff returns true if the hero has an active luck buff.
func HasLuckBuff(hero *model.Hero, now time.Time) bool {
	for _, ab := range hero.Buffs {
		if ab.Buff.Type == model.BuffLuck && !ab.IsExpired(now) {
			return true
		}
	}
	return false
}

// LuckMultiplier returns the loot multiplier when the Luck buff is active (tuning.LuckBuffMultiplier).
func LuckMultiplier(hero *model.Hero, now time.Time) float64 {
	if HasLuckBuff(hero, now) {
		return tuning.Get().LuckBuffMultiplier
	}
	return 1.0
}

// ProcessSummonDamage applies bonus damage from summoned minions (Skeleton King).
// MVP: minions are modeled as periodic bonus damage (25% of Skeleton King's attack)
// applied every tick, rather than as full entity spawns.
// The spec says summons appear every 15 seconds; we approximate by checking
// the combat duration and applying minion damage when a 15s boundary is crossed.
func ProcessSummonDamage(hero *model.Hero, enemy *model.Enemy, combatStart time.Time, lastTick time.Time, now time.Time) int {
	if !enemy.HasAbility(model.AbilitySummon) {
		return 0
	}

	dv := tuning.DefaultValues()
	// How many summon cycles have elapsed since combat start.
	cycleSec := tuning.Get().SummonCycleSeconds
	if cycleSec < 1 {
		cycleSec = dv.SummonCycleSeconds
	}
	prevCycles := int(lastTick.Sub(combatStart).Seconds()) / int(cycleSec)
	currCycles := int(now.Sub(combatStart).Seconds()) / int(cycleSec)

	if currCycles <= prevCycles {
		return 0
	}

	// Each summon wave deals (1/divisor) of the enemy's base attack as minion damage.
	div := tuning.Get().SummonDamageDivisor
	if div < 1 {
		div = dv.SummonDamageDivisor
	}
	minionDmg := max(1, enemy.Attack/int(div))
	hero.HP -= minionDmg
	if hero.HP < 0 {
		hero.HP = 0
	}
	return minionDmg
}
