package tui

import (
	"albedo/cli/internal/daemon"
	"albedo/cli/internal/presentation"
	"cmp"
	"fmt"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
)

// ── logo ───────────────────────────────────────────────────────────────────

// A four-point sparkle with a small companion star, drawn with half blocks so
// each pixel is roughly square.
var svLogoPixels = []string{
	"........#...............",
	"........#...........#...",
	".......###..........#...",
	".......###........#####.",
	"......#####.........#...",
	".....#######........#...",
	"...###########..........",
	"#################.......",
	"...###########..........",
	".....#######............",
	"......#####.............",
	".......###.........#....",
	".......###..............",
	"........#...............",
	"........#...............",
	"........................",
}

func svLogo() []string {
	rows := len(svLogoPixels) / 2
	width := len(svLogoPixels[0])
	lines := make([]string, rows)
	for r := range rows {
		var b strings.Builder
		for c := range width {
			top := svLogoPixels[2*r][c] == '#'
			bottom := svLogoPixels[2*r+1][c] == '#'
			ch := " "
			switch {
			case top && bottom:
				ch = "█"
			case top:
				ch = "▀"
			case bottom:
				ch = "▄"
			}
			// Diagonal sweep so the gradient reads as light glancing off the mark.
			if ch != " " {
				t := (float64(c)/float64(width) + float64(r)/float64(rows)) / 2
				ch = brandInk(t).Render(ch)
			}
			b.WriteString(ch)
		}
		lines[r] = b.String()
	}
	return lines
}

// ── helpers ────────────────────────────────────────────────────────────────

func svCompactAge(ts *int64, now time.Time) string {
	if ts == nil {
		return "—"
	}
	d := now.Sub(time.Unix(*ts, 0))
	switch {
	case d < time.Minute:
		return "now"
	case d < time.Hour:
		return fmt.Sprintf("%dm", int(d.Minutes()))
	case d < 24*time.Hour:
		return fmt.Sprintf("%dh", int(d.Hours()))
	case d < 14*24*time.Hour:
		return fmt.Sprintf("%dd", int(d.Hours()/24))
	case d < 60*24*time.Hour:
		return fmt.Sprintf("%dw", int(d.Hours()/24/7))
	}
	return fmt.Sprintf("%dmo", int(d.Hours()/24/30))
}

// svCell fits plain text into exactly w columns.
func svCell(s string, w int, right bool) string {
	if w <= 0 {
		return ""
	}
	s = ansi.Truncate(s, w, "…")
	pad := strings.Repeat(" ", max(0, w-ansi.StringWidth(s)))
	if right {
		return pad + s
	}
	return s + pad
}

type svColumns struct {
	title, model, workspace, age int
}

func svLayout(width int) svColumns {
	// Row: bar(1) icon(2) space(1) title … model … workspace … age, two-space gaps.
	cols := svColumns{}
	rest := width - 4
	if width >= 32 {
		cols.age = 4
		rest -= cols.age + 2
	}
	if width >= 56 {
		cols.model = min(20, max(14, width/5))
		rest -= cols.model + 2
	}
	if width >= 90 {
		cols.workspace = min(18, max(10, width/8))
		rest -= cols.workspace + 2
	}
	cols.title = max(1, rest-1)
	return cols
}

// svSep divides the columns.
func svSep() string { return DefaultStyles.Decor.Render(" │ ") }

// ── view ───────────────────────────────────────────────────────────────────

// View shows the grouped session list beside its preview when there is room,
// and the list alone on narrow terminals.
func (m SessionViewer) View() string {
	width, height := m.Width, m.Height
	if width <= 0 {
		width = 80
	}
	if height <= 0 {
		height = 24
	}
	now := m.clock()
	roomy := height >= 12

	lines := []string{m.titleRule(width)}
	if roomy {
		lines = append(lines, "")
	}
	lines = append(lines, " "+promptLead()+m.SearchInput.View())
	if height >= 9 {
		lines = append(lines, "")
	}
	tail := strings.Split(m.footer(width, now), "\n")
	if roomy {
		tail = append([]string{""}, tail...)
	}
	body := max(1, height-len(lines)-len(tail))

	var columns [][]string
	if width >= 96 && height >= 14 {
		listW := width / 2
		columns = [][]string{
			m.column(listW, body, now),
			m.preview(width-listW-ansi.StringWidth(svSep()), body, now),
		}
	} else {
		columns = [][]string{m.column(width, body, now)}
	}
	for row := range body {
		var b strings.Builder
		for c, col := range columns {
			if c > 0 {
				b.WriteString(svSep())
			}
			b.WriteString(col[row])
		}
		lines = append(lines, b.String())
	}
	lines = append(lines, tail...)

	if len(lines) > height {
		lines = append(lines[:max(0, height-1)], lines[len(lines)-1])
	}
	for i, l := range lines {
		lines[i] = ansi.Truncate(l, width, "…")
	}
	return strings.Join(lines, "\n")
}

