package tui

import (
	"albedo/cli/internal/daemon"
	"cmp"
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"

	"charm.land/bubbles/v2/textinput"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"
)

type PageTone = daemon.PageTone

const (
	TonePlain   = daemon.TonePlain
	ToneActive  = daemon.ToneActive
	ToneWarning = daemon.ToneWarning
	ToneMuted   = daemon.ToneMuted
)

type PageRow = daemon.PageRow
type PageAction = daemon.PageAction
type PageGlance = daemon.PageGlance
type PageDocument = daemon.PageDocument

type PageCancelMsg struct{}
type PageViewChangedMsg struct{}

type pageLoadedMsg struct {
	Doc *PageDocument
	Err error
	Gen int
}

type pageActionExecutedMsg struct {
	Err     error
	Message string
	Action  PageAction
	Gen     int
}

type pageModeKind int

const (
	modeBrowse pageModeKind = iota
	modeText
	modeChoice
	modeConfirm
)

type PageViewModel struct {
	Styles        Styles
	Conn          *daemon.Connection
	Doc           *PageDocument
	CurrentAction *PageAction
	SessionID     string
	Command       string
	SelectedID    string
	TextInput     textinput.Model
	page
	Mode        pageModeKind
	ChoiceIndex int
	Busy        bool
}

func NewPageViewModel(conn *daemon.Connection, sessionID, command string) PageViewModel {
	return PageViewModel{
		Conn:      conn,
		SessionID: sessionID,
		Command:   command,
		TextInput: newField(),
		page:      page{Generation: nextPageGeneration()},
		Busy:      true,
		Styles:    DefaultStyles,
	}
}

func (m *PageViewModel) SetSize(width, height int) {
	m.Width = width
	m.Height = height
	m.TextInput.SetWidth(max(10, width-20))
}

func (m PageViewModel) Init() tea.Cmd {
	return m.loadPageCmd(m.Generation)
}

func (m PageViewModel) loadPageCmd(gen int) tea.Cmd {
	return func() tea.Msg {
		if m.Conn == nil {
			return pageLoadedMsg{Err: errors.New("daemon connection unavailable"), Gen: gen}
		}
		doc, err := daemon.LoadPage(context.Background(), m.Conn, m.SessionID, m.Command)
		if err != nil {
			return pageLoadedMsg{Err: err, Gen: gen}
		}

		return pageLoadedMsg{Doc: doc, Gen: gen}
	}
}

func (m PageViewModel) executeActionCmd(act PageAction, row *PageRow, entered string, gen int) tea.Cmd {
	return func() tea.Msg {
		if m.Conn == nil {
			return pageActionExecutedMsg{Action: act, Err: errors.New("daemon connection unavailable"), Gen: gen}
		}

		res, err := daemon.ExecutePageAction(context.Background(), m.Conn, m.SessionID, daemon.PageActionRequest{Command: m.Command, Action: act, Row: row, Value: entered})
		return pageActionExecutedMsg{Message: res.Message, Action: act, Err: err, Gen: gen}
	}
}

func (m PageViewModel) currentRow() *PageRow {
	if m.Doc == nil || len(m.Doc.Rows) == 0 {
		return nil
	}
	return &m.Doc.Rows[m.currentIndex()]
}

func (m PageViewModel) currentIndex() int {
	if m.Doc != nil {
		if i := slices.IndexFunc(m.Doc.Rows, func(r PageRow) bool { return r.ID == m.SelectedID }); i >= 0 {
			return i
		}
	}
	return 0
}

// runAction marks the page busy and asks the daemon to run act.
func (m *PageViewModel) runAction(act PageAction, entered string) tea.Cmd {
	m.Mode, m.CurrentAction = modeBrowse, nil
	m.Busy, m.Error, m.Notice = true, "", ""
	m.Generation = nextPageGeneration()
	return m.executeActionCmd(act, m.currentRow(), entered, m.Generation)
}

