package game

import (
	"math/rand"
	"time"

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

const (
	offlineAutoPotionChance   = 0.02
	offlineAutoPotionHPThresh = 0.40

	// CombatSimMaxStepsDefault is the iteration cap when CombatSimOptions.MaxSteps <= 0 (offline, tests).
	CombatSimMaxStepsDefault = 200_000
	// CombatSimMaxStepsLong is used by balance CLIs and admin combat sim so long fights (DoT/regen) are not cut off early.
	CombatSimMaxStepsLong = 3_000_000
)

// CombatSimDeterministicStart is the fixed combat timeline origin for balance tools and admin sim parity (avoids wall-clock drift in tests).
var CombatSimDeterministicStart = time.Unix(1_700_000_000, 0)

// CombatSimOptions configures the shared combat resolution loop.
type CombatSimOptions struct {
	// TickRate matches the engine combat tick cadence (used for periodic effects).
	TickRate time.Duration
	// AutoUsePotion decides whether to consume a potion after damage ticks/attacks.
	// It should return true when a potion was used.
	AutoUsePotion func(hero *model.Hero, now time.Time) bool
	// WallClockDelay adds optional real-time delay between simulation steps.
	// 0 means instant simulation (default).
	WallClockDelay time.Duration
	// OnEvent receives attack/tick/death events emitted by the simulator.
	OnEvent func(evt model.CombatEvent)
	// MaxSteps caps the simulation loop (default CombatSimMaxStepsDefault). Use CombatSimMaxStepsLong for balance/admin parity on long fights.
	MaxSteps int
}

// ResolveCombatToEnd runs a combat loop using the same mechanics as the online engine.
// It mutates hero and enemy until one side dies, returning whether the hero survived.
func ResolveCombatToEnd(hero *model.Hero, enemy *model.Enemy, start time.Time, opts CombatSimOptions) bool {
	survived, _ := resolveCombatToEnd(hero, enemy, start, opts)
	return survived
}

// ResolveCombatToEndWithDuration is like ResolveCombatToEnd but also returns simulated combat
// elapsed time (last event time minus start), using the same timeline as the online engine.
func ResolveCombatToEndWithDuration(hero *model.Hero, enemy *model.Enemy, start time.Time, opts CombatSimOptions) (survived bool, elapsed time.Duration) {
	return resolveCombatToEnd(hero, enemy, start, opts)
}

func resolveCombatToEnd(hero *model.Hero, enemy *model.Enemy, start time.Time, opts CombatSimOptions) (survived bool, elapsed time.Duration) {
	if hero == nil || enemy == nil {
		return false, 0
	}
	tickRate := opts.TickRate
	if tickRate <= 0 {
		tickRate = 100 * time.Millisecond
	}

	now := start
	heroNext := now.Add(attackInterval(hero.EffectiveSpeed()))
	enemyNext := now.Add(attackIntervalEnemy(enemy.Speed))
	nextTick := now.Add(tickRate)
	lastTickAt := now
	var regenRemainder float64

	step := 0
	maxSteps := opts.MaxSteps
	if maxSteps <= 0 {
		maxSteps = CombatSimMaxStepsDefault
	}

	for step < maxSteps {
		step++
		next := heroNext
		if enemyNext.Before(next) {
			next = enemyNext
		}
		if nextTick.Before(next) {
			next = nextTick
		}
		now = next

		if now.Equal(nextTick) {
			tickDur := now.Sub(lastTickAt)
			if tickDur > 0 {
				ProcessDebuffDamage(hero, tickDur, now)
				ProcessEnemyRegen(enemy, tickDur, &regenRemainder)
				ProcessSummonDamage(hero, enemy, start, lastTickAt, now)
				lastTickAt = now
				if CheckDeath(hero, now) {
					hero.HP = 0
					emitSimEvent(opts, model.CombatEvent{
						Type:      "death",
						Source:    "enemy",
						HeroID:    hero.ID,
						HeroHP:    hero.HP,
						EnemyHP:   enemy.HP,
						Timestamp: now,
					})
					return false, now.Sub(start)
				}
			}
			emitSimEvent(opts, model.CombatEvent{
				Type:      "tick",
				Source:    "system",
				HeroID:    hero.ID,
				HeroHP:    hero.HP,
				EnemyHP:   enemy.HP,
				Timestamp: now,
			})
			simStepDelay(opts)
			nextTick = nextTick.Add(tickRate)
			continue
		}

		if !heroNext.After(enemyNext) && now.Equal(heroNext) {
			evt := ProcessAttack(hero, enemy, now)
			emitSimEvent(opts, evt)
			simStepDelay(opts)
			if !enemy.IsAlive() {
				return true, now.Sub(start)
			}
			heroNext = now.Add(attackInterval(hero.EffectiveSpeed()))
			continue
		}

		if now.Equal(enemyNext) {
			evt := ProcessEnemyAttack(hero, enemy, now)
			emitSimEvent(opts, evt)
			simStepDelay(opts)
			if CheckDeath(hero, now) {
				hero.HP = 0
				emitSimEvent(opts, model.CombatEvent{
					Type:      "death",
					Source:    "enemy",
					HeroID:    hero.ID,
					HeroHP:    hero.HP,
					EnemyHP:   enemy.HP,
					Timestamp: now,
				})
				return false, now.Sub(start)
			}
			if opts.AutoUsePotion != nil {
				_ = opts.AutoUsePotion(hero, now)
			}
			enemyNext = now.Add(attackIntervalEnemy(enemy.Speed))
		}
	}

	win := hero.HP > 0 && enemy.IsAlive() == false
	return win, now.Sub(start)
}

func emitSimEvent(opts CombatSimOptions, evt model.CombatEvent) {
	if opts.OnEvent != nil {
		opts.OnEvent(evt)
	}
}

func simStepDelay(opts CombatSimOptions) {
	if opts.WallClockDelay > 0 {
		time.Sleep(opts.WallClockDelay)
	}
}

// OfflineAutoPotionHook is a low-probability offline-only potion usage policy.
func OfflineAutoPotionHook(hero *model.Hero, now time.Time) bool {
	if hero == nil || hero.Potions <= 0 || hero.HP <= 0 {
		return false
	}
	hpThresh := int(float64(hero.MaxHP) * offlineAutoPotionHPThresh)
	if hero.HP >= hpThresh {
		return false
	}
	if rand.Float64() >= offlineAutoPotionChance {
		return false
	}
	hero.Potions--
	healAmount := int(float64(hero.MaxHP) * tuning.Get().PotionHealPercent)
	if healAmount < 1 {
		healAmount = 1
	}
	hero.HP += healAmount
	if hero.HP > hero.MaxHP {
		hero.HP = hero.MaxHP
	}
	return true
}
