package main

import (
	"context"
	"fmt"
	"strings"
	"time"

	"charm.land/bubbles/v2/list"
	"charm.land/bubbles/v2/textarea"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"github.com/atotto/clipboard"
	"github.com/charmbracelet/x/ansi"
	lipgloss "charm.land/lipgloss/v2"
)

type message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type modelRef struct {
	provider string
	model    string
}

func (m modelRef) Title() string       { return m.provider + "/" + m.model }
func (m modelRef) Description() string { return "" }
func (m modelRef) FilterValue() string { return m.provider + "/" + m.model }

type screenMode int

const (
	screenChat screenMode = iota
	screenModelPicker
	screenSessionPicker
)

type chatbot struct {
	screen    screenMode
	messages  []message
	viewport  viewport.Model
	textarea  textarea.Model
	modelList list.Model
	sessList  list.Model
	ready     bool
	width     int
	height    int
	streaming bool
	follow    bool
	respBuf   strings.Builder

	currentProvider string
	currentModel    string
	providers       map[string]ProviderConfig
	styles          Styles
	err             error
	streamCh        <-chan tea.Msg

	history    []string
	historyIdx int
	historyDft string

	sessions   []SessionData
	sessionIdx int

	hasAlternateKeys bool
	animTick         int
	cancel           context.CancelFunc
	textareaHeight   int
	pasteBuffer      string
	selecting        bool
	selStartY        int
	selEndY          int
	selMode          bool
	selVisual        bool
	selCurY          int
	selMarkY         int
}

var promptFrames []string

type animTickMsg struct{}

func tickAnim() tea.Cmd {
	return tea.Tick(150*time.Millisecond, func(t time.Time) tea.Msg {
		return animTickMsg{}
	})
}

func newChatbot(providers map[string]ProviderConfig, sessions []SessionData, sty Styles) (*chatbot, error) {
	ta := textarea.New()
	ta.Placeholder = ""
	ta.ShowLineNumbers = false
	ta.CharLimit = 0
	ta.KeyMap.InsertNewline.SetEnabled(true)
	ta.Focus()
	ta.Prompt = "⋮⋮⋮ "
	ta.DynamicHeight = true
	ta.MinHeight = 1
	ta.MaxHeight = 10
	ta.MaxWidth = 0

	inputBg := sty.BgSubtle
	inputFg := sty.Fg

	bgStyle := lipgloss.NewStyle().Background(inputBg)
	fgStyle := bgStyle.Foreground(inputFg)

	taStyles := ta.Styles()
	taStyles.Focused.Base = bgStyle
	taStyles.Focused.Text = fgStyle
	taStyles.Focused.CursorLine = fgStyle
	taStyles.Focused.LineNumber = bgStyle
	taStyles.Focused.EndOfBuffer = bgStyle
	taStyles.Focused.Prompt = sty.PromptFocused
	taStyles.Focused.Placeholder = bgStyle.Foreground(sty.Help.GetForeground())
	taStyles.Blurred.Base = bgStyle
	taStyles.Blurred.Text = fgStyle
	taStyles.Blurred.CursorLine = fgStyle
	taStyles.Blurred.LineNumber = bgStyle
	taStyles.Blurred.EndOfBuffer = bgStyle
	taStyles.Blurred.Prompt = sty.PromptBlurred
	taStyles.Blurred.Placeholder = bgStyle.Foreground(sty.Help.GetForeground())
	taStyles.Cursor.Color = sty.LogoGradFrom
	taStyles.Cursor.Shape = tea.CursorBlock
	taStyles.Cursor.Blink = true
	ta.SetStyles(taStyles)

	promptFrames = []string{
		"·⋮⋮ ",
		"⋮·⋮ ",
		"⋮⋮· ",
	}

	vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(20))

	dd := list.NewDefaultDelegate()
	dd.Styles.SelectedTitle = lipgloss.NewStyle().
		Border(lipgloss.ThickBorder(), false, false, false, true).
		BorderForeground(sty.ModelPickerBorder).
		Bold(true).
		Padding(0, 0, 0, 1)
	dd.Styles.SelectedDesc = dd.Styles.SelectedTitle

	items := buildModelItems(providers)
	if len(items) == 0 {
		return nil, fmt.Errorf("no models available from any provider")
	}

	ml := list.New(items, dd, 40, 15)
	ml.Title = "Select model (enter/esc, type to filter)"
	ml.SetShowStatusBar(false)
	ml.SetFilteringEnabled(true)
	ml.Styles.Title = sty.Help

	sl := list.New(nil, dd, 40, 15)
	sl.Title = "Sessions (enter: switch, n: new, d: delete, esc: close)"
	sl.SetShowStatusBar(false)
	sl.SetFilteringEnabled(true)
	sl.Styles.Title = sty.Help

	ref := items[0].(modelRef)

	if len(sessions) == 0 {
		sessions = []SessionData{NewSessionData(ref.provider, ref.model)}
	}
	si := 0
	cur := sessions[si]
	var msgs []message
	var hist []string
	prov := cur.Provider
	mod := cur.Model
	if cur.Messages != nil {
		msgs = cur.Messages
	}
	if cur.History != nil {
		hist = cur.History
	}
	if _, ok := providers[prov]; !ok {
		prov = ref.provider
	}
	if mod == "" {
		mod = ref.model
	}

	cb := &chatbot{
		screen:           screenChat,
		messages:         msgs,
		viewport:         vp,
		textarea:         ta,
		modelList:        ml,
		sessList:         sl,
		currentProvider:  prov,
		currentModel:     mod,
		providers:        providers,
		styles:           sty,
		historyIdx:       -1,
		history:          hist,
		sessions:         sessions,
		sessionIdx:       si,
		hasAlternateKeys: false,
		follow:           true,
	}

	return cb, nil
}