// beginAction opens the interaction act needs: Update asks for a
// confirmation first, then come text entry or a choice, or the action runs
// at once. fresh clears a stale notice and error, as acting from the row
// list does.
func (m *PageViewModel) beginAction(act PageAction, fresh bool) tea.Cmd {
	if fresh {
		m.Notice, m.Error = "", ""
	}
	m.CurrentAction = &act
	switch act.Input {
	case "text", "secret":
		m.Mode = modeText
		value := ""
		if act.Prefill && m.currentRow() != nil {
			value = m.currentRow().Text
		}
		return ask(&m.TextInput, value, act.Input == "secret")
	case "choice":
		m.Mode = modeChoice
		m.ChoiceIndex = 0
		if row := m.currentRow(); row != nil {
			if i := slices.Index(act.Options, row.Badge); i >= 0 {
				m.ChoiceIndex = i
			}
		}
		return nil
	}
	return m.runAction(act, "")
}

func (m PageViewModel) Update(msg tea.Msg) (PageViewModel, tea.Cmd) {
	switch msg := msg.(type) {
	case pageLoadedMsg:
		if !m.settle(msg.Gen, msg.Err, &m.Busy) {
			return m, nil
		}
		m.Doc = msg.Doc
		m.Error = ""
		if row := m.currentRow(); row != nil {
			m.SelectedID = row.ID
		}
		return m, nil

	case pageActionExecutedMsg:
		if !m.settle(msg.Gen, msg.Err, &m.Busy) {
			return m, nil
		}
		m.Notice = pageNotice(msg)
		m.Error = ""
		m.Busy = true
		m.Generation = nextPageGeneration()
		return m, tea.Batch(
			func() tea.Msg { return PageViewChangedMsg{} },
			m.loadPageCmd(m.Generation),
		)

	case tea.KeyPressMsg:
		if msg.String() == "ctrl+c" || msg.String() == "esc" {
			if m.Mode == modeBrowse {
				return m, func() tea.Msg { return PageCancelMsg{} }
			}
			m.Mode = modeBrowse
			m.CurrentAction = nil
			return m, nil
		}
		if m.Busy {
			return m, nil
		}
		if m.Doc == nil {
			if strings.ToLower(msg.String()) == "r" {
				m.Busy = true
				m.Error = ""
				m.Generation = nextPageGeneration()
				return m, m.loadPageCmd(m.Generation)
			}
			return m, nil
		}
		switch m.Mode {
		case modeConfirm:
			if msg.String() == "enter" && m.CurrentAction != nil {
				act := *m.CurrentAction
				m.CurrentAction = nil
				m.Mode = modeBrowse
				return m, m.beginAction(act, false)
			}
		case modeChoice:
			if m.CurrentAction != nil && len(m.CurrentAction.Options) > 0 {
				opts := m.CurrentAction.Options
				switch msg.String() {
				case "left", "up":
					m.ChoiceIndex = (m.ChoiceIndex - 1 + len(opts)) % len(opts)
				case "right", "down":
					m.ChoiceIndex = (m.ChoiceIndex + 1) % len(opts)
				case "enter":
					return m, m.runAction(*m.CurrentAction, opts[m.ChoiceIndex])
				}
			}
		case modeText:
			if msg.String() != "enter" {
				var cmd tea.Cmd
				m.TextInput, cmd = m.TextInput.Update(msg)
				return m, cmd
			}
			if val := strings.TrimSpace(m.TextInput.Value()); val != "" && m.CurrentAction != nil {
				act := *m.CurrentAction
				m.TextInput.Reset()
				return m, m.runAction(act, val)
			}
		case modeBrowse:
			idx := m.currentIndex()
			switch msg.String() {
			case "up", "ctrl+p":
				if idx > 0 {
					m.SelectedID = m.Doc.Rows[idx-1].ID
				}
			case "down", "ctrl+n":
				if idx < len(m.Doc.Rows)-1 {
					m.SelectedID = m.Doc.Rows[idx+1].ID
				}
			default:
				for _, act := range m.Doc.Actions {
					if act.Key != msg.String() {
						continue
					}
					if act.Row && m.currentRow() == nil {
						continue
					}
					if act.Confirm {
						m.Notice, m.Error = "", ""
						m.Mode = modeConfirm
						m.CurrentAction = &act
						return m, nil
					}
					return m, m.beginAction(act, true)
				}
			}
		}
	}
	if m.Mode == modeText && m.TextInput.Focused() {
		var cmd tea.Cmd
		m.TextInput, cmd = m.TextInput.Update(msg)
		return m, cmd
	}
	return m, nil
}

