package combat

import (
	"time"

	"a301_game_server/internal/entity"
	"a301_game_server/internal/network"
	"a301_game_server/pkg/mathutil"
	pb "a301_game_server/proto/gen/pb"
)

// Combatant is an entity that can participate in combat.
type Combatant interface {
	entity.Entity
	HP() int32
	SetHP(int32)
	MaxHP() int32
	MP() int32
	SetMP(int32)
	IsAlive() bool
	Stats() CombatStats
}

// CombatStats provides stat access for damage calculation.
type CombatStats struct {
	Str   int32
	Dex   int32
	Int   int32
	Level int32
}

// CombatantWithConn is a combatant that can receive messages (player).
type CombatantWithConn interface {
	Combatant
	Connection() *network.Connection
}

// Manager handles all combat logic for a zone.
type Manager struct {
	skills *SkillRegistry
	buffs  *BuffRegistry

	// Per-entity state
	cooldowns    map[uint64]map[uint32]time.Time // entityID -> skillID -> ready time
	activeBuffs  map[uint64][]*ActiveBuff         // entityID -> active buffs

	// Broadcast function (set by zone to send to AOI)
	broadcastToNearby func(ent entity.Entity, msgType uint16, msg interface{})
	sendToEntity       func(entityID uint64, msgType uint16, msg interface{})
}

// NewManager creates a combat manager.
func NewManager() *Manager {
	return &Manager{
		skills:      NewSkillRegistry(),
		buffs:       NewBuffRegistry(),
		cooldowns:   make(map[uint64]map[uint32]time.Time),
		activeBuffs: make(map[uint64][]*ActiveBuff),
	}
}

// Skills returns the skill registry.
func (m *Manager) Skills() *SkillRegistry { return m.skills }

// SetBroadcast configures the broadcast callback.
func (m *Manager) SetBroadcast(
	broadcast func(ent entity.Entity, msgType uint16, msg interface{}),
	send func(entityID uint64, msgType uint16, msg interface{}),
) {
	m.broadcastToNearby = broadcast
	m.sendToEntity = send
}

// UseSkill attempts to execute a skill.
func (m *Manager) UseSkill(
	caster Combatant,
	skillID uint32,
	targetID uint64,
	targetPos mathutil.Vec3,
	getEntity func(uint64) entity.Entity,
	getEntitiesInRadius func(center mathutil.Vec3, radius float32) []entity.Entity,
) (bool, string) {

	if !caster.IsAlive() {
		return false, "you are dead"
	}

	skill := m.skills.Get(skillID)
	if skill == nil {
		return false, "unknown skill"
	}

	// Cooldown check.
	if cd, ok := m.cooldowns[caster.EntityID()]; ok {
		if readyAt, ok := cd[skillID]; ok && time.Now().Before(readyAt) {
			return false, "skill on cooldown"
		}
	}

	// Mana check.
	if caster.MP() < skill.ManaCost {
		return false, "not enough mana"
	}

	// Resolve targets.
	var targets []Combatant

	switch skill.TargetType {
	case TargetSelf:
		targets = []Combatant{caster}

	case TargetSingleEnemy:
		ent := getEntity(targetID)
		if ent == nil {
			return false, "target not found"
		}
		target, ok := ent.(Combatant)
		if !ok || !target.IsAlive() {
			return false, "invalid target"
		}
		if caster.Position().DistanceXZ(target.Position()) > skill.Range {
			return false, "target out of range"
		}
		targets = []Combatant{target}

	case TargetSingleAlly:
		ent := getEntity(targetID)
		if ent == nil {
			return false, "target not found"
		}
		target, ok := ent.(Combatant)
		if !ok || !target.IsAlive() {
			return false, "invalid target"
		}
		if caster.Position().DistanceXZ(target.Position()) > skill.Range {
			return false, "target out of range"
		}
		targets = []Combatant{target}

	case TargetAoEGround:
		if caster.Position().DistanceXZ(targetPos) > skill.Range {
			return false, "target position out of range"
		}
		entities := getEntitiesInRadius(targetPos, skill.AoERadius)
		for _, e := range entities {
			if c, ok := e.(Combatant); ok && c.IsAlive() && c.EntityID() != caster.EntityID() {
				targets = append(targets, c)
			}
		}

	case TargetAoETarget:
		ent := getEntity(targetID)
		if ent == nil {
			return false, "target not found"
		}
		if caster.Position().DistanceXZ(ent.Position()) > skill.Range {
			return false, "target out of range"
		}
		entities := getEntitiesInRadius(ent.Position(), skill.AoERadius)
		for _, e := range entities {
			if c, ok := e.(Combatant); ok && c.IsAlive() && c.EntityID() != caster.EntityID() {
				targets = append(targets, c)
			}
		}
	}

	// Consume mana.
	caster.SetMP(caster.MP() - skill.ManaCost)

	// Set cooldown.
	if m.cooldowns[caster.EntityID()] == nil {
		m.cooldowns[caster.EntityID()] = make(map[uint32]time.Time)
	}
	m.cooldowns[caster.EntityID()][skillID] = time.Now().Add(skill.Cooldown)

	// Calculate effective stats (base + buff modifiers).
	casterStats := m.effectiveStats(caster)

	// Apply effects to each target.
	for _, target := range targets {
		m.applyEffects(caster, target, skill, casterStats)
	}

	return true, ""
}