func buildModelItems(providers map[string]ProviderConfig) []list.Item {
	var items []list.Item
	for pid := range providers {
		pid2 := pid
		models, err := fetchModels(providers[pid].BaseURL)
		if err != nil {
			continue
		}
		for _, m := range models {
			items = append(items, modelRef{provider: pid2, model: m})
		}
	}
	return items
}

func (m *chatbot) Init() tea.Cmd {
	return nil
}

func (m *chatbot) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		return m.handleResize(msg)

	case tea.KeyPressMsg:
		return m.handleKey(msg)

	case tea.KeyboardEnhancementsMsg:
		m.hasAlternateKeys = msg.SupportsAlternateKeys()
		return m, nil

	case streamChunkMsg:
		m.respBuf.WriteString(string(msg))
		m.updateViewport()
		return m, readNext(m.streamCh)

	case streamDoneMsg:
		m.streaming = false
		m.streamCh = nil
		m.textarea.Prompt = "⋮⋮⋮ "
		content := m.respBuf.String()
		m.respBuf.Reset()
		if content != "" {
			m.messages = append(m.messages, message{Role: "assistant", Content: content})
		}
		m.saveCurrentSession()
		if m.cancel == nil {
			m.messages = append(m.messages, message{Role: "system", Content: "Canceled"})
		}
		m.recalcLayout()
		m.updateViewport()
		return m, nil

	case streamErrMsg:
		m.streaming = false
		m.streamCh = nil
		m.textarea.Prompt = "⋮⋮⋮ "
		m.err = msg.err
		m.messages = append(m.messages, message{Role: "system", Content: msg.err.Error()})
		m.recalcLayout()
		m.updateViewport()
		return m, nil

	case animTickMsg:
		if m.streaming {
			m.animTick = (m.animTick + 1) % len(promptFrames)
			m.textarea.Prompt = promptFrames[m.animTick]
			return m, tickAnim()
		}
		return m, nil

	case tea.PasteMsg:
		content := msg.Content
		lines := strings.Split(content, "\n")
		if len(lines) <= 1 {
			for _, r := range content {
				m.textarea.InsertRune(r)
			}
		} else {
			nonEmpty := 0
			for _, l := range lines {
				if strings.TrimSpace(l) != "" {
					nonEmpty++
				}
			}
			m.pasteBuffer = content
			m.textarea.SetValue(fmt.Sprintf("[Pasted ~%d lines]", nonEmpty))
			m.textarea.MoveToEnd()
			m.recalcLayout()
		}
		return m, nil

	case tea.MouseWheelMsg:
		vp, _ := m.viewport.Update(msg)
		m.viewport = vp
		switch msg.Button {
		case tea.MouseWheelUp:
			m.follow = false
		case tea.MouseWheelDown:
			if m.viewport.AtBottom() {
				m.follow = true
			}
		}
		return m, nil

	case tea.MouseClickMsg:
		if msg.Button == tea.MouseLeft && !m.isLanding() && msg.Y > 0 && msg.Y <= m.viewport.Height() && msg.X < m.width-1 {
			m.selecting = true
			m.selStartY = msg.Y - 1 + m.viewport.YOffset()
			m.selEndY = m.selStartY
		}
		return m, nil

	case tea.MouseMotionMsg:
		if m.selecting && !m.isLanding() && msg.Y > 0 && msg.Y <= m.viewport.Height() && msg.X < m.width-1 {
			m.selEndY = msg.Y - 1 + m.viewport.YOffset()
		}
		return m, nil

	case tea.MouseReleaseMsg:
		if m.selecting {
			m.selecting = false
			if m.selStartY != m.selEndY {
				content := m.buildViewportContent()
				lines := strings.Split(content, "\n")
				start, end := m.selStartY, m.selEndY
				if start > end {
					start, end = end, start
				}
				if start < 0 {
					start = 0
				}
				if end >= len(lines) {
					end = len(lines) - 1
				}
				selected := strings.Join(lines[start:end+1], "\n")
				selected = ansi.Strip(selected)
				selected = strings.TrimSpace(selected)
				if selected != "" {
					clipboard.WriteAll(selected)
				}
			}
		}
		return m, nil
	}
	return m, nil
}

