package tui

import (
	"fmt"
	"math/rand"
	"strings"
	"time"
)

// This file implements the "working" spinner shown while an agent run is in
// flight, mirroring Claude Code's animated status line: a cycling asterisk
// glyph, a whimsical present-progressive verb ("Whirring…"), and a live stats
// readout — elapsed wall-clock time, an estimate of streamed output tokens, and
// the configured thinking effort. It renders on the row just above the input
// while running and disappears when the run ends.

// spinnerTickMsg advances the spinner animation. The model re-issues a tick
// after each frame while a run is in flight and lets the tick lapse once the run
// ends, so the animation stops without a running goroutine.
type spinnerTickMsg time.Time

// spinnerInterval is the frame cadence. ~120ms is brisk enough to read as motion
// without churning the render loop.
const spinnerInterval = 120 * time.Millisecond

// verbRerollFrames re-picks the verb roughly every this many frames (~5s) so a
// long run cycles through several verbs the way Claude Code does.
const verbRerollFrames = 40

// spinnerFrames is the asterisk animation cycled one glyph per tick. The glyphs
// grow from a dim dot to a full star and back, reading as a pulsing sparkle.
var spinnerFrames = []string{"·", "✢", "✳", "∗", "✺", "✻", "✽", "✻", "✺", "∗", "✳", "✢"}

// spinner is the animated working indicator. It is a plain value held by the
// Model: begin() arms it at run start, advance() steps the frame on each tick,
// addTokens() grows the streamed-token estimate, and view() renders the line.
type spinner struct {
	theme    Theme
	running  bool
	frame    int
	verb     string
	start    time.Time
	chars    int    // runes streamed this run (the token estimate divides this)
	thinking string // thinking-effort label, e.g. "medium"; "" hides that stat
	pinned   string // when set, overrides the random verb and stops re-rolling
}

// newSpinner builds an idle spinner bound to the theme.
func newSpinner(theme Theme) spinner {
	return spinner{theme: theme}
}

// begin arms the spinner for a fresh run: it records the start time, picks the
// first verb, resets the frame and token estimate, and stores the thinking-effort
// label to show in the stats.
func (s *spinner) begin(now time.Time, thinking string) {
	s.running = true
	s.frame = 0
	s.start = now
	s.chars = 0
	s.thinking = thinking
	s.verb = randomVerb()
	s.pinned = ""
}

// pin fixes the spinner label to a specific phrase (e.g. "Compacting
// conversation") and stops verb re-rolling until unpin, so a long-running phase
// reads as one steady message rather than cycling words.
func (s *spinner) pin(label string) { s.pinned = label }

// unpin restores the normal cycling verb after a pinned phase ends.
func (s *spinner) unpin() { s.pinned = "" }

// stop parks the spinner when a run ends so view() renders nothing.
func (s *spinner) stop() { s.running = false }

// advance steps the animation one frame and periodically re-rolls the verb so a
// long run does not sit on one word.
func (s *spinner) advance() {
	s.frame++
	if s.pinned == "" && s.frame%verbRerollFrames == 0 {
		s.verb = randomVerb()
	}
}

// addTokens folds a streamed text delta into the running output-token estimate.
// The count is approximate (≈4 chars per token) — enough for a live spinner
// readout, not billing.
func (s *spinner) addTokens(delta string) {
	s.chars += len([]rune(delta))
}

// view renders the spinner line, e.g. "✻ Whirring… (1m 54s · ↓ 242 tokens ·
// medium effort)". It returns "" when not running or before a width is known.
// The glyph and verb take the accent color; the parenthetical stats are dim.
func (s spinner) view(width int) string {
	if !s.running || width <= 0 {
		return ""
	}
	glyph := spinnerFrames[s.frame%len(spinnerFrames)]
	verb := s.verb
	if s.pinned != "" {
		verb = s.pinned
	}
	head := s.theme.Spinner.Render(glyph + " " + verb + "…")

	var stats strings.Builder
	fmt.Fprintf(&stats, "%s", formatElapsed(time.Since(s.start)))
	if tokens := s.chars / 4; tokens > 0 {
		fmt.Fprintf(&stats, " · ↓ %s tokens", humanizeInt(tokens))
	}
	if s.thinking != "" {
		fmt.Fprintf(&stats, " · %s effort", s.thinking)
	}
	line := head + " " + s.theme.System.Render("("+stats.String()+")")
	return TruncateToWidth(line, width)
}

