package plugin

import (
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"strings"

	"gogobee/internal/db"
	"maunium.net/go/mautrix/id"
)

// Phase 6 — active abilities via pre-arm model.
//
// The combat engine is one-shot, so player-activated abilities use a pre-arm
// pattern: !arm <ability> reserves it (consumes one resource immediately)
// and the next combat auto-fires it via existing CombatModifiers fields.
// The armed flag is cleared on combat completion regardless of outcome.
//
// Resources refresh on long rest (Phase 6 simplification — short-rest classes
// also use long-rest refresh until we split short/long resource pools).

// ── Ability definitions ──────────────────────────────────────────────────────

type DnDAbility struct {
	ID    string
	Name  string
	Class DnDClass
	// Subclass: if non-empty, ability is only available when the player's
	// subclass matches. Phase 10 adds the first such ability (Berserker
	// rage). Used by parseAbility/arm gating and by classActiveAbilities
	// when listing for !arm / !abilities.
	Subclass    DnDSubclass
	Resource    string // "stamina", "spell_slot", "favor", etc.
	Description string
	// Effect — applied to mods at combat start when armed
	Apply func(c *DnDCharacter, mods *CombatModifiers)
}

var dndActiveAbilities = map[string]DnDAbility{
	"second_wind": {
		ID:          "second_wind",
		Name:        "Second Wind",
		Class:       ClassFighter,
		Resource:    "stamina",
		Description: "Restore HP at low health: 1d10 + level (consumes 1 stamina).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// HealItem fires once when player drops below 50% HP.
			// Avg of 1d10 + level: 5.5 + level.
			mods.HealItem += 5 + c.Level
		},
	},
	"magic_missile": {
		ID:          "magic_missile",
		Name:        "Magic Missile",
		Class:       ClassMage,
		Resource:    "spell_slot",
		Description: "Three darts strike unerringly at the start of combat: 3 × (1d4+1) force damage.",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// 3 darts × avg 3.5 = ~10. Auto-hit pre-combat damage.
			mods.FlatDmgStart += 9 + abilityModifier(c.INT)
		},
	},
	"healing_word": {
		ID:          "healing_word",
		Name:        "Healing Word",
		Class:       ClassCleric,
		Resource:    "favor",
		Description: "Restore 1d4 + WIS HP at low health (consumes 1 divine favor).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.HealItem += 3 + abilityModifier(c.WIS)
		},
	},
}

func parseAbility(s string) (DnDAbility, bool) {
	s = strings.ToLower(strings.TrimSpace(strings.ReplaceAll(s, " ", "_")))
	s = strings.ReplaceAll(s, "-", "_")
	if a, ok := dndActiveAbilities[s]; ok {
		return a, true
	}
	for _, a := range dndActiveAbilities {
		if strings.EqualFold(a.Name, s) {
			return a, true
		}
	}
	return DnDAbility{}, false
}

// classActiveAbilities returns the active abilities a class can know,
// excluding subclass-gated entries (those are listed by characterActiveAbilities).
func classActiveAbilities(class DnDClass) []DnDAbility {
	var out []DnDAbility
	for _, a := range dndActiveAbilities {
		if a.Class == class && a.Subclass == "" {
			out = append(out, a)
		}
	}
	return out
}

// characterActiveAbilities returns abilities available to a given character,
// including their subclass-gated abilities. Used for !arm listing so the
// Berserker sees `rage` once they've chosen the subclass.
func characterActiveAbilities(c *DnDCharacter) []DnDAbility {
	var out []DnDAbility
	for _, a := range dndActiveAbilities {
		if a.Class != c.Class {
			continue
		}
		if a.Subclass != "" && a.Subclass != c.Subclass {
			continue
		}
		out = append(out, a)
	}
	return out
}

// ── Resource pool ────────────────────────────────────────────────────────────

// classResourceMax returns (resource_type, max_value) for a class.
// Phase 6: each class has one active resource pool.
func classResourceMax(class DnDClass) (string, int) {
	switch class {
	case ClassFighter:
		return "stamina", 3
	case ClassRogue:
		return "focus", 2
	case ClassMage:
		return "spell_slot", 1
	case ClassCleric:
		return "favor", 3
	case ClassRanger:
		return "focus", 2
	}
	return "", 0
}