func (m *chatbot) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
	m.width = msg.Width
	m.height = msg.Height

	if !m.ready {
		m.ready = true
		vp := viewport.New(viewport.WithWidth(msg.Width-1), viewport.WithHeight(1))
		m.viewport = vp
		m.textarea.SetWidth(msg.Width - 6)
	}
	m.viewport.SetWidth(msg.Width - 1)
	if m.isLanding() {
		w := min(m.width*2/5, 200)
		m.textarea.SetWidth(w - 6)
	} else {
		m.textarea.SetWidth(msg.Width - 8)
	}
	m.recalcLayout()
	return m, nil
}

func (m *chatbot) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	key := msg.String()

	if key == "ctrl+c" {
		m.saveCurrentSession()
		return m, tea.Quit
	}

	if key == "esc" && m.streaming {
		if m.cancel != nil {
			m.cancel()
			m.cancel = nil
		}
		return m, nil
	}

	if m.selMode {
		return m.handleSelKey(key, msg)
	}

	if m.screen == screenModelPicker {
		return m.handleModelPickerKey(key, msg)
	}
	if m.screen == screenSessionPicker {
		return m.handleSessionPickerKey(key, msg)
	}

	if key == "ctrl+v" {
		if !m.selMode {
			m.selMode = true
			m.selVisual = false
			m.selCurY = m.viewport.YOffset()
			m.selMarkY = -1
			m.textarea.Blur()
		}
		return m, nil
	}

	if key == "enter" {
		text := strings.TrimSpace(m.textarea.Value())
		if text == "" {
			return m, nil
		}

		if m.isLanding() {
			m.newSessionInner()
		}

		if m.pasteBuffer != "" {
			text = strings.TrimSpace(m.pasteBuffer)
			m.pasteBuffer = ""
		}

		m.textarea.Reset()
		m.history = append(m.history, text)
		m.historyIdx = -1

		m.messages = append(m.messages, message{Role: "user", Content: text})
		m.saveCurrentSession()
		m.follow = true
		m.updateViewport()

		m.streaming = true
		m.respBuf.Reset()
		m.animTick = 0
		m.textarea.Prompt = "⋮⋮⋮ "
		m.recalcLayout()
		m.updateViewport()

		api := make([]chatMessage, len(m.messages))
		for i, msg := range m.messages {
			api[i] = chatMessage{Role: msg.Role, Content: msg.Content}
		}
		prov := m.providers[m.currentProvider]
		ctx, cancel := context.WithCancel(context.Background())
		m.cancel = cancel
		ch, cmd := startStream(ctx, api, m.currentModel, prov.BaseURL)
		m.streamCh = ch
		return m, tea.Batch(cmd, tickAnim())
	}

	if key == "ctrl+o" {
		m.modelList.ResetFilter()
		m.screen = screenModelPicker
		return m, nil
	}

	if key == "ctrl+s" {
		m.saveCurrentSession()
		m.sessList.SetItems(buildSessionItems(m.sessions, m.sessionIdx))
		m.sessList.ResetFilter()
		m.screen = screenSessionPicker
		return m, nil
	}

	if key == "ctrl+n" {
		return m.newSession()
	}

	if key == "ctrl+w" {
		return m.closeCurrentSession()
	}

	if key == "pgup" {
		m.follow = false
		m.viewport.ScrollUp(m.height / 2)
		return m, nil
	}

	if key == "pgdown" {
		m.viewport.ScrollDown(m.height / 2)
		if m.viewport.AtBottom() {
			m.follow = true
		}
		return m, nil
	}

	if key == "up" || key == "ctrl+p" {
		if len(m.history) == 0 {
			return m, nil
		}
		if m.historyIdx == -1 {
			m.historyDft = m.textarea.Value()
			m.historyIdx = len(m.history) - 1
		} else if m.historyIdx > 0 {
			m.historyIdx--
		}
		m.textarea.Reset()
		m.textarea.SetValue(m.history[m.historyIdx])
		m.textarea.MoveToEnd()
		return m, nil
	}

	if key == "down" {
		if m.historyIdx == -1 {
			return m, nil
		}
		m.historyIdx++
		if m.historyIdx >= len(m.history) {
			m.historyIdx = -1
			m.textarea.Reset()
			if m.historyDft != "" {
				m.textarea.SetValue(m.historyDft)
				m.historyDft = ""
			}
		} else {
			m.textarea.Reset()
			m.textarea.SetValue(m.history[m.historyIdx])
		}
		m.textarea.MoveToEnd()
		return m, nil
	}

	m.historyIdx = -1
	prevH := m.textarea.Height()
	ta, cmd := m.textarea.Update(msg)
	m.textarea = ta
	if ta.Height() != prevH {
		m.recalcLayout()
	}
	return m, cmd
}

