//go:build with_tui

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/YspCoder/clawgo/pkg/config"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/gorilla/websocket"
)

type tuiOptions struct {
	baseURL   string
	token     string
	session   string
	noHistory bool
}

var tuiEnabled = true

type tuiMessage struct {
	Role      string        `json:"role"`
	Content   string        `json:"content"`
	ToolCalls []interface{} `json:"tool_calls"`
}

type tuiSession struct {
	Key string `json:"key"`
}

type tuiChatEntry struct {
	Role      string
	Content   string
	Streaming bool
}

type tuiPane struct {
	ID       int
	Session  string
	Viewport viewport.Model
	Entries  []tuiChatEntry
	Loading  bool
	Sending  bool
	ErrText  string
	Unread   bool
}

type tuiRuntime struct {
	program *tea.Program
}

func (r *tuiRuntime) send(msg tea.Msg) {
	if r == nil || r.program == nil {
		return
	}
	r.program.Send(msg)
}

type tuiHistoryLoadedMsg struct {
	PaneID   int
	Session  string
	Messages []tuiMessage
	Err      error
}

type tuiSessionsLoadedMsg struct {
	Sessions []tuiSession
	Err      error
}

type tuiChatChunkMsg struct {
	PaneID int
	Delta  string
}

type tuiChatDoneMsg struct {
	PaneID int
}

type tuiChatErrorMsg struct {
	PaneID int
	Err    error
}

type tuiModel struct {
	client  *tuiClient
	runtime *tuiRuntime
	input   textinput.Model

	width        int
	height       int
	bodyHeight   int
	sidebarWidth int

	panes         []tuiPane
	focus         int
	nextPaneID    int
	sessions      []tuiSession
	sessionCursor int
	sessionFilter string
	sessionActive map[string]time.Time
	inputHistory  []string
	historyCursor int
	historyDraft  string
	status        string
	globalErr     string
}

func tuiCmd() {
	opts, err := parseTUIOptions(os.Args[2:])
	if err != nil {
		fmt.Printf("Error: %v\n\n", err)
		printTUIHelp()
		os.Exit(1)
	}

	client := &tuiClient{
		baseURL: strings.TrimRight(opts.baseURL, "/"),
		token:   strings.TrimSpace(opts.token),
		http: &http.Client{
			Timeout: 20 * time.Second,
		},
	}
	if err := client.ping(); err != nil {
		fmt.Printf("Error connecting to gateway: %v\n", err)
		os.Exit(1)
	}

	runtime := &tuiRuntime{}
	model := newTUIModel(client, runtime, opts)
	program := tea.NewProgram(model, tea.WithAltScreen())
	runtime.program = program

	if _, err := program.Run(); err != nil {
		fmt.Printf("Error running TUI: %v\n", err)
		os.Exit(1)
	}
}

func newTUIModel(client *tuiClient, runtime *tuiRuntime, opts tuiOptions) tuiModel {
	input := textinput.New()
	input.Placeholder = "Type a message for the focused pane or /help"
	input.Focus()
	input.CharLimit = 0
	input.Prompt = "› "
	input.PromptStyle = lipgloss.NewStyle().Bold(true).Foreground(tuiColorShell)
	input.TextStyle = lipgloss.NewStyle().Foreground(tuiColorText)
	input.PlaceholderStyle = lipgloss.NewStyle().Foreground(tuiColorTextSoft)
	input.Cursor.Style = lipgloss.NewStyle().Foreground(tuiColorShell)

	pane := newTUIPane(1, opts.session)
	if opts.noHistory {
		pane.Entries = []tuiChatEntry{{Role: "system", Content: "History loading skipped. Start chatting."}}
	}

	return tuiModel{
		client:        client,
		runtime:       runtime,
		input:         input,
		panes:         []tuiPane{pane},
		focus:         0,
		nextPaneID:    2,
		historyCursor: -1,
		sessionActive: map[string]time.Time{
			strings.TrimSpace(opts.session): time.Now(),
		},
		status: "Tab/Shift+Tab: focus • Ctrl+↑/↓: select session • Ctrl+O: open selected • Enter: send • Ctrl+C: quit",
	}
}

func newTUIPane(id int, session string) tuiPane {
	vp := viewport.New(0, 0)
	vp.SetContent("")
	return tuiPane{
		ID:       id,
		Session:  strings.TrimSpace(session),
		Viewport: vp,
	}
}

func (m tuiModel) Init() tea.Cmd {
	cmds := []tea.Cmd{textinput.Blink}
	cmds = append(cmds, m.loadSessionsCmd())
	for _, pane := range m.panes {
		if len(pane.Entries) == 0 {
			cmds = append(cmds, m.loadHistoryCmd(pane.ID, pane.Session))
		}
	}
	return tea.Batch(cmds...)
}