// initResources writes a fresh resource row for the class. Idempotent —
// won't overwrite an existing pool. Called on !setup confirm and on
// auto-migration.
func initResources(userID id.UserID, class DnDClass) error {
	resType, max := classResourceMax(class)
	if resType == "" {
		return nil
	}
	_, err := db.Get().Exec(`
		INSERT OR IGNORE INTO dnd_resources (user_id, resource_type, current_value, max_value)
		VALUES (?, ?, ?, ?)`,
		string(userID), resType, max, max)
	return err
}

// subclassResourceMax — resources granted by a subclass on top of the class
// pool. Phase 10 SUB2a-ii: Battle Master gets 4 superiority dice (5e
// long-rest refresh in our model; spec says short-rest, but we keep parity
// with the existing class pools' refresh cadence). Phase 10 SUB3a-ii: at
// L15 Relentless gives "regen 1 die when initiative rolled with empty pool" —
// proxied here as a +1 max (5 dice total) so the player always has at least
// one extra encounter's worth across the long-rest cycle. Returns ("", 0) if
// the subclass has no extra pool.
func subclassResourceMax(sub DnDSubclass, level int) (string, int) {
	switch sub {
	case SubclassBattleMaster:
		max := 4
		if level >= 15 {
			max = 5
		}
		return "superiority", max
	case SubclassLifeDomain, SubclassWarDomain, SubclassTrickeryDomain:
		// Phase 10 SUB2c — shared Channel Divinity pool for Cleric domains.
		// 5e is 1/short-rest at L2 and 2/short-rest at L6; long-rest refresh
		// in our model.
		return "channel_divinity", 2
	}
	return "", 0
}

// initSubclassResources adds the subclass-specific resource pool, or grows
// the existing pool's max if the level-gated cap increased (e.g. Battle
// Master L15 Relentless bumps the cap from 4 → 5). Called from
// applySubclassChoice and the level-up loop; idempotent. If a player
// switches subclasses, the prior pool's row is left in place —
// refreshAllResources still touches it but no ability references it once the
// subclass changes, so it's inert.
func initSubclassResources(userID id.UserID, sub DnDSubclass, level int) error {
	resType, max := subclassResourceMax(sub, level)
	if resType == "" {
		return nil
	}
	if _, err := db.Get().Exec(`
		INSERT OR IGNORE INTO dnd_resources (user_id, resource_type, current_value, max_value)
		VALUES (?, ?, ?, ?)`,
		string(userID), resType, max, max); err != nil {
		return err
	}
	// Grow max (and current) if the cap rose since the row was first written.
	// Don't shrink — players who somehow have a higher cap stored should keep it.
	_, err := db.Get().Exec(`
		UPDATE dnd_resources
		   SET current_value = current_value + (? - max_value),
		       max_value     = ?
		 WHERE user_id = ? AND resource_type = ? AND max_value < ?`,
		max, max, string(userID), resType, max)
	return err
}

// getResource returns (current, max). Returns (0, 0, true) if no row exists.
func getResource(userID id.UserID, resType string) (int, int, error) {
	var cur, max int
	err := db.Get().QueryRow(
		`SELECT current_value, max_value FROM dnd_resources
		 WHERE user_id = ? AND resource_type = ?`,
		string(userID), resType,
	).Scan(&cur, &max)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, 0, nil
	}
	return cur, max, err
}

// spendResource decrements current_value if at least amount is available.
// Returns true on success.
func spendResource(userID id.UserID, resType string, amount int) (bool, error) {
	res, err := db.Get().Exec(`
		UPDATE dnd_resources
		   SET current_value = current_value - ?,
		       last_reset_at = last_reset_at  -- no-op, keep timestamp
		 WHERE user_id = ? AND resource_type = ?
		   AND current_value >= ?`,
		amount, string(userID), resType, amount)
	if err != nil {
		return false, err
	}
	n, _ := res.RowsAffected()
	return n > 0, nil
}

// refreshAllResources sets current_value = max_value for every resource of a user.
// Called on long rest.
func refreshAllResources(userID id.UserID) error {
	_, err := db.Get().Exec(`
		UPDATE dnd_resources SET current_value = max_value, last_reset_at = CURRENT_TIMESTAMP
		WHERE user_id = ?`,
		string(userID))
	return err
}