func (m *chatbot) handleModelPickerKey(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	if key == "esc" {
		m.screen = screenChat
		return m, nil
	}
	if key == "enter" {
		if item, ok := m.modelList.SelectedItem().(modelRef); ok {
			m.currentProvider = item.provider
			m.currentModel = item.model
		}
		m.screen = screenChat
		return m, nil
	}
	ml, cmd := m.modelList.Update(msg)
	m.modelList = ml
	return m, cmd
}

func (m *chatbot) handleSessionPickerKey(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	if key == "esc" {
		m.screen = screenChat
		return m, nil
	}
	if key == "enter" {
		if item, ok := m.sessList.SelectedItem().(sessionItem); ok {
			if idx := m.findSessionIndex(item.session.ID); idx >= 0 && idx != m.sessionIdx {
				m.switchToSession(idx)
			}
		}
		m.screen = screenChat
		return m, nil
	}
	if key == "n" {
		m.saveCurrentSession()
		return m.newSessionInner()
	}
	if key == "d" {
		if item, ok := m.sessList.SelectedItem().(sessionItem); ok {
			return m.deleteSession(item.session.ID)
		}
		return m, nil
	}
	sl, cmd := m.sessList.Update(msg)
	m.sessList = sl
	return m, cmd
}

func (m *chatbot) findSessionIndex(id string) int {
	for i, s := range m.sessions {
		if s.ID == id {
			return i
		}
	}
	return -1
}

