package actions

import (
	"dk/internal/database"
	"dk/internal/helpers/exp"
	"dk/internal/models/fightlogs"
	"dk/internal/models/fights"
	"dk/internal/models/monsters"
	"dk/internal/models/spells"
	"dk/internal/models/towns"
	"dk/internal/models/users"
	"math"
	"math/rand"
)

type FightResult struct {
	FightUpdates map[string]any
	UserUpdates  map[string]any
	LogAction    func() error
	Ended        bool
	Victory      bool
	Won          bool
}

func HandleAttack(fight *fights.Fight, user *users.User) *FightResult {
	monster, err := monsters.Find(fight.MonsterID)
	if err != nil {
		return &FightResult{
			LogAction: func() error { return fightlogs.AddAction(fight.ID, "Monster not found!") },
		}
	}

	// Calculate damage
	attackPower := float64(user.Attack)
	minAttack := attackPower * 0.75
	maxAttack := attackPower
	rawAttack := math.Ceil(rand.Float64()*(maxAttack-minAttack) + minAttack)
	tohit := rawAttack / (1.2 + math.Sqrt(attackPower)*0.05)

	// Critical hit
	criticalRoll := rand.Intn(150) + 1
	if float64(criticalRoll) <= math.Sqrt(float64(user.Strength)) {
		tohit *= 2
	}

	// Monster defense
	armor := float64(monster.Armor)
	minBlock := armor * 0.75
	maxBlock := armor
	rawBlock := math.Ceil(rand.Float64()*(maxBlock-minBlock) + minBlock)
	toblock := rawBlock / (1.8 + math.Sqrt(armor)*0.08)

	damage := tohit - toblock
	if damage < 1 {
		damage = 1
	}

	if fight.UberDamage > 0 {
		bonus := math.Ceil(damage * float64(fight.UberDamage) / 100)
		damage += bonus
	}

	finalDamage := int(damage)
	newMonsterHP := fight.MonsterHP - finalDamage
	if newMonsterHP < 0 {
		newMonsterHP = 0
	}

	result := &FightResult{
		FightUpdates: map[string]any{"monster_hp": newMonsterHP},
		LogAction:    func() error { return fightlogs.AddAttackHit(fight.ID, finalDamage) },
	}

	// Check if monster defeated
	if newMonsterHP <= 0 {
		rewardGold, rewardExp := calculateRewards(monster, user)

		result.FightUpdates["victory"] = true
		result.FightUpdates["won"] = true
		result.FightUpdates["reward_gold"] = rewardGold
		result.FightUpdates["reward_exp"] = rewardExp

		result.UserUpdates = map[string]any{
			"fight_id":  0,
			"currently": "Exploring",
			"gold":      user.Gold + rewardGold,
			"exp":       user.Exp + rewardExp,
		}

		// Handle level up
		newLevel, newStats := calculateLevelUp(user.Level, user.Exp+rewardExp, user.Strength, user.Dexterity)
		if newLevel > user.Level {
			result.UserUpdates["level"] = newLevel
			result.UserUpdates["strength"] = newStats.Strength
			result.UserUpdates["dexterity"] = newStats.Dexterity
		}

		result.LogAction = func() error {
			if err := fightlogs.AddAttackHit(fight.ID, finalDamage); err != nil {
				return err
			}
			return fightlogs.AddMonsterDeath(fight.ID, monster.Name)
		}
		result.Ended = true
		result.Victory = true
		result.Won = true
	}

	return result
}

func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResult {
	spell, err := spells.Find(spellID)
	if err != nil {
		return &FightResult{
			LogAction: func() error { return fightlogs.AddAction(fight.ID, "Spell not found!") },
		}
	}

	if user.MP < spell.MP {
		return &FightResult{
			LogAction: func() error { return fightlogs.AddAction(fight.ID, "Not enough MP to cast "+spell.Name+"!") },
		}
	}

	if !user.HasSpell(spellID) {
		return &FightResult{
			LogAction: func() error { return fightlogs.AddAction(fight.ID, "You don't know that spell!") },
		}
	}

	result := &FightResult{
		UserUpdates: map[string]any{"mp": user.MP - spell.MP},
	}

	switch spell.Type {
	case spells.TypeHealing:
		newHP := user.HP + spell.Attribute
		if newHP > user.MaxHP {
			newHP = user.MaxHP
		}
		result.UserUpdates["hp"] = newHP
		result.LogAction = func() error { return fightlogs.AddSpellHeal(fight.ID, spell.Name, spell.Attribute) }

	case spells.TypeHurt:
		newMonsterHP := fight.MonsterHP - spell.Attribute
		if newMonsterHP < 0 {
			newMonsterHP = 0
		}
		result.FightUpdates = map[string]any{"monster_hp": newMonsterHP}
		result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Attribute) }

		if newMonsterHP <= 0 {
			result.FightUpdates["victory"] = true
			result.FightUpdates["won"] = true
			result.FightUpdates["reward_gold"] = 10
			result.FightUpdates["reward_exp"] = 5
			result.UserUpdates["fight_id"] = 0
			result.UserUpdates["currently"] = "Exploring"
			result.Ended = true
			result.Victory = true
			result.Won = true
		}

	default:
		result.LogAction = func() error { return fightlogs.AddAction(fight.ID, "You cast "+spell.Name+" but nothing happened!") }
	}

	return result
}

