package game

import (
	"fmt"
	"math"
	"math/rand"
	"sort"
	"time"

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

// DefaultProgressionBandTargets are wall-clock budgets per plan: sum of time for
// level-ups L→L+1 within each band (1→10, 10→20, …, 40→50).
var DefaultProgressionBandTargets = [5]time.Duration{
	1 * 7 * 24 * time.Hour,
	3 * 7 * 24 * time.Hour,
	6 * 7 * 24 * time.Hour,
	10 * 7 * 24 * time.Hour,
	20 * 7 * 24 * time.Hour,
}

// Hero level L means the step L→L+1 is in the band that contains L (1→10 = L 1..9, …).
var progressionBandLevelStart = [...]int{1, 10, 20, 30, 40}
var progressionBandLevelEnd = [...]int{9, 19, 29, 39, 49}

// SimulatedLevelCountInBand returns how many level-up steps in band bandIdx are included
// when simulating hero levels 1..maxHeroLevel (inclusive upper step).
func SimulatedLevelCountInBand(bandIdx int, maxHeroLevel int) int {
	if bandIdx < 0 || bandIdx > 4 {
		return 0
	}
	lo := progressionBandLevelStart[bandIdx]
	hi := progressionBandLevelEnd[bandIdx]
	if maxHeroLevel < lo {
		return 0
	}
	upper := hi
	if maxHeroLevel < upper {
		upper = maxHeroLevel
	}
	return upper - lo + 1
}

func fullLevelCountInBand(bandIdx int) int {
	return progressionBandLevelEnd[bandIdx] - progressionBandLevelStart[bandIdx] + 1
}

// ProratedBandTargets scales each band target by the fraction of level steps simulated
// when maxHeroLevel is below 49 (partial run, e.g. -max-level 29).
func ProratedBandTargets(maxHeroLevel int, full [5]time.Duration) [5]time.Duration {
	var out [5]time.Duration
	for i := 0; i < 5; i++ {
		c := SimulatedLevelCountInBand(i, maxHeroLevel)
		if c <= 0 {
			continue
		}
		denom := fullLevelCountInBand(i)
		out[i] = time.Duration(float64(full[i]) * float64(c) / float64(denom))
	}
	return out
}

// SumBandTargets sums per-band targets (e.g. prorated).
func SumBandTargets(t [5]time.Duration) time.Duration {
	var s time.Duration
	for _, d := range t {
		s += d
	}
	return s
}

// ProgressionSimParams configures long-run XP / time estimation.
type ProgressionSimParams struct {
	// IterationsPerLevel is Monte Carlo samples per hero level (1..49).
	IterationsPerLevel int
	// Seed drives RNG for enemy pick and combat rolls (via rand.Seed per iteration).
	Seed int64
	// RestAfterCombat is added to each fight duration (post-battle downtime).
	RestAfterCombat time.Duration
	// Gear is ReferenceGearMedian or ReferenceGearRolled.
	Gear ReferenceGearProfile
	// AccountLosses: if true, XP rate = sum(xp)/sum(cycle) over all fights; if false, wins-only ratio.
	AccountLosses bool
	// MinHeroLevel and MaxHeroLevelInclusive bound the simulated level-ups (default 1 and 49).
	MinHeroLevel, MaxHeroLevelInclusive int
}

func (p ProgressionSimParams) normalized() ProgressionSimParams {
	out := p
	if out.IterationsPerLevel < 1 {
		out.IterationsPerLevel = 80
	}
	if out.RestAfterCombat < 0 {
		out.RestAfterCombat = 0
	}
	if out.MinHeroLevel < 1 {
		out.MinHeroLevel = 1
	}
	if out.MaxHeroLevelInclusive < out.MinHeroLevel {
		out.MaxHeroLevelInclusive = 49
	}
	if out.MaxHeroLevelInclusive > 49 {
		out.MaxHeroLevelInclusive = 49
	}
	return out
}

// ProgressionBandResult holds simulated time sums and diagnostics.
type ProgressionBandResult struct {
	BandDurations [5]time.Duration
	Total         time.Duration
	// TotalSec is the sum of per-level times in seconds (may be +Inf if some levels never award XP).
	TotalSec float64
	// Per-level seconds (hero level L → L+1), index L-1.
	LevelUpSec []float64
	WinRates   []float64 // per hero level, fraction of wins in MC iterations
}

// EnemyTemplatesFromSlice indexes DB rows by Slug for balance tooling.
func EnemyTemplatesFromSlice(templates []model.Enemy) map[string]model.Enemy {
	m := make(map[string]model.Enemy, len(templates))
	for _, e := range templates {
		if e.Slug != "" {
			m[e.Slug] = e
		}
	}
	return m
}

// EnemySliceFromMap converts a slug-keyed map to a slice for SetEnemyTemplates.
func EnemySliceFromMap(m map[string]model.Enemy) []model.Enemy {
	out := make([]model.Enemy, 0, len(m))
	for _, e := range m {
		out = append(out, e)
	}
	return out
}

// CloneEnemyTemplates returns a shallow copy of the map with copied Enemy values (keys = slug).
func CloneEnemyTemplates(src map[string]model.Enemy) map[string]model.Enemy {
	if src == nil {
		return nil
	}
	out := make(map[string]model.Enemy, len(src))
	for k, v := range src {
		cp := v
		if v.SpecialAbilities != nil {
			cp.SpecialAbilities = append([]model.SpecialAbility(nil), v.SpecialAbilities...)
		}
		out[k] = cp
	}
	return out
}

// TemplateProgressionBand maps an enemy template to a band index 0..4 using the
// midpoint of [min_level..max_level] (or base level) for content-tier grouping.
func TemplateProgressionBand(t model.Enemy) int {
	mid := t.BaseLevel
	if t.MinLevel > 0 && t.MaxLevel >= t.MinLevel {
		mid = (t.MinLevel + t.MaxLevel) / 2
	}
	if mid <= 10 {
		return 0
	}
	if mid <= 20 {
		return 1
	}
	if mid <= 30 {
		return 2
	}
	if mid <= 40 {
		return 3
	}
	return 4
}

// XPRewardScaleSpec defines multipliers applied to template.XPReward (before instance scaling).
type XPRewardScaleSpec struct {
	Global float64
	// Elite multiplies xp_reward on templates with IsElite (in addition to Global).
	Elite float64
	// PerType multipliers by enemy slug; missing keys default to 1.
	PerType map[string]float64
	// PerBand scales template by TemplateProgressionBand; length 5, values default to 1.
	PerBand [5]float64
}

func (s XPRewardScaleSpec) effectiveType(slug string) float64 {
	if s.PerType != nil {
		if v, ok := s.PerType[slug]; ok && v > 0 {
			return v
		}
	}
	return 1
}

func (s XPRewardScaleSpec) effectiveBand(band int) float64 {
	if band < 0 || band > 4 {
		return 1
	}
	v := s.PerBand[band]
	if v <= 0 {
		return 1
	}
	return v
}

func (s XPRewardScaleSpec) eliteMul(e model.Enemy) float64 {
	m := s.Elite
	if m <= 0 {
		m = 1
	}
	if e.IsElite {
		return m
	}
	return 1
}

// ApplyXPRewardScaleSpec returns cloned templates with scaled XPReward (rounded, min 1).
func ApplyXPRewardScaleSpec(base map[string]model.Enemy, spec XPRewardScaleSpec) map[string]model.Enemy {
	out := CloneEnemyTemplates(base)
	g := spec.Global
	if g <= 0 {
		g = 1
	}
	for slug, e := range out {
		band := TemplateProgressionBand(e)
		mult := g * spec.effectiveType(slug) * spec.effectiveBand(band) * spec.eliteMul(e)
		x := float64(e.XPReward) * mult
		if x < 1 {
			x = 1
		}
		e.XPReward = int64(math.Round(x))
		out[slug] = e
	}
	return out
}

// WithEnemyTemplates sets global enemy templates for the duration of fn, then restores.
func WithEnemyTemplates(templates map[string]model.Enemy, fn func()) {
	prev := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
	model.SetEnemyTemplates(EnemySliceFromMap(templates))
	defer func() {
		model.SetEnemyTemplates(EnemySliceFromMap(prev))
	}()
	fn()
}

// SimulateProgressionBands runs Monte Carlo time estimates per level-up and sums bands.
func SimulateProgressionBands(params ProgressionSimParams, templates map[string]model.Enemy) (ProgressionBandResult, error) {
	p := params.normalized()
	if len(templates) == 0 {
		return ProgressionBandResult{}, fmt.Errorf("empty enemy templates")
	}

	nLevels := p.MaxHeroLevelInclusive - p.MinHeroLevel + 1
	levelUpSec := make([]float64, nLevels)
	winRates := make([]float64, nLevels)

	// Accumulate seconds in float64 so +Inf from zero-win levels does not cast to 0 Duration.
	var bandSec [5]float64

	WithEnemyTemplates(templates, func() {
		for idx, L := range levelRange(p.MinHeroLevel, p.MaxHeroLevelInclusive) {
			sec, wr := estimateLevelUpSeconds(L, p)
			levelUpSec[idx] = sec
			winRates[idx] = wr
			bi := bandIndexForHeroLevel(L)
			if bi >= 0 {
				bandSec[bi] += sec
			}
		}
	})

	var bandAcc [5]time.Duration
	for i := range bandSec {
		bandAcc[i] = secondsToDuration(bandSec[i])
	}
	var totalSec float64
	for _, s := range bandSec {
		totalSec += s
	}
	total := secondsToDuration(totalSec)
	return ProgressionBandResult{
		BandDurations: bandAcc,
		Total:         total,
		TotalSec:      totalSec,
		LevelUpSec:    levelUpSec,
		WinRates:      winRates,
	}, nil
}

func levelRange(minL, maxL int) []int {
	out := make([]int, 0, maxL-minL+1)
	for L := minL; L <= maxL; L++ {
		out = append(out, L)
	}
	return out
}

// bandIndexForHeroLevel returns which progression band hero level L belongs to when leveling L→L+1.
// secondsToDuration converts simulated seconds to a Duration. +Inf maps to max duration
// (impossible-to-finish level); NaN maps to 0.
func secondsToDuration(sec float64) time.Duration {
	if math.IsNaN(sec) {
		return 0
	}
	if math.IsInf(sec, 1) {
		return time.Duration(1<<63 - 1)
	}
	if math.IsInf(sec, -1) {
		return 0
	}
	maxF := float64((1<<63 - 1) / int64(time.Second))
	if sec >= maxF {
		return time.Duration(1<<63 - 1)
	}
	if sec <= 0 {
		return 0
	}
	return time.Duration(sec * float64(time.Second))
}

func bandIndexForHeroLevel(L int) int {
	switch {
	case L >= 1 && L <= 9:
		return 0
	case L >= 10 && L <= 19:
		return 1
	case L >= 20 && L <= 29:
		return 2
	case L >= 30 && L <= 39:
		return 3
	case L >= 40 && L <= 49:
		return 4
	default:
		return -1
	}
}

func estimateLevelUpSeconds(heroLevel int, p ProgressionSimParams) (seconds float64, winRate float64) {
	xpNeed := float64(model.XPToNextLevel(heroLevel))
	if xpNeed <= 0 {
		return 0, 1
	}

	n := p.IterationsPerLevel
	var sumCycle float64
	var sumXP float64
	var sumCycleWin float64
	var sumXPWin float64
	wins := 0

	for i := 0; i < n; i++ {
		seed := p.Seed + int64(heroLevel)*1_000_003 + int64(i)*97_981
		rand.Seed(seed)

		var gearRng *rand.Rand
		if p.Gear == ReferenceGearRolled {
			gearRng = rand.New(rand.NewSource(seed + 42))
		}
		baseHero := NewReferenceHeroForBalance(heroLevel, p.Gear, gearRng)
		hero := CloneHeroForCombatSim(baseHero)

		pickRNG := rand.New(rand.NewSource(seed + 11_111))
		enemy := PickEnemyForLevelWithRNG(heroLevel, pickRNG, hero)

		survived, elapsed := ResolveCombatToEndWithDuration(hero, &enemy, CombatSimDeterministicStart, CombatSimOptions{
			TickRate: 100 * time.Millisecond,
			MaxSteps: CombatSimMaxStepsLong,
		})
		cycle := elapsed.Seconds() + p.RestAfterCombat.Seconds()
		xp := float64(enemy.XPReward)
		if !survived {
			xp = 0
		} else {
			wins++
		}

		sumCycle += cycle
		sumXP += xp
		if survived {
			sumCycleWin += cycle
			sumXPWin += float64(enemy.XPReward)
		}
	}

	winRate = float64(wins) / float64(n)

	var xpPerSec float64
	if p.AccountLosses {
		if sumCycle > 0 {
			xpPerSec = sumXP / sumCycle
		}
	} else {
		if sumCycleWin > 0 && sumXPWin > 0 {
			xpPerSec = sumXPWin / sumCycleWin
		}
	}

	if xpPerSec <= 0 {
		return math.Inf(1), winRate
	}
	return xpNeed / xpPerSec, winRate
}

// BandErrors returns relative errors (sim/target - 1) per band; targets must be > 0.
func BandErrors(sim, targets [5]time.Duration) [5]float64 {
	var e [5]float64
	for i := range sim {
		if targets[i] <= 0 {
			e[i] = 0
			continue
		}
		e[i] = float64(sim[i])/float64(targets[i]) - 1
	}
	return e
}

// SquaredErrorSum returns sum of squared relative band errors.
func SquaredErrorSum(sim, targets [5]time.Duration) float64 {
	var s float64
	for i := range sim {
		if targets[i] <= 0 {
			continue
		}
		r := float64(sim[i])/float64(targets[i]) - 1
		s += r * r
	}
	return s
}

// EnemyLevelTierMid is a single sort key from catalog level band (higher = later content).
// Uses midpoint of [min_level..max_level] when set; otherwise base_level.
func EnemyLevelTierMid(e model.Enemy) int {
	if e.MinLevel > 0 && e.MaxLevel >= e.MinLevel {
		return (e.MinLevel + e.MaxLevel) / 2
	}
	bl := e.BaseLevel
	if bl <= 0 {
		bl = 1
	}
	return bl
}

// SortEnemyTypesByLevelTier sorts by ascending EnemyLevelTierMid, then slug.
func SortEnemyTypesByLevelTier(m map[string]model.Enemy) []string {
	types := make([]string, 0, len(m))
	for t := range m {
		types = append(types, t)
	}
	sort.Slice(types, func(i, j int) bool {
		mi := EnemyLevelTierMid(m[types[i]])
		mj := EnemyLevelTierMid(m[types[j]])
		if mi != mj {
			return mi < mj
		}
		return types[i] < types[j]
	})
	return types
}

// EnforceMonotonicXPRewardByTier ensures non-decreasing xp_reward with level tier; when tier
// strictly increases, xp_reward must strictly increase (>= previous + 1).
func EnforceMonotonicXPRewardByTier(templates map[string]model.Enemy) map[string]model.Enemy {
	out := CloneEnemyTemplates(templates)
	order := SortEnemyTypesByLevelTier(out)
	prevMid := -1
	var prevXP int64
	first := true
	for _, typ := range order {
		e := out[typ]
		mid := EnemyLevelTierMid(e)
		x := e.XPReward
		if x < 1 {
			x = 1
		}
		if !first {
			if mid > prevMid {
				if x <= prevXP {
					x = prevXP + 1
				}
			} else {
				if x < prevXP {
					x = prevXP
				}
			}
		}
		e.XPReward = x
		out[typ] = e
		prevXP = x
		prevMid = mid
		first = false
	}
	return out
}

// MaxRelativeErrorVsTargets returns max |sim/target-1| over bands with target>0, plus total error entry.
func MaxRelativeErrorVsTargets(sim [5]time.Duration, targets [5]time.Duration, totalSim, totalTarget time.Duration) float64 {
	maxE := 0.0
	for i := range sim {
		if targets[i] <= 0 {
			continue
		}
		e := math.Abs(float64(sim[i])/float64(targets[i]) - 1)
		if e > maxE {
			maxE = e
		}
	}
	if totalTarget > 0 {
		e := math.Abs(float64(totalSim)/float64(totalTarget) - 1)
		if e > maxE {
			maxE = e
		}
	}
	return maxE
}

func clonePerTypeMap(m map[string]float64) map[string]float64 {
	out := make(map[string]float64, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}

// OptimizePerTypeScales adjusts each enemy row's xp_reward via PerType multipliers (Global fixed at 1).
// Elite templates still use elite multiplier from spec. Resulting XPReward are integers (rounded).
// If enforceMonotonic is true, xp_reward is non-decreasing with EnemyLevelTierMid (strictly up when tier rises).
func OptimizePerTypeScales(
	base map[string]model.Enemy,
	params ProgressionSimParams,
	targets [5]time.Duration,
	elite float64,
	maxIters int,
	enforceMonotonic bool,
) (map[string]float64, map[string]model.Enemy, ProgressionBandResult, float64) {
	if maxIters < 1 {
		maxIters = 120
	}
	types := SortEnemyTypesByLevelTier(base)
	perType := make(map[string]float64, len(types))
	for _, t := range types {
		perType[t] = 1
	}

	applyAndSim := func(spec XPRewardScaleSpec) (ProgressionBandResult, error) {
		tmpl := ApplyXPRewardScaleSpec(base, spec)
		if enforceMonotonic {
			tmpl = EnforceMonotonicXPRewardByTier(tmpl)
		}
		return SimulateProgressionBands(params, tmpl)
	}

	bestSpec := XPRewardScaleSpec{Global: 1, Elite: elite, PerType: perType, PerBand: [5]float64{1, 1, 1, 1, 1}}
	res, _ := applyAndSim(bestSpec)
	bestErr := SquaredErrorSum(res.BandDurations, targets)

	// Wide factors so integer xp_reward can move (DB often has 1–3); include coarse steps to approach targets.
	factors := []float64{8, 4, 2, 1.5, 1.25, 1.1, 1.05, 1.02, 1.01, 0.99, 0.98, 0.95, 0.9, 0.75, 0.5, 0.25}
	for iter := 0; iter < maxIters; iter++ {
		improved := false
		for _, typ := range types {
			for _, f := range factors {
				cand := clonePerTypeMap(perType)
				next := cand[typ] * f
				if next < 0.05 || next > 200 {
					continue
				}
				cand[typ] = next
				spec := XPRewardScaleSpec{Global: 1, Elite: elite, PerType: cand, PerBand: [5]float64{1, 1, 1, 1, 1}}
				r, err := applyAndSim(spec)
				if err != nil {
					continue
				}
				errVal := SquaredErrorSum(r.BandDurations, targets)
				if errVal < bestErr {
					bestErr = errVal
					perType = cand
					res = r
					improved = true
				}
			}
		}
		if !improved {
			break
		}
	}

	finalSpec := XPRewardScaleSpec{Global: 1, Elite: elite, PerType: perType, PerBand: [5]float64{1, 1, 1, 1, 1}}
	scaled := ApplyXPRewardScaleSpec(base, finalSpec)
	if enforceMonotonic {
		scaled = EnforceMonotonicXPRewardByTier(scaled)
	}
	res, _ = SimulateProgressionBands(params, scaled)
	return perType, scaled, res, SquaredErrorSum(res.BandDurations, targets)
}

// OptimizeBandScales searches PerBand multipliers to minimize squared relative error vs targets.
// global and elite are fixed; only PerBand[5] is optimized (coordinate descent).
func OptimizeBandScales(
	base map[string]model.Enemy,
	params ProgressionSimParams,
	targets [5]time.Duration,
	global, elite float64,
	maxIters int,
) ([5]float64, ProgressionBandResult, float64) {
	if maxIters < 1 {
		maxIters = 120
	}

	scales := [5]float64{1, 1, 1, 1, 1}
	bestSpec := XPRewardScaleSpec{Global: global, Elite: elite, PerBand: scales}
	templates := ApplyXPRewardScaleSpec(base, bestSpec)
	res, _ := SimulateProgressionBands(params, templates)
	bestErr := SquaredErrorSum(res.BandDurations, targets)

	step := 0.08
	for iter := 0; iter < maxIters; iter++ {
		improved := false
		for g := 0; g < 5; g++ {
			for _, mult := range []float64{1 + step, 1 - step, 1 + step/2, 1 - step/2} {
				if mult <= 0.05 {
					continue
				}
				cand := scales
				cand[g] *= mult
				if cand[g] < 0.05 || cand[g] > 200 {
					continue
				}
				spec := XPRewardScaleSpec{Global: global, Elite: elite, PerBand: cand}
				tmpl := ApplyXPRewardScaleSpec(base, spec)
				r, err := SimulateProgressionBands(params, tmpl)
				if err != nil {
					continue
				}
				errVal := SquaredErrorSum(r.BandDurations, targets)
				if errVal < bestErr {
					bestErr = errVal
					scales = cand
					res = r
					improved = true
				}
			}
		}
		if !improved {
			step *= 0.5
			if step < 0.005 {
				break
			}
		}
	}

	finalSpec := XPRewardScaleSpec{Global: global, Elite: elite, PerBand: scales}
	templates = ApplyXPRewardScaleSpec(base, finalSpec)
	res, _ = SimulateProgressionBands(params, templates)
	return scales, res, SquaredErrorSum(res.BandDurations, targets)
}
