package tui

import (
	"fmt"
	"os"
	"strings"

	"charm.land/lipgloss/v2"

	"github.com/smallnest/pigo/internal/cli/ui"
)

// statusBar renders the persistent bottom line described in the SPEC (US-003,
// Section 5.1): model name, thinking level, cwd (with $HOME abbreviated to ~),
// git branch + dirty/ahead markers, context-usage %, and the current task text.
// It holds no styling of its own beyond the Theme's StatusBar style; all width
// fitting is done against ui.Width so CJK/emoji count as two columns.
//
// The component is a plain value: Update-side code copies it into the Model,
// mutates the exported-to-package snapshot fields via the setters, and calls
// Render(width) from View. It never performs I/O — the git probe lives in
// gitinfo.go and feeds it via SetGit.
type statusBar struct {
	theme Theme

	// Static-ish config sourced from Options.
	model    string
	thinking string

	// cwd is the launch directory with $HOME already abbreviated to "~".
	cwd string

	// git is the latest probe result; rendered only when git.ok is true.
	git gitInfoMsg

	// contextPct is the latest context-window utilization in percent [0,100],
	// derived from telemetryMsg (ContextUtilization * 100). -1 means unknown, so
	// the segment is hidden until the first telemetry arrives.
	contextPct int

	// tokens is the most recently observed context-token count (ContextTokens),
	// shown alongside the percentage. 0 means unknown/not yet reported.
	tokens int

	// task is the current activity text (e.g. the running tool or turn state).
	task string
}

// newStatusBar builds a status bar from the theme, resolved Options, and the
// launch directory. contextPct starts at -1 (unknown) so the token segment stays
// hidden until telemetry arrives.
func newStatusBar(theme Theme, opts Options, cwd string) statusBar {
	return statusBar{
		theme:      theme,
		model:      opts.Model,
		thinking:   string(opts.ThinkingLevel),
		cwd:        abbreviateHome(cwd),
		contextPct: -1,
	}
}

// SetGit stores the latest git probe result.
func (s *statusBar) SetGit(g gitInfoMsg) { s.git = g }

// SetModel updates the displayed model name after a /model switch.
func (s *statusBar) SetModel(model string) { s.model = model }

// SetThinking updates the displayed reasoning-effort level after a /think switch.
func (s *statusBar) SetThinking(level string) { s.thinking = level }

// SetTelemetry updates the context-usage percentage from a telemetry event.
// A zero/unknown window (ContextWindow == 0) leaves the segment hidden.
func (s *statusBar) SetTelemetry(ev telemetryEventView) {
	if ev.window <= 0 {
		s.contextPct = -1
		s.tokens = 0
		return
	}
	pct := int(ev.util*100 + 0.5)
	if pct < 0 {
		pct = 0
	}
	if pct > 100 {
		pct = 100
	}
	s.contextPct = pct
	if ev.tokens > 0 {
		s.tokens = ev.tokens
	}
}

// SetTask records the current activity text shown at the far right / high
// priority slot of the bar.
func (s *statusBar) SetTask(task string) { s.task = task }

// telemetryEventView is the minimal projection of agentcore.TelemetryEvent the
// status bar needs, so the caller (model.go) adapts the event rather than this
// file depending on agentcore directly for a two-field read.
type telemetryEventView struct {
	util   float64
	window int
	tokens int
}

// appName is the badge shown at the far left of the bar.
const appName = "pigo"

// Glyphs prefixing each segment plus the powerline separator, matching the
// decorated Claude-Code-plugin look. The segment icons are common Unicode; the
// separator (sepArrow) is a powerline glyph in the private-use area that Nerd
// Fonts and most modern terminal fonts render. Each measures one display column
// and ui.Width accounts for it during truncation.
const (
	glyphGit   = "⎇" // git branch
	glyphDirty = "●" // uncommitted changes
	glyphAhead = "⇡" // commits ahead of upstream
	glyphModel = "✱" // model name
	glyphThink = "✽" // thinking level
	glyphCwd   = "▸" // working directory
	glyphCtx   = "◔" // context-window usage
	glyphTask  = "⏵" // current activity

	sepArrow = "" // filled right arrow — used at a background transition
)