func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		m.resize()
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c":
			return m, tea.Quit
		case "ctrl+w":
			if len(m.panes) <= 1 {
				m.globalErr = "Cannot close the last pane"
				return m, nil
			}
			closed := m.panes[m.focus].Session
			m.panes = append(m.panes[:m.focus], m.panes[m.focus+1:]...)
			if m.focus >= len(m.panes) {
				m.focus = len(m.panes) - 1
			}
			m.globalErr = ""
			m.status = "Closed pane: " + closed
			m.resize()
			return m, nil
		case "ctrl+l":
			if len(m.panes) == 0 {
				return m, nil
			}
			m.panes[m.focus].Loading = true
			m.panes[m.focus].ErrText = ""
			m.globalErr = ""
			m.status = "Reloading history: " + m.panes[m.focus].Session
			return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session)
		case "esc":
			if strings.TrimSpace(m.sessionFilter) != "" {
				m.sessionFilter = ""
				filtered := m.filteredSessions()
				if len(filtered) == 0 {
					m.sessionCursor = 0
				} else if m.sessionCursor >= len(filtered) {
					m.sessionCursor = len(filtered) - 1
				}
				m.globalErr = ""
				m.status = "Session filter cleared"
				return m, nil
			}
		case "tab":
			if len(m.panes) > 1 {
				m.focus = (m.focus + 1) % len(m.panes)
				m.panes[m.focus].Unread = false
				m.markSessionActive(m.focusedPane().Session)
				m.status = "Focused pane: " + m.focusedPane().Session
			}
			return m, nil
		case "alt+left":
			if len(m.panes) > 1 {
				m.focus = (m.focus - 1 + len(m.panes)) % len(m.panes)
				m.panes[m.focus].Unread = false
				m.markSessionActive(m.focusedPane().Session)
				m.status = "Focused pane: " + m.focusedPane().Session
			}
			return m, nil
		case "alt+right":
			if len(m.panes) > 1 {
				m.focus = (m.focus + 1) % len(m.panes)
				m.panes[m.focus].Unread = false
				m.markSessionActive(m.focusedPane().Session)
				m.status = "Focused pane: " + m.focusedPane().Session
			}
			return m, nil
		case "up":
			if m.recallHistory(-1) {
				return m, nil
			}
			return m, nil
		case "down":
			if m.recallHistory(1) {
				return m, nil
			}
			return m, nil
		case "ctrl+up":
			filtered := m.filteredSessions()
			if len(filtered) > 0 && m.sessionCursor > 0 {
				m.sessionCursor--
				m.status = "Selected session: " + filtered[m.sessionCursor].Key
			}
			return m, nil
		case "ctrl+down":
			filtered := m.filteredSessions()
			if len(filtered) > 0 && m.sessionCursor < len(filtered)-1 {
				m.sessionCursor++
				m.status = "Selected session: " + filtered[m.sessionCursor].Key
			}
			return m, nil
		case "shift+tab":
			if len(m.panes) > 1 {
				m.focus = (m.focus - 1 + len(m.panes)) % len(m.panes)
				m.status = "Focused pane: " + m.focusedPane().Session
			}
			return m, nil
		case "alt+1", "alt+2", "alt+3", "alt+4":
			target := 0
			switch msg.String() {
			case "alt+2":
				target = 1
			case "alt+3":
				target = 2
			case "alt+4":
				target = 3
			}
			if target < len(m.panes) {
				m.focus = target
				m.panes[m.focus].Unread = false
				m.status = "Focused pane: " + m.focusedPane().Session
			}
			return m, nil
		case "ctrl+o":
			return m.openSelectedSessionInPane(false)
		case "ctrl+n":
			return m.openSelectedSessionInPane(false)
		case "ctrl+r":
			return m.openSelectedSessionInPane(true)
		case "pgdown", "pgup", "home", "end":
			if len(m.panes) > 0 {
				var cmd tea.Cmd
				pane := &m.panes[m.focus]
				pane.Viewport, cmd = pane.Viewport.Update(msg)
				return m, cmd
			}
		case "enter":
			value := strings.TrimSpace(m.input.Value())
			if value == "" {
				return m.openSelectedSessionInPane(false)
			}
			if len(m.panes) == 0 || m.focusedPane().Sending || m.focusedPane().Loading {
				return m, nil
			}
			m.pushInputHistory(value)
			m.input.SetValue("")
			if strings.HasPrefix(value, "/") {
				return m.handleCommand(value)
			}
			return m.startSend(value)
		}
	case tuiHistoryLoadedMsg:
		idx := m.findPane(msg.PaneID)
		if idx < 0 {
			return m, nil
		}
		pane := &m.panes[idx]
		pane.Loading = false
		if msg.Err != nil {
			pane.ErrText = msg.Err.Error()
			m.status = "History load failed for " + pane.Session
			return m, nil
		}
		pane.Session = msg.Session
		m.markSessionActive(pane.Session)
		pane.Entries = historyEntries(msg.Messages)
		if len(pane.Entries) == 0 {
			pane.Entries = []tuiChatEntry{{Role: "system", Content: "No history in this session yet."}}
		}
		pane.ErrText = ""
		m.status = "Session loaded: " + pane.Session
		m.syncPaneViewport(idx, true)
		return m, nil
	case tuiSessionsLoadedMsg:
		if msg.Err != nil {
			m.globalErr = msg.Err.Error()
			return m, nil
		}
		m.sessions = filterTUISessions(msg.Sessions)
		if len(m.sessions) == 0 {
			m.sessions = []tuiSession{{Key: "main"}}
		}
		filtered := m.filteredSessions()
		if len(filtered) == 0 {
			m.sessionCursor = 0
		} else if m.sessionCursor >= len(filtered) {
			m.sessionCursor = len(filtered) - 1
		}
		keys := make([]string, 0, len(msg.Sessions))
		for _, session := range m.sessions {
			if strings.TrimSpace(session.Key) != "" {
				keys = append(keys, session.Key)
			}
		}
		m.appendSystemToFocused("Sessions: " + strings.Join(keys, ", "))
		m.globalErr = ""
		m.status = fmt.Sprintf("%d sessions", len(keys))
		return m, nil
	case tuiChatChunkMsg:
		idx := m.findPane(msg.PaneID)
		if idx < 0 {
			return m, nil
		}
		pane := &m.panes[idx]
		if len(pane.Entries) == 0 || !pane.Entries[len(pane.Entries)-1].Streaming {
			pane.Entries = append(pane.Entries, tuiChatEntry{Role: "assistant", Streaming: true})
		}
		pane.Entries[len(pane.Entries)-1].Content += msg.Delta
		if idx != m.focus {
			pane.Unread = true
		}
		m.markSessionActive(pane.Session)
		m.syncPaneViewport(idx, true)
		return m, nil
	case tuiChatDoneMsg:
		idx := m.findPane(msg.PaneID)
		if idx < 0 {
			return m, nil
		}
		pane := &m.panes[idx]
		pane.Sending = false
		if len(pane.Entries) > 0 && pane.Entries[len(pane.Entries)-1].Streaming {
			pane.Entries[len(pane.Entries)-1].Streaming = false
		}
		pane.ErrText = ""
		if idx == m.focus {
			pane.Unread = false
		}
		m.markSessionActive(pane.Session)
		m.status = "Reply complete: " + pane.Session
		m.syncPaneViewport(idx, true)
		return m, nil
	case tuiChatErrorMsg:
		idx := m.findPane(msg.PaneID)
		if idx < 0 {
			return m, nil
		}
		pane := &m.panes[idx]
		pane.Sending = false
		if len(pane.Entries) > 0 && pane.Entries[len(pane.Entries)-1].Streaming {
			pane.Entries[len(pane.Entries)-1].Streaming = false
		}
		pane.ErrText = msg.Err.Error()
		m.status = "Reply failed: " + pane.Session
		m.syncPaneViewport(idx, true)
		return m, nil
	}

	var cmd tea.Cmd
	m.input, cmd = m.input.Update(msg)
	return m, cmd
}