func (m *chatbot) newSession() (tea.Model, tea.Cmd) {
	m.saveCurrentSession()
	return m.newSessionInner()
}

func (m *chatbot) newSessionInner() (tea.Model, tea.Cmd) {
	s := NewSessionData(m.currentProvider, m.currentModel)
	m.sessions = append(m.sessions, s)
	m.sessionIdx = len(m.sessions) - 1
	m.messages = nil
	m.history = nil
	m.historyIdx = -1
	m.updateViewport()
	m.screen = screenChat
	return m, nil
}

func (m *chatbot) closeCurrentSession() (tea.Model, tea.Cmd) {
	if len(m.sessions) <= 1 {
		return m, nil
	}
	_ = DeleteSession(m.sessions[m.sessionIdx].ID)
	m.sessions = append(m.sessions[:m.sessionIdx], m.sessions[m.sessionIdx+1:]...)
	if m.sessionIdx >= len(m.sessions) {
		m.sessionIdx = len(m.sessions) - 1
	}
	m.switchToSession(m.sessionIdx)
	return m, nil
}

func (m *chatbot) deleteSession(id string) (tea.Model, tea.Cmd) {
	idx := m.findSessionIndex(id)
	if idx < 0 || len(m.sessions) <= 1 {
		return m, nil
	}
	_ = DeleteSession(m.sessions[idx].ID)
	m.sessions = append(m.sessions[:idx], m.sessions[idx+1:]...)
	if m.sessionIdx == idx {
		if m.sessionIdx >= len(m.sessions) {
			m.sessionIdx = len(m.sessions) - 1
		}
		m.switchToSession(m.sessionIdx)
	} else if m.sessionIdx > idx {
		m.sessionIdx--
	}
	m.sessList.SetItems(buildSessionItems(m.sessions, m.sessionIdx))
	return m, nil
}

func (m *chatbot) switchToSession(idx int) {
	m.sessionIdx = idx
	s := m.sessions[idx]
	m.messages = nil
	if s.Messages != nil {
		m.messages = s.Messages
	}
	m.history = nil
	if s.History != nil {
		m.history = s.History
	}
	m.historyIdx = -1
	if s.Provider != "" {
		m.currentProvider = s.Provider
	}
	if s.Model != "" {
		m.currentModel = s.Model
	}
	m.updateViewport()
}

func (m *chatbot) handleSelKey(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	totalLines := m.viewport.TotalLineCount()

	switch key {
	case "j", "down":
		if m.selCurY < totalLines-1 {
			m.selCurY++
		}
		m.follow = false
		m.ensureSelCursorVisible()
		return m, nil

	case "k", "up":
		if m.selCurY > 0 {
			m.selCurY--
		}
		m.follow = false
		m.ensureSelCursorVisible()
		return m, nil

	case "h", "left", "l", "right":
		return m, nil

	case "v":
		if m.selVisual {
			m.selVisual = false
			m.selMarkY = -1
		} else {
			m.selVisual = true
			m.selMarkY = m.selCurY
		}
		return m, nil

	case "enter":
		if m.selVisual {
			m.extractVisualSelection()
		}
		m.selMode = false
		m.selVisual = false
		m.selMarkY = -1
		m.textarea.Focus()
		return m, nil

	case "esc":
		m.selMode = false
		m.selVisual = false
		m.selMarkY = -1
		m.textarea.Focus()
		return m, nil

	default:
		return m, nil
	}
}

func (m *chatbot) ensureSelCursorVisible() {
	if m.selCurY < m.viewport.YOffset() {
		m.viewport.SetYOffset(m.selCurY)
	} else if m.selCurY >= m.viewport.YOffset()+m.viewport.Height() {
		m.viewport.SetYOffset(m.selCurY - m.viewport.Height() + 1)
	}
}

