package game

import (
	"context"
	"errors"
	"log/slog"
	"time"

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

type GearStore interface {
	CreateItem(ctx context.Context, item *model.GearItem) error
	DeleteGearItem(ctx context.Context, itemID int64) error
	AddToInventory(ctx context.Context, heroID int64, itemID int64) error
	EquipItem(ctx context.Context, heroID int64, slot model.EquipmentSlot, itemID int64) error
}

type QuestProgressor interface {
	IncrementQuestProgress(ctx context.Context, heroID int64, questType string, enemySlug, enemyArchetype string, amount int) error
	IncrementCollectItemProgress(ctx context.Context, heroID int64, enemySlug, enemyArchetype string) error
}

type AchievementChecker interface {
	CheckAndUnlock(ctx context.Context, hero *model.Hero) ([]model.Achievement, error)
}

type TaskProgressor interface {
	EnsureHeroTasks(ctx context.Context, heroID int64, now time.Time) error
	IncrementTaskProgress(ctx context.Context, heroID int64, taskType string, amount int) error
}

// VictoryRewardDeps provides optional services for applying rewards.
type VictoryRewardDeps struct {
	GearStore        GearStore
	QuestProgressor  QuestProgressor
	AchievementCheck AchievementChecker
	TaskProgressor   TaskProgressor
	LogWriter        func(heroID int64, line model.AdventureLogLine)
	LootRecorder     func(entry model.LootHistory)
	InTown           func(ctx context.Context, posX, posY float64) bool
	Logger           *slog.Logger
}

// ApplyVictoryRewards is the single source of truth for post-kill rewards.
// It awards XP, generates loot (gold/equipment per tuning + luck), processes equipment drops,
// runs the level-up loop, updates stats, and triggers optional meta-progress hooks.
func ApplyVictoryRewards(hero *model.Hero, enemy *model.Enemy, now time.Time, deps VictoryRewardDeps) []model.LootDrop {
	if hero == nil || enemy == nil {
		return nil
	}
	oldLevel := hero.Level
	hero.XP += enemy.XPReward
	levelsGained := 0
	for hero.LevelUp() {
		levelsGained++
	}
	hero.State = model.StateWalking

	luckMult := LuckMultiplier(hero, now)
	drops := model.GenerateLoot(enemy.Slug, luckMult)

	inTown := false
	if deps.InTown != nil {
		ctxTown, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		inTown = deps.InTown(ctxTown, hero.PositionX, hero.PositionY)
		cancel()
	}

	var goldGained int64
	for i := range drops {
		drop := &drops[i]

		switch drop.ItemType {
		case "gold":
			hero.Gold += drop.GoldAmount
			goldGained += drop.GoldAmount

		case "potion":
			hero.Potions++

		default:
			slot := model.EquipmentSlot(drop.ItemType)
			family := model.PickGearFamily(slot)
			if family != nil {
				ilvl := model.RollIlvl(enemy.MinLevel, enemy.IsElite)
				item := model.NewGearItem(family, ilvl, drop.Rarity)

				if deps.GearStore != nil {
					ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
					if err := deps.GearStore.CreateItem(ctx, item); err != nil {
						cancel()
						if deps.Logger != nil {
							deps.Logger.Warn("failed to create gear item", "slot", slot, "error", err)
						}
						if inTown {
							sellPrice := model.AutoSellPrice(drop.Rarity)
							hero.Gold += sellPrice
							goldGained += sellPrice
							drop.GoldAmount = sellPrice
						} else {
							drop.GoldAmount = 0
						}
						goto recordLoot
					}
					cancel()
				}

				drop.ItemID = item.ID
				drop.ItemName = item.Name

				hero.EnsureGearMap()
				prev := hero.Gear[item.Slot]
				if TryAutoEquipInMemory(hero, item, now) {
					if deps.GearStore != nil && item.ID != 0 {
						ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
						err := deps.GearStore.EquipItem(ctx, hero.ID, item.Slot, item.ID)
						cancel()
						if err != nil {
							if prev == nil {
								delete(hero.Gear, item.Slot)
							} else {
								hero.Gear[item.Slot] = prev
							}
							hero.RefreshDerivedCombatStats(now)
							if deps.Logger != nil {
								if errors.Is(err, storage.ErrInventoryFull) {
									deps.Logger.Warn("persist gear equip skipped: inventory full (free a slot to swap)",
										"hero_id", hero.ID, "slot", item.Slot)
								} else {
									deps.Logger.Warn("failed to persist gear equip",
										"hero_id", hero.ID, "slot", item.Slot, "error", err)
								}
							}
							goto recordLoot
						}
					}
					if deps.LogWriter != nil {
						deps.LogWriter(hero.ID, model.AdventureLogLine{
							Event: &model.AdventureLogEvent{
								Code: model.LogPhraseEquippedNew,
								Args: map[string]any{
									"slot": string(slot), "itemId": item.ID, "rarity": string(item.Rarity), "formId": item.FormID,
								},
							},
						})
					}
					if prev != nil && prev.ID != item.ID {
						hero.EnsureInventorySlice()
						hero.Inventory = append(hero.Inventory, prev)
					}
					goto recordLoot
				}

				hero.EnsureInventorySlice()
				if len(hero.Inventory) >= model.MaxInventorySlots {
					if deps.GearStore != nil && item.ID != 0 {
						ctxDel, cancelDel := context.WithTimeout(context.Background(), 2*time.Second)
						if err := deps.GearStore.DeleteGearItem(ctxDel, item.ID); err != nil && deps.Logger != nil {
							deps.Logger.Warn("failed to delete gear (inventory full)", "gear_id", item.ID, "error", err)
						}
						cancelDel()
					}
					drop.ItemID = 0
					drop.ItemName = ""
					drop.GoldAmount = 0
					if deps.LogWriter != nil {
						deps.LogWriter(hero.ID, model.AdventureLogLine{
							Event: &model.AdventureLogEvent{
								Code: model.LogPhraseInventoryFullDropped,
								Args: map[string]any{
									"itemId": item.ID, "slot": string(item.Slot), "rarity": string(item.Rarity), "formId": item.FormID,
								},
							},
						})
					}
				} else {
					if deps.GearStore != nil {
						ctxInv, cancelInv := context.WithTimeout(context.Background(), 2*time.Second)
						err := deps.GearStore.AddToInventory(ctxInv, hero.ID, item.ID)
						cancelInv()
						if err != nil {
							if deps.Logger != nil {
								deps.Logger.Warn("failed to stash gear", "hero_id", hero.ID, "gear_id", item.ID, "error", err)
							}
							ctxDel, cancelDel := context.WithTimeout(context.Background(), 2*time.Second)
							if deps.GearStore != nil && item.ID != 0 {
								_ = deps.GearStore.DeleteGearItem(ctxDel, item.ID)
							}
							cancelDel()
							drop.ItemID = 0
							drop.ItemName = ""
							drop.GoldAmount = 0
						} else {
							hero.Inventory = append(hero.Inventory, item)
							drop.GoldAmount = 0
						}
					} else {
						hero.Inventory = append(hero.Inventory, item)
						drop.GoldAmount = 0
					}
				}
			} else if inTown {
				sellPrice := model.AutoSellPrice(drop.Rarity)
				hero.Gold += sellPrice
				goldGained += sellPrice
				drop.GoldAmount = sellPrice
			}
		}

	recordLoot:
		if deps.LootRecorder != nil {
			entry := model.LootHistory{
				HeroID:     hero.ID,
				EnemyType:  enemy.Slug,
				ItemType:   drop.ItemType,
				ItemID:     drop.ItemID,
				Rarity:     drop.Rarity,
				GoldAmount: drop.GoldAmount,
				CreatedAt:  now,
			}
			deps.LootRecorder(entry)
		}
	}

	if deps.LogWriter != nil {
		deps.LogWriter(hero.ID, model.AdventureLogLine{
			Event: &model.AdventureLogEvent{
				Code: model.LogPhraseDefeatedEnemy,
				Args: map[string]any{
					"enemyType": enemy.Slug,
					"xp":        enemy.XPReward, "gold": goldGained,
				},
			},
		})
		for l := oldLevel + 1; l <= oldLevel+levelsGained; l++ {
			deps.LogWriter(hero.ID, model.AdventureLogLine{
				Event: &model.AdventureLogEvent{
					Code: model.LogPhraseLeveledUp,
					Args: map[string]any{"level": l},
				},
			})
		}
	}

	hero.TotalKills++
	hero.KillsSinceDeath++
	if enemy.IsElite {
		hero.EliteKills++
	}
	for _, drop := range drops {
		if drop.Rarity == model.RarityLegendary && drop.ItemType != "gold" {
			hero.LegendaryDrops++
		}
	}

	if deps.QuestProgressor != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		if err := deps.QuestProgressor.IncrementQuestProgress(ctx, hero.ID, "kill_count", enemy.Slug, enemy.Archetype, 1); err != nil && deps.Logger != nil {
			deps.Logger.Warn("quest kill_count progress failed", "hero_id", hero.ID, "error", err)
		}
		if err := deps.QuestProgressor.IncrementCollectItemProgress(ctx, hero.ID, enemy.Slug, enemy.Archetype); err != nil && deps.Logger != nil {
			deps.Logger.Warn("quest collect_item progress failed", "hero_id", hero.ID, "error", err)
		}
		cancel()
	}

	if deps.AchievementCheck != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		newlyUnlocked, err := deps.AchievementCheck.CheckAndUnlock(ctx, hero)
		cancel()
		if err != nil {
			if deps.Logger != nil {
				deps.Logger.Warn("achievement check failed", "hero_id", hero.ID, "error", err)
			}
		} else if deps.LogWriter != nil {
			for _, a := range newlyUnlocked {
				switch a.RewardType {
				case "gold":
					hero.Gold += int64(a.RewardAmount)
				case "potion":
					hero.Potions += a.RewardAmount
				}
				deps.LogWriter(hero.ID, model.AdventureLogLine{
					Event: &model.AdventureLogEvent{
						Code: model.LogPhraseAchievementUnlocked,
						Args: map[string]any{
							"achievementId": a.ID,
							"rewardAmount":  a.RewardAmount,
							"rewardType":    a.RewardType,
						},
					},
				})
			}
		}
	}

	if deps.TaskProgressor != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		if err := deps.TaskProgressor.EnsureHeroTasks(ctx, hero.ID, time.Now()); err != nil {
			if deps.Logger != nil {
				deps.Logger.Warn("task ensure failed", "hero_id", hero.ID, "error", err)
			}
			cancel()
			return drops
		}
		if err := deps.TaskProgressor.IncrementTaskProgress(ctx, hero.ID, "kill_count", 1); err != nil && deps.Logger != nil {
			deps.Logger.Warn("task kill_count progress failed", "hero_id", hero.ID, "error", err)
		}
		if enemy.IsElite {
			if err := deps.TaskProgressor.IncrementTaskProgress(ctx, hero.ID, "elite_kill", 1); err != nil && deps.Logger != nil {
				deps.Logger.Warn("task elite_kill progress failed", "hero_id", hero.ID, "error", err)
			}
		}
		if goldGained > 0 {
			if err := deps.TaskProgressor.IncrementTaskProgress(ctx, hero.ID, "collect_gold", int(goldGained)); err != nil && deps.Logger != nil {
				deps.Logger.Warn("task collect_gold progress failed", "hero_id", hero.ID, "error", err)
			}
		}
		cancel()
	}

	return drops
}
