package plugin

import (
	"fmt"
	"hash/fnv"
	"math/rand/v2"
	"strings"
)

// Phase 11 D2a — Tier 1 trap catalog. Implements `gogobee_dungeon_zones.md`
// §6 (Environmental Traps) for tier 1, replacing the flat-percent HP nick
// shipped in D1e with proper trap kinds.
//
// Each trap defines a detect skill + DC, optional damage dice, and an
// "alerts" flag for tripwire-style hazards that don't deal damage. Higher
// tiers add their own trap pools in later sub-phases (D3a/D4a).
//
// Selection is deterministic on (runID, roomIdx, "trap") so re-reading
// !zone status after a trap room resolves yields the same trap.

type ZoneTrapKind string

const (
	TrapPit            ZoneTrapKind = "pit"
	TrapTripwireAlarm  ZoneTrapKind = "tripwire_alarm"
	TrapPoisonDart     ZoneTrapKind = "poison_dart"
	TrapFlameJet       ZoneTrapKind = "flame_jet"
	TrapCollapsingCeil ZoneTrapKind = "collapsing_ceiling"
	TrapGlyphOfWarding ZoneTrapKind = "glyph_of_warding"
	TrapAntimagicField ZoneTrapKind = "antimagic_field"
	TrapNecroticMiasma ZoneTrapKind = "necrotic_miasma"
	TrapSymbolOfDeath  ZoneTrapKind = "symbol_of_death"
	TrapPlanarRift     ZoneTrapKind = "planar_rift"
)

// zoneTrapDef — static definition of a zone trap. Damage dice are in
// (count, sides) form; AlertsEnemies flags traps that have no damage
// but raise the alarm. Weight biases the room-level random pick.
type zoneTrapDef struct {
	Kind            ZoneTrapKind
	Display         string
	Trigger         string // short flavor descriptor for the trip narration
	DetectSkill     DnDSkill
	DetectDC        int
	DamageDiceN     int    // 0 if no direct damage
	DamageDiceD     int    // sides
	DamageType      string // "bludgeoning", "piercing", "poison", ...
	AlertsEnemies   bool   // tripwire-class: 0 damage, raises alarm
	SuppressesMagic bool   // antimagic-class: 0 damage, narrative suppression
	Tier            ZoneTier
	Weight          int // 1..10, default 5
}

// tier1TrapCatalog — design doc §6 Tier 1 table.
var tier1TrapCatalog = []zoneTrapDef{
	{
		Kind:        TrapPit,
		Display:     "Pit Trap",
		Trigger:     "the floor gives way",
		DetectSkill: SkillPerception,
		DetectDC:    12,
		DamageDiceN: 2,
		DamageDiceD: 6,
		DamageType:  "bludgeoning",
		Tier:        ZoneTierBeginner,
		Weight:      5,
	},
	{
		Kind:          TrapTripwireAlarm,
		Display:       "Tripwire Alarm",
		Trigger:       "a wire snags your ankle and a clatter of bells erupts",
		DetectSkill:   SkillPerception,
		DetectDC:      10,
		AlertsEnemies: true,
		Tier:          ZoneTierBeginner,
		Weight:        4,
	},
	{
		Kind:        TrapPoisonDart,
		Display:     "Poison Dart",
		Trigger:     "a dart hisses out of a hole you didn't see",
		DetectSkill: SkillInvestigation,
		DetectDC:    12,
		DamageDiceN: 2, // 1d4 piercing + 1d4 simulated poison flattened to 2d4
		DamageDiceD: 4,
		DamageType:  "piercing+poison",
		Tier:        ZoneTierBeginner,
		Weight:      3,
	},
}