func (m *chatbot) extractVisualSelection() {
	start, end := m.selMarkY, m.selCurY
	if start > end {
		start, end = end, start
	}

	content := m.buildViewportContent()
	contentLines := strings.Split(content, "\n")
	vpWidth := m.width - 1

	visualCount := 0
	var selectedLines []string
	for _, line := range contentLines {
		w := lipgloss.Width(line)
		numVisual := 1
		if w > vpWidth {
			numVisual = (w + vpWidth - 1) / vpWidth
		}
		visualStart := visualCount
		visualEnd := visualCount + numVisual - 1
		if visualStart <= end && visualEnd >= start {
			selectedLines = append(selectedLines, line)
		}
		visualCount += numVisual
		if visualCount > end {
			break
		}
	}

	result := strings.Join(selectedLines, "\n")
	result = ansi.Strip(result)
	result = strings.TrimSpace(result)
	if result != "" {
		clipboard.WriteAll(result)
	}
}

func (m *chatbot) isLanding() bool {
	return len(m.messages) == 0 && !m.streaming && m.screen == screenChat
}

func (m *chatbot) View() tea.View {
	var v tea.View
	v.AltScreen = true
	v.BackgroundColor = m.styles.Bg
	v.MouseMode = tea.MouseModeCellMotion
	v.KeyboardEnhancements.ReportEventTypes = true

	if !m.ready {
		v.Content = "Initializing..."
		return v
	}

	if m.screen == screenModelPicker {
		v.Content = m.renderModelPicker()
		return v
	}
	if m.screen == screenSessionPicker {
		v.Content = m.renderSessionPicker()
		return v
	}

	sessionTag := fmt.Sprintf("[%d/%d] %s", m.sessionIdx+1, len(m.sessions), m.currentModel)

	var modeStr string
	if m.selVisual {
		modeStr = "--VISUAL--"
	} else if m.selMode {
		modeStr = "--SELECT--"
	}
	header := renderHeader(m.styles, sessionTag, m.width, modeStr)

	var body string
	if m.isLanding() {
		w := min(m.width*2/5, 200)
		m.textarea.SetWidth(w - 6)
		body = m.renderLanding()
	} else {
		m.textarea.SetWidth(m.width - 8)
		m.updateViewport()
		vpContent := m.viewport.View()
		scrollbar := renderScrollbar(m.viewport.Height(), m.viewport.TotalLineCount(), m.viewport.Height(), m.viewport.YOffset(), m.styles.ScrollbarThumb, m.styles.ScrollbarTrack)
		if scrollbar != "" {
			body = lipgloss.JoinHorizontal(lipgloss.Top, vpContent, scrollbar)
		} else {
			body = vpContent
		}
	}

	inputContent := m.textarea.View()
	var inputBox string
	if m.isLanding() {
		w := min(m.width*2/5, 200)
		inputBox = m.styles.InputBorder.Width(w).Render(inputContent)
		v.Content = header + "\n" + body + "\n" + lipgloss.NewStyle().Width(m.width).Align(lipgloss.Center).Render(inputBox)
	} else {
		inputBox = m.styles.InputBorder.Width(m.width).Render(inputContent)
		v.Content = header + "\n" + body + "\n" + inputBox
	}
	return v
}

func (m *chatbot) renderLanding() string {
	name := applyBoldGrad(m.styles.Logo, "LUDMILA", m.styles.LogoGradFrom, m.styles.LogoGradTo)
	diag := m.styles.LogoDiagonals.Render("╱╱╱╱╱")
	logoLine := name + " " + diag

	hint := m.styles.Help.Render("how can I help?")

	content := lipgloss.JoinVertical(lipgloss.Center,
		logoLine,
		"",
		hint,
	)

	avail := m.height - 8
	contentH := strings.Count(content, "\n") + 1
	topPad := (avail - contentH) / 2
	if topPad < 0 {
		topPad = 0
	}

	return strings.Repeat("\n", topPad) + lipgloss.NewStyle().Width(m.width).Align(lipgloss.Center).Render(content)
}

