package plugin

import (
	"errors"
	"fmt"
	"math/rand/v2"
)

// Phase 13 — Turn-based combat state machine.
//
// Where SimulateCombat runs a whole fight in one call, the turn-based engine
// advances a persisted CombatSession one phase at a time:
//
//	player_turn -> enemy_turn -> round_end -> player_turn (next round) -> ...
//
// each `step` resolving exactly one phase so a manual elite/boss fight can be
// suspended and resumed (or reaped) from exact mid-state. Attack resolution
// reuses the shared primitives in combat_primitives.go / combat_engine.go, so
// a hit lands identically to auto-resolve. The round_end phase is where
// between-round status effects (poison, etc.) tick — the deferred poison tick
// from the schema commit lands here, now that the round-loop shape exists.

// errCombatSessionOver is returned by step when the fight has already reached
// a terminal status — callers should not keep advancing it.
var errCombatSessionOver = errors.New("combat session already over")

// turnCombatPhase is the single neutral phase the turn-based engine resolves
// attacks under. Auto-resolve sequences named phases with weight curves; a
// manual duel has no phase clock, so weights are flat 1.0 — neutral for the
// legacy penetration formula (calcDamage) and irrelevant on the weapon-dice
// path. The name surfaces on every CombatEvent the fight emits.
var turnCombatPhase = CombatPhase{
	Name:          "Duel",
	Rounds:        0,
	AttackWeight:  1.0,
	DefenseWeight: 1.0,
	SpeedWeight:   1.0,
}

// Player action kinds accepted by the state machine on a player_turn step.
const (
	ActionAttack  = "attack"
	ActionFlee    = "flee"
	ActionCast    = "cast"
	ActionConsume = "consume"
)

// PlayerAction is the player's choice for a player_turn step. It is ignored on
// enemy_turn / round_end steps (pass the zero value).
type PlayerAction struct {
	Kind string
	// Effect is set for ActionCast / ActionConsume: a pre-resolved outcome
	// handed in by the command handler. The handler owns spell/item lookup,
	// dice rolls, and the resource spend; the engine just applies the HP
	// deltas and emits the event so the round flows on into the enemy turn.
	Effect *turnActionEffect
}

// turnActionEffect is the resolved payload of a !cast / !consume player turn.
// Damage, heal, and the one-round enemy-skip all land within the casting round.
type turnActionEffect struct {
	Label       string // "Fireball — crit!", "Berry Poultice"
	Action      string // CombatEvent.Action: "spell_cast" or "use_consumable"
	EnemyDamage int
	PlayerHeal  int
	EnemySkip   bool // control spell: enemy forfeits its attack this round
}

// turnEngine wraps a combatState reconstructed from a persisted CombatSession
// so a single phase can be resolved and then written back.
type turnEngine struct {
	sess   *CombatSession
	player *Combatant
	enemy  *Combatant
	st     *combatState
	result *CombatResult
}

// combatSessionRNG seeds a deterministic generator from the session id and the
// phase being resolved, so a fight replays reproducibly no matter how many bot
// restarts occur mid-fight. Each (round, phase) gets a distinct stream — a flat
// per-session seed would correlate the player's and enemy's d20s within a round.
func combatSessionRNG(sess *CombatSession) *rand.Rand {
	var seed uint64 = 1469598103934665603 // FNV-ish offset basis
	for _, c := range sess.SessionID {
		seed = (seed ^ uint64(c)) * 1099511628211
	}
	stream := uint64(sess.Round)*4 + phaseOrdinal(sess.Phase)
	return rand.New(rand.NewPCG(seed, stream))
}

func phaseOrdinal(phase string) uint64 {
	switch phase {
	case CombatPhasePlayerTurn:
		return 0
	case CombatPhaseEnemyTurn:
		return 1
	case CombatPhaseRoundEnd:
		return 2
	default: // CombatPhaseOver
		return 3
	}
}