// pageLayout places the list and the selected row's detail under the title
// the way the sessions view does: side by side when there is room, the
// detail under the list when there is not, and the list alone when no row
// has one or the stack would leave the detail too little.
type pageLayout struct {
	list, pane int // widths side by side; pane is 0 when stacked or alone
	listRows   int // rows the list keeps
	paneRows   int // rows the detail keeps under the list; 0 unless stacked
}

func (m PageViewModel) layout(body, listed int) pageLayout {
	l := pageLayout{list: m.Width, listRows: body}
	if !slices.ContainsFunc(m.Doc.Rows, func(r PageRow) bool { return r.Detail != "" }) {
		return l
	}
	if m.Width >= 96 && m.Height >= 14 {
		l.list = m.Width / 2
		l.pane = m.Width - l.list - ansi.StringWidth(svSep())
		return l
	}
	// Stacked, the list keeps what it needs up to two fifths of the body, or
	// more when the detail is short, and the rule between them takes a row.
	rows := min(listed, max(3, body*2/5, body-1-len(m.detailLines(m.Width))))
	if body-rows-1 >= 4 {
		l.listRows, l.paneRows = rows, body-rows-1
	}
	return l
}

// list is the rows grouped under a rule per run of one badge, at exactly
// width × height, scrolled to keep the selection in view.
func (m PageViewModel) list(width, height int) []string {
	return scrollWindow(m.listLines(width, height), m.selectedAt(height), width, height)
}

func (m PageViewModel) listLines(width, height int) []string {
	if len(m.Doc.Rows) == 0 {
		return []string{m.Styles.Faint.Render("   " + m.Doc.Empty)}
	}
	idW := 0
	for _, row := range m.Doc.Rows {
		idW = max(idW, ansi.StringWidth(rowID(row)))
	}
	selected := m.currentIndex()
	var lines []string
	for i, row := range m.Doc.Rows {
		if i == 0 || row.Badge != m.Doc.Rows[i-1].Badge {
			if i > 0 && height >= 10 {
				lines = append(lines, "")
			}
			run := 1
			for run < len(m.Doc.Rows)-i && m.Doc.Rows[i+run].Badge == row.Badge {
				run++
			}
			lines = append(lines, sectionRule(row.Badge, run, width))
		}
		lines = append(lines, m.row(row, i == selected, width, idW))
	}
	return lines
}

// selectedAt is where the selected row sits in listLines.
func (m PageViewModel) selectedAt(height int) int {
	if len(m.Doc.Rows) == 0 {
		return 0
	}
	at := 0
	for i, row := range m.Doc.Rows[:m.currentIndex()+1] {
		if i == 0 || row.Badge != m.Doc.Rows[i-1].Badge {
			at++
			if i > 0 && height >= 10 {
				at++
			}
		}
		at++
	}
	return at - 1
}

// rowID is the quiet #id a row shows, unless its id is its text.
func rowID(row PageRow) string {
	if row.ID != row.Text {
		return "#" + row.ID
	}
	return ""
}