func (m *Manager) applyEffects(caster, target Combatant, skill *SkillDef, casterStats CombatStats) {
	for _, effect := range skill.Effects {
		switch effect.Type {
		case EffectDamage:
			targetStats := m.effectiveStats(target)
			result := CalcDamage(effect.Value, casterStats.Str, targetStats.Dex)
			target.SetHP(target.HP() - result.FinalDamage)

			died := !target.IsAlive()

			evt := &pb.CombatEvent{
				CasterId:    caster.EntityID(),
				TargetId:    target.EntityID(),
				SkillId:     skill.ID,
				Damage:      result.FinalDamage,
				IsCritical:  result.IsCritical,
				TargetDied:  died,
				TargetHp:    target.HP(),
				TargetMaxHp: target.MaxHP(),
				EventType:   pb.CombatEventType_COMBAT_EVENT_DAMAGE,
			}

			if m.broadcastToNearby != nil {
				m.broadcastToNearby(target, network.MsgCombatEvent, evt)
			}

			if died {
				deathEvt := &pb.CombatEvent{
					CasterId:  caster.EntityID(),
					TargetId:  target.EntityID(),
					EventType: pb.CombatEventType_COMBAT_EVENT_DEATH,
				}
				if m.broadcastToNearby != nil {
					m.broadcastToNearby(target, network.MsgCombatEvent, deathEvt)
				}
			}

		case EffectHeal:
			heal := CalcHeal(effect.Value, casterStats.Int)
			target.SetHP(target.HP() + heal)

			evt := &pb.CombatEvent{
				CasterId:    caster.EntityID(),
				TargetId:    target.EntityID(),
				SkillId:     skill.ID,
				Heal:        heal,
				TargetHp:    target.HP(),
				TargetMaxHp: target.MaxHP(),
				EventType:   pb.CombatEventType_COMBAT_EVENT_HEAL,
			}
			if m.broadcastToNearby != nil {
				m.broadcastToNearby(target, network.MsgCombatEvent, evt)
			}

		case EffectBuff, EffectDebuff:
			buffDef := m.buffs.Get(uint32(effect.Value))
			if buffDef == nil {
				continue
			}
			ab := &ActiveBuff{
				Def:          buffDef,
				CasterID:     caster.EntityID(),
				Remaining:    effect.Duration,
				TickInterval: effect.TickInterval,
				NextTick:     effect.TickInterval,
			}
			m.activeBuffs[target.EntityID()] = append(m.activeBuffs[target.EntityID()], ab)

			evt := &pb.BuffApplied{
				TargetId: target.EntityID(),
				BuffId:   buffDef.ID,
				BuffName: buffDef.Name,
				Duration: float32(effect.Duration.Seconds()),
				IsDebuff: buffDef.IsDebuff,
			}
			if m.broadcastToNearby != nil {
				m.broadcastToNearby(target, network.MsgBuffApplied, evt)
			}
		}
	}
}