// resumeTurnEngine rebuilds the in-memory combatState from a persisted session.
// rng is the deterministic source for this step (see combatSessionRNG).
func resumeTurnEngine(sess *CombatSession, player, enemy *Combatant, rng *rand.Rand) *turnEngine {
	st := &combatState{
		playerHP:       sess.PlayerHP,
		enemyHP:        sess.EnemyHP,
		round:          sess.Round,
		poisonTicks:    sess.Statuses.PoisonTicks,
		poisonDmg:      sess.Statuses.PoisonDmg,
		stunPlayer:     sess.Statuses.StunPlayer,
		enraged:        sess.Statuses.Enraged,
		armorBroken:    sess.Statuses.ArmorBroken,
		armorBreakAmt:  sess.Statuses.ArmorBreakAmt,
		enemySkipFirst: sess.Statuses.EnemySkipNext,
		// Fight-scoped depleting resources + once-per-fight one-shots: restored
		// from the persisted statuses so a charge or "already used" flag can't
		// reset across a suspend/resume. commit writes the updated values back.
		wardCharges:           sess.Statuses.WardCharges,
		sporeRounds:           sess.Statuses.SporeRounds,
		reflectFrac:           sess.Statuses.ReflectFrac,
		autoCrit:              sess.Statuses.AutoCritFirst,
		arcaneWardHP:          sess.Statuses.ArcaneWardHP,
		healChargesLeft:       sess.Statuses.HealChargesLeft,
		deathSaveUsed:         sess.Statuses.DeathSaveUsed,
		luckyUsed:             sess.Statuses.LuckyUsed,
		raged:                 sess.Statuses.Raged,
		pendingRageAttack:     sess.Statuses.PendingRage,
		firstAttackBonusUsed:  sess.Statuses.FirstAtkBonusUsed,
		assassinateRerollUsed: sess.Statuses.AssassinateReroll,
		assassinateBonusUsed:  sess.Statuses.AssassinateBonus,
		// Slice-3 stateful monster-ability effects — armed by applyAbility,
		// round-tripped here so they survive a suspend/resume or reaper auto-play.
		enemyEvadeNext:     sess.Statuses.EnemyEvadeNext,
		enemyBlockUp:       sess.Statuses.EnemyBlockUp,
		enemyAdvantage:     sess.Statuses.EnemyAdvantage,
		enemyRetaliateFrac: sess.Statuses.EnemyRetaliateFrac,
		enemyRegen:         sess.Statuses.EnemyRegen,
		enemySurviveArmed:  sess.Statuses.EnemySurviveArmed,
		playerAtkDrain:     sess.Statuses.PlayerAtkDrain,
		playerACDebuff:     sess.Statuses.PlayerACDebuff,
		maxHPDrain:         sess.Statuses.MaxHPDrain,
		// Slice-4 monster-ability effects — the former flavor-only placeholders.
		enemySpellResist: sess.Statuses.EnemySpellResist,
		enemyRevealNext:  sess.Statuses.EnemyRevealNext,
		enemyFearImmune:  sess.Statuses.EnemyFearImmune,
		enemyAtkBuff:     sess.Statuses.EnemyAtkBuff,
		rng:              rng,
	}
	return &turnEngine{
		sess:   sess,
		player: player,
		enemy:  enemy,
		st:     st,
		result: &CombatResult{},
	}
}

// step resolves exactly one phase of the fight and advances sess.Phase. The
// events generated this step are returned (also accumulated by commit into
// sess.TurnLog). It does not persist — call commit then saveCombatSession, or
// use advanceCombatSession which does both.
func (te *turnEngine) step(action PlayerAction) ([]CombatEvent, error) {
	if !te.sess.IsActive() {
		return nil, errCombatSessionOver
	}
	te.st.events = nil
	switch te.sess.Phase {
	case CombatPhasePlayerTurn:
		te.stepPlayerTurn(action)
	case CombatPhaseEnemyTurn:
		te.stepEnemyTurn()
	case CombatPhaseRoundEnd:
		te.stepRoundEnd()
	case CombatPhaseOver:
		return nil, errCombatSessionOver
	default:
		return nil, fmt.Errorf("combat session %s in unknown phase %q", te.sess.SessionID, te.sess.Phase)
	}
	return te.st.events, nil
}