// svFit pads or cuts a styled line to exactly w columns.
func svFit(s string, w int) string { return svCell(s, w, false) }

// titleRule is the brand at the workspace over a rule.
func (m SessionViewer) titleRule(width int) string {
	label := "albedo"
	if m.ArchiveView {
		label = "albedo  archive"
	}
	return " " + titleRule(width-1, located(label, sessionText(m.workspacePlace())), "")
}

func (m SessionViewer) footer(width int, now time.Time) string {
	if m.ConfirmDelete != "" {
		message := "This permanently deletes the session, its child sessions, and their history. Delete? y confirms · any other key cancels"
		wrapped := ansi.Wrap(message, max(1, width-1), "")
		rows := strings.Split(wrapped, "\n")
		for i, row := range rows {
			rows[i] = " " + DefaultStyles.Error.Render(row)
		}
		return strings.Join(rows, "\n")
	}
	esc := "quit"
	if m.HasActive {
		esc = "back"
	}
	left := " " + keyHints(hint{"↑↓", "move"}, hint{"tab", "switch"}, hint{"enter", "open"}, hint{"^r", "rename"}, hint{"^s", "pin"}, hint{"^a", "archive"}, hint{"^f", "folders"}, hint{"esc", esc})
	switch {
	case m.rename.active():
		left = " " + renameHints("restores the automatic title")
	case m.ArchiveView:
		left = " " + keyHints(hint{"↑↓", "move"}, hint{"enter", "open"}, hint{"^r", "rename"}, hint{"^a", "restore"}, hint{"^d", "delete"}, hint{"esc", "back"})
	}

	var right string
	if m.notice != "" {
		right = DefaultStyles.Error.Render(m.notice)
	} else if m.Loading {
		right = DefaultStyles.Faint.Render("loading…")
	} else {
		count, today := len(m.Sessions), 0
		if m.ArchiveView {
			count = len(m.prefs.Archived)
		} else {
			for _, s := range m.Sessions {
				if dateSection(s, now) == secToday {
					today++
				}
			}
		}
		right = DefaultStyles.Faint.Render(fmt.Sprintf("%d sessions", count))
		if today > 0 {
			right += DefaultStyles.Decor.Render(" · ") + DefaultStyles.Success.Render(fmt.Sprintf("%d today", today))
		}
	}
	gap := width - ansi.StringWidth(left) - ansi.StringWidth(right) - 1
	if gap < 3 {
		return left
	}
	return left + strings.Repeat(" ", gap) + right
}