// randomVerb picks one of the built-in present-progressive verbs.
func randomVerb() string {
	return spinnerVerbs[rand.Intn(len(spinnerVerbs))]
}

// formatElapsed renders a duration compactly: "42s", "1m 54s", or "1h 2m".
func formatElapsed(d time.Duration) string {
	if d < 0 {
		d = 0
	}
	secs := int(d.Seconds())
	if secs < 60 {
		return fmt.Sprintf("%ds", secs)
	}
	mins := secs / 60
	secs %= 60
	if mins < 60 {
		return fmt.Sprintf("%dm %ds", mins, secs)
	}
	hours := mins / 60
	mins %= 60
	return fmt.Sprintf("%dh %dm", hours, mins)
}

// spinnerVerbs is Claude Code's 185 built-in spinner verbs (present-progressive
// flavor words shown while working). Sourced from the community catalog at
// github.com/wynandw87/claude-code-spinner-verbs.
var spinnerVerbs = []string{
	"Accomplishing", "Actioning", "Actualizing", "Architecting", "Baking",
	"Beaming", "Beboppin'", "Befuddling", "Billowing", "Blanching",
	"Bloviating", "Boogieing", "Boondoggling", "Booping", "Bootstrapping",
	"Brewing", "Burrowing", "Calculating", "Canoodling", "Caramelizing",
	"Cascading", "Catapulting", "Cerebrating", "Channeling", "Channelling",
	"Choreographing", "Churning", "Clauding", "Coalescing", "Cogitating",
	"Combobulating", "Composing", "Computing", "Concocting", "Considering",
	"Contemplating", "Cooking", "Crafting", "Creating", "Crunching",
	"Crystallizing", "Cultivating", "Deciphering", "Deliberating", "Determining",
	"Dilly-dallying", "Discombobulating", "Doing", "Doodling", "Drizzling",
	"Ebbing", "Effecting", "Elucidating", "Embellishing", "Enchanting",
	"Envisioning", "Evaporating", "Fermenting", "Fiddle-faddling", "Finagling",
	"Flambeing", "Flibbertigibbeting", "Flowing", "Flummoxing", "Fluttering",
	"Forging", "Forming", "Frolicking", "Frosting", "Gallivanting",
	"Galloping", "Garnishing", "Generating", "Germinating", "Gitifying",
	"Grooving", "Gusting", "Harmonizing", "Hashing", "Hatching",
	"Herding", "Honking", "Hullaballooing", "Hyperspacing", "Ideating",
	"Imagining", "Improvising", "Incubating", "Inferring", "Infusing",
	"Ionizing", "Jitterbugging", "Julienning", "Kneading", "Leavening",
	"Levitating", "Lollygagging", "Manifesting", "Marinating", "Meandering",
	"Metamorphosing", "Misting", "Moonwalking", "Moseying", "Mulling",
	"Mustering", "Musing", "Nebulizing", "Nesting", "Newspapering",
	"Noodling", "Nucleating", "Orbiting", "Orchestrating", "Osmosing",
	"Perambulating", "Percolating", "Perusing", "Philosophising", "Photosynthesizing",
	"Pollinating", "Pondering", "Pontificating", "Pouncing", "Precipitating",
	"Prestidigitating", "Processing", "Proofing", "Propagating", "Puttering",
	"Puzzling", "Quantumizing", "Razzle-dazzling", "Razzmatazzing", "Recombobulating",
	"Reticulating", "Roosting", "Ruminating", "Sauteing", "Scampering",
	"Schlepping", "Scurrying", "Seasoning", "Shenaniganing", "Shimmying",
	"Simmering", "Skedaddling", "Sketching", "Slithering", "Smooshing",
	"Sock-hopping", "Spelunking", "Spinning", "Sprouting", "Stewing",
	"Sublimating", "Swirling", "Swooping", "Symbioting", "Synthesizing",
	"Tempering", "Thinking", "Thundering", "Tinkering", "Tomfoolering",
	"Topsy-turvying", "Transfiguring", "Transmuting", "Twisting", "Undulating",
	"Unfurling", "Unravelling", "Vibing", "Waddling", "Wandering",
	"Warping", "Whatchamacalliting", "Whirlpooling", "Whirring", "Whisking",
	"Wibbling", "Working", "Wrangling", "Zesting", "Zigzagging",
}

