package main

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

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

// enemyTemplateHasPeriodicDoT is true if the template applies poison/burn DoT in combat
// (longer fights stack more periodic damage — conflicts with naive hp×atk grid balance).
func enemyTemplateHasPeriodicDoT(t model.Enemy) bool {
	for _, a := range t.SpecialAbilities {
		switch a {
		case model.AbilityBurn, model.AbilityPoison:
			return true
		}
	}
	return false
}

// gridScenario: fair fight heroLv == enemyLv at each tier, × gear variants (median + rolled).
type gridScenario struct {
	heroLv  int
	enemyLv int
	gearIdx int
}

func hashGridScenario(et string, sc gridScenario) uint64 {
	h := fnv.New64a()
	_, _ = h.Write([]byte(fmt.Sprintf("%s|%d|%d|%d", et, sc.heroLv, sc.enemyLv, sc.gearIdx)))
	return h.Sum64()
}

// gridScenariosForTemplate builds heroLv == enemyLv for each level in [MinLevel..MaxLevel] × gearMods.
func gridScenariosForTemplate(t model.Enemy, gearMods int) []gridScenario {
	if gearMods < 2 {
		gearMods = 4
	}
	minL := t.MinLevel
	maxL := t.MaxLevel
	if minL <= 0 || maxL < minL {
		lvl := (t.MinLevel + t.MaxLevel) / 2
		if lvl < 1 {
			lvl = 1
		}
		if t.BaseLevel > 0 {
			lvl = t.BaseLevel
		}
		out := make([]gridScenario, 0, gearMods)
		for g := 0; g < gearMods; g++ {
			out = append(out, gridScenario{heroLv: lvl, enemyLv: lvl, gearIdx: g})
		}
		return out
	}
	out := make([]gridScenario, 0, (maxL-minL+1)*gearMods)
	for lv := minL; lv <= maxL; lv++ {
		for g := 0; g < gearMods; g++ {
			out = append(out, gridScenario{heroLv: lv, enemyLv: lv, gearIdx: g})
		}
	}
	return out
}

func buildBaseHeroGrid(sc gridScenario) *model.Hero {
	if sc.gearIdx == 0 {
		return game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearMedian, nil)
	}
	seed := int64(500_000 + sc.heroLv*10_000 + sc.enemyLv*100 + sc.gearIdx*17)
	rng := rand.New(rand.NewSource(seed))
	return game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearRolled, rng)
}

type gridScenResult struct {
	medianWinSec float64
	medianHeroHp float64
	winRate      float64
}

func runOneGridScenario(tmpl model.Enemy, base *model.Hero, enemyLv int, n int, seedBase int64, h uint64) gridScenResult {
	var winDur []time.Duration
	var winHpPct []float64
	wins := 0
	for i := 0; i < n; i++ {
		rand.Seed(seedBase + int64(i)*1_000_003 + int64(h))
		hero := game.CloneHeroForCombatSim(base)
		if hero.HP <= 0 {
			hero.HP = hero.MaxHP
		}
		maxH := hero.MaxHP
		if maxH <= 0 {
			maxH = 1
		}
		e := game.BuildEnemyInstanceForLevel(tmpl, enemyLv)
		survived, elapsed := game.ResolveCombatToEndWithDuration(hero, &e, game.CombatSimDeterministicStart, game.CombatSimOptions{
			TickRate: 100 * time.Millisecond,
			MaxSteps: game.CombatSimMaxStepsLong,
		})
		if survived {
			wins++
			winDur = append(winDur, elapsed)
			winHpPct = append(winHpPct, float64(hero.HP)/float64(maxH))
		}
	}
	wr := float64(wins) / float64(n)
	if len(winDur) == 0 {
		return gridScenResult{winRate: wr}
	}
	sort.Slice(winDur, func(i, j int) bool { return winDur[i] < winDur[j] })
	sort.Float64s(winHpPct)
	med := winDur[len(winDur)/2]
	medHp := winHpPct[len(winHpPct)/2]
	return gridScenResult{
		medianWinSec: med.Seconds(),
		medianHeroHp: medHp,
		winRate:      wr,
	}
}