// ── !arm command ─────────────────────────────────────────────────────────────

func (p *AdventurePlugin) handleDnDArmCmd(ctx MessageContext, args string) error {
	userMu := p.advUserLock(ctx.Sender)
	userMu.Lock()
	defer userMu.Unlock()

	args = strings.TrimSpace(args)
	advChar, _ := loadAdvCharacter(ctx.Sender)
	c, err := p.ensureCharForDnDCmd(ctx.Sender, advChar)
	if err != nil || c == nil {
		return p.SendDM(ctx.Sender, "Couldn't load your character.")
	}

	if args == "" || strings.ToLower(args) == "list" {
		return p.SendDM(ctx.Sender, renderArmList(c))
	}
	if strings.ToLower(args) == "clear" {
		c.ArmedAbility = ""
		_ = SaveDnDCharacter(c)
		return p.SendDM(ctx.Sender, "Disarmed. Resource is not refunded.")
	}

	ab, ok := parseAbility(args)
	if !ok {
		return p.SendDM(ctx.Sender, "Unknown ability. Run `!arm` to see your options.")
	}
	if ab.Class != c.Class {
		return p.SendDM(ctx.Sender, fmt.Sprintf(
			"%s is a %s ability — your class is %s.", ab.Name, titleClass(ab.Class), titleClass(c.Class)))
	}
	if ab.Subclass != "" && ab.Subclass != c.Subclass {
		needed, _ := subclassInfo(ab.Subclass)
		return p.SendDM(ctx.Sender, fmt.Sprintf(
			"%s is a %s subclass ability — choose that subclass via `!subclass`.",
			ab.Name, needed.Display))
	}

	if c.ArmedAbility != "" {
		return p.SendDM(ctx.Sender, fmt.Sprintf(
			"You already have **%s** armed. Run `!arm clear` to disarm first.",
			displayAbility(c.ArmedAbility)))
	}

	cur, _, err := getResource(ctx.Sender, ab.Resource)
	if err != nil {
		return p.SendDM(ctx.Sender, "Couldn't load resources.")
	}
	if cur < 1 {
		return p.SendDM(ctx.Sender, fmt.Sprintf(
			"No %s remaining. Take a long rest to refresh.", ab.Resource))
	}

	// Audit fix C: save the armed-ability flag FIRST. If save fails, no
	// resource was spent. If save succeeds and spend fails (rare — single
	// writer SQLite makes it nearly impossible after a successful save in
	// the same connection), revert the armed flag.
	c.ArmedAbility = ab.ID
	if err := SaveDnDCharacter(c); err != nil {
		return p.SendDM(ctx.Sender, "Couldn't save armed state: "+err.Error())
	}

	ok, err = spendResource(ctx.Sender, ab.Resource, 1)
	if err != nil || !ok {
		// Roll back the armed flag.
		c.ArmedAbility = ""
		if rerr := SaveDnDCharacter(c); rerr != nil {
			slog.Error("dnd: failed to revert armed flag after spend failure",
				"user", ctx.Sender, "err", rerr)
		}
		return p.SendDM(ctx.Sender, "Couldn't spend resource — armed state reverted.")
	}

	curAfter, max, _ := getResource(ctx.Sender, ab.Resource)
	return p.SendDM(ctx.Sender, fmt.Sprintf(
		"⚡ **%s** armed for next combat. (%s: %d/%d remaining)",
		ab.Name, ab.Resource, curAfter, max))
}

func renderArmList(c *DnDCharacter) string {
	abilities := characterActiveAbilities(c)
	var b strings.Builder
	b.WriteString("**Active Abilities** (pre-arm via `!arm <name>`)\n\n")

	if len(abilities) == 0 {
		b.WriteString("_No active abilities for your class yet._\n")
	} else {
		resType, _ := classResourceMax(c.Class)
		cur, max, _ := getResource(c.UserID, resType)
		b.WriteString(fmt.Sprintf("Resource: **%s** %d/%d\n\n", resType, cur, max))
		for _, a := range abilities {
			b.WriteString(fmt.Sprintf("  • **%s** (1 %s) — %s\n", a.Name, a.Resource, a.Description))
		}
	}
	if c.ArmedAbility != "" {
		b.WriteString(fmt.Sprintf("\n_Currently armed: **%s** (will fire on next combat)_\n", displayAbility(c.ArmedAbility)))
	}
	b.WriteString("\nResources refresh on long rest.")
	return b.String()
}