func (m tuiModel) View() string {
	if m.width == 0 || m.height == 0 {
		return "Loading..."
	}
	banner := renderTUIBanner(m.width)
	focusSession := ""
	if len(m.panes) > 0 {
		focusSession = m.focusedPane().Session
	}
	header := tuiHeaderStyle.Width(m.width).Render(
		lipgloss.JoinVertical(lipgloss.Left,
			banner,
			tuiMutedStyle.Render(fmt.Sprintf("Focus %s  •  Panes %d  •  %s", focusSession, len(m.panes), compactGatewayLabel(m.client.baseURL))),
		),
	)
	body := m.renderPanes()
	input := tuiInputBoxStyle.Width(m.width).Render(m.input.View())
	footerText := m.status
	if m.globalErr != "" {
		footerText = "Error: " + m.globalErr
	}
	footerStyle := tuiFooterStyle
	if m.globalErr != "" {
		footerStyle = tuiErrorStyle
	}
	footerLeft := footerStyle.Render(footerText)
	footerRight := tuiFooterHintStyle.Render("Enter send  •  Ctrl+N open  •  Ctrl+W close  •  Alt+1..4 focus")
	footer := footerStyle.Width(m.width).Render(
		lipgloss.JoinHorizontal(
			lipgloss.Top,
			footerLeft,
			strings.Repeat(" ", max(1, m.width-lipgloss.Width(footerLeft)-lipgloss.Width(footerRight)-2)),
			footerRight,
		),
	)
	return lipgloss.JoinVertical(lipgloss.Left, header, body, input, footer)
}

func (m *tuiModel) resize() {
	headerHeight := lipgloss.Height(
		tuiHeaderStyle.Width(max(m.width, 20)).Render(
			lipgloss.JoinVertical(lipgloss.Left,
				renderTUIBanner(max(m.width, 20)),
				tuiMutedStyle.Render(fmt.Sprintf("Gateway %s", m.client.baseURL)),
			),
		),
	)
	inputHeight := 3
	footerHeight := 2
	bodyHeight := m.height - headerHeight - inputHeight - footerHeight
	if bodyHeight < 6 {
		bodyHeight = 6
	}
	m.bodyHeight = bodyHeight
	m.sidebarWidth = min(28, max(20, m.width/5))
	gapSize := lipgloss.Width(m.paneGap())
	availablePaneWidth := m.width - m.sidebarWidth - gapSize
	if availablePaneWidth < 24 {
		availablePaneWidth = 24
	}
	switch len(m.panes) {
	case 0:
	case 1:
		m.setPaneSize(0, availablePaneWidth, bodyHeight)
	case 2:
		leftWidth := (availablePaneWidth - gapSize) / 2
		rightWidth := availablePaneWidth - gapSize - leftWidth
		m.setPaneSize(0, leftWidth, bodyHeight)
		m.setPaneSize(1, rightWidth, bodyHeight)
	case 3:
		leftWidth := (availablePaneWidth - gapSize) / 2
		rightWidth := availablePaneWidth - gapSize - leftWidth
		topHeight := (bodyHeight - gapSize) / 2
		bottomHeight := bodyHeight - gapSize - topHeight
		m.setPaneSize(0, leftWidth, bodyHeight)
		m.setPaneSize(1, rightWidth, topHeight)
		m.setPaneSize(2, rightWidth, bottomHeight)
	default:
		leftWidth := (availablePaneWidth - gapSize) / 2
		rightWidth := availablePaneWidth - gapSize - leftWidth
		topHeight := (bodyHeight - gapSize) / 2
		bottomHeight := bodyHeight - gapSize - topHeight
		m.setPaneSize(0, leftWidth, topHeight)
		m.setPaneSize(1, rightWidth, topHeight)
		m.setPaneSize(2, leftWidth, bottomHeight)
		m.setPaneSize(3, rightWidth, bottomHeight)
	}
	m.input.Width = max(10, m.width-6)
}

func (m *tuiModel) renderPanes() string {
	sidebar := m.renderSessionSidebar()
	panes := m.renderPaneGrid()
	return lipgloss.JoinHorizontal(lipgloss.Top, sidebar, tuiPaneGapStyle.Render(m.paneGap()), panes)
}

func (m *tuiModel) renderPane(index int, pane tuiPane) string {
	titleStyle := tuiPaneTitleStyle
	borderStyle := tuiPaneStyle
	if index == m.focus {
		titleStyle = tuiPaneTitleActiveStyle
		borderStyle = tuiPaneActiveStyle
	}
	statusBadge := ""
	switch {
	case pane.Sending:
		statusBadge = tuiPaneReplyingStyle.Render(" replying ")
	case pane.Loading:
		statusBadge = tuiPaneLoadingStyle.Render(" loading ")
	case pane.ErrText != "":
		statusBadge = tuiPaneErrorBadgeStyle.Render(" error ")
	}
	titleText := fmt.Sprintf("[%d] %s", index+1, pane.Session)
	if pane.Unread {
		titleText += " •"
	}
	title := titleStyle.Render(titleText)
	if index == m.focus {
		title = tuiPaneTitleActiveBadgeStyle.Render(" " + titleText + " ")
	}
	meta := []string{}
	if pane.Loading {
		meta = append(meta, "loading")
	}
	if pane.Sending {
		meta = append(meta, "replying")
	}
	if pane.ErrText != "" {
		meta = append(meta, "error")
	}
	header := title
	if statusBadge != "" {
		header = lipgloss.JoinHorizontal(lipgloss.Left, header, " ", statusBadge)
	}
	if len(meta) > 0 {
		header = lipgloss.JoinHorizontal(lipgloss.Left, title, tuiMutedStyle.Render("  "+strings.Join(meta, " • ")))
		if statusBadge != "" {
			header = lipgloss.JoinHorizontal(lipgloss.Left, title, " ", statusBadge, tuiMutedStyle.Render("  "+strings.Join(meta, " • ")))
		}
	}
	content := pane.Viewport.View()
	if pane.ErrText != "" {
		content += "\n\n" + tuiErrorStyle.Render("Error: "+pane.ErrText)
	}
	view := lipgloss.JoinVertical(lipgloss.Left, header, content)
	return borderStyle.Width(pane.Viewport.Width + 4).Height(pane.Viewport.Height + 4).Render(view)
}