type gridAggResult struct {
	medOfMedDur float64
	medOfMedHp  float64
	minWinRate  float64
	medWinRate  float64
	minMedDur   float64
	maxMedDur   float64
	minMedHp    float64
	maxMedHp    float64
}

func medianFloat(xs []float64) float64 {
	if len(xs) == 0 {
		return 0
	}
	sort.Float64s(xs)
	return xs[len(xs)/2]
}

func aggregateGrid(tmpl model.Enemy, et string, scenarios []gridScenario, hpScale, atkScale float64, n int, seedBase int64) gridAggResult {
	t := scaleEnemyForSim(tmpl, hpScale, atkScale)
	var medDurs []float64
	var medHps []float64
	var winRates []float64
	minWR := 1.0
	minDur := 1e12
	maxDur := 0.0
	minHp := 1.0
	maxHp := 0.0
	for _, sc := range scenarios {
		base := buildBaseHeroGrid(sc)
		h := hashGridScenario(et, sc)
		r := runOneGridScenario(t, base, sc.enemyLv, n, seedBase, h)
		winRates = append(winRates, r.winRate)
		if r.winRate < minWR {
			minWR = r.winRate
		}
		if r.medianWinSec > 0 {
			medDurs = append(medDurs, r.medianWinSec)
			medHps = append(medHps, r.medianHeroHp)
			if r.medianWinSec < minDur {
				minDur = r.medianWinSec
			}
			if r.medianWinSec > maxDur {
				maxDur = r.medianWinSec
			}
			if r.medianHeroHp < minHp {
				minHp = r.medianHeroHp
			}
			if r.medianHeroHp > maxHp {
				maxHp = r.medianHeroHp
			}
		}
	}
	return gridAggResult{
		medOfMedDur: medianFloat(medDurs),
		medOfMedHp:  medianFloat(medHps),
		minWinRate:  minWR,
		medWinRate:  medianFloat(winRates),
		minMedDur:   minDur,
		maxMedDur:   maxDur,
		minMedHp:    minHp,
		maxMedHp:    maxHp,
	}
}

func findHPScaleForAggDurationGrid(tmpl model.Enemy, et string, scenarios []gridScenario, n int, seedBase int64, lowSec, highSec float64, minMedWin float64) float64 {
	r0 := aggregateGrid(tmpl, et, scenarios, 1.0, 1.0, n, seedBase)
	if r0.medOfMedDur >= lowSec && r0.medOfMedDur <= highSec && r0.medWinRate >= minMedWin {
		return 1.0
	}
	lo, hi := 0.04, 28.0
	best := 1.0
	for iter := 0; iter < 44; iter++ {
		mid := (lo + hi) / 2
		r := aggregateGrid(tmpl, et, scenarios, mid, 1.0, n, seedBase)
		if r.medWinRate < minMedWin*0.5 {
			hi = mid
			continue
		}
		if r.medOfMedDur == 0 {
			hi = mid
			continue
		}
		if r.medOfMedDur < lowSec {
			lo = mid
		} else if r.medOfMedDur > highSec {
			hi = mid
		} else {
			return mid
		}
		best = mid
		if hi-lo < 0.012 {
			break
		}
	}
	r := aggregateGrid(tmpl, et, scenarios, best, 1.0, n, seedBase)
	if r.medOfMedDur > 0 && r.medOfMedDur >= lowSec && r.medOfMedDur <= highSec && r.medWinRate >= minMedWin {
		return best
	}
	// Fallback: duration vs hpScale can be non-monotone for some ability mixes; coarse scan.
	durTol := 2.5
	for s := 1; s <= 48; s++ {
		try := 0.06 + float64(s)*0.58
		if try > 28.5 {
			break
		}
		r2 := aggregateGrid(tmpl, et, scenarios, try, 1.0, n, seedBase)
		if r2.medWinRate < minMedWin*0.55 {
			continue
		}
		if r2.medOfMedDur > 0 && r2.medOfMedDur >= lowSec-durTol && r2.medOfMedDur <= highSec+durTol {
			return try
		}
	}
	// DoT / non-monotone duration: linear scan (binary search can miss a valid island).
	for s := 1; s <= 55; s++ {
		try := 0.12 + float64(s)*0.16
		if try > 9.2 {
			break
		}
		r2 := aggregateGrid(tmpl, et, scenarios, try, 1.0, n, seedBase)
		if r2.medWinRate < minMedWin*0.5 {
			continue
		}
		if r2.medOfMedDur > 0 && r2.medOfMedDur >= lowSec && r2.medOfMedDur <= highSec {
			return try
		}
	}
	return -1
}

