package tui

import (
	"fmt"
	"strings"
	"time"
)

// This file renders the multi-line sub-agent status panel (SPEC 4.4, US-006): a
// block shown just above the working spinner while one or more sub-agents
// dispatched by the `task` tool are running. Each active sub-agent contributes
// exactly one status line of the form:
//
//	⏺ {desc} · {activity} ({elapsed} · ↓{tokens})
//
// The panel is also interactive: while the input box is empty, ↑/↓ move a
// selection cursor over the rows and Enter expands the selected row to show that
// sub-agent's accumulated text output inline (below its status line), Esc
// collapses. The panel is a pure function of the model's ordered active-subagent
// set plus its selection state; it is re-rendered every spinner tick so the
// elapsed clock stays live without a dedicated timer. When there are no active
// sub-agents it renders nothing (zero lines, zero height), leaving the existing
// single-run layout untouched.

// maxExpandedLines caps how many wrapped output lines an expanded row shows. The
// output can grow without bound, so only the most recent lines are kept visible;
// older content scrolls off the top of the inline pane.
const maxExpandedLines = 12

// subagentRow is one live sub-agent's status, keyed by the parent task tool-call
// id. start is recorded when the row is added so elapsed can be computed at
// render time; activity/tokens are refreshed by subagentProgressMsg; output
// accumulates the sub-agent's forwarded text (toolUpdate deltas + final result)
// for the inline expanded view.
type subagentRow struct {
	id       string
	desc     string
	activity string
	tokens   int
	start    time.Time
	output   string
}

// subagentPanel is the ordered set of live sub-agents. order preserves insertion
// order (so rows render stably, oldest first) while byID gives O(1) lookup for
// progress updates and removal. selecting reports whether a row is cursored;
// selected is that row's index into order (meaningful only while selecting is
// true); expanded reports whether the selected row shows its output inline. The
// zero value is a valid empty, unselected panel — selecting defaults false so the
// selected int's zero value never spuriously marks row 0.
type subagentPanel struct {
	order     []string
	byID      map[string]*subagentRow
	selecting bool
	selected  int
	expanded  bool
}

// add records a newly dispatched sub-agent (a toolStartMsg with name=="task").
// It is idempotent on the id: a duplicate start refreshes the description and
// resets the start clock rather than adding a second row.
func (p *subagentPanel) add(id, desc string, now time.Time) {
	if p.byID == nil {
		p.byID = make(map[string]*subagentRow)
	}
	if row, ok := p.byID[id]; ok {
		row.desc = desc
		row.start = now
		return
	}
	p.byID[id] = &subagentRow{id: id, desc: desc, start: now}
	p.order = append(p.order, id)
}

// update folds a progress event into the row for id, refreshing its activity and
// token estimate. A progress for an unknown id (late/out-of-order, arriving
// before or without a start) adds the row so no update is lost; now seeds its
// start clock in that case.
func (p *subagentPanel) update(id, desc, activity string, tokens int, now time.Time) {
	if p.byID == nil {
		p.byID = make(map[string]*subagentRow)
	}
	row, ok := p.byID[id]
	if !ok {
		row = &subagentRow{id: id, desc: desc, start: now}
		p.byID[id] = row
		p.order = append(p.order, id)
	}
	if activity != "" {
		row.activity = activity
	}
	if desc != "" {
		row.desc = desc
	}
	row.tokens = tokens
}

// appendOutput accumulates a forwarded text delta into the row for id, so the
// expanded view can show the sub-agent's running output. Deltas for an unknown id
// are ignored (the row's start/end brackets its output; nothing to attach to).
func (p *subagentPanel) appendOutput(id, delta string) {
	if delta == "" {
		return
	}
	if row, ok := p.byID[id]; ok {
		row.output += delta
	}
}

// remove drops the row for id (the task's toolEndMsg). It is a no-op when id is
// absent, so an end without a matching start — or a duplicate end — is safe.
// The selection is clamped to the shrunken order so the cursor never dangles past
// the end; removing the last row clears the selection entirely.
func (p *subagentPanel) remove(id string) {
	if _, ok := p.byID[id]; !ok {
		return
	}
	delete(p.byID, id)
	for i, v := range p.order {
		if v == id {
			p.order = append(p.order[:i], p.order[i+1:]...)
			break
		}
	}
	if len(p.order) == 0 {
		p.clearSelection()
		return
	}
	if p.selected >= len(p.order) {
		p.selected = len(p.order) - 1
	}
}

// active reports the number of live sub-agents (status rows the panel would
// render), ignoring any extra rows an expanded row contributes.
func (p *subagentPanel) active() int { return len(p.order) }

// hasSelection reports whether a row is currently cursored.
func (p *subagentPanel) hasSelection() bool {
	return p.selecting && p.selected >= 0 && p.selected < len(p.order)
}

// clearSelection drops the cursor and collapses any expansion.
func (p *subagentPanel) clearSelection() {
	p.selecting = false
	p.selected = 0
	p.expanded = false
}

// selectUp moves the cursor to the previous row. With no current selection the
// first press lands on the last (bottom-most) row; moving up collapses any open
// expansion so it re-anchors to the newly selected row.
func (p *subagentPanel) selectUp() {
	if len(p.order) == 0 {
		return
	}
	if !p.selecting {
		p.selecting = true
		p.selected = len(p.order) - 1
	} else if p.selected > 0 {
		p.selected--
	}
	p.expanded = false
}