func (m *chatbot) renderModelPicker() string {
	info := m.currentModel
	header := renderHeader(m.styles, info, m.width, "")

	lw := min(m.width*3/5, 400)
	if lw < 20 {
		lw = 20
	}

	titleLine := dialogTitle("Select model", m.styles.Logo, lw, m.styles.LogoGradTo, m.styles.LogoGradFrom)
	m.modelList.SetWidth(lw)
	listView := m.modelList.View()
	body := titleLine + "\n" + listView

	box := m.styles.DialogView.
		Width(lw + 2).
		Padding(0, 1).
		Render(body)

	return header + "\n" + lipgloss.NewStyle().Width(m.width).Align(lipgloss.Center).Render(box)
}

func (m *chatbot) renderSessionPicker() string {
	label := fmt.Sprintf("[%d/%d]", m.sessionIdx+1, len(m.sessions))
	header := renderHeader(m.styles, label, m.width, "")

	lw := min(m.width*3/5, 400)
	if lw < 20 {
		lw = 20
	}

	titleLine := dialogTitle("Sessions", m.styles.Logo, lw, m.styles.LogoGradTo, m.styles.LogoGradFrom)
	m.sessList.SetWidth(lw)
	listView := m.sessList.View()
	body := titleLine + "\n" + listView

	box := m.styles.DialogView.
		Width(lw + 2).
		Padding(0, 1).
		Render(body)

	return header + "\n" + lipgloss.NewStyle().Width(m.width).Align(lipgloss.Center).Render(box)
}

func (m *chatbot) buildViewportContent() string {
	var b strings.Builder
	w := m.width - 2
	innerW := w - 2
	lineIdx := 0

	for _, msg := range m.messages {
		var rendered string
		switch msg.Role {
		case "user":
			rendered = renderMsgWithCode(msg.Content, innerW, m.styles.UserFocused, m.styles.CodeBlock, m.styles.InlineBold, m.styles.InlineItalic, m.styles.InlineCode, m.styles.Heading, m.styles.H1)
		case "assistant":
			rendered = renderMsgWithCode(msg.Content, innerW, m.styles.AsstFocused, m.styles.CodeBlock, m.styles.InlineBold, m.styles.InlineItalic, m.styles.InlineCode, m.styles.Heading, m.styles.H1)
		case "system":
			rendered = m.styles.Error.Render("  " + msg.Content)
		default:
			rendered = renderMsgWithCode(msg.Content, innerW, m.styles.AsstFocused, m.styles.CodeBlock, m.styles.InlineBold, m.styles.InlineItalic, m.styles.InlineCode, m.styles.Heading, m.styles.H1)
		}
		msgLines := strings.Split(rendered, "\n")
		for _, line := range msgLines {
			line = m.applySelHighlight(lineIdx, line)
			b.WriteString(line)
			b.WriteByte('\n')
			lineIdx++
		}
		b.WriteString("\n")
		lineIdx++
	}
	if m.streaming {
		partial := m.respBuf.String()
		if partial != "" {
			rendered := renderMsgWithCode(partial, innerW, m.styles.AsstFocused, m.styles.CodeBlock, m.styles.InlineBold, m.styles.InlineItalic, m.styles.InlineCode, m.styles.Heading, m.styles.H1)
			pLines := strings.Split(rendered, "\n")
			for _, line := range pLines {
				line = m.applySelHighlight(lineIdx, line)
				b.WriteString(line)
				b.WriteByte('\n')
				lineIdx++
			}
		} else {
			b.WriteString("\n")
		}
	}
	res := strings.TrimRight(b.String(), "\n")
	if res != "" {
		res += "\n"
	}
	return res
}

func (m *chatbot) applySelHighlight(lineIdx int, line string) string {
	if !m.selMode {
		return line
	}
	var bg string
	if m.selVisual {
		start, end := m.selMarkY, m.selCurY
		if start > end {
			start, end = end, start
		}
		if lineIdx >= start && lineIdx <= end {
			bg = m.styles.SelVisualBg
		}
	}
	if bg == "" && lineIdx == m.selCurY {
		bg = m.styles.SelCursorBg
	}
	if bg == "" {
		return line
	}
	close := "\033[0m"
	return bg + strings.ReplaceAll(line, close, close+bg)
}