// row is one page row in the sessions view's grammar: bar, a glyph in the
// row's tone, the title, then its id right-aligned in idW columns.
func (m PageViewModel) row(row PageRow, selected bool, width, idW int) string {
	glyph, glyphStyle := "· ", m.Styles.Faint
	switch {
	case selected:
		glyph, glyphStyle = "◆ ", DefaultStyles.Agent
	case row.Tone == ToneWarning:
		glyph, glyphStyle = "● ", DefaultStyles.Warning
	case row.Tone == ToneActive:
		glyph, glyphStyle = "● ", DefaultStyles.Success
	}
	textStyle := lipgloss.NewStyle()
	switch {
	case selected:
		textStyle = DefaultStyles.Bold
	case row.Tone == ToneMuted:
		textStyle = m.Styles.Faint
	}
	textW := width - 4
	if idW > 0 {
		textW -= idW + 2
	}
	textW = max(1, textW)
	marker := " "
	if selected {
		marker = selectBar()
	}
	line := marker + glyphStyle.Render(glyph) + " " + textStyle.Render(svCell(row.Text, textW, false))
	if idW > 0 {
		line += "  " + m.Styles.Faint.Render(svCell(rowID(row), idW, true))
	}
	if selected {
		return selectedLine(line, width)
	}
	return line
}

// detail is the selected row at exactly width × height: its title and badge
// over a rule, then its detail wrapped to the pane, cut short with ··· when
// it runs past the bottom.
func (m PageViewModel) detail(width, height int) []string {
	lines := m.detailLines(width)
	if len(lines) > height && height > 0 {
		lines = append(lines[:height-1], m.Styles.Faint.Render("···"))
	}
	return paneBox(lines, max(1, width-2), width, height)
}

func (m PageViewModel) detailLines(width int) []string {
	inner := max(1, width-2)
	row := m.currentRow()
	if row == nil {
		return nil
	}
	var lines []string
	for _, l := range svWrap(row.Text, inner, 2) {
		lines = append(lines, DefaultStyles.Bold.Render(l))
	}
	meta := []string{toneStyle(row.Tone, m.Styles).Render(row.Badge)}
	if id := rowID(*row); id != "" {
		meta = append(meta, m.Styles.Faint.Render(id))
	}
	lines = append(lines, strings.Join(meta, DefaultStyles.Decor.Render(" · ")), DefaultStyles.Decor.Render(strings.Repeat("─", inner)))
	for paragraph := range strings.SplitSeq(row.Detail, "\n\n") {
		if paragraph = strings.TrimSpace(paragraph); paragraph != "" {
			lines = append(append(lines, ""), strings.Split(ansi.Wrap(paragraph, inner, " -"), "\n")...)
		}
	}
	return lines
}

// toneStyle is the color a badge wears for its tone.
func toneStyle(tone PageTone, styles Styles) lipgloss.Style {
	switch tone {
	case ToneActive:
		return DefaultStyles.Success
	case ToneWarning:
		return DefaultStyles.Warning
	case ToneMuted:
		return styles.Faint
	}
	return DefaultStyles.Muted
}

// pageNotice picks the wording for a finished action: the daemon's message
// when it sent one, else the action's label.
func pageNotice(msg pageActionExecutedMsg) string {
	notice := fmt.Sprintf("%s completed", msg.Action.Label)
	if msg.Message != "" {
		return msg.Message
	}
	return notice
}

func (m PageViewModel) View() string {
	width, height := cmp.Or(m.Width, 80), cmp.Or(m.Height, 24)
	summary := ""
	if m.Doc != nil {
		summary = m.Styles.Faint.Render(m.Doc.Summary)
	}
	lines := []string{" " + titleRule(width-1, brand("albedo")+" "+m.Styles.Muted.Render(m.Command), summary), ""}
	tail := []string{m.prompt(), m.footer(width)}
	body := max(1, height-len(lines)-len(tail))
	switch m.Doc {
	case nil:
		note := "loading " + m.Command + "…"
		if m.Error != "" {
			note = "r retry · esc back"
		}
		lines = append(lines, " "+m.Styles.Faint.Render(note))
		lines = append(lines, make([]string, max(0, body-1))...)
	default:
		l := m.layout(body, len(m.listLines(width, body)))
		list := m.list(l.list, l.listRows)
		if l.pane > 0 {
			pane := m.detail(l.pane, l.listRows)
			for i := range list {
				list[i] += svSep() + pane[i]
			}
		}
		lines = append(lines, list...)
		if l.paneRows > 0 {
			lines = append(lines, DefaultStyles.Decor.Render(strings.Repeat("─", width)))
			lines = append(lines, m.detail(width, l.paneRows)...)
		}
	}
	lines = append(lines, tail...)
	if len(lines) > height {
		lines = append(lines[:max(0, height-1)], lines[len(lines)-1])
	}
	for i, line := range lines {
		lines[i] = ansi.Truncate(line, width, "…")
	}
	return strings.Join(lines, "\n")
}