func (te *turnEngine) stepPlayerTurn(action PlayerAction) {
	switch action.Kind {
	case ActionFlee:
		te.st.events = append(te.st.events, CombatEvent{
			Round: te.st.round, Phase: turnCombatPhase.Name, Actor: "player", Action: "flee",
			PlayerHP: te.st.playerHP, EnemyHP: te.st.enemyHP,
		})
		te.finish(CombatStatusFled)
		return
	case ActionCast, ActionConsume:
		te.stepPlayerActionEffect(action.Effect)
		return
	}
	// Default: weapon attack. resolvePlayerSwings rolls the base swing plus
	// Mods.ExtraAttacks follow-ups (5e Extra Attack at Fighter L5/L11/L20,
	// Ranger/Paladin L5, Bard College of Valor L7). One !attack press in the
	// turn-based engine == one 5e Attack action == all swings in sequence.
	// Before this, the turn path called single-swing resolvePlayerAttack and
	// Extra Attack only fired in the auto-resolve SimulateCombat path; that
	// left every elite/boss !fight at L11+ short two swings/turn, which the
	// J1 boss-trace surfaced as Fighter's T3+ wall.
	// Returns true once the fight is decided — usually the enemy is down,
	// but a retaliate aura can drop the player on their own swing, so
	// disambiguate the outcome by HP.
	if resolvePlayerSwings(te.st, te.player, te.enemy, &turnCombatPhase, te.result) {
		if te.st.playerHP <= 0 {
			te.finish(CombatStatusLost)
		} else {
			te.finish(CombatStatusWon)
		}
		return
	}
	if te.petStrike() {
		te.finish(CombatStatusWon)
		return
	}
	if te.spiritWeaponStrike() {
		te.finish(CombatStatusWon)
		return
	}
	te.sess.Phase = CombatPhaseEnemyTurn
}

// petStrike resolves the player's pet attack for a turn-based fight. The pet
// rolls fresh on every player-acting turn (PetAttackProc), mirroring the
// auto-resolve engine's per-round chance rather than a once-per-fight strike.
// The roll rides the per-(round,phase) step RNG, so a suspend/resume or reaper
// auto-play of the same turn reproduces the same outcome. Damage reuses the
// auto-resolve formula (PetAttackDmg + d5), and PetAttackDmg already carries any
// mid-fight buff delta via applySessionBuffs. Returns true if the strike dropped
// the enemy.
func (te *turnEngine) petStrike() bool {
	st := te.st
	if te.player.Mods.PetAttackProc <= 0 || st.randFloat() >= te.player.Mods.PetAttackProc {
		return false
	}
	petDmg := te.player.Mods.PetAttackDmg + st.roll(5)
	st.enemyHP = max(0, st.enemyHP-petDmg)
	st.events = append(st.events, CombatEvent{
		Round: st.round, Phase: turnCombatPhase.Name, Actor: "pet", Action: "pet_attack",
		Damage: petDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
	})
	return enemyDown(st, turnCombatPhase.Name)
}

// spiritWeaponStrike resolves the spell's bonus-action attack each round when
// the spiritual_weapon buff is active. Same per-turn cadence as petStrike, but
// rolls and narrates on its own channel so the spectral mace doesn't borrow
// pet flavor on a petless caster. Returns true if the strike dropped the enemy.
func (te *turnEngine) spiritWeaponStrike() bool {
	st := te.st
	if te.player.Mods.SpiritWeaponProc <= 0 || st.randFloat() >= te.player.Mods.SpiritWeaponProc {
		return false
	}
	dmg := te.player.Mods.SpiritWeaponDmg + st.roll(5)
	st.enemyHP = max(0, st.enemyHP-dmg)
	st.events = append(st.events, CombatEvent{
		Round: st.round, Phase: turnCombatPhase.Name, Actor: "spirit_weapon", Action: "spirit_weapon_strike",
		Damage: dmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
	})
	return enemyDown(st, turnCombatPhase.Name)
}