func findAtkScaleForAggHeroHpGridRelaxed(tmpl model.Enemy, et string, scenarios []gridScenario, hpScale float64, n int, seedBase int64, hpLow, hpHigh float64, minMedWin float64, dotHeavy bool) float64 {
	a := findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minMedWin)
	if a >= 0 {
		return a
	}
	extra := 0.10
	if dotHeavy {
		extra = 0.16
	}
	hpLo2 := hpLow - extra
	if hpLo2 < 0.12 {
		hpLo2 = 0.12
	}
	hpHi2 := hpHigh + 0.02
	if dotHeavy {
		hpHi2 = hpHigh + 0.06
	}
	return findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLo2, hpHi2, minMedWin)
}

func findAtkScaleForAggHeroHpGrid(tmpl model.Enemy, et string, scenarios []gridScenario, hpScale float64, n int, seedBase int64, hpLow, hpHigh float64, minMedWin float64) float64 {
	r0 := aggregateGrid(tmpl, et, scenarios, hpScale, 1.0, n, seedBase)
	// Median win rate across cells can dip when HP is scaled for long fights; do not abort too early.
	earlyFrac := 0.55
	if minMedWin < 0.24 {
		earlyFrac = 0.35 // DoT / relaxed gate: allow ~10–15% median cell win rate through to binary search
	}
	if r0.medWinRate < minMedWin*earlyFrac {
		return -1
	}
	if r0.medOfMedHp >= hpLow && r0.medOfMedHp <= hpHigh {
		return 1.0
	}
	var lo, hi float64
	if r0.medOfMedHp < hpLow {
		lo, hi = 0.02, 1.0
	} else {
		lo, hi = 1.0, 22.0
	}
	best := 1.0
	for iter := 0; iter < 56; iter++ {
		mid := (lo + hi) / 2
		r := aggregateGrid(tmpl, et, scenarios, hpScale, mid, n, seedBase)
		binReject := minMedWin * 0.5
		if minMedWin < 0.24 {
			binReject = minMedWin * 0.38
		}
		if r.medWinRate < binReject {
			hi = mid
			best = mid
			continue
		}
		if r.medOfMedHp < hpLow {
			hi = mid
		} else if r.medOfMedHp > hpHigh {
			lo = mid
		} else {
			return mid
		}
		best = mid
		if hi-lo < 0.002 {
			break
		}
	}
	r := aggregateGrid(tmpl, et, scenarios, hpScale, best, n, seedBase)
	if r.medWinRate >= minMedWin && r.medOfMedHp >= hpLow && r.medOfMedHp <= hpHigh {
		return best
	}
	// Fallback: medOfMedHp vs atk is not always monotone (DoT, stuns); scan a coarse grid.
	hpTol := 0.03
	for step := 0; step < 22; step++ {
		try := 0.02 + float64(step)*0.28
		if try > 8.5 {
			break
		}
		r2 := aggregateGrid(tmpl, et, scenarios, hpScale, try, n, seedBase)
		if r2.medWinRate < minMedWin {
			continue
		}
		if r2.medOfMedHp >= hpLow-hpTol && r2.medOfMedHp <= hpHigh+hpTol {
			return try
		}
	}
	return -1
}

func ensurePositiveMinWinGrid(tmpl model.Enemy, et string, scenarios []gridScenario, hpScale, atkScale *float64, n int, seedBase int64) {
	for round := 0; round < 35; round++ {
		a := aggregateGrid(tmpl, et, scenarios, *hpScale, *atkScale, n, seedBase)
		if a.minWinRate > 0 {
			return
		}
		*atkScale *= 0.98
		*hpScale *= 1.012
		if *atkScale < 0.15 {
			return
		}
	}
}

