package plugin

import (
	"maunium.net/go/mautrix/id"
)

// §6 — a caster casts in an auto-resolved fight.
//
// An ordinary room fight is runZoneCombatRoster → SimulateCombat on an 8-round
// clock: one breath, no turn engine, no action picker. The only spell that has
// ever landed there is a PendingCast the player queued BY HAND with `!cast`
// before walking in. Under autopilot nobody queues one — so for every room of
// every expedition, a caster fought with a weapon and nothing else.
//
// A cleric is the extreme case: no Extra Attack, a mace, and its whole kit
// unusable. Measured on the room path at L10, a wounded cleric loses 8% of
// ORDINARY room fights (a fighter loses 0.0%), and a room loss ends the entire
// expedition — "the fight drags on, X outlasts you, you retreat". That is why
// cleric fled 167 of 500 runs in the corpus while fighter, ranger and paladin
// fled none, and it is the whole of the "cleric is a weak class" gap.
//
// The tell: dnd_class_balance.go — the harness the class corpus was tuned on —
// ALWAYS hands a caster their best damage spell (pickBestDamageSpell +
// applyHarnessSpellCast) before it simulates. So the numbers we balanced against
// modelled a caster who casts, and the live room modelled one who does not. This
// closes that gap: the character fights with the kit it actually owns.
//
// It is NOT a new mechanic. A folded-in spell is additive pre-damage, exactly as
// a hand-queued PendingCast has always been, which is what keeps this comparable
// to the corpus rather than a fresh invention.

// autoCastForAutoResolve picks the caster's best damage spell, spends the slot,
// and folds the damage into the Combatant pair. Reports whether it cast.
//
// It reads the seat's ACTUAL remaining slots, not the class slot table.
// pickBestDamageSpell (the harness one) reads slotsForClassLevel — the
// theoretical pool — which is right for a one-fight harness and would be an
// infinite spell here: the same "no row to persist onto, so it arrives fresh"
// bug that gave the companion an unlimited body and an unlimited slot pool.
// A leveled spell cast here costs a real slot and stays spent until a rest.
func (p *AdventurePlugin) autoCastForAutoResolve(
	uid id.UserID,
	c *DnDCharacter,
	playerStats *CombatStats,
	playerMods *CombatModifiers,
	enemyStats *CombatStats,
) bool {
	// A hand-queued spell wins: the player already chose, applyPendingCast owns
	// it, and casting a second one would be two spells in one action.
	if c == nil || c.PendingCast != "" || !isSpellcaster(c) {
		return false
	}
	spell, slot, ok := pickAutoResolveSpell(uid, c)
	if !ok {
		return false
	}
	// Leveled spells cost a slot. Spend it BEFORE the damage lands, and bail if
	// the debit fails — a spell that could not be paid for must not be cast.
	if slot > 0 {
		spent, err := consumeSpellSlot(uid, slot)
		if err != nil || !spent {
			return false
		}
	}
	applyHarnessSpellCast(c, spell, slot, playerStats, playerMods, enemyStats)
	return true
}

// roomSlotCap is the highest slot level an ordinary room may spend. Cantrips are
// free and always available; anything above this is reserved for the elite and
// the boss.
//
// The first cut of §6 had no cap — "never upcast" was supposed to be enough. It
// was not, and the sweep said so: room deaths fell for every caster (mage 131 →
// 105) while elite and boss deaths exploded (mage 7 → 38 and 19 → 61, sorcerer
// 14 → 51 and 15 → 65). Casters were winning the trash rooms and arriving at the
// thing that matters with an empty pool. Native-level is not a reserve, because
// a mage's native-level spells ARE its boss kit; only a level cap is.
//
// 2 is where the two demands stop overlapping. Every caster owns a damage spell
// at or below it (a cleric's guiding_bolt and inflict_wounds are both 1st), so
// the rooms still get a real spell — but a fireball, a spirit_guardians, a
// flame_strike, and every slot the turn engine would upcast into stays in the
// caster's pocket until there is something worth spending it on.
const roomSlotCap = 2

// pickAutoResolveSpell is the best damage spell this caster can actually cast
// right now: known, prepared, on their list, at or under the room slot cap, and
// backed by a slot they still hold.
//
// It casts at the spell's NATIVE level and never upcasts. simPickSpell upcasts
// aggressively — correct there, because it runs in the turn engine at an elite
// or a boss, which is what the big slots are for. A trash room is not, and a
// picker that nukes a goblin with a 5th-level slot would leave the caster
// swinging a stick at the thing that actually matters.
func pickAutoResolveSpell(uid id.UserID, c *DnDCharacter) (SpellDefinition, int, bool) {
	known, err := listKnownSpells(uid)
	if err != nil || len(known) == 0 {
		return SpellDefinition{}, 0, false
	}
	slots, err := getSpellSlots(uid)
	if err != nil {
		return SpellDefinition{}, 0, false
	}

	var best SpellDefinition
	bestSlot := 0
	bestScore := -1.0
	for _, k := range known {
		if !k.Prepared {
			continue
		}
		sp, ok := lookupSpell(k.SpellID)
		if !ok {
			continue
		}
		switch sp.Effect {
		case EffectDamageAttack, EffectDamageSave, EffectDamageAuto:
		default:
			continue
		}
		// The auto-resolve engine has no reaction window, same as everywhere else.
		if sp.CastTime == CastReaction {
			continue
		}
		onList := false
		for _, cl := range sp.Classes {
			if cl == c.Class {
				onList = true
				break
			}
		}
		if !onList {
			continue
		}
		// A cantrip is free; a leveled spell needs a slot still in hand, and the
		// room only gets to reach for the small ones.
		if sp.Level > 0 {
			if sp.Level > roomSlotCap {
				continue
			}
			if pair, ok := slots[sp.Level]; !ok || pair[0]-pair[1] <= 0 {
				continue
			}
		}
		if score := spellExpectedDamage(sp, sp.Level, c.Level); score > bestScore {
			best, bestSlot, bestScore = sp, sp.Level, score
		}
	}
	return best, bestSlot, bestScore >= 0
}