func (m *tuiModel) syncPaneViewport(index int, toBottom bool) {
	if index < 0 || index >= len(m.panes) {
		return
	}
	pane := &m.panes[index]
	pane.Viewport.SetContent(renderEntries(pane.Entries, pane.Viewport.Width))
	if toBottom {
		pane.Viewport.GotoBottom()
	}
}

func (m *tuiModel) renderSessionSidebar() string {
	lines := []string{tuiPaneTitleStyle.Render("Sessions")}
	filtered := m.filteredSessions()
	if len(filtered) == 0 {
		lines = append(lines, tuiMutedStyle.Render("No sessions"))
	} else {
		for i, session := range filtered {
			style := tuiSidebarItemStyle
			prefix := "  "
			suffix := m.sidebarSessionMarker(session.Key)
			if i == m.sessionCursor {
				style = tuiSidebarItemActiveStyle
				prefix = "› "
			}
			lines = append(lines, style.Width(max(8, m.sidebarWidth-4)).Render(prefix+session.Key+suffix))
		}
	}
	lines = append(lines, "")
	if strings.TrimSpace(m.sessionFilter) != "" {
		lines = append(lines, tuiMutedStyle.Render("Filter: "+m.sessionFilter))
	}
	lines = append(lines, tuiMutedStyle.Render("Ctrl+Up/Down: select"))
	lines = append(lines, tuiMutedStyle.Render("Up/Down: input history"))
	lines = append(lines, tuiMutedStyle.Render("Enter: open selected"))
	lines = append(lines, tuiMutedStyle.Render("Ctrl+O: open pane"))
	lines = append(lines, tuiMutedStyle.Render("Ctrl+N: new pane"))
	lines = append(lines, tuiMutedStyle.Render("Ctrl+R: replace pane"))
	lines = append(lines, tuiMutedStyle.Render("Ctrl+L: reload pane"))
	lines = append(lines, tuiMutedStyle.Render("Ctrl+W: close pane"))
	lines = append(lines, tuiMutedStyle.Render("Alt+←/→: cycle pane"))
	lines = append(lines, tuiMutedStyle.Render("Alt+1..4: focus pane"))
	lines = append(lines, tuiMutedStyle.Render("Esc: clear filter"))
	lines = append(lines, tuiMutedStyle.Render("/filter <text>: filter"))
	return tuiSidebarStyle.Width(m.sidebarWidth).Height(m.bodyHeight).Render(strings.Join(lines, "\n"))
}

func (m *tuiModel) renderPaneGrid() string {
	if len(m.panes) == 0 {
		return tuiViewportStyle.Width(max(20, m.width-m.sidebarWidth-1)).Height(m.bodyHeight).Render("No panes.")
	}
	gap := tuiPaneGapStyle.Render(m.paneGap())
	switch len(m.panes) {
	case 1:
		return m.renderPane(0, m.panes[0])
	case 2:
		return lipgloss.JoinHorizontal(lipgloss.Top,
			m.renderPane(0, m.panes[0]),
			gap,
			m.renderPane(1, m.panes[1]),
		)
	case 3:
		right := lipgloss.JoinVertical(lipgloss.Left,
			m.renderPane(1, m.panes[1]),
			gap,
			m.renderPane(2, m.panes[2]),
		)
		return lipgloss.JoinHorizontal(lipgloss.Top,
			m.renderPane(0, m.panes[0]),
			gap,
			right,
		)
	default:
		top := lipgloss.JoinHorizontal(lipgloss.Top,
			m.renderPane(0, m.panes[0]),
			gap,
			m.renderPane(1, m.panes[1]),
		)
		bottom := lipgloss.JoinHorizontal(lipgloss.Top,
			m.renderPane(2, m.panes[2]),
			gap,
			m.renderPane(3, m.panes[3]),
		)
		return lipgloss.JoinVertical(lipgloss.Left,
			top,
			gap,
			bottom,
		)
	}
}

func (m *tuiModel) paneGap() string {
	if len(m.panes) >= 4 {
		return ""
	}
	return " "
}

func (m *tuiModel) pushInputHistory(value string) {
	value = strings.TrimSpace(value)
	if value == "" {
		return
	}
	if n := len(m.inputHistory); n > 0 && m.inputHistory[n-1] == value {
		m.historyCursor = len(m.inputHistory)
		m.historyDraft = ""
		return
	}
	m.inputHistory = append(m.inputHistory, value)
	m.historyCursor = len(m.inputHistory)
	m.historyDraft = ""
}

func (m *tuiModel) recallHistory(delta int) bool {
	if len(m.inputHistory) == 0 {
		return false
	}
	if m.historyCursor < 0 || m.historyCursor > len(m.inputHistory) {
		m.historyCursor = len(m.inputHistory)
	}
	if m.historyCursor == len(m.inputHistory) {
		m.historyDraft = m.input.Value()
	}
	next := m.historyCursor + delta
	if next < 0 {
		next = 0
	}
	if next > len(m.inputHistory) {
		next = len(m.inputHistory)
	}
	if next == m.historyCursor {
		return true
	}
	m.historyCursor = next
	if m.historyCursor == len(m.inputHistory) {
		m.input.SetValue(m.historyDraft)
	} else {
		m.input.SetValue(m.inputHistory[m.historyCursor])
	}
	m.input.CursorEnd()
	m.input.Focus()
	return true
}

func (m tuiModel) loadHistoryCmd(paneID int, session string) tea.Cmd {
	return func() tea.Msg {
		messages, err := m.client.history(context.Background(), session)
		return tuiHistoryLoadedMsg{PaneID: paneID, Session: session, Messages: messages, Err: err}
	}
}

func (m tuiModel) loadSessionsCmd() tea.Cmd {
	return func() tea.Msg {
		sessions, err := m.client.sessions(context.Background())
		return tuiSessionsLoadedMsg{Sessions: sessions, Err: err}
	}
}

func (m tuiModel) startSend(content string) (tea.Model, tea.Cmd) {
	if len(m.panes) == 0 {
		return m, nil
	}
	pane := &m.panes[m.focus]
	pane.Entries = append(pane.Entries,
		tuiChatEntry{Role: "user", Content: content},
		tuiChatEntry{Role: "assistant", Streaming: true},
	)
	pane.Sending = true
	pane.ErrText = ""
	m.markSessionActive(pane.Session)
	m.status = "Waiting for reply: " + pane.Session
	m.syncPaneViewport(m.focus, true)
	return m, m.streamChatCmd(pane.ID, pane.Session, content)
}