// balanceArchetypeGrid runs grid balance for one enemy type; returns ok=false if skipped/failed.
// dotHeavy: burn/poison — shorter target duration band (set in caller) and looser final duration/atk checks.
func balanceArchetypeGrid(
	tmpl model.Enemy,
	et string,
	scenarios []gridScenario,
	iterations int,
	typeSeed int64,
	lowSec, highSec float64,
	targetSec float64,
	hpLow, hpHigh float64,
	minWinRate float64,
	refinePasses int,
	dotHeavy bool,
) (hpScale, atkScale float64, final gridAggResult, ok bool) {
	if len(scenarios) == 0 {
		return 0, 0, gridAggResult{}, false
	}
	n := iterations
	if n < 20 {
		n = 20
	}
	seedBase := typeSeed

	maxRounds := 6 + refinePasses*2
	if maxRounds < 10 {
		maxRounds = 10
	}

	minWinGate := minWinRate
	if dotHeavy {
		// Grid cells with burn vary wildly; median-of-medians win rate can sit ~12–20%.
		minWinGate = minWinRate * 0.52
	}

	hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec, highSec, minWinGate)
	if hpScale < 0 {
		hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec-18, highSec+18, minWinGate*0.80)
	}
	if hpScale < 0 {
		pad := 40.0
		if dotHeavy {
			pad = 55
		}
		hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec-pad, highSec+pad, minWinGate*0.72)
	}
	if hpScale < 0 {
		return 0, 0, gridAggResult{}, false
	}
	atkScale = findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinGate, dotHeavy)
	if atkScale < 0 {
		return 0, 0, gridAggResult{}, false
	}

	for round := 0; round < maxRounds; round++ {
		a := aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase)
		durOk := a.medOfMedDur > 0 && a.medOfMedDur >= lowSec && a.medOfMedDur <= highSec
		hpOk := a.medOfMedHp >= hpLow && a.medOfMedHp <= hpHigh
		if durOk && hpOk && a.medWinRate >= minWinGate {
			break
		}
		if a.medOfMedDur > 0 && !durOk {
			corr := targetSec / a.medOfMedDur
			hpScale *= corr
			if hpScale < 0.02 || hpScale > 28 {
				break
			}
		}
		if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinGate, dotHeavy); atk2 > 0 {
			atkScale = atk2
		} else {
			break
		}
		ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase)
	}

	ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase)
	if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinGate, dotHeavy); atk2 > 0 {
		atkScale = atk2
	}
	ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase)

	for fix := 0; fix < 10; fix++ {
		a := aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase)
		if a.medOfMedDur <= 0 {
			break
		}
		if a.medOfMedDur >= lowSec && a.medOfMedDur <= highSec {
			break
		}
		hpScale *= targetSec / a.medOfMedDur
		if hpScale < 0.02 || hpScale > 28 {
			break
		}
		if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinGate, dotHeavy); atk2 > 0 {
			atkScale = atk2
		}
		ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase)
	}

	final = aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase)
	if final.medWinRate < minWinGate {
		return 0, 0, gridAggResult{}, false
	}
	minAtk := 0.22
	if dotHeavy {
		minAtk = 0.18
	}
	if atkScale < minAtk {
		return 0, 0, gridAggResult{}, false
	}
	durLowChk, durHighChk := lowSec, highSec
	if dotHeavy {
		durLowChk -= 25
		durHighChk += 35
	}
	if final.medOfMedDur > 0 && (final.medOfMedDur < durLowChk || final.medOfMedDur > durHighChk) {
		return 0, 0, gridAggResult{}, false
	}
	return hpScale, atkScale, final, true
}

func printGridSQL(tmpl model.Enemy, et string, hpScale, atkScale float64) {
	newHP := max(1, int(math.Round(float64(tmpl.MaxHP)*hpScale)))
	newHPL := tmpl.HPPerLevel * hpScale
	newAtk := max(1, int(math.Round(float64(tmpl.Attack)*atkScale)))
	newAtkL := tmpl.AttackPerLevel * atkScale
	fmt.Printf("UPDATE enemies SET hp = %d, max_hp = %d, hp_per_level = %.4f, attack = %d, attack_per_level = %.4f WHERE type = '%s';\n",
		newHP, newHP, newHPL, newAtk, newAtkL, et)
}