// stepPlayerActionEffect resolves a !cast / !consume turn: the command handler
// has already rolled the spell / picked the item and spent the resource, so the
// engine only applies the HP deltas and emits the event before handing off to
// the enemy turn. A control-spell skip is parked in st.enemySkipFirst, which
// commit() persists as Statuses.EnemySkipNext for the enemy_turn step to read.
func (te *turnEngine) stepPlayerActionEffect(eff *turnActionEffect) {
	st := te.st
	if eff == nil {
		// Defensive: a kind with no payload just passes the turn.
		te.sess.Phase = CombatPhaseEnemyTurn
		return
	}
	action := eff.Action
	if action == "" {
		action = "spell_cast"
	}
	// spell_resist (Spell Immunity): a spell's damage against the enemy is
	// halved. Consumables that happen to deal damage are not spells, so the
	// resist is gated on the spell_cast action.
	enemyDmg := eff.EnemyDamage
	spellFizzled := enemyDmg > 0 && action == "spell_cast" && enemyResistsSpells(te.enemy, st)
	if spellFizzled {
		enemyDmg = max(1, enemyDmg/2)
	}
	if enemyDmg > 0 {
		st.enemyHP = max(0, st.enemyHP-enemyDmg)
	}
	if eff.PlayerHeal > 0 {
		// Respect any max_hp_drain monster ability — a drained player can't be
		// healed back past the lowered ceiling.
		hpCap := max(1, te.sess.PlayerHPMax-st.maxHPDrain)
		st.playerHP = min(hpCap, st.playerHP+eff.PlayerHeal)
	}
	st.events = append(st.events, CombatEvent{
		Round: st.round, Phase: turnCombatPhase.Name, Actor: "player", Action: action,
		Damage: enemyDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP, Desc: eff.Label,
	})
	if spellFizzled {
		st.events = append(st.events, CombatEvent{
			Round: st.round, Phase: turnCombatPhase.Name, Actor: "enemy", Action: "spell_fizzle",
			Damage: enemyDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
		})
	}
	if enemyDown(st, turnCombatPhase.Name) {
		te.finish(CombatStatusWon)
		return
	}
	if te.petStrike() {
		te.finish(CombatStatusWon)
		return
	}
	if te.spiritWeaponStrike() {
		te.finish(CombatStatusWon)
		return
	}
	if eff.EnemySkip {
		// fear_immune enemies shrug off control spells — the skip never arms.
		if enemyImmuneToControl(te.enemy, st) {
			st.events = append(st.events, CombatEvent{
				Round: st.round, Phase: turnCombatPhase.Name, Actor: "enemy", Action: "fear_resist",
				PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
			})
		} else {
			st.enemySkipFirst = true
		}
	}
	te.sess.Phase = CombatPhaseEnemyTurn
}