func (m tuiModel) handleCommand(input string) (tea.Model, tea.Cmd) {
	fields := strings.Fields(input)
	cmd := strings.ToLower(strings.TrimPrefix(fields[0], "/"))
	switch cmd {
	case "quit", "exit":
		return m, tea.Quit
	case "help":
		m.appendSystemToFocused("Commands: /help, /sessions, /split <session>, /close, /session <name>, /focus <n>, /history, /clear, /filter <text>, /quit")
		m.status = "Help shown"
		return m, nil
	case "sessions":
		m.status = "Loading sessions..."
		return m, m.loadSessionsCmd()
	case "history":
		if len(m.panes) == 0 {
			return m, nil
		}
		m.panes[m.focus].Loading = true
		m.status = "Loading history..."
		return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session)
	case "clear":
		if len(m.panes) == 0 {
			return m, nil
		}
		m.panes[m.focus].Entries = nil
		m.panes[m.focus].ErrText = ""
		m.status = "Transcript cleared: " + m.panes[m.focus].Session
		m.syncPaneViewport(m.focus, true)
		return m, nil
	case "session":
		if len(fields) < 2 || len(m.panes) == 0 {
			m.globalErr = "/session requires a session name"
			return m, nil
		}
		m.panes[m.focus].Session = strings.TrimSpace(fields[1])
		m.markSessionActive(m.panes[m.focus].Session)
		m.panes[m.focus].Loading = true
		m.status = "Switched pane to " + m.panes[m.focus].Session
		return m, m.loadHistoryCmd(m.panes[m.focus].ID, m.panes[m.focus].Session)
	case "split", "new":
		if len(fields) < 2 {
			m.globalErr = fmt.Sprintf("/%s requires a session name", cmd)
			return m, nil
		}
		if len(m.panes) >= 4 {
			m.globalErr = "Maximum 4 panes"
			return m, nil
		}
		session := strings.TrimSpace(fields[1])
		pane := newTUIPane(m.nextPaneID, session)
		m.nextPaneID++
		pane.Loading = true
		m.panes = append(m.panes, pane)
		m.focus = len(m.panes) - 1
		m.globalErr = ""
		m.status = "Opened pane: " + session
		m.resize()
		return m, tea.Batch(m.loadHistoryCmd(pane.ID, pane.Session), m.loadSessionsCmd())
	case "close":
		if len(m.panes) <= 1 {
			m.globalErr = "Cannot close the last pane"
			return m, nil
		}
		closed := m.panes[m.focus].Session
		m.panes = append(m.panes[:m.focus], m.panes[m.focus+1:]...)
		if m.focus >= len(m.panes) {
			m.focus = len(m.panes) - 1
		}
		m.globalErr = ""
		m.status = "Closed pane: " + closed
		m.resize()
		return m, nil
	case "focus":
		if len(fields) < 2 {
			m.globalErr = "/focus requires a pane number"
			return m, nil
		}
		var n int
		if _, err := fmt.Sscanf(fields[1], "%d", &n); err != nil || n < 1 || n > len(m.panes) {
			m.globalErr = "Invalid pane number"
			return m, nil
		}
		m.focus = n - 1
		m.panes[m.focus].Unread = false
		m.markSessionActive(m.panes[m.focus].Session)
		m.globalErr = ""
		m.status = "Focused pane: " + m.panes[m.focus].Session
		return m, nil
	case "filter":
		value := ""
		if len(fields) > 1 {
			value = strings.TrimSpace(strings.Join(fields[1:], " "))
		}
		m.sessionFilter = value
		filtered := m.filteredSessions()
		if len(filtered) == 0 {
			m.sessionCursor = 0
			m.globalErr = "No matching sessions"
		} else {
			m.sessionCursor = 0
			m.globalErr = ""
			m.status = fmt.Sprintf("Filtered sessions: %d", len(filtered))
		}
		return m, nil
	default:
		m.globalErr = "Unknown command: " + input
		return m, nil
	}
}

func (m tuiModel) streamChatCmd(paneID int, session, content string) tea.Cmd {
	client := m.client
	runtime := m.runtime
	return func() tea.Msg {
		go func() {
			err := client.streamChat(context.Background(), session, content, func(msg tea.Msg) {
				switch typed := msg.(type) {
				case tuiChatChunkMsg:
					typed.PaneID = paneID
					runtime.send(typed)
				default:
					runtime.send(msg)
				}
			})
			if err != nil {
				runtime.send(tuiChatErrorMsg{PaneID: paneID, Err: err})
			}
		}()
		return nil
	}
}

func (m *tuiModel) appendSystemToFocused(content string) {
	if len(m.panes) == 0 {
		return
	}
	m.panes[m.focus].Entries = append(m.panes[m.focus].Entries, tuiChatEntry{Role: "system", Content: content})
	m.syncPaneViewport(m.focus, true)
}

func (m *tuiModel) setPaneSize(index, width, height int) {
	if index < 0 || index >= len(m.panes) {
		return
	}
	if width < 24 {
		width = 24
	}
	if height < 6 {
		height = 6
	}
	m.panes[index].Viewport.Width = width - 4
	m.panes[index].Viewport.Height = height - 4
	if m.panes[index].Viewport.Height < 3 {
		m.panes[index].Viewport.Height = 3
	}
	m.syncPaneViewport(index, false)
}

func (m *tuiModel) hasPaneSession(session string) bool {
	needle := strings.TrimSpace(session)
	if needle == "" {
		return false
	}
	for i := range m.panes {
		if strings.TrimSpace(m.panes[i].Session) == needle {
			return true
		}
	}
	return false
}

func (m *tuiModel) hasUnreadSession(session string) bool {
	needle := strings.TrimSpace(session)
	if needle == "" {
		return false
	}
	for i := range m.panes {
		if strings.TrimSpace(m.panes[i].Session) == needle && m.panes[i].Unread {
			return true
		}
	}
	return false
}