// prompt is the line above the footer where an action asks what it needs,
// blank while browsing.
func (m PageViewModel) prompt() string {
	act, row := m.CurrentAction, m.currentRow()
	if act == nil {
		return ""
	}
	target := ""
	if act.Row && row != nil {
		target = " "
		if id := rowID(*row); id != "" {
			target += id + " "
		}
		target += row.Text
	}
	switch m.Mode {
	case modeConfirm:
		return " " + DefaultStyles.Warning.Render(act.Label+target+"?")
	case modeChoice:
		var choice strings.Builder
		choice.WriteByte(' ')
		choice.WriteString(m.Styles.Prompt.Render(act.Label + target))
		choice.WriteByte(' ')
		choice.WriteString(promptLead())
		for i, opt := range act.Options {
			label := " " + opt + " "
			if i == m.ChoiceIndex {
				label = selectedLine(label, 0)
			}
			choice.WriteString(label)
		}
		return choice.String()
	case modeText:
		return " " + m.Styles.Prompt.Render(act.Label+target+" · "+cmp.Or(act.Prompt, act.Label)) + " " + promptLead() + m.TextInput.View()
	}
	return ""
}

// fitHints is " " and the hints in room columns, shedding what the screen
// already says first: moving, then leaving, then every label but the keys.
// Empty when not even the keys fit.
func fitHints(hints []hint, room int) string {
	keysOnly := make([]hint, len(hints))
	for i, h := range hints {
		keysOnly[i] = hint{key: h.key}
		if h.key == "" {
			keysOnly[i].does = h.does
		}
	}
	still := slices.DeleteFunc(slices.Clone(hints), func(h hint) bool { return h.key == "↑↓" })
	staying := slices.DeleteFunc(slices.Clone(still), func(h hint) bool { return h.key == "esc" })
	for _, candidate := range [][]hint{hints, still, staying, keysOnly} {
		if line := " " + keyHints(candidate...); len(candidate) > 0 && ansi.StringWidth(line) <= room {
			return line
		}
	}
	return ""
}

// footer is the keys on the left and the last outcome on the right, the
// way the folder picker words it; a long outcome takes the whole line.
func (m PageViewModel) footer(width int) string {
	var hints []hint
	switch {
	case m.Busy:
		hints = []hint{{"working…", ""}}
	case m.Doc == nil:
		hints = []hint{{"esc", "back"}}
	case m.Mode == modeBrowse:
		if len(m.Doc.Rows) > 1 {
			hints = append(hints, hint{"↑↓", "move"})
		}
		for _, act := range m.Doc.Actions {
			if !act.Row || m.currentRow() != nil {
				hints = append(hints, hint{act.Key, act.Label})
			}
		}
		hints = append(hints, hint{"esc", "back"})
	case m.Mode == modeText:
		hints = []hint{{"enter", "save"}, {"esc", "cancel"}}
	case m.Mode == modeChoice:
		hints = []hint{{"←→", "choose"}, {"enter", "apply"}, {"esc", "cancel"}}
	default:
		hints = []hint{{"enter", "confirm"}, {"esc", "cancel"}}
	}
	var right string
	switch {
	case m.Error != "":
		right = DefaultStyles.Error.Render(m.Error)
	case m.Notice != "":
		right = m.Styles.Faint.Render(m.Notice)
	}
	room := width - 1
	if right != "" {
		room = width - ansi.StringWidth(right) - 3
	}
	left := fitHints(hints, room)
	if left == "" && right == "" {
		left = " " + ansi.Truncate(keyHints(hints...), width-2, "…")
	}
	return left + strings.Repeat(" ", max(1, width-ansi.StringWidth(left)-ansi.StringWidth(right)-1)) + right
}