// Powerline palette (ANSI 256-color cube, so it renders without true-color).
// Every segment is its own colored block. The arrow between two segments is
// drawn in the LEFT block's background color so it reads as that item's color
// spilling into the next; a closing arrow caps the final block back to the bar.
const (
	sbBarBg = "236" // bar background behind the trailing pad

	sbAppFg = "233" // app badge text (dark, on light gray)
	sbAppBg = "252" // app badge block (light gray)

	sbGitFg = "231" // git text
	sbGitBg = "65"  // git block (muted green)

	sbModelFg = "231" // model text
	sbModelBg = "97"  // model block (muted purple)

	sbThinkFg = "231" // thinking text
	sbThinkBg = "60"  // thinking block (slate)

	sbCwdFg = "231" // cwd text
	sbCwdBg = "67"  // cwd block (steel blue)

	sbCtxFg = "236" // context text (dark, on amber)
	sbCtxBg = "179" // context block (amber/gold)

	sbTaskFg = "231" // task text
	sbTaskBg = "131" // task block (muted terracotta)
)

// segment is one labelled field of the bar together with its colors and
// truncation priority. bg == "" means the segment sits on the bar background;
// a non-empty bg gives it a filled powerline block. Higher priority survives
// longer when the terminal is too narrow.
type segment struct {
	text     string
	fg       string
	bg       string // "" => bar background
	priority int    // larger = kept longer under truncation
}

// Priority order (SPEC: task > model/app > token > git > cwd). Higher is more
// important and dropped/truncated last.
const (
	prioCwd   = 0
	prioGit   = 1
	prioToken = 2
	prioModel = 3
	prioApp   = 3 // the app badge rides at the model tier
	prioTask  = 4
)

// Render lays the bar out to exactly the configured width as a colored powerline
// ribbon. Each segment is a filled block joined to the next by an arrow drawn in
// the left block's background color, and the tail is padded with the bar
// background so the whole row is filled. When the ribbon would exceed the width
// it drops whole segments from lowest to highest priority; if even the single
// highest-priority segment still overflows it hard-truncates that segment's
// text. The rendered row's display width (ui.Width, which ignores ANSI) is
// always exactly width for width > 0; a non-positive width yields the empty
// string.
func (s statusBar) Render(width int) string {
	if width <= 0 {
		return ""
	}

	segs := s.segments()
	for len(segs) > 0 {
		ribbon, w := renderRibbon(segs)
		if w <= width {
			return ribbon + barPad(width-w)
		}
		if len(segs) == 1 {
			break
		}
		segs = dropLowest(segs)
	}

	// Even a single highest-priority segment overflows: hard-truncate its text
	// onto the bar background (no separators, so width stays bounded).
	txt := TruncateToWidth(s.highestText(), width)
	base := lipgloss.NewStyle().Foreground(lipgloss.Color(sbAppFg)).Background(lipgloss.Color(sbBarBg))
	return base.Render(txt) + barPad(width-ui.Width(txt))
}

// renderRibbon builds the styled powerline string for segs and returns it with
// its visible width (excluding ANSI). The left edge starts at the first
// segment's background; a closing arrow caps any trailing block back to the bar.
func renderRibbon(segs []segment) (string, int) {
	resolve := func(bg string) string {
		if bg == "" {
			return sbBarBg
		}
		return bg
	}

	var b strings.Builder
	vis := 0
	for i, seg := range segs {
		curBg := resolve(seg.bg)
		if i > 0 {
			// The separator arrow is filled with the LEFT block's background, so
			// it matches the item it flows out of, sitting on the next block's bg.
			leftBg := resolve(segs[i-1].bg)
			b.WriteString(lipgloss.NewStyle().
				Foreground(lipgloss.Color(leftBg)).
				Background(lipgloss.Color(curBg)).
				Render(sepArrow))
			vis += ui.Width(sepArrow)
		}
		content := " " + seg.text + " "
		b.WriteString(lipgloss.NewStyle().
			Foreground(lipgloss.Color(seg.fg)).
			Background(lipgloss.Color(curBg)).
			Render(content))
		vis += ui.Width(content)
	}

	// Cap a trailing colored block with an arrow back to the bar background.
	if lastBg := resolve(segs[len(segs)-1].bg); lastBg != sbBarBg {
		b.WriteString(lipgloss.NewStyle().
			Foreground(lipgloss.Color(lastBg)).
			Background(lipgloss.Color(sbBarBg)).
			Render(sepArrow))
		vis += ui.Width(sepArrow)
	}
	return b.String(), vis
}

// barPad returns n spaces painted with the bar background so the row fills the
// full terminal width. n <= 0 yields the empty string.
func barPad(n int) string {
	if n <= 0 {
		return ""
	}
	return lipgloss.NewStyle().
		Background(lipgloss.Color(sbBarBg)).
		Render(strings.Repeat(" ", n))
}