func (m *tuiModel) sidebarSessionMarker(session string) string {
	focused := m.focusedPane() != nil && strings.TrimSpace(m.focusedPane().Session) == strings.TrimSpace(session)
	open := m.hasPaneSession(session)
	unread := m.hasUnreadSession(session)
	switch {
	case focused && unread:
		return " ◉•"
	case focused:
		return " ◉"
	case unread:
		return " •"
	case open:
		return " ●"
	default:
		return ""
	}
}

func (m tuiModel) openSelectedSessionInPane(replace bool) (tea.Model, tea.Cmd) {
	filtered := m.filteredSessions()
	if len(filtered) == 0 {
		m.globalErr = "No sessions available"
		return m, nil
	}
	if m.sessionCursor >= len(filtered) {
		m.sessionCursor = len(filtered) - 1
	}
	session := strings.TrimSpace(filtered[m.sessionCursor].Key)
	if session == "" {
		m.globalErr = "Invalid session"
		return m, nil
	}
	if idx := m.findPaneBySession(session); idx >= 0 && !replace {
		m.focus = idx
		m.panes[m.focus].Unread = false
		m.markSessionActive(session)
		m.globalErr = ""
		m.status = "Focused pane: " + session
		return m, nil
	}
	if replace {
		if len(m.panes) == 0 {
			return m, nil
		}
		m.panes[m.focus].Session = session
		m.markSessionActive(session)
		m.panes[m.focus].Loading = true
		m.panes[m.focus].ErrText = ""
		m.status = "Replaced pane with " + session
		return m, m.loadHistoryCmd(m.panes[m.focus].ID, session)
	}
	if len(m.panes) >= 4 {
		m.globalErr = "Maximum 4 panes"
		return m, nil
	}
	pane := newTUIPane(m.nextPaneID, session)
	m.nextPaneID++
	pane.Loading = true
	m.panes = append(m.panes, pane)
	m.focus = len(m.panes) - 1
	m.markSessionActive(session)
	m.globalErr = ""
	m.status = "Opened pane: " + session
	m.resize()
	return m, m.loadHistoryCmd(pane.ID, session)
}

func (m *tuiModel) findPane(id int) int {
	for i := range m.panes {
		if m.panes[i].ID == id {
			return i
		}
	}
	return -1
}

func (m *tuiModel) findPaneBySession(session string) int {
	needle := strings.TrimSpace(session)
	for i := range m.panes {
		if strings.TrimSpace(m.panes[i].Session) == needle {
			return i
		}
	}
	return -1
}

func (m *tuiModel) filteredSessions() []tuiSession {
	out := make([]tuiSession, 0, len(m.sessions))
	if strings.TrimSpace(m.sessionFilter) == "" {
		out = append(out, m.sessions...)
	} else {
		query := strings.ToLower(strings.TrimSpace(m.sessionFilter))
		for _, session := range m.sessions {
			if strings.Contains(strings.ToLower(session.Key), query) {
				out = append(out, session)
			}
		}
	}
	sort.SliceStable(out, func(i, j int) bool {
		iOpen := m.hasPaneSession(out[i].Key)
		jOpen := m.hasPaneSession(out[j].Key)
		if iOpen != jOpen {
			return iOpen
		}
		iActive := m.sessionActiveAt(out[i].Key)
		jActive := m.sessionActiveAt(out[j].Key)
		if !iActive.Equal(jActive) {
			return iActive.After(jActive)
		}
		return strings.ToLower(out[i].Key) < strings.ToLower(out[j].Key)
	})
	return out
}

func (m *tuiModel) markSessionActive(session string) {
	key := strings.TrimSpace(session)
	if key == "" {
		return
	}
	if m.sessionActive == nil {
		m.sessionActive = map[string]time.Time{}
	}
	m.sessionActive[key] = time.Now()
}

func (m *tuiModel) sessionActiveAt(session string) time.Time {
	if m.sessionActive == nil {
		return time.Time{}
	}
	return m.sessionActive[strings.TrimSpace(session)]
}

func (m *tuiModel) focusedPane() *tuiPane {
	if len(m.panes) == 0 {
		return nil
	}
	return &m.panes[m.focus]
}

func parseTUIOptions(args []string) (tuiOptions, error) {
	opts := tuiOptions{session: "main"}
	cfg, err := loadConfig()
	if err == nil {
		opts.baseURL = gatewayBaseURLFromConfig(cfg)
		opts.token = strings.TrimSpace(cfg.Gateway.Token)
	}
	for i := 0; i < len(args); i++ {
		switch args[i] {
		case "--help", "-h":
			printTUIHelp()
			os.Exit(0)
		case "--gateway":
			if i+1 >= len(args) {
				return opts, fmt.Errorf("--gateway requires a value")
			}
			opts.baseURL = strings.TrimSpace(args[i+1])
			i++
		case "--token":
			if i+1 >= len(args) {
				return opts, fmt.Errorf("--token requires a value")
			}
			opts.token = strings.TrimSpace(args[i+1])
			i++
		case "--session", "-s":
			if i+1 >= len(args) {
				return opts, fmt.Errorf("--session requires a value")
			}
			opts.session = strings.TrimSpace(args[i+1])
			i++
		case "--no-history":
			opts.noHistory = true
		default:
			return opts, fmt.Errorf("unknown option: %s", args[i])
		}
	}
	if strings.TrimSpace(opts.baseURL) == "" {
		opts.baseURL = "http://127.0.0.1:8080"
	}
	if !strings.HasPrefix(opts.baseURL, "http://") && !strings.HasPrefix(opts.baseURL, "https://") {
		opts.baseURL = "http://" + opts.baseURL
	}
	if strings.TrimSpace(opts.session) == "" {
		opts.session = "main"
	}
	return opts, nil
}

func printTUIHelp() {
	fmt.Println("Usage: clawgo tui [options]")
	fmt.Println()
	fmt.Println("Options:")
	fmt.Println("  --gateway <url>       Gateway base URL, for example http://127.0.0.1:8080")
	fmt.Println("  --token <value>       Gateway token")
	fmt.Println("  --session, -s <key>   Initial session key (default: main)")
	fmt.Println("  --no-history          Skip loading session history on startup")
	fmt.Println()
	fmt.Println("In TUI:")
	fmt.Println("  /split <session>      Open a new pane for another session")
	fmt.Println("  /close                Close focused pane")
	fmt.Println("  /focus <n>            Focus pane number")
}