// column renders items with section headings at exactly width × height,
// scrolled to keep the cursor visible.
func (m SessionViewer) column(width, height int, now time.Time) []string {
	if len(m.Filtered) == 0 {
		message := "   No matching sessions"
		switch {
		case m.ArchiveView && m.SearchInput.Value() == "":
			message = "   No archived sessions yet"
		case m.Loading:
			message = "   loading sessions…"
		}
		return scrollWindow([]string{DefaultStyles.Faint.Render(message)}, -1, width, height)
	}

	selectedAt, total := -1, 0
	for _, group := range m.groups {
		if group.section != secAction {
			if total > 0 && height >= 10 {
				total++
			}
			total++
		}
		if m.Cursor >= group.first && m.Cursor < group.first+group.count {
			selectedAt = total + m.Cursor - group.first
		}
		total += group.count
	}
	start := 0
	if total > height {
		start = min(max(0, selectedAt-height/2), total-height)
	}
	end := min(total, start+height)
	out := make([]string, height)
	blank := strings.Repeat(" ", width)
	for i := range out {
		out[i] = blank
	}
	// Overflow markers replace edge rows rather than adding to the window.
	first, last := start, end
	if total > height && height >= 3 {
		more := svFit(DefaultStyles.Faint.Render("   ···"), width)
		if start > 0 {
			out[0] = more
			first++
		}
		if end < total {
			out[height-1] = more
			last--
		}
	}

	cols := svLayout(width)
	position := 0
	for _, group := range m.groups {
		if group.section != secAction {
			if position > 0 && height >= 10 {
				position++
			}
			if position >= first && position < last {
				out[position-start] = svFit(sectionRule(sectionTitles[group.section], m.sectionCounts[group.section], width), width)
			}
			position++
		}
		for offset := max(0, first-position); offset < min(group.count, last-position); offset++ {
			index := group.first + offset
			item := m.Filtered[index]
			session, _ := m.session(item.ID)
			out[position+offset-start] = svFit(m.row(item, session, group.section, index == m.Cursor, width, cols, now), width)
		}
		position += group.count
	}
	return out
}

// scrollWindow shows exactly width × height of lines, centred on selectedAt
// when they overflow, with ··· where lines were cut off.
func scrollWindow(all []string, selectedAt, width, height int) []string {
	if len(all) > height {
		start := min(max(0, selectedAt-height/2), len(all)-height)
		window := slices.Clone(all[start : start+height])
		if height >= 3 {
			more := DefaultStyles.Faint.Render("   ···")
			if start > 0 {
				window[0] = more
			}
			if start+height < len(all) {
				window[len(window)-1] = more
			}
		}
		all = window
	}
	out := make([]string, height)
	for i := range out {
		if i < len(all) {
			out[i] = svFit(all[i], width)
		} else {
			out[i] = strings.Repeat(" ", width)
		}
	}
	return out
}

func (m SessionViewer) row(item PickerItem, s daemon.Session, sec int, selected bool, width int, cols svColumns, now time.Time) string {
	// A row being renamed drops the selection surface and takes the prompt's
	// color, so it reads as a place to type rather than a highlight.
	editing := m.rename.id == item.ID
	selected = selected && !editing
	st := func(style lipgloss.Style) lipgloss.Style {
		if selected {
			return style.Inherit(DefaultStyles.Selected)
		}
		return style
	}
	bar := " "
	if selected {
		bar = st(DefaultStyles.Agent).Render("▌")
	}
	titleStyle := lipgloss.NewStyle()
	if selected {
		titleStyle = DefaultStyles.Bold
	}
	fill := func(line string) string {
		if pad := width - ansi.StringWidth(line); pad > 0 {
			line += st(lipgloss.NewStyle()).Render(strings.Repeat(" ", pad))
		}
		return line
	}

	if sec == secAction {
		glyph, title, hint, fg := "✦ ", "New session", "in "+sessionText(filepath.Base(m.Workspace)), DefaultStyles.You
		switch item.ID {
		case "archive":
			glyph, title, hint, fg = "▤ ", "Archive", fmt.Sprintf("%d sessions", len(m.prefs.Archived)), DefaultStyles.Muted
		case "login":
			glyph, title, hint, fg = "◇ ", "Accounts", "providers", DefaultStyles.Muted
		}
		titleWidth := min(ansi.StringWidth(title), cols.title)
		return fill(bar + st(fg).Render(glyph) + st(lipgloss.NewStyle()).Render(" ") +
			st(titleStyle.Inherit(fg)).Render(svCell(title, titleWidth, false)) +
			st(DefaultStyles.Faint).Render(svCell("  "+hint, max(0, width-4-titleWidth), false)))
	}

	recent := s.LastAssistantAt != nil && now.Sub(time.Unix(*s.LastAssistantAt, 0)) < time.Hour
	glyph, iconStyle := "· ", DefaultStyles.Faint
	switch {
	case editing:
		bar, glyph, iconStyle = DefaultStyles.Prompt.Render("▌"), "✎ ", DefaultStyles.Prompt
	case selected:
		glyph, iconStyle = "◆ ", DefaultStyles.Agent
	case sec == secPinned:
		glyph, iconStyle = "★ ", DefaultStyles.Agent
	case recent:
		glyph, iconStyle = "● ", DefaultStyles.Success
	}
	gap := st(lipgloss.NewStyle()).Render("  ")
	title := st(titleStyle).Render(svCell(item.Label, cols.title, false))
	if editing {
		title = m.rename.view(cols.title)
	}
	line := bar + st(iconStyle).Render(glyph) + st(lipgloss.NewStyle()).Render(" ") + title
	if cols.model > 0 {
		ms := DefaultStyles.Muted
		if selected {
			ms = lipgloss.NewStyle()
		}
		line += gap + st(ms).Render(svCell(cmp.Or(sessionText(s.Model), "—"), cols.model, false))
	}
	if cols.workspace > 0 {
		ws := DefaultStyles.Muted
		if s.Workspace == m.Workspace {
			ws = DefaultStyles.Faint
		}
		line += gap + st(ws).Render(svCell(sessionText(filepath.Base(s.Workspace)), cols.workspace, false))
	}
	if cols.age > 0 {
		as := DefaultStyles.Faint
		if recent {
			as = DefaultStyles.Success
		}
		line += gap + st(as).Render(svCell(svCompactAge(s.LastAssistantAt, now), cols.age, true))
	}
	return fill(line)
}