// selectDown moves the cursor to the next row. With no current selection the
// first press lands on the first (top-most) row; moving down collapses any open
// expansion so it re-anchors to the newly selected row.
func (p *subagentPanel) selectDown() {
	if len(p.order) == 0 {
		return
	}
	if !p.selecting {
		p.selecting = true
		p.selected = 0
	} else if p.selected < len(p.order)-1 {
		p.selected++
	}
	p.expanded = false
}

// toggleExpand flips the expanded state of the selected row. It is a no-op when
// nothing is selected.
func (p *subagentPanel) toggleExpand() {
	if p.hasSelection() {
		p.expanded = !p.expanded
	}
}

// expandedID returns the id of the currently expanded row, or "" when no row is
// expanded. It lets the model relayout only when a streamed delta lands on the
// row whose inline output pane is on screen.
func (p *subagentPanel) expandedID() string {
	if p.expanded && p.hasSelection() {
		return p.order[p.selected]
	}
	return ""
}

// lineCount reports how many terminal rows the panel occupies at the given width:
// one status line per active sub-agent, plus the wrapped output lines when the
// selected row is expanded. relayout uses this to reserve exactly the right
// height so the transcript never overlaps the panel.
func (p subagentPanel) lineCount(width int) int {
	if len(p.order) == 0 || width <= 0 {
		return 0
	}
	n := len(p.order)
	if p.expanded && p.hasSelection() {
		if row := p.byID[p.order[p.selected]]; row != nil {
			n += len(p.expandedLines(row, width))
		}
	}
	return n
}

// view renders the panel to a string, one status line per active sub-agent in
// insertion order, each truncated to width display columns. The selected row is
// marked with a leading cursor and, when expanded, its accumulated output is
// rendered on the following (indented, wrapped) lines. It returns "" when there
// are no active sub-agents or width is non-positive, so an empty panel
// contributes zero rows and zero height. now is the reference time elapsed is
// measured from (the spinner tick's time) so the clock advances each frame.
func (p subagentPanel) view(theme Theme, width int, now time.Time) string {
	if len(p.order) == 0 || width <= 0 {
		return ""
	}
	lines := make([]string, 0, len(p.order)+1)
	for i, id := range p.order {
		row := p.byID[id]
		if row == nil {
			continue
		}
		cursored := p.selecting && i == p.selected
		lines = append(lines, TruncateToWidth(row.render(theme, now, cursored), width))
		if cursored && p.expanded {
			for _, out := range p.expandedLines(row, width) {
				lines = append(lines, theme.System.Render(out))
			}
		}
	}
	return strings.Join(lines, "\n")
}

// expandedLines builds the wrapped, indented, tail-capped output lines shown
// under an expanded row. An empty output yields a single placeholder line so the
// pane is never blank. Lines are already truncated to width (as plain text); the
// caller styles them.
func (p subagentPanel) expandedLines(row *subagentRow, width int) []string {
	const indent = "  "
	out := strings.TrimRight(row.output, "\n")
	if out == "" {
		return []string{indent + "(no output yet)"}
	}
	var wrapped []string
	for _, para := range strings.Split(out, "\n") {
		for _, seg := range wrapToWidth(para, width-len(indent)) {
			wrapped = append(wrapped, indent+seg)
		}
	}
	if len(wrapped) > maxExpandedLines {
		wrapped = wrapped[len(wrapped)-maxExpandedLines:]
	}
	return wrapped
}

// wrapToWidth breaks s into segments no wider than width display columns, cutting
// on the column boundary (there is no word-aware wrapping here — sub-agent output
// is arbitrary text/code). An empty line yields one empty segment so blank lines
// in the output are preserved.
func wrapToWidth(s string, width int) []string {
	if width <= 0 {
		return []string{s}
	}
	if s == "" {
		return []string{""}
	}
	var segs []string
	for s != "" {
		seg := TruncateToWidth(s, width)
		if seg == "" { // guard against no forward progress on odd-width runes
			segs = append(segs, s)
			break
		}
		segs = append(segs, seg)
		s = s[len(seg):]
	}
	return segs
}

// render builds one status line for a row: "{cursor}⏺ {desc} · {activity}
// ({elapsed} · ↓{tokens})". A blank description is omitted (the line leads with
// the glyph and activity); a zero token estimate drops the "↓" stat. When
// selected, the line leads with a "❯ " cursor and the head takes the accent color
// to stand out; otherwise the glyph + head take the spinner color and the
// parenthetical stats are dim, mirroring the spinner line.
func (r subagentRow) render(theme Theme, now time.Time, selected bool) string {
	var head strings.Builder
	head.WriteString("⏺")
	if r.desc != "" {
		fmt.Fprintf(&head, " %s ·", r.desc)
	}
	fmt.Fprintf(&head, " %s", r.activity)

	stats := formatElapsed(now.Sub(r.start))
	if r.tokens > 0 {
		stats += " · ↓" + humanizeInt(r.tokens)
	}

	headStyle := theme.Spinner
	cursor := "  "
	if selected {
		headStyle = theme.Accent
		cursor = theme.Accent.Render("❯ ")
	}
	return cursor + headStyle.Render(head.String()) + " " + theme.System.Render("("+stats+")")
}