// segments builds the ordered list of visible segments. Order in the slice is
// the left-to-right display order; priority governs truncation, not position.
func (s statusBar) segments() []segment {
	var segs []segment

	// App badge leads the bar as a filled block.
	segs = append(segs, segment{text: appName, fg: sbAppFg, bg: sbAppBg, priority: prioApp})

	if s.git.ok {
		segs = append(segs, segment{text: s.gitText(), fg: sbGitFg, bg: sbGitBg, priority: prioGit})
	}
	if s.model != "" {
		segs = append(segs, segment{text: glyphModel + " " + s.model, fg: sbModelFg, bg: sbModelBg, priority: prioModel})
	}
	if s.thinking != "" {
		// Thinking rides with the model priority — it is cheap and contextual.
		segs = append(segs, segment{text: glyphThink + " " + s.thinking, fg: sbThinkFg, bg: sbThinkBg, priority: prioModel})
	}
	if s.cwd != "" {
		segs = append(segs, segment{text: glyphCwd + " " + s.cwd, fg: sbCwdFg, bg: sbCwdBg, priority: prioCwd})
	}
	if s.contextPct >= 0 {
		// The context readout is the highlighted amber block on the right.
		segs = append(segs, segment{text: s.ctxText(), fg: sbCtxFg, bg: sbCtxBg, priority: prioToken})
	}
	if s.task != "" {
		segs = append(segs, segment{text: glyphTask + " " + s.task, fg: sbTaskFg, bg: sbTaskBg, priority: prioTask})
	}
	return segs
}

// gitText formats the git segment, e.g. "⎇ master ●3 ⇡4": branch, then "●N" for
// N dirty entries and "⇡N" for N commits ahead, each shown only when non-zero.
func (s statusBar) gitText() string {
	var b strings.Builder
	b.WriteString(glyphGit + " " + s.git.branch)
	if s.git.dirty > 0 {
		fmt.Fprintf(&b, " %s%d", glyphDirty, s.git.dirty)
	}
	if s.git.ahead > 0 {
		fmt.Fprintf(&b, " %s%d", glyphAhead, s.git.ahead)
	}
	return b.String()
}

// ctxText formats the context segment, e.g. "◔ 90,866 (46%)" when the token
// count is known, or "◔ 46%" before the first token count arrives.
func (s statusBar) ctxText() string {
	if s.tokens > 0 {
		return fmt.Sprintf("%s %s (%d%%)", glyphCtx, humanizeInt(s.tokens), s.contextPct)
	}
	return fmt.Sprintf("%s %d%%", glyphCtx, s.contextPct)
}

// humanizeInt renders n with thousands separators, e.g. 90866 → "90,866".
func humanizeInt(n int) string {
	s := fmt.Sprintf("%d", n)
	neg := strings.HasPrefix(s, "-")
	if neg {
		s = s[1:]
	}
	var b strings.Builder
	for i, r := range s {
		if i > 0 && (len(s)-i)%3 == 0 {
			b.WriteByte(',')
		}
		b.WriteRune(r)
	}
	if neg {
		return "-" + b.String()
	}
	return b.String()
}

// highestText returns the text of the highest-priority segment, used as the last
// thing standing when the terminal cannot even fit one full segment.
func (s statusBar) highestText() string {
	segs := s.segments()
	if len(segs) == 0 {
		return ""
	}
	best := segs[0]
	for _, seg := range segs[1:] {
		if seg.priority > best.priority {
			best = seg
		}
	}
	return best.text
}

// dropLowest removes one occurrence of the lowest-priority segment, preserving
// display order among the rest. It returns the shortened slice.
func dropLowest(segs []segment) []segment {
	if len(segs) == 0 {
		return segs
	}
	lowIdx := 0
	for i, seg := range segs {
		if seg.priority < segs[lowIdx].priority {
			lowIdx = i
		}
	}
	out := make([]segment, 0, len(segs)-1)
	out = append(out, segs[:lowIdx]...)
	out = append(out, segs[lowIdx+1:]...)
	return out
}

// abbreviateHome replaces a leading $HOME in path with "~" so the status bar
// stays compact. It leaves paths outside $HOME untouched and never fails.
func abbreviateHome(path string) string {
	home := homeDir()
	if home == "" || path == "" {
		return path
	}
	if path == home {
		return "~"
	}
	if strings.HasPrefix(path, home+"/") {
		return "~" + strings.TrimPrefix(path, home)
	}
	return path
}

// homeDir returns the user's home directory, or "" when it cannot be
// determined. Kept as a tiny wrapper so abbreviateHome stays testable.
func homeDir() string {
	h, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return h
}