// ── preview ────────────────────────────────────────────────────────────────

// paneBox frames lines into width × height with a one-column side margin.
func paneBox(lines []string, inner, width, height int) []string {
	out := make([]string, 0, height)
	for _, l := range lines[:min(len(lines), height)] {
		out = append(out, " "+svFit(l, inner)+" ")
	}
	for len(out) < height {
		out = append(out, strings.Repeat(" ", width))
	}
	return out
}

// preview renders the highlighted row's pane at exactly width × height: the
// session's title and metadata over its latest turns, newest at the bottom.
func (m SessionViewer) preview(width, height int, now time.Time) []string {
	inner := max(1, width-2)
	item, ok := m.Highlighted()
	s, isSession := m.session(item.ID)
	switch {
	case !ok:
		return paneBox([]string{"", DefaultStyles.Muted.Render("No matching sessions"), DefaultStyles.Faint.Render("Try fewer search terms")}, inner, width, height)
	case !isSession:
		return paneBox(m.actionPreview(item.ID, inner, height), inner, width, height)
	}

	var top []string
	for _, l := range svWrap(sessionTitle(s), inner, 2) {
		top = append(top, DefaultStyles.Bold.Render(l))
	}
	meta := []string{}
	if m.prefs.pinned(s.ID) {
		meta = append(meta, DefaultStyles.Agent.Render("★ pinned"))
	}
	if s.Model != "" {
		meta = append(meta, DefaultStyles.Muted.Render(sessionText(s.Model)))
	}
	if s.Workspace != "" {
		meta = append(meta, DefaultStyles.Muted.Render(sessionText(sessionPlace(s))))
	}
	meta = append(meta, DefaultStyles.Faint.Render(presentation.AssistantAge(s.LastAssistantAt, now)))
	if c := m.previews[s.ID]; c != nil && !c.loading && !c.err {
		meta = append(meta, DefaultStyles.Faint.Render(fmt.Sprintf("%d messages", c.Total)))
	}
	top = append(top, strings.Join(meta, DefaultStyles.Decor.Render(" · ")), DefaultStyles.Decor.Render(strings.Repeat("─", inner)))
	bottom := m.transcript(s, inner, max(0, height-len(top)-1))
	return paneBox(append(top, bottom...), inner, width, height)
}