// UpdateBuffs processes active buffs each tick. Call once per zone tick.
func (m *Manager) UpdateBuffs(dt time.Duration, getEntity func(uint64) Combatant) {
	for entityID, buffs := range m.activeBuffs {
		target := getEntity(entityID)
		if target == nil {
			delete(m.activeBuffs, entityID)
			continue
		}

		var remaining []*ActiveBuff
		for _, b := range buffs {
			tickValue := b.Tick(dt)

			if tickValue != 0 && target.IsAlive() {
				if b.Def.IsDebuff {
					// DoT damage.
					target.SetHP(target.HP() - tickValue)
					evt := &pb.CombatEvent{
						CasterId:    b.CasterID,
						TargetId:    entityID,
						Damage:      tickValue,
						TargetHp:    target.HP(),
						TargetMaxHp: target.MaxHP(),
						EventType:   pb.CombatEventType_COMBAT_EVENT_DAMAGE,
					}
					if m.broadcastToNearby != nil {
						m.broadcastToNearby(target, network.MsgCombatEvent, evt)
					}
				} else {
					// HoT heal.
					target.SetHP(target.HP() + tickValue)
					evt := &pb.CombatEvent{
						CasterId:    b.CasterID,
						TargetId:    entityID,
						Heal:        tickValue,
						TargetHp:    target.HP(),
						TargetMaxHp: target.MaxHP(),
						EventType:   pb.CombatEventType_COMBAT_EVENT_HEAL,
					}
					if m.broadcastToNearby != nil {
						m.broadcastToNearby(target, network.MsgCombatEvent, evt)
					}
				}
			}

			if !b.IsExpired() {
				remaining = append(remaining, b)
			} else {
				// Notify buff removed.
				evt := &pb.BuffRemoved{
					TargetId: entityID,
					BuffId:   b.Def.ID,
				}
				if m.broadcastToNearby != nil {
					m.broadcastToNearby(target, network.MsgBuffRemoved, evt)
				}
			}
		}

		if len(remaining) == 0 {
			delete(m.activeBuffs, entityID)
		} else {
			m.activeBuffs[entityID] = remaining
		}
	}
}

// Respawn resets a dead entity to full HP at a spawn position.
func (m *Manager) Respawn(ent Combatant, spawnPos mathutil.Vec3) {
	ent.SetHP(ent.MaxHP())
	ent.SetPosition(spawnPos)
	m.clearCooldowns(ent.EntityID())
	m.clearBuffs(ent.EntityID())
}

// RemoveEntity cleans up combat state for a removed entity.
func (m *Manager) RemoveEntity(entityID uint64) {
	m.clearCooldowns(entityID)
	m.clearBuffs(entityID)
}

// effectiveStats returns stats with buff modifiers applied.
func (m *Manager) effectiveStats(c Combatant) CombatStats {
	base := c.Stats()
	buffs := m.activeBuffs[c.EntityID()]
	for _, b := range buffs {
		base.Str += b.Def.StatModifier.StrBonus
		base.Dex += b.Def.StatModifier.DexBonus
		base.Int += b.Def.StatModifier.IntBonus
	}
	return base
}

func (m *Manager) clearCooldowns(entityID uint64) {
	delete(m.cooldowns, entityID)
}

func (m *Manager) clearBuffs(entityID uint64) {
	delete(m.activeBuffs, entityID)
}