func (te *turnEngine) stepEnemyTurn() {
	// A control spell cast last phase forfeits the enemy's attack this round —
	// unless the enemy is fear_immune, in which case the control fizzled and it
	// acts as normal.
	if te.st.enemySkipFirst {
		te.st.enemySkipFirst = false
		if enemyImmuneToControl(te.enemy, te.st) {
			te.st.events = append(te.st.events, CombatEvent{
				Round: te.st.round, Phase: turnCombatPhase.Name, Actor: "enemy", Action: "fear_resist",
				PlayerHP: te.st.playerHP, EnemyHP: te.st.enemyHP,
			})
		} else {
			te.st.events = append(te.st.events, CombatEvent{
				Round: te.st.round, Phase: turnCombatPhase.Name, Actor: "enemy", Action: "spell_held",
				PlayerHP: te.st.playerHP, EnemyHP: te.st.enemyHP,
			})
			te.sess.Phase = CombatPhaseRoundEnd
			return
		}
	}

	// Monster ability fires at the top of the enemy turn. cleave / lifesteal
	// resolve their own damage and stand in for the attack action this round;
	// every other effect (poison / stun / enrage / armor_break) is a rider that
	// leaves the multiattack below intact. applyAbility returns true when the
	// player went down without a save.
	abilityDealtDamage := false
	if te.enemy.Ability != nil && turnAbilityFires(te.enemy.Ability, te.st, te.enemy) {
		if applyAbility(te.st, te.player, te.enemy, &turnCombatPhase, te.result) {
			te.finish(CombatStatusLost)
			return
		}
		switch te.enemy.Ability.Effect {
		case "cleave", "lifesteal":
			abilityDealtDamage = true
		}
	}

	if !abilityDealtDamage {
		// Pet defensive procs are a single proc per enemy turn: roll once, then
		// spend it on the first swing only. Whiff makes that one swing a
		// guaranteed miss; deflect halves its damage. Against a multiattack the
		// remaining swings resolve normally — a single proc shouldn't nullify a
		// boss's whole multiattack round. (This deliberately diverges from the
		// auto-resolve engine's apply-to-all model.)
		petWhiff := te.player.Mods.PetWhiffProc > 0 && te.st.randFloat() < te.player.Mods.PetWhiffProc
		petDeflect := te.player.Mods.PetDeflectProc > 0 && te.st.randFloat() < te.player.Mods.PetDeflectProc
		if petDeflect {
			te.result.PetDeflected = true
		}

		// SRD multiattack: each profile entry is one attack roll resolved
		// through the shared primitive. A registered elite/boss swings its full
		// profile; everyone else gets a single attack from the template stats.
		// resolveEnemyAttack returns true when the fight is decided — either the
		// player went down without a death save, or a reflect consumable killed
		// the enemy. Disambiguate by inspecting HP.
		for i, atk := range enemyAttackProfile(te.sess.EnemyID, te.enemy.Stats) {
			swing := *te.enemy
			swing.Stats.Attack = atk.Damage
			swing.Stats.AttackBonus = atk.AttackBonus
			// Spend the proc on the first swing only; later swings see false.
			swingWhiff := petWhiff && i == 0
			swingDeflect := petDeflect && i == 0
			decided := resolveEnemyAttack(te.st, te.player, &swing, &turnCombatPhase, te.result, swingWhiff, swingDeflect, false)
			if te.st.playerHP <= 0 {
				te.finish(CombatStatusLost)
				return
			}
			if decided || te.st.enemyHP <= 0 {
				te.finish(CombatStatusWon)
				return
			}
		}
	}
	te.sess.Phase = CombatPhaseRoundEnd
}

// turnAbilityFires decides whether a monster ability triggers this enemy turn.
// The auto-resolve engine gates abilities on its named phase clock (Opening /
// Clash / Decisive); the turn-based duel has no phase clock, so the phase tag
// is remapped to fight progress: "opening" is round-1 only, "decisive" unlocks
// once the enemy is bloodied (<40% HP), "clash" covers the rounds in between,
// and "any" is always eligible. ProcChance is rolled once eligible.
func turnAbilityFires(ability *MonsterAbility, st *combatState, enemy *Combatant) bool {
	eligible := false
	switch ability.Phase {
	case "any":
		eligible = true
	case "opening":
		eligible = st.round <= 1
	case "clash":
		eligible = st.round > 1
	case "decisive":
		eligible = st.enemyHP < int(float64(enemy.Stats.MaxHP)*0.4)
	}
	if !eligible {
		return false
	}
	return st.randFloat() < ability.ProcChance
}

// stepRoundEnd applies between-round status effects, then opens the next round.
func (te *turnEngine) stepRoundEnd() {
	st := te.st
	if st.poisonTicks > 0 {
		st.playerHP = max(0, st.playerHP-st.poisonDmg)
		st.poisonTicks--
		st.events = append(st.events, CombatEvent{
			Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "enemy", Action: "poison_tick",
			Damage: st.poisonDmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
		})
		if st.playerHP <= 0 {
			if !trySave(st, te.player, CombatPhaseRoundEnd) {
				te.finish(CombatStatusLost)
				return
			}
		}
	}
	// Regenerate (monster ability): the enemy knits its wounds at round end.
	if st.enemyRegen > 0 && st.enemyHP > 0 && st.enemyHP < te.enemy.Stats.MaxHP {
		st.enemyHP = min(te.enemy.Stats.MaxHP, st.enemyHP+st.enemyRegen)
		st.events = append(st.events, CombatEvent{
			Round: st.round, Phase: CombatPhaseRoundEnd, Actor: "enemy", Action: "regen_tick",
			Damage: st.enemyRegen, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
		})
	}
	st.round++
	te.sess.Phase = CombatPhasePlayerTurn
}