// actionPreview is a centred splash for the New session and Accounts rows.
func (m SessionViewer) actionPreview(id string, inner, height int) []string {
	var lines []string
	switch id {
	case "new":
		if logo := svLogo(); height >= len(logo)+8 {
			lines = append(append(lines, ""), logo...)
		}
		lines = append(lines, "", gradientText("New session", true), "",
			DefaultStyles.Muted.Render("start fresh in ")+lipgloss.NewStyle().Render(sessionText(m.workspacePlace())), "",
			DefaultStyles.Faint.Render("enter ")+DefaultStyles.Muted.Render("begin"))
	case "archive":
		lines = []string{"", "", DefaultStyles.Muted.Bold(true).Render("▤ Archive"), "",
			DefaultStyles.Muted.Render("Sessions kept out of the main list."), "",
			DefaultStyles.Faint.Render("enter ") + DefaultStyles.Muted.Render("browse")}
	default:
		lines = []string{"", "", DefaultStyles.Muted.Bold(true).Render("◇ Accounts"), "",
			DefaultStyles.Muted.Render("add a provider, sign in, or pick"), DefaultStyles.Muted.Render("which one new sessions use."), "",
			DefaultStyles.Faint.Render("enter ") + DefaultStyles.Muted.Render("manage")}
	}
	for i, l := range lines {
		lines[i] = strings.Repeat(" ", max(0, (inner-ansi.StringWidth(l))/2)) + l
	}
	return lines
}

// transcript is the tail of the session's conversation in at most height lines.
func (m SessionViewer) transcript(s daemon.Session, width, height int) []string {
	if height <= 0 {
		return nil
	}
	c := m.previews[s.ID]
	note := func(lines ...string) []string {
		out := []string{""}
		for _, l := range lines {
			out = append(out, DefaultStyles.Faint.Render(l))
		}
		return out
	}
	switch {
	case m.Fetch == nil:
		return nil
	case c == nil || c.loading:
		return note("loading conversation…")
	case c.err:
		return note("Could not load this preview.", "Open the session to read its messages.")
	case len(c.Items) == 0:
		return note("no messages yet")
	}

	// Consecutive tool calls collapse into one line of names with counts.
	type block struct {
		counts     map[string]int
		kind, text string
		tools      []string
	}
	var blocks []*block
	for _, it := range c.Items {
		text := sessionText(it.Preview)
		if it.Type == "assistant" {
			text = svMarkdownMarks.Replace(text)
		}
		if it.Type != "tool" {
			blocks = append(blocks, &block{kind: it.Type, text: text})
			continue
		}
		if len(blocks) == 0 || blocks[len(blocks)-1].kind != "tool" {
			blocks = append(blocks, &block{kind: "tool", counts: map[string]int{}})
		}
		b := blocks[len(blocks)-1]
		for name := range strings.SplitSeq(text, ", ") {
			if b.counts[name] == 0 {
				b.tools = append(b.tools, name)
			}
			b.counts[name]++
		}
	}
	// the same rails as the transcript: one per turn, dimmer through tools
	r := NewTranscriptRenderer()
	lanes := map[string]lane{"user": laneYou, "assistant": laneAgent, "tool": laneBusy}
	var lines []string
	prev := laneNone
	for bi, b := range blocks {
		own := lanes[b.kind]
		textStyle, maxLines := lipgloss.NewStyle(), 3
		if b.kind == "tool" {
			textStyle, maxLines = DefaultStyles.Faint, 2
			for i, name := range b.tools {
				if n := b.counts[name]; n > 1 {
					b.tools[i] = fmt.Sprintf("%s ×%d", name, n)
				}
			}
			b.text = strings.Join(b.tools, ", ")
		}
		if bi > 0 && joint(prev, own) == laneNone {
			lines = append(lines, "")
		}
		for _, l := range svWrap(b.text, max(1, width-railWidth), maxLines) {
			lines = append(lines, r.rail(own)+textStyle.Render(l))
		}
		prev = own
	}
	lines = append([]string{""}, lines...)
	if len(lines) > height {
		lines = lines[len(lines)-height:]
		lines[0] = DefaultStyles.Faint.Render("··· earlier")
	}
	return lines
}

// svMarkdownMarks drops emphasis and code markers that only cost width in a
// one-paragraph excerpt.
var svMarkdownMarks = strings.NewReplacer("**", "", "__", "", "`", "")

// svWrap word-wraps plain text into at most maxLines lines of width columns.
func svWrap(text string, width, maxLines int) []string {
	lines := strings.Split(ansi.Wrap(text, width, " -"), "\n")
	if len(lines) > maxLines {
		lines = lines[:maxLines]
		last := strings.TrimRight(lines[maxLines-1], " ")
		lines[maxLines-1] = ansi.Truncate(last+" …", width, "…")
	}
	return lines
}