func renderInline(text string, boldStyle, italicStyle, codeStyle, headingStyle, h1Style lipgloss.Style) string {
	if strings.HasPrefix(text, "# ") {
		rest := strings.TrimSpace(text[2:])
		return h1Style.Render(" " + rest + " ")
	}
	if strings.HasPrefix(text, "## ") || strings.HasPrefix(text, "### ") ||
		strings.HasPrefix(text, "#### ") || strings.HasPrefix(text, "##### ") ||
		strings.HasPrefix(text, "###### ") {
		idx := strings.IndexByte(text, ' ')
		rest := strings.TrimSpace(text[idx+1:])
		return headingStyle.Render(rest)
	}

	if strings.HasPrefix(text, "* ") {
		text = "• " + text[2:]
	}
	if strings.HasPrefix(text, "- ") {
		text = "• " + text[2:]
	}

	runes := []rune(text)
	var b strings.Builder
	i := 0
	for i < len(runes) {
		if i+1 < len(runes) && runes[i] == '*' && runes[i+1] == '*' {
			j := i + 2
			closed := false
			for j < len(runes)-1 {
				if runes[j] == '*' && runes[j+1] == '*' {
					b.WriteString(boldStyle.Render(string(runes[i+2 : j])))
					i = j + 2
					closed = true
					break
				}
				j++
			}
			if closed {
				continue
			}
			b.WriteRune(runes[i])
			i++
		} else if runes[i] == '*' {
			j := i + 1
			closed := false
			for j < len(runes) {
				if runes[j] == '*' {
					b.WriteString(italicStyle.Render(string(runes[i+1 : j])))
					i = j + 1
					closed = true
					break
				}
				j++
			}
			if closed {
				continue
			}
			b.WriteRune(runes[i])
			i++
		} else if runes[i] == '`' {
			j := i + 1
			closed := false
			for j < len(runes) {
				if runes[j] == '`' {
					b.WriteString(codeStyle.Render(string(runes[i+1 : j])))
					i = j + 1
					closed = true
					break
				}
				j++
			}
			if closed {
				continue
			}
			b.WriteRune(runes[i])
			i++
		} else {
			b.WriteRune(runes[i])
			i++
		}
	}
	return b.String()
}

func renderMsgWithCode(content string, innerWidth int, msgBorder, codeBlock, inlineBold, inlineItalic, inlineCode, headingStyle, h1Style lipgloss.Style) string {
	const fence = "```"
	segments := strings.Split(content, fence)
	if len(segments) <= 1 {
		wrapped := wrapText(content, innerWidth)
		lines := strings.Split(wrapped, "\n")
		for li, line := range lines {
			lines[li] = renderInline(line, inlineBold, inlineItalic, inlineCode, headingStyle, h1Style)
		}
		return msgBorder.Render(strings.Join(lines, "\n"))
	}

	var b strings.Builder
	for i, seg := range segments {
		if seg == "" {
			continue
		}
		if i%2 == 1 {
			if idx := strings.IndexByte(seg, '\n'); idx >= 0 {
				seg = seg[idx+1:]
			}
			if seg != "" {
				b.WriteString(codeBlock.Render(seg))
			}
		} else {
			wrapped := wrapText(seg, innerWidth)
			lines := strings.Split(wrapped, "\n")
			for li, line := range lines {
				lines[li] = renderInline(line, inlineBold, inlineItalic, inlineCode, headingStyle, h1Style)
			}
			b.WriteString(strings.Join(lines, "\n"))
		}
	}
	return msgBorder.Render(b.String())
}

func (m *chatbot) updateViewport() {
	m.viewport.SetContent(m.buildViewportContent())
	if m.follow {
		m.viewport.GotoBottom()
	}
}

func (m *chatbot) recalcLayout() {
	taH := m.textarea.Height()
	if taH < 1 {
		taH = 1
	}
	m.textareaHeight = taH
	overhead := 7
	vpH := m.height - overhead - taH
	if vpH < 1 {
		vpH = 1
	}
	m.viewport.SetHeight(vpH)
	m.updateViewport()
}