// finish parks the session in a terminal status + the 'over' phase.
func (te *turnEngine) finish(status string) {
	te.sess.Status = status
	te.sess.Phase = CombatPhaseOver
}

// commit folds this step's combatState back into the session struct: HP,
// round, the between-round status snapshot, and the appended event log.
// Phase / Status were already set by step. saveCombatSession persists it.
//
// The Buff* stat deltas on Statuses are NOT combatState fields — they're owned
// by the command layer (a !cast / !consume folds them in) and applied to the
// rebuilt combatant by applySessionBuffs — so commit mutates Statuses in place
// rather than replacing it, leaving those deltas untouched.
func (te *turnEngine) commit() {
	st := te.st
	te.sess.Round = st.round
	te.sess.PlayerHP = st.playerHP
	te.sess.EnemyHP = st.enemyHP
	s := &te.sess.Statuses
	s.PoisonTicks = st.poisonTicks
	s.PoisonDmg = st.poisonDmg
	s.StunPlayer = st.stunPlayer
	s.Enraged = st.enraged
	s.ArmorBroken = st.armorBroken
	s.ArmorBreakAmt = st.armorBreakAmt
	s.EnemySkipNext = st.enemySkipFirst
	s.WardCharges = st.wardCharges
	s.SporeRounds = st.sporeRounds
	s.ReflectFrac = st.reflectFrac
	s.AutoCritFirst = st.autoCrit
	s.ArcaneWardHP = st.arcaneWardHP
	s.HealChargesLeft = st.healChargesLeft
	s.DeathSaveUsed = st.deathSaveUsed
	s.LuckyUsed = st.luckyUsed
	s.Raged = st.raged
	s.PendingRage = st.pendingRageAttack
	s.FirstAtkBonusUsed = st.firstAttackBonusUsed
	s.AssassinateReroll = st.assassinateRerollUsed
	s.AssassinateBonus = st.assassinateBonusUsed
	s.EnemyEvadeNext = st.enemyEvadeNext
	s.EnemyBlockUp = st.enemyBlockUp
	s.EnemyAdvantage = st.enemyAdvantage
	s.EnemyRetaliateFrac = st.enemyRetaliateFrac
	s.EnemyRegen = st.enemyRegen
	s.EnemySurviveArmed = st.enemySurviveArmed
	s.PlayerAtkDrain = st.playerAtkDrain
	s.PlayerACDebuff = st.playerACDebuff
	s.MaxHPDrain = st.maxHPDrain
	s.EnemySpellResist = st.enemySpellResist
	s.EnemyRevealNext = st.enemyRevealNext
	s.EnemyFearImmune = st.enemyFearImmune
	s.EnemyAtkBuff = st.enemyAtkBuff
	te.sess.TurnLog = append(te.sess.TurnLog, st.events...)
}

// advanceCombatSession resolves one phase of the fight and persists the result.
// It is the single entry point callers (commands, reaper) should use: it seeds
// the deterministic RNG, resumes the engine, steps, commits, and saves. The
// passed sess is mutated in place. The events generated this step are returned.
func advanceCombatSession(sess *CombatSession, player, enemy *Combatant, action PlayerAction) ([]CombatEvent, error) {
	if sess == nil {
		return nil, ErrNoActiveCombatSession
	}
	rng := combatSessionRNG(sess)
	te := resumeTurnEngine(sess, player, enemy, rng)
	events, err := te.step(action)
	if err != nil {
		return nil, err
	}
	te.commit()
	if err := saveCombatSession(sess); err != nil {
		return events, fmt.Errorf("persist combat session %s: %w", sess.SessionID, err)
	}
	return events, nil
}