func gatewayBaseURLFromConfig(cfg *config.Config) string {
	host := strings.TrimSpace(cfg.Gateway.Host)
	switch host {
	case "", "0.0.0.0", "::", "[::]":
		host = "127.0.0.1"
	}
	return fmt.Sprintf("http://%s:%d", host, cfg.Gateway.Port)
}

func historyEntries(messages []tuiMessage) []tuiChatEntry {
	out := make([]tuiChatEntry, 0, len(messages))
	for _, message := range messages {
		content := strings.TrimSpace(message.Content)
		if content == "" && len(message.ToolCalls) > 0 {
			content = fmt.Sprintf("[tool calls: %d]", len(message.ToolCalls))
		}
		if content == "" {
			continue
		}
		out = append(out, tuiChatEntry{Role: strings.ToLower(strings.TrimSpace(message.Role)), Content: content})
	}
	return out
}

func renderEntries(entries []tuiChatEntry, width int) string {
	if len(entries) == 0 {
		return tuiMutedStyle.Render("No messages yet.")
	}
	parts := make([]string, 0, len(entries))
	for _, entry := range entries {
		label := tuiRoleLabel(entry.Role)
		content := entry.Content
		if strings.TrimSpace(content) == "" && entry.Streaming {
			content = "..."
		}
		style := tuiContentStyle
		switch entry.Role {
		case "user":
			style = tuiUserStyle
		case "assistant":
			style = tuiAssistantStyle
		case "system":
			style = tuiSystemStyle
		case "tool":
			style = tuiToolStyle
		}
		if width > 8 {
			style = style.Width(width - 2)
		}
		if entry.Streaming {
			content += " ▌"
		}
		parts = append(parts, lipgloss.JoinVertical(lipgloss.Left, tuiLabelStyle.Render(label), style.Render(content)))
	}
	return strings.Join(parts, "\n\n")
}

func tuiRoleLabel(role string) string {
	switch role {
	case "user":
		return "You"
	case "system":
		return "System"
	case "tool":
		return "Tool"
	default:
		return "Assistant"
	}
}

type tuiClient struct {
	baseURL string
	token   string
	http    *http.Client
}

func (c *tuiClient) ping() error {
	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
	defer cancel()
	req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/version", nil)
	if err != nil {
		return err
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("%s", strings.TrimSpace(string(body)))
	}
	return nil
}

func (c *tuiClient) history(ctx context.Context, session string) ([]tuiMessage, error) {
	req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/chat/history?session="+url.QueryEscape(session), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return nil, fmt.Errorf("%s", strings.TrimSpace(string(body)))
	}
	var payload struct {
		Messages []tuiMessage `json:"messages"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}
	return payload.Messages, nil
}

func (c *tuiClient) sessions(ctx context.Context) ([]tuiSession, error) {
	req, err := c.newRequest(ctx, http.MethodGet, "/webui/api/sessions", nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return nil, fmt.Errorf("%s", strings.TrimSpace(string(body)))
	}
	var payload struct {
		Sessions []tuiSession `json:"sessions"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, err
	}
	return payload.Sessions, nil
}

func (c *tuiClient) streamChat(ctx context.Context, session, content string, send func(tea.Msg)) error {
	conn, err := c.openChatSocket(ctx)
	if err != nil {
		return err
	}
	defer conn.Close()
	if err := conn.WriteJSON(map[string]string{"session": session, "message": content}); err != nil {
		return err
	}
	for {
		var frame struct {
			Type  string `json:"type"`
			Delta string `json:"delta"`
			Error string `json:"error"`
		}
		if err := conn.ReadJSON(&frame); err != nil {
			return err
		}
		switch frame.Type {
		case "chat_chunk":
			send(tuiChatChunkMsg{Delta: frame.Delta})
		case "chat_done":
			send(tuiChatDoneMsg{})
			return nil
		case "chat_error":
			return fmt.Errorf("%s", strings.TrimSpace(frame.Error))
		}
	}
}

func (c *tuiClient) openChatSocket(ctx context.Context) (*websocket.Conn, error) {
	wsURL, err := c.websocketURL("/webui/api/chat/live")
	if err != nil {
		return nil, err
	}
	header := http.Header{}
	if c.token != "" {
		header.Set("Authorization", "Bearer "+c.token)
	}
	conn, resp, err := websocket.DefaultDialer.DialContext(ctx, wsURL, header)
	if err != nil {
		if resp != nil && resp.Body != nil {
			defer resp.Body.Close()
			body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
			if strings.TrimSpace(string(body)) != "" {
				return nil, fmt.Errorf("%s", strings.TrimSpace(string(body)))
			}
		}
		return nil, err
	}
	return conn, nil
}

func (c *tuiClient) websocketURL(path string) (string, error) {
	u, err := url.Parse(c.baseURL)
	if err != nil {
		return "", err
	}
	switch u.Scheme {
	case "http":
		u.Scheme = "ws"
	case "https":
		u.Scheme = "wss"
	default:
		return "", fmt.Errorf("unsupported gateway scheme: %s", u.Scheme)
	}
	u.Path = path
	q := u.Query()
	if c.token != "" {
		q.Set("token", c.token)
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}

func (c *tuiClient) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
	if err != nil {
		return nil, err
	}
	if c.token != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}
	return req, nil
}