func HandleRun(fight *fights.Fight, user *users.User) *FightResult {
	result := &FightResult{}

	if rand.Float32() < 0.2 {
		result.FightUpdates = map[string]any{"ran_away": true}
		result.UserUpdates = map[string]any{
			"fight_id":  0,
			"currently": "Exploring",
		}
		result.LogAction = func() error { return fightlogs.AddRunSuccess(fight.ID) }
		result.Ended = true
	} else {
		result.LogAction = func() error { return fightlogs.AddRunFail(fight.ID) }
	}

	return result
}

func HandleMonsterAttack(fight *fights.Fight, user *users.User) *FightResult {
	monster, err := monsters.Find(fight.MonsterID)
	if err != nil {
		return &FightResult{}
	}

	// Calculate damage
	attackPower := float64(monster.MaxDmg)
	minAttack := attackPower * 0.75
	maxAttack := attackPower
	tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3

	defense := float64(user.Defense)
	minBlock := defense * 0.75
	maxBlock := defense
	toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3

	damage := tohit - toblock
	if damage < 1 {
		damage = 1
	}

	if fight.UberDefense > 0 {
		reduction := math.Ceil(damage * float64(fight.UberDefense) / 100)
		damage -= reduction
		if damage < 1 {
			damage = 1
		}
	}

	finalDamage := int(damage)
	newHP := user.HP - finalDamage
	if newHP < 0 {
		newHP = 0
	}

	result := &FightResult{
		UserUpdates: map[string]any{"hp": newHP},
		LogAction:   func() error { return fightlogs.AddMonsterAttack(fight.ID, monster.Name, finalDamage) },
	}

	if newHP <= 0 {
		closestTown := findClosestTown(user.X, user.Y)
		townX, townY := 0, 0
		if closestTown != nil {
			townX, townY = closestTown.X, closestTown.Y
		}

		result.FightUpdates = map[string]any{
			"victory": true,
			"won":     false,
		}
		result.UserUpdates = map[string]any{
			"fight_id":  0,
			"currently": "In Town",
			"hp":        user.MaxHP / 4,
			"gold":      (user.Gold * 3) / 4,
			"x":         townX,
			"y":         townY,
		}
		result.Ended = true
		result.Victory = true
		result.Won = false
	}

	return result
}

type LevelStats struct {
	Strength  int
	Dexterity int
}

func calculateLevelUp(currentLevel, newExp, currentStr, currentDex int) (int, LevelStats) {
	level := currentLevel
	str := currentStr
	dex := currentDex
	nexp := newExp

	for {
		expNeeded := exp.Calc(level + 1)
		if nexp < expNeeded {
			break
		}
		level++
		str++
		dex++
		nexp -= expNeeded
	}

	return level, LevelStats{Strength: str, Dexterity: dex}
}

func findClosestTown(x, y int) *towns.Town {
	allTowns, err := towns.All()
	if err != nil || len(allTowns) == 0 {
		return nil
	}

	var closest *towns.Town
	var minDistance float64

	for _, town := range allTowns {
		distance := town.DistanceFromSquared(x, y)
		if closest == nil || distance < minDistance {
			closest = town
			minDistance = distance
		}
	}

	return closest
}

func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) {
	minExp := (monster.MaxExp * 5) / 6
	maxExp := monster.MaxExp
	exp := rand.Intn(maxExp-minExp+1) + minExp

	minGold := (monster.MaxGold * 5) / 6
	maxGold := monster.MaxGold
	gold := rand.Intn(maxGold-minGold+1) + minGold

	expBonus := (user.ExpBonus * exp) / 100
	exp += expBonus

	goldBonus := (user.GoldBonus * gold) / 100
	gold += goldBonus

	return gold, exp
}

func ExecuteFightAction(fightID int, result *FightResult) error {
	return database.Transaction(func() error {
		// Update fight
		if len(result.FightUpdates) > 0 {
			if err := database.Update("fights", result.FightUpdates, "id", fightID); err != nil {
				return err
			}
		}

		// Update user
		if len(result.UserUpdates) > 0 {
			fight, err := fights.Find(fightID)
			if err != nil {
				return err
			}
			if err := database.Update("users", result.UserUpdates, "id", fight.UserID); err != nil {
				return err
			}
		}

		// Add log entry
		if result.LogAction != nil {
			return result.LogAction()
		}

		return nil
	})
}
