package plugin

import (
	"fmt"
	"math/rand/v2"
	"strings"

	"gogobee/internal/flavor"

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

// Phase 12 E2b — Wandering monster system & night phase resolution.
// Spec: gogobee_expedition_system.md §6.
//
// The check fires once per night during the 21:00 recap when the player
// has an active camp. Result is logged with a TwinBee narration and
// surfaced in the recap body. Combat resolution itself ("player wakes
// to combat") is deferred — expeditions don't yet host their own combat
// loop, so for E2b a triggered encounter persists as a NightEvents
// entry on the camp and a flavored "night" log line. The combat-link
// phase will read pending night encounters at !advance time.

// NightOutcome enumerates the d20 result buckets per §6.1.
type NightOutcome int

const (
	NightOutcomePeaceful NightOutcome = iota
	NightOutcomePassage
	NightOutcomeMinor
	NightOutcomeStandard
	NightOutcomeElite
	NightOutcomeAmbush
)

// NightCheck is the resolved roll + chosen monster (if any).
type NightCheck struct {
	Outcome      NightOutcome
	Roll         int    // raw d20
	Total        int    // roll + modifiers
	ThreatMod    int    // §6.1 threat modifier
	CampMod      int    // rough +3, fortified -4
	ClassMod     int    // ranger -2 (wilderness)
	MonsterID    string // bestiary id of the encounter, if any
	MonsterName  string
	Summary      string // short factual line
	Flavor       string // I voice line, may be empty
	ThreatBumped bool   // signs-of-passage adds +2 threat
}

// Class-tag for ranger discount: §6.3 "while in a wilderness zone".
// We treat outdoor / forest / underdark / feywild zones as wilderness.
var wildernessZones = map[ZoneID]bool{
	ZoneForestShadows:   true,
	ZoneSunkenTemple:    true, // tidal exterior
	ZoneUnderdark:       true,
	ZoneFeywildCrossing: true,
	ZoneDragonsLair:     true, // mountain
}

// resolveWanderingCheck rolls the d20 and applies modifiers per §6.1–6.3.
// charClass is the active character's class (used for ranger/rogue mods);
// pass "" if unknown. roll1d20 is injectable for testing.
func resolveWanderingCheck(e *Expedition, charClass DnDClass, roll1d20 func() int) NightCheck {
	if e.Camp == nil || !e.Camp.Active {
		return NightCheck{Outcome: NightOutcomePeaceful, Summary: "no camp — no night check"}
	}
	if roll1d20 == nil {
		roll1d20 = func() int { return rand.IntN(20) + 1 }
	}
	r := roll1d20()

	threatMod := 0
	if e.ThreatLevel > 30 {
		// §6.1: +1 per full 10 points above 30. Level 40 → +1; 35 → +0.
		threatMod = (e.ThreatLevel - 30) / 10
	}

	// Babysit safe-rest: an active subscription gives Standard camps the
	// fortified-tier night-risk reduction (the babysitter is watching).
	effectiveCamp := e.Camp.Type
	if effectiveCamp == CampTypeStandard && BabysitSafeRest(id.UserID(e.UserID)) {
		effectiveCamp = CampTypeFortified
	}
	campMod := 0
	switch effectiveCamp {
	case CampTypeRough:
		campMod = 3
	case CampTypeFortified:
		campMod = -4
	case CampTypeBase:
		campMod = -6
	}

	classMod := 0
	if charClass == ClassRanger && wildernessZones[e.ZoneID] {
		classMod = -2
	}

	total := r + threatMod + campMod + classMod
	out := wanderingOutcome(total)

	nc := NightCheck{
		Outcome:   out,
		Roll:      r,
		Total:     total,
		ThreatMod: threatMod,
		CampMod:   campMod,
		ClassMod:  classMod,
	}

	switch out {
	case NightOutcomePeaceful:
		nc.Summary = "Peaceful night."
	case NightOutcomePassage:
		nc.Summary = "Signs of passage near camp; no encounter."
		nc.ThreatBumped = true
	case NightOutcomeMinor, NightOutcomeStandard, NightOutcomeElite, NightOutcomeAmbush:
		mid, mname := pickWanderingMonster(e.ZoneID, out)
		nc.MonsterID = mid
		nc.MonsterName = mname
		nc.Summary = fmt.Sprintf("%s — %s.", outcomeLabel(out), describeMonsterCount(out, mname))
	}

	nc.Flavor = wanderingFlavor(out, e.ThreatLevel, e.SiegeMode)
	return nc
}

// wanderingOutcome buckets the modified total per §6.1.
func wanderingOutcome(total int) NightOutcome {
	switch {
	case total >= 20:
		return NightOutcomeAmbush
	case total >= 18:
		return NightOutcomeElite
	case total >= 15:
		return NightOutcomeStandard
	case total >= 11:
		return NightOutcomeMinor
	case total >= 6:
		return NightOutcomePassage
	default:
		return NightOutcomePeaceful
	}
}

func outcomeLabel(o NightOutcome) string {
	switch o {
	case NightOutcomePeaceful:
		return "Peaceful night"
	case NightOutcomePassage:
		return "Signs of passage"
	case NightOutcomeMinor:
		return "Minor encounter"
	case NightOutcomeStandard:
		return "Standard encounter"
	case NightOutcomeElite:
		return "Elite encounter"
	case NightOutcomeAmbush:
		return "Ambush"
	}
	return "Night check"
}

func describeMonsterCount(o NightOutcome, name string) string {
	switch o {
	case NightOutcomeMinor:
		return fmt.Sprintf("1–2 %s wake you", pluralize(name))
	case NightOutcomeStandard:
		return fmt.Sprintf("a group of %s closes in", pluralize(name))
	case NightOutcomeElite:
		return fmt.Sprintf("an elite %s steps out of the dark", name)
	case NightOutcomeAmbush:
		return fmt.Sprintf("an elite %s catches you mid-sleep — surprise round", name)
	}
	return name
}

func pluralize(name string) string {
	lower := strings.ToLower(name)
	switch {
	case strings.HasSuffix(lower, "s"), strings.HasSuffix(lower, "x"), strings.HasSuffix(lower, "z"):
		return name + "es"
	default:
		return name + "s"
	}
}

// pickWanderingMonster pulls a roster entry from the zone, biased by elite
// flag for elite/ambush outcomes. Returns "", "" if the zone has no roster.
func pickWanderingMonster(zoneID ZoneID, o NightOutcome) (string, string) {
	zone, ok := getZone(zoneID)
	if !ok || len(zone.Enemies) == 0 {
		return "", ""
	}
	wantElite := o == NightOutcomeElite || o == NightOutcomeAmbush
	var pool []ZoneEnemy
	for _, en := range zone.Enemies {
		if en.IsElite == wantElite {
			pool = append(pool, en)
		}
	}
	if len(pool) == 0 {
		// Fallback: any enemy regardless of elite flag.
		pool = zone.Enemies
	}
	// Weighted pick by SpawnWeight; default 5 if zero.
	totalW := 0
	for _, en := range pool {
		w := en.SpawnWeight
		if w <= 0 {
			w = 5
		}
		totalW += w
	}
	pickW := rand.IntN(totalW) + 1
	cum := 0
	for _, en := range pool {
		w := en.SpawnWeight
		if w <= 0 {
			w = 5
		}
		cum += w
		if pickW <= cum {
			if m, ok := dndBestiary[en.BestiaryID]; ok {
				return en.BestiaryID, m.Name
			}
			return en.BestiaryID, en.BestiaryID
		}
	}
	return "", ""
}

// wanderingFlavor picks a TwinBee line from the existing ThreatClock pools
// when the band's voice fits the outcome; falls back to "" so the recap
// renders just the factual summary.
func wanderingFlavor(o NightOutcome, threat int, siege bool) string {
	if o == NightOutcomePeaceful || o == NightOutcomePassage {
		return ""
	}
	band := threatBandFor(threat, siege)
	switch band {
	case ThreatBandSiege:
		return flavor.Pick(flavor.ThreatClockSiege)
	case ThreatBandHostile:
		return flavor.Pick(flavor.ThreatClockHostile)
	case ThreatBandAlert:
		return flavor.Pick(flavor.ThreatClockAlert)
	case ThreatBandStirring:
		return flavor.Pick(flavor.ThreatClockStirring)
	}
	return ""
}

// renderNightCheck formats a recap-bottom block for the wandering result.
func renderNightCheck(nc NightCheck) string {
	var b strings.Builder
	b.WriteString(fmt.Sprintf("🌙 **Night phase — %s**\n", outcomeLabel(nc.Outcome)))
	if nc.Summary != "" {
		b.WriteString(nc.Summary + "\n")
	}
	b.WriteString(fmt.Sprintf("_d20=%d, total=%d (threat%+d, camp%+d, class%+d)_\n",
		nc.Roll, nc.Total, nc.ThreatMod, nc.CampMod, nc.ClassMod))
	if nc.Flavor != "" {
		b.WriteString("\n" + nc.Flavor + "\n")
	}
	if nc.Outcome == NightOutcomeMinor || nc.Outcome == NightOutcomeStandard ||
		nc.Outcome == NightOutcomeElite || nc.Outcome == NightOutcomeAmbush {
		b.WriteString("\n_Encounter pending — resolves at your next `!advance`._\n")
	}
	return b.String()
}

// processNightCheck applies side-effects: log entry, threat bump for
// signs-of-passage, persist NightEvents on the camp.
func processNightCheck(e *Expedition, nc NightCheck) error {
	summary := nc.Summary
	if nc.MonsterName != "" {
		summary = fmt.Sprintf("%s [d20=%d, total=%d, mods: threat%+d camp%+d class%+d]",
			summary, nc.Roll, nc.Total, nc.ThreatMod, nc.CampMod, nc.ClassMod)
	} else {
		summary = fmt.Sprintf("%s [d20=%d, total=%d]", summary, nc.Roll, nc.Total)
	}
	if err := appendExpeditionLog(e.ID, e.CurrentDay, "night", summary, nc.Flavor); err != nil {
		return err
	}
	if nc.ThreatBumped {
		_ = applyThreatDelta(e.ID, 2, "signs of passage near camp")
	}
	if e.Camp != nil {
		e.Camp.NightEvents = append(e.Camp.NightEvents, summary)
		if err := updateCamp(e.ID, e.Camp); err != nil {
			return err
		}
	}
	return nil
}