func displayAbility(id string) string {
	if a, ok := dndActiveAbilities[id]; ok {
		return a.Name
	}
	return id
}

// ── Combat hook ──────────────────────────────────────────────────────────────

// simAutoArmEnabled, when true, lets the combat builders pre-arm a
// class-default ability for any character entering combat with an empty
// ArmedAbility slot. The expedition-sim flips this on so synthetic
// players model a competent real player (who would `!arm` Second Wind
// before each fight) instead of a player who never touches the
// !arm command. Untouched in production code paths.
var simAutoArmEnabled = false

// simAutoArmDefaultFor returns the class-default ability id the sim
// should pre-arm. Defensive heals (Second Wind, Healing Word) only —
// burning a Mage's spell slot every fight would mask J2, not help it.
func simAutoArmDefaultFor(class DnDClass) string {
	switch class {
	case ClassFighter:
		return "second_wind"
	case ClassCleric:
		return "healing_word"
	}
	return ""
}

// trySimAutoArm pre-arms the class-default ability if the global toggle
// is on, no ability is currently armed, and the resource pool has at
// least one charge. Mirrors handleDnDArmCmd's spend-and-save flow.
// Returns the ability name (or "" when nothing was armed).
func trySimAutoArm(c *DnDCharacter) string {
	if !simAutoArmEnabled || c == nil || c.ArmedAbility != "" {
		return ""
	}
	id := simAutoArmDefaultFor(c.Class)
	if id == "" {
		return ""
	}
	ab, ok := dndActiveAbilities[id]
	if !ok {
		return ""
	}
	cur, _, err := getResource(c.UserID, ab.Resource)
	if err != nil || cur < 1 {
		return ""
	}
	c.ArmedAbility = ab.ID
	if err := SaveDnDCharacter(c); err != nil {
		return ""
	}
	if ok, _ := spendResource(c.UserID, ab.Resource, 1); !ok {
		c.ArmedAbility = ""
		_ = SaveDnDCharacter(c)
		return ""
	}
	return ab.Name
}

// consumeArmedAbility disarms the character, returning the ability id that was
// armed. It is the *mutating* half of arming: the flag is cleared and saved, so
// it must run exactly once per fight, at fight start.
//
// It is split from applyAbilityByID because a turn-based fight rebuilds its
// combatants from the DB on every player command. A rebuild that consumed would
// fire the ability on round 1, clear the flag, and then hand every later round a
// character with no ability at all — the player pays the resource for one round
// of a buff that is supposed to span the fight. So the fight consumes once and
// persists the id on the session; each rebuild re-applies it from there.
//
// An id that is no longer in the ability table is disarmed and reported as "".
func consumeArmedAbility(c *DnDCharacter) string {
	if c == nil || c.ArmedAbility == "" {
		return ""
	}
	armed := c.ArmedAbility
	c.ArmedAbility = ""
	_ = SaveDnDCharacter(c)
	if _, ok := dndActiveAbilities[armed]; !ok {
		return ""
	}
	return armed
}

// applyAbilityByID folds an ability's effect into a freshly-derived set of
// CombatModifiers. It is pure with respect to persistence — no DB write, no
// disarm — so it is safe to call on every rebuild of an in-flight fight.
//
// abilityID is what consumeArmedAbility returned at fight start. Empty (nobody
// armed anything) and unknown ids are both no-ops.
func applyAbilityByID(c *DnDCharacter, abilityID string, mods *CombatModifiers) (string, bool) {
	if c == nil || abilityID == "" {
		return "", false
	}
	ab, ok := dndActiveAbilities[abilityID]
	if !ok {
		return "", false
	}
	ab.Apply(c, mods)
	return ab.Name, true
}

// armAbilityForFight consumes whatever the character armed and applies it in one
// step, for the auto-resolve callers that build a combatant and immediately
// fight with it. A turn-based fight must not use this — see consumeArmedAbility.
func armAbilityForFight(c *DnDCharacter, mods *CombatModifiers) (string, bool) {
	return applyAbilityByID(c, consumeArmedAbility(c), mods)
}