var (
	tuiColorBorder      = lipgloss.Color("#F7B08A")
	tuiColorClaw        = lipgloss.Color("#FFB36B")
	tuiColorShell       = lipgloss.Color("#F05A28")
	tuiColorShellDeep   = lipgloss.Color("#D9481C")
	tuiColorAccent      = lipgloss.Color("#C44B21")
	tuiColorAccentDeep  = lipgloss.Color("#A43A18")
	tuiColorText        = lipgloss.Color("#472018")
	tuiColorTextSoft    = lipgloss.Color("#6E3B2E")
	tuiColorError       = lipgloss.Color("#B93815")
	tuiColorSurfaceText = lipgloss.Color("#FFF4EE")

	tuiHeaderStyle = lipgloss.NewStyle().
			Padding(0, 1).
			BorderBottom(true).
			BorderStyle(lipgloss.NormalBorder()).
			Foreground(tuiColorText).
			BorderForeground(tuiColorBorder)

	tuiTitleStyle = lipgloss.NewStyle().
			Bold(true).
			Foreground(tuiColorShellDeep)

	tuiBannerClawStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorClaw)

	tuiBannerGoStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorShell)

	tuiMutedStyle = lipgloss.NewStyle().
			Foreground(tuiColorTextSoft)

	tuiInputBoxStyle = lipgloss.NewStyle().
				Padding(0, 1).
				BorderTop(true).
				BorderStyle(lipgloss.NormalBorder()).
				Foreground(tuiColorText).
				BorderForeground(tuiColorBorder)

	tuiViewportStyle = lipgloss.NewStyle().Padding(0, 1)

	tuiFooterStyle = lipgloss.NewStyle().
			Padding(0, 1).
			Foreground(tuiColorText)

	tuiErrorStyle = lipgloss.NewStyle().
			Padding(0, 1).
			Foreground(tuiColorError)

	tuiPaneStyle = lipgloss.NewStyle().
			Padding(0, 1).
			BorderStyle(lipgloss.RoundedBorder()).
			Foreground(tuiColorText).
			BorderForeground(tuiColorBorder)

	tuiPaneActiveStyle = lipgloss.NewStyle().
				Padding(0, 1).
				BorderStyle(lipgloss.RoundedBorder()).
				Foreground(tuiColorText).
				BorderForeground(tuiColorShell)

	tuiPaneTitleStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorAccentDeep)

	tuiPaneTitleActiveStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorShellDeep)

	tuiPaneTitleActiveBadgeStyle = lipgloss.NewStyle().
					Bold(true).
					Foreground(tuiColorSurfaceText).
					Background(tuiColorShell)

	tuiPaneGapStyle = lipgloss.NewStyle()

	tuiSidebarStyle = lipgloss.NewStyle().
			Padding(0, 1).
			BorderStyle(lipgloss.RoundedBorder()).
			Foreground(tuiColorText).
			BorderForeground(tuiColorBorder)

	tuiSidebarItemStyle = lipgloss.NewStyle().
				Foreground(tuiColorText)

	tuiSidebarItemActiveStyle = lipgloss.NewStyle().
					Bold(true).
					Foreground(tuiColorShellDeep)
	tuiFooterHintStyle = lipgloss.NewStyle().
				Foreground(tuiColorTextSoft)

	tuiPaneReplyingStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorSurfaceText).
				Background(tuiColorShellDeep)

	tuiPaneLoadingStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorText).
				Background(tuiColorClaw)

	tuiPaneErrorBadgeStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(tuiColorSurfaceText).
				Background(tuiColorError)

	tuiLabelStyle = lipgloss.NewStyle().
			Bold(true).
			Foreground(tuiColorAccent)

	tuiContentStyle   = lipgloss.NewStyle().PaddingLeft(1).Foreground(tuiColorText)
	tuiUserStyle      = lipgloss.NewStyle().PaddingLeft(1).Foreground(tuiColorShellDeep)
	tuiAssistantStyle = lipgloss.NewStyle().
				PaddingLeft(1).
				Foreground(tuiColorText)
	tuiSystemStyle = lipgloss.NewStyle().PaddingLeft(1).Foreground(tuiColorAccent)
	tuiToolStyle   = lipgloss.NewStyle().PaddingLeft(1).Foreground(tuiColorAccentDeep)
)

func renderTUIBanner(width int) string {
	claw := strings.Join([]string{
		" ██████╗██╗      █████╗ ██╗    ██╗",
		"██╔════╝██║     ██╔══██╗██║    ██║",
		"██║     ██║     ███████║██║ █╗ ██║",
		"██║     ██║     ██╔══██║██║███╗██║",
		"╚██████╗███████╗██║  ██║╚███╔███╔╝",
		" ╚═════╝╚══════╝╚═╝  ╚═╝ ╚══╝╚══╝ ",
	}, "\n")
	goText := strings.Join([]string{
		" ██████╗  ██████╗ ",
		"██╔════╝ ██╔═══██╗",
		"██║  ███╗██║   ██║",
		"██║   ██║██║   ██║",
		"╚██████╔╝╚██████╔╝",
		" ╚═════╝  ╚═════╝ ",
	}, "\n")
	left := strings.Split(claw, "\n")
	right := strings.Split(goText, "\n")
	lines := make([]string, 0, len(left))
	for i := range left {
		lines = append(lines, tuiBannerClawStyle.Render(left[i])+"  "+tuiBannerGoStyle.Render(right[i]))
	}
	full := strings.Join(lines, "\n")
	if width <= 0 || lipgloss.Width(full) <= width-2 {
		return full
	}
	compactLines := []string{
		tuiBannerClawStyle.Render("██████╗██╗      █████╗ ██╗    ██╗") + " " + tuiBannerGoStyle.Render(" ██████╗  ██████╗ "),
		tuiBannerClawStyle.Render("██╔═══╝██║     ██╔══██╗██║ █╗ ██║") + " " + tuiBannerGoStyle.Render("██╔════╝ ██╔═══██╗"),
		tuiBannerClawStyle.Render("██║    ██║     ███████║██║███╗██║") + " " + tuiBannerGoStyle.Render("██║  ███╗██║   ██║"),
		tuiBannerClawStyle.Render("╚██████╗███████╗██║  ██║╚███╔███╔╝") + " " + tuiBannerGoStyle.Render("╚██████╔╝╚██████╔╝"),
	}
	compact := strings.Join(compactLines, "\n")
	if lipgloss.Width(compact) <= width-2 {
		return compact
	}
	short := tuiBannerClawStyle.Render("Claw") + tuiBannerGoStyle.Render("Go")
	if lipgloss.Width(short) <= width-2 {
		return short
	}
	return tuiTitleStyle.Render("ClawGo")
}

func compactGatewayLabel(raw string) string {
	text := strings.TrimSpace(raw)
	text = strings.TrimPrefix(text, "http://")
	text = strings.TrimPrefix(text, "https://")
	return text
}

func intersperse(items []string, sep string) []string {
	if len(items) <= 1 {
		return items
	}
	out := make([]string, 0, len(items)*2-1)
	for i, item := range items {
		if i > 0 {
			out = append(out, sep)
		}
		out = append(out, item)
	}
	return out
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func filterTUISessions(items []tuiSession) []tuiSession {
	out := make([]tuiSession, 0, len(items))
	seen := map[string]struct{}{}
	for _, item := range items {
		key := strings.TrimSpace(item.Key)
		if key == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, tuiSession{Key: key})
	}
	return out
}