// tier2TrapCatalog — design doc §6 Tier 2 table. Save mechanics ("DEX DC X
// half") are not modeled at this layer; full dice resolve on a failed
// detect, matching the Tier 1 convention.
var tier2TrapCatalog = []zoneTrapDef{
	{
		Kind:        TrapFlameJet,
		Display:     "Flame Jet",
		Trigger:     "a pressure plate sinks under your boot and gouts of flame roar up",
		DetectSkill: SkillPerception,
		DetectDC:    14,
		DamageDiceN: 3,
		DamageDiceD: 6,
		DamageType:  "fire",
		Tier:        ZoneTierApprentice,
		Weight:      5,
	},
	{
		Kind:        TrapCollapsingCeil,
		Display:     "Collapsing Ceiling",
		Trigger:     "a rigged support snaps and a slab of ceiling crashes down",
		DetectSkill: SkillInvestigation,
		DetectDC:    14,
		DamageDiceN: 4,
		DamageDiceD: 6,
		DamageType:  "bludgeoning",
		Tier:        ZoneTierApprentice,
		Weight:      4,
	},
	{
		Kind:        TrapGlyphOfWarding,
		Display:     "Glyph of Warding",
		Trigger:     "a sigil flares on the doorframe and arcs of lightning lash out",
		DetectSkill: SkillArcana,
		DetectDC:    14,
		DamageDiceN: 5,
		DamageDiceD: 8,
		DamageType:  "lightning",
		Tier:        ZoneTierApprentice,
		Weight:      3,
	},
}

// tier3TrapCatalog — design doc §6 Tier 3–5 table. The doc lumps all
// high-tier traps into a single list, so tiers 3, 4, and 5 share this
// pool. Two of the four entries (Antimagic Field, Symbol of Death) have
// effects that don't fit the dice-only damage model:
//
//   - Antimagic Field: no damage; SuppressesMagic flag flips the
//     narration to a non-damaging suppression beat. The actual
//     spell-suppression mechanic is left to a later phase — for now
//     the line tells the player magic is choked in this room.
//   - Necrotic Miasma: design doc says "6d6 + max HP -10 until long
//     rest". The max-HP debuff isn't modeled here; trap delivers the
//     6d6 necrotic damage only.
//   - Symbol of Death: doc reads "INT DC 20 or drop to 0 HP". We
//     approximate with 8d10 psychic; the existing near-KO cap in
//     applyTrapEffectWithDetect prevents an outright kill.
//   - Planar Rift: 4d10 psychic. The teleport-to-random-room mechanic
//     isn't modeled — trap just deals psychic damage for now.
var tier3TrapCatalog = []zoneTrapDef{
	{
		Kind:            TrapAntimagicField,
		Display:         "Antimagic Field",
		Trigger:         "the air thickens and every charm at your belt goes cold",
		DetectSkill:     SkillArcana,
		DetectDC:        18,
		SuppressesMagic: true,
		Tier:            ZoneTierJourneyman,
		Weight:          3,
	},
	{
		Kind:        TrapNecroticMiasma,
		Display:     "Necrotic Miasma",
		Trigger:     "the sarcophagus lid shifts and a green-black fume pours out",
		DetectSkill: SkillPerception,
		DetectDC:    16,
		DamageDiceN: 6,
		DamageDiceD: 6,
		DamageType:  "necrotic",
		Tier:        ZoneTierJourneyman,
		Weight:      5,
	},
	{
		Kind:        TrapSymbolOfDeath,
		Display:     "Symbol of Death",
		Trigger:     "an inscribed sigil flares and your vision blanks to white",
		DetectSkill: SkillInvestigation,
		DetectDC:    20,
		DamageDiceN: 8,
		DamageDiceD: 10,
		DamageType:  "psychic",
		Tier:        ZoneTierJourneyman,
		Weight:      2,
	},
	{
		Kind:        TrapPlanarRift,
		Display:     "Planar Rift",
		Trigger:     "the floor folds wrong and a tear of starless dark grasps at you",
		DetectSkill: SkillArcana,
		DetectDC:    20,
		DamageDiceN: 4,
		DamageDiceD: 10,
		DamageType:  "psychic",
		Tier:        ZoneTierJourneyman,
		Weight:      3,
	},
}

// trapsByTier returns all traps registered at the given tier. Tiers 3,
// 4, and 5 share the Tier 3 catalog per the design doc's combined
// "Tier 3–5 Traps" table; later phases may split them.
func trapsByTier(t ZoneTier) []zoneTrapDef {
	switch t {
	case ZoneTierBeginner:
		return tier1TrapCatalog
	case ZoneTierApprentice:
		return tier2TrapCatalog
	case ZoneTierJourneyman, ZoneTierVeteran, ZoneTierLegendary:
		return tier3TrapCatalog
	}
	return nil
}

