package model

import (
	"math"
	"time"

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

const (
	// MaxInventorySlots is the maximum unequipped gear items carried at once.
	MaxInventorySlots = 40
)

type Hero struct {
	ID           int64        `json:"id"`
	TelegramID   int64        `json:"telegramId"`
	Name         string       `json:"name"`
	HP           int          `json:"hp"`
	MaxHP        int          `json:"maxHp"`
	Attack       int          `json:"attack"`
	Defense      int          `json:"defense"`
	Speed        float64      `json:"speed"` // attacks per second base rate
	Strength     int          `json:"strength"`
	Constitution int          `json:"constitution"`
	Agility      int          `json:"agility"`
	Luck         int          `json:"luck"`
	State        GameState    `json:"state"`
	WeaponID     *int64       `json:"weaponId,omitempty"`  // Deprecated: kept for DB backward compat
	ArmorID      *int64       `json:"armorId,omitempty"`   // Deprecated: kept for DB backward compat
	Gear         map[EquipmentSlot]*GearItem `json:"gear"`
	// Inventory holds unequipped gear (order matches DB slot_index). Max length: MaxInventorySlots.
	Inventory    []*GearItem `json:"inventory,omitempty"`
	Buffs        []ActiveBuff `json:"buffs,omitempty"`
	Debuffs      []ActiveDebuff `json:"debuffs,omitempty"`
	Gold         int64        `json:"gold"`
	XP           int64        `json:"xp"`
	Level        int          `json:"level"`
	XPToNext     int64        `json:"xpToNext"`
	AttackSpeed  float64      `json:"attackSpeed,omitempty"`
	AttackPower  int          `json:"attackPower,omitempty"`
	DefensePower int          `json:"defensePower,omitempty"`
	MoveSpeed    float64      `json:"moveSpeed,omitempty"`
	PositionX    float64      `json:"positionX"`
	PositionY    float64      `json:"positionY"`
	Potions      int          `json:"potions"`
	ReviveCount  int          `json:"reviveCount"`
	SubscriptionActive    bool       `json:"subscriptionActive"`
	SubscriptionExpiresAt *time.Time  `json:"subscriptionExpiresAt,omitempty"`
	// Deprecated: BuffFreeChargesRemaining is the legacy shared counter. Use BuffCharges instead.
	BuffFreeChargesRemaining int        `json:"buffFreeChargesRemaining"`
	// Deprecated: BuffQuotaPeriodEnd is the legacy shared period end. Use BuffCharges instead.
	BuffQuotaPeriodEnd       *time.Time `json:"buffQuotaPeriodEnd,omitempty"`
	// BuffCharges holds per-buff-type free charge state (remaining count + period window).
	BuffCharges map[string]BuffChargeState `json:"buffCharges"`
	// Stat tracking for achievements.
	TotalKills     int `json:"totalKills"`
	EliteKills     int `json:"eliteKills"`
	TotalDeaths    int `json:"totalDeaths"`
	KillsSinceDeath int `json:"killsSinceDeath"`
	LegendaryDrops int `json:"legendaryDrops"`

	// Movement state (persisted to DB for reconnect recovery).
	CurrentTownID     *int64  `json:"currentTownId,omitempty"`
	DestinationTownID *int64  `json:"destinationTownId,omitempty"`
	MoveState string   `json:"moveState"`
	RestKind  RestKind `json:"restKind,omitempty"`
	// TownPause holds resting, in-town NPC tour, and roadside rest timers (DB town_pause JSONB only).
	TownPause *TownPausePersisted `json:"-"`

	LastOnlineAt *time.Time `json:"lastOnlineAt,omitempty"`
	CreatedAt    time.Time  `json:"createdAt"`
	UpdatedAt    time.Time  `json:"updatedAt"`
}

// BuffChargeState tracks the remaining free charges and period window for a single buff type.
type BuffChargeState struct {
	Remaining int        `json:"remaining"`
	PeriodEnd *time.Time `json:"periodEnd,omitempty"`
}

// XPToNextLevel returns the XP delta required to advance from the given level
// to level+1.  Phase-based curve (spec §9) — v3 scales bases ×10 vs v2 for ~10×
// slower leveling when paired with reduced kill XP:
//
//	L  1–9:  round(180 * 1.28^(L-1))
//	L 10–29: round(1450 * 1.15^(L-10))
//	L 30+:   round(23000 * 1.10^(L-30))
func XPToNextLevel(level int) int64 {
	cfg := tuning.Get()
	if level < 1 {
		level = 1
	}
	switch {
	case level <= 9:
		return int64(math.Round(cfg.XPCurveEarlyBase * math.Pow(cfg.XPCurveEarlyScale, float64(level-1))))
	case level <= 29:
		return int64(math.Round(cfg.XPCurveMidBase * math.Pow(cfg.XPCurveMidScale, float64(level-10))))
	default:
		return int64(math.Round(cfg.XPCurveLateBase * math.Pow(cfg.XPCurveLateScale, float64(level-30))))
	}
}

// CanLevelUp returns true if the hero has enough XP to advance to the next level.
func (h *Hero) CanLevelUp() bool {
	return h.XP >= XPToNextLevel(h.Level)
}

// LevelUp advances the hero to the next level, deducts the required XP, and
// applies stat growth.  HP is NOT restored on level-up (spec §3.3).
// Returns true if the hero leveled up, false if insufficient XP.
func (h *Hero) LevelUp() bool {
	if !h.CanLevelUp() {
		return false
	}

	h.XP -= XPToNextLevel(h.Level)
	h.Level++

	// v3: ~10× rarer than v2 — same formulas, cadences ×10 (spec §3.3).
	cfg := tuning.Get()
	if cfg.LevelUpHPEvery > 0 && h.Level%int(cfg.LevelUpHPEvery) == 0 {
		h.MaxHP += 1 + h.Constitution/6
	}
	if cfg.LevelUpATKEvery > 0 && h.Level%int(cfg.LevelUpATKEvery) == 0 {
		h.Attack++
	}
	if cfg.LevelUpDEFEvery > 0 && h.Level%int(cfg.LevelUpDEFEvery) == 0 {
		h.Defense++
	}

	if cfg.LevelUpSTREvery > 0 && h.Level%int(cfg.LevelUpSTREvery) == 0 {
		h.Strength++
	}
	if cfg.LevelUpCONEvery > 0 && h.Level%int(cfg.LevelUpCONEvery) == 0 {
		h.Constitution++
	}
	if cfg.LevelUpAGIEvery > 0 && h.Level%int(cfg.LevelUpAGIEvery) == 0 {
		h.Agility++
	}
	if cfg.LevelUpLUCKEvery > 0 && h.Level%int(cfg.LevelUpLUCKEvery) == 0 {
		h.Luck++
	}

	return true
}

type statBonuses struct {
	strengthBonus     int
	constitutionBonus int
	agilityBonus      int
	attackMultiplier  float64
	speedMultiplier   float64
	defenseMultiplier float64
	critChanceBonus   float64
	critDamageBonus  float64
	blockChanceBonus float64
	movementMultiplier float64
}

func (h *Hero) activeStatBonuses(now time.Time) statBonuses {
	out := statBonuses{
		strengthBonus: 0,
		constitutionBonus: 0,
		agilityBonus: 0,
		attackMultiplier: 1.0,
		speedMultiplier:  1.0,
		defenseMultiplier: 1.0,
		critChanceBonus: 0.0,
		critDamageBonus: 0.0,
		blockChanceBonus: 0.0,
		movementMultiplier: 1.0,
	}
	for _, ab := range h.Buffs {
		if ab.IsExpired(now) {
			continue
		}
		switch ab.Buff.Type {
		case BuffRush:
			out.movementMultiplier *= (1 + ab.Buff.Magnitude)
		case BuffRage:
			out.attackMultiplier *= (1 + ab.Buff.Magnitude)
			out.strengthBonus += 10
		case BuffPowerPotion:
			out.attackMultiplier *= (1 + ab.Buff.Magnitude)
			out.strengthBonus += 12
		case BuffWarCry:
			out.speedMultiplier *= (1 + ab.Buff.Magnitude)
			out.strengthBonus += 6
			out.agilityBonus += 6
		case BuffShield:
			out.constitutionBonus += 10
			out.defenseMultiplier *= (1 + ab.Buff.Magnitude)
		}
	}
	return out
}

// EffectiveSpeed returns the hero's attack speed after weapon, armor, buff, and debuff modifiers.
func (h *Hero) EffectiveSpeed() float64 {
	return h.EffectiveSpeedAt(time.Now())
}

func (h *Hero) EffectiveSpeedAt(now time.Time) float64 {
	bonuses := h.activeStatBonuses(now)
	effectiveAgility := h.Agility + bonuses.agilityBonus
	if chest := h.Gear[SlotChest]; chest != nil {
		effectiveAgility += chest.AgilityBonus
	}

	// Base attack speed derives from base speed + agility coefficient.
	cfg := tuning.Get()
	speed := h.Speed + float64(effectiveAgility)*cfg.AgilityCoef
	if speed < cfg.MinAttackSpeed {
		speed = cfg.MinAttackSpeed
	}
	if weapon := h.Gear[SlotMainHand]; weapon != nil {
		speed *= weapon.SpeedModifier
	}
	if chest := h.Gear[SlotChest]; chest != nil {
		speed *= chest.SpeedModifier
	}
	speed *= bonuses.speedMultiplier
	// Apply debuffs that reduce attack speed.
	// Slow is movement-only per spec §7.2 and does not affect attack speed.
	for _, ad := range h.Debuffs {
		if ad.IsExpired(now) {
			continue
		}
		switch ad.Debuff.Type {
		case DebuffFreeze:
			speed *= (1 - ad.Debuff.Magnitude) // -50% attack speed
		case DebuffIceSlow:
			speed *= (1 - ad.Debuff.Magnitude) // -20% attack speed (Ice Guardian)
		}
	}
	if speed > cfg.MaxAttackSpeed {
		speed = cfg.MaxAttackSpeed
	}
	if speed < cfg.MinAttackSpeed {
		speed = cfg.MinAttackSpeed
	}
	return speed
}

// EffectiveAttack returns the hero's attack after weapon, buff, and debuff modifiers.
func (h *Hero) EffectiveAttack() int {
	return h.EffectiveAttackAt(time.Now())
}

func (h *Hero) EffectiveAttackAt(now time.Time) int {
	bonuses := h.activeStatBonuses(now)
	effectiveStrength := h.Strength + bonuses.strengthBonus
	effectiveAgility := h.Agility + bonuses.agilityBonus
	effectiveConstitution := h.Constitution + bonuses.constitutionBonus
	if chest := h.Gear[SlotChest]; chest != nil {
		effectiveAgility += chest.AgilityBonus
	}

	atk := h.Attack + effectiveStrength*2 + effectiveAgility/4 + effectiveConstitution/8
	if weapon := h.Gear[SlotMainHand]; weapon != nil {
		atk += weapon.PrimaryStat
	}
	atkF := float64(atk)
	atkF *= bonuses.attackMultiplier
	// Apply weaken debuff.
	for _, ad := range h.Debuffs {
		if ad.IsExpired(now) {
			continue
		}
		if ad.Debuff.Type == DebuffWeaken {
			atkF *= (1 - ad.Debuff.Magnitude) // -30% outgoing damage
		}
	}
	if atkF < 1 {
		atkF = 1
	}
	return int(atkF)
}

// EffectiveDefense returns the hero's defense after armor and buff modifiers.
func (h *Hero) EffectiveDefense() int {
	return h.EffectiveDefenseAt(time.Now())
}

func (h *Hero) EffectiveDefenseAt(now time.Time) int {
	bonuses := h.activeStatBonuses(now)
	effectiveConstitution := h.Constitution + bonuses.constitutionBonus
	effectiveAgility := h.Agility + bonuses.agilityBonus
	if chest := h.Gear[SlotChest]; chest != nil {
		effectiveAgility += chest.AgilityBonus
	}

	def := h.Defense + effectiveConstitution/4 + effectiveAgility/8
	if chest := h.Gear[SlotChest]; chest != nil {
		def += chest.PrimaryStat
	}
	def = int(float64(def) * bonuses.defenseMultiplier)
	if def < 0 {
		def = 0
	}
	return def
}

// MovementSpeedMultiplier returns the hero's movement speed modifier (1.0 = normal).
// Rush buff and Slow debuff affect movement, not attack speed, per spec §7.
func (h *Hero) MovementSpeedMultiplier(now time.Time) float64 {
	mult := 1.0
	for _, ab := range h.Buffs {
		if ab.IsExpired(now) {
			continue
		}
		if ab.Buff.Type == BuffRush {
			mult *= (1 + ab.Buff.Magnitude) // +50% movement
		}
	}
	for _, ad := range h.Debuffs {
		if ad.IsExpired(now) {
			continue
		}
		if ad.Debuff.Type == DebuffSlow {
			mult *= (1 - ad.Debuff.Magnitude) // -40% movement
		}
	}
	if mult < 0.1 {
		mult = 0.1
	}
	return mult
}

// EnsureGearMap guarantees Gear is a non-nil map so JSON encodes "gear":{} instead of null
// (clients treat null as missing equipment).
func (h *Hero) EnsureGearMap() {
	if h.Gear == nil {
		h.Gear = make(map[EquipmentSlot]*GearItem)
	}
}

// EnsureInventorySlice guarantees Inventory is a non-nil slice for append/count.
func (h *Hero) EnsureInventorySlice() {
	if h.Inventory == nil {
		h.Inventory = []*GearItem{}
	}
}

// RefreshDerivedCombatStats updates exported derived combat fields for API/state usage.
func (h *Hero) RefreshDerivedCombatStats(now time.Time) {
	h.XPToNext = XPToNextLevel(h.Level)
	h.AttackSpeed = h.EffectiveSpeedAt(now)
	h.AttackPower = h.EffectiveAttackAt(now)
	h.DefensePower = h.EffectiveDefenseAt(now)
	h.MoveSpeed = h.MovementSpeedMultiplier(now)
}

// CombatRatingAt computes a single-number combat effectiveness score used by
// the auto-equip system to decide whether new gear is an upgrade.
func (h *Hero) CombatRatingAt(now time.Time) float64 {
	return float64(h.EffectiveAttackAt(now))*h.EffectiveSpeedAt(now) + float64(h.EffectiveDefenseAt(now))*0.35
}

// IsAlive returns true if the hero has HP remaining.
func (h *Hero) IsAlive() bool {
	return h.HP > 0
}

// IsStunned returns true if the hero currently has an active stun debuff.
func (h *Hero) IsStunned(now time.Time) bool {
	for _, ad := range h.Debuffs {
		if ad.Debuff.Type == DebuffStun && !ad.IsExpired(now) {
			return true
		}
	}
	return false
}