// pickZoneTrap selects a trap deterministically from the zone's tier
// catalog based on (runID, roomIdx). Returns ok=false if no traps are
// registered for the tier (defensive — Tier 1 always has entries).
func pickZoneTrap(zone ZoneDefinition, runID string, roomIdx int) (zoneTrapDef, bool) {
	pool := trapsByTier(zone.Tier)
	if len(pool) == 0 {
		return zoneTrapDef{}, false
	}
	total := 0
	for _, tr := range pool {
		w := tr.Weight
		if w <= 0 {
			w = 5
		}
		total += w
	}
	roll := int(zoneTrapSelectorHash(runID, roomIdx) % uint64(total))
	cum := 0
	for _, tr := range pool {
		w := tr.Weight
		if w <= 0 {
			w = 5
		}
		cum += w
		if roll < cum {
			return tr, true
		}
	}
	return pool[len(pool)-1], true
}

// zoneTrapSelectorHash — separate namespace from enemy/loot hashes so a
// trap pick on the same (run, room) doesn't collide with their seeds.
func zoneTrapSelectorHash(runID string, roomIdx int) uint64 {
	h := fnv.New64a()
	h.Write([]byte("trap:"))
	h.Write([]byte(runID))
	var sb [8]byte
	for i := 0; i < 8; i++ {
		sb[i] = byte(roomIdx >> (8 * i))
	}
	h.Write(sb[:])
	return h.Sum64()
}

// rollTrapDamage rolls a trap's damage dice with a seeded RNG so the
// same (run, room) yields the same number each read. Returns 0 for
// alert-only traps (Tripwire). DamageType "piercing+poison" is treated
// as a single combined dice pool — we don't model the Poisoned condition
// here; the design-doc CON DC 11 save is folded into the same dice pool
// as a flattened average.
func rollTrapDamage(tr zoneTrapDef, runID string, roomIdx int) int {
	if tr.AlertsEnemies || tr.SuppressesMagic || tr.DamageDiceN == 0 || tr.DamageDiceD == 0 {
		return 0
	}
	rng := rand.New(rand.NewPCG(zoneTrapSelectorHash(runID, roomIdx), 0x712D2A))
	total := 0
	for i := 0; i < tr.DamageDiceN; i++ {
		total += rng.IntN(tr.DamageDiceD) + 1
	}
	return total
}

// trapDamageHeader builds the user-facing one-liner for a damaging trap
// trip — "💢 Pit Trap (2d6 bludgeoning) — 9 HP". Pure formatter.
func trapDamageHeader(tr zoneTrapDef, dmg int, hpCur, hpMax int) string {
	if tr.AlertsEnemies {
		return fmt.Sprintf("🔔 **%s** — %s. The next room knows you're here.", tr.Display, tr.Trigger)
	}
	if tr.SuppressesMagic {
		return fmt.Sprintf("🚫 **%s** — %s. Spells fizzle and enchanted gear sleeps until you leave the room.", tr.Display, tr.Trigger)
	}
	dice := fmt.Sprintf("%dd%d", tr.DamageDiceN, tr.DamageDiceD)
	// Lead with the trap's own mechanism-specific Trigger so the narrative
	// matches the trap kind (e.g. a Collapsing Ceiling no longer reads as
	// a dart in the air). Trigger is required on every catalog entry.
	return fmt.Sprintf("💢 **%s** — %s. (%s %s) — **%d HP** (%d/%d remaining).",
		tr.Display, tr.Trigger, dice, tr.DamageType, dmg, hpCur, hpMax)
}

// trapSpottedHeader — narration for a successful detection roll. No
// damage; the player gets a beat of competence flavor.
func trapSpottedHeader(tr zoneTrapDef, res SkillCheckResult) string {
	var b strings.Builder
	b.WriteString(fmt.Sprintf("👁️ **%s spotted** — ", tr.Display))
	info, _ := skillInfo(tr.DetectSkill)
	b.WriteString(fmt.Sprintf("%s check %d vs DC %d.", info.Display, res.Total, tr.DetectDC))
	return b.String()
}
