package app

import (
	"strings"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/correx/tui-go/internal/protocol"
)

// clarcard.go renders and drives the clarification view: a stage's open questions
// presented as an interactive form — a header chip, the prompt, a row of selectable
// option chips, and a custom free-text slot, like the AskUserQuestion widget.
// Submitting sends the answers back; the parked stage re-runs with them in context.

// --- per-question widget state ---

func (m *Model) clarInitState(n int) {
	m.clarFocus = 0
	m.clarCursor = 0
	m.clarTyping = false
	m.clarDismissed = false
	m.clarChosen = make([]map[int]bool, n)
	for i := range m.clarChosen {
		m.clarChosen[i] = map[int]bool{}
	}
	m.clarText = make([]string, n)
}

func (m *Model) clarResetState() {
	m.clarChosen = nil
	m.clarText = nil
	m.clarFocus = 0
	m.clarCursor = 0
	m.clarTyping = false
	m.clarDismissed = false
}

// clarEnsureState (re)initializes the per-question buffers when they don't match the
// selected session's question set — e.g. a clarification that arrived for a session
// before it was selected, so onClarificationRequired didn't size the buffers.
func (m *Model) clarEnsureState() {
	s := m.session(m.selectedID)
	if s == nil || s.Clar == nil {
		return
	}
	if len(m.clarChosen) != len(s.Clar.Questions) || len(m.clarText) != len(s.Clar.Questions) {
		m.clarInitState(len(s.Clar.Questions))
	}
}

func (m Model) clarChosenAt(i, oi int) bool {
	if i < 0 || i >= len(m.clarChosen) || m.clarChosen[i] == nil {
		return false
	}
	return m.clarChosen[i][oi]
}

func (m Model) clarTextAt(i int) string {
	if i < 0 || i >= len(m.clarText) {
		return ""
	}
	return m.clarText[i]
}

func (m Model) clarAnswered(i int, q ClarQuestion) bool {
	if strings.TrimSpace(m.clarTextAt(i)) != "" {
		return true
	}
	for oi := range q.Options {
		if m.clarChosenAt(i, oi) {
			return true
		}
	}
	return false
}

// clarAnswerValue is the submitted value for question i: the custom text if present,
// else the chosen option(s) joined in option order.
func (m Model) clarAnswerValue(i int, q ClarQuestion) string {
	if t := strings.TrimSpace(m.clarTextAt(i)); t != "" {
		return t
	}
	var sel []string
	for oi, opt := range q.Options {
		if m.clarChosenAt(i, oi) {
			sel = append(sel, opt)
		}
	}
	return strings.Join(sel, ", ")
}

// --- key handling ---

func (m Model) handleClarificationKey(k keyMsg) (tea.Model, tea.Cmd) {
	s := m.session(m.selectedID)
	if s == nil || s.Clar == nil {
		return m, nil
	}
	qs := s.Clar.Questions
	if m.clarTyping {
		return m.handleClarTyping(k), nil
	}
	switch k.Type {
	case keyEsc:
		m.clarDismissed = true
	case keyUp:
		m.clarFocusMove(-1, qs)
	case keyDown:
		m.clarFocusMove(1, qs)
	case keyLeft:
		m.clarCursorMove(-1, qs)
	case keyRight:
		m.clarCursorMove(1, qs)
	case keySpace:
		m.clarSelect(qs)
	case keyEnter:
		return m.clarSubmit()
	case keyRunes:
		switch string(k.Runes) {
		case "k":
			m.clarFocusMove(-1, qs)
		case "j":
			m.clarFocusMove(1, qs)
		case "h":
			m.clarCursorMove(-1, qs)
		case "l":
			m.clarCursorMove(1, qs)
		case "e":
			m.clarCursor = len(qs[m.clarFocus].Options) // jump to the custom slot
			m.clarTyping = true
		case "s":
			return m.clarSubmit()
		}
	}
	return m, nil
}

func (m Model) handleClarTyping(k keyMsg) tea.Model {
	if m.clarFocus >= len(m.clarText) {
		return m
	}
	switch k.Type {
	case keyEsc:
		m.clarTyping = false
	case keyEnter:
		m.clarTyping = false
		// committing custom text supersedes any chosen option for this question
		if m.clarFocus < len(m.clarChosen) {
			m.clarChosen[m.clarFocus] = map[int]bool{}
		}
	case keyBackspace:
		if t := m.clarText[m.clarFocus]; len(t) > 0 {
			m.clarText[m.clarFocus] = t[:len(t)-1]
		}
	case keyRunes, keySpace:
		m.clarText[m.clarFocus] += string(k.Runes)
	}
	return m
}

func (m *Model) clarFocusMove(d int, qs []ClarQuestion) {
	if len(qs) == 0 {
		return
	}
	m.clarFocus = clampInt(m.clarFocus+d, 0, len(qs)-1)
	m.clarCursor = clampInt(m.clarCursor, 0, len(qs[m.clarFocus].Options))
}

func (m *Model) clarCursorMove(d int, qs []ClarQuestion) {
	if m.clarFocus >= len(qs) {
		return
	}
	m.clarCursor = clampInt(m.clarCursor+d, 0, len(qs[m.clarFocus].Options))
}

func (m *Model) clarSelect(qs []ClarQuestion) {
	if m.clarFocus >= len(qs) || m.clarFocus >= len(m.clarChosen) {
		return
	}
	q := qs[m.clarFocus]
	if m.clarCursor >= len(q.Options) { // custom slot
		m.clarTyping = true
		return
	}
	if q.MultiSelect {
		if m.clarChosen[m.clarFocus] == nil {
			m.clarChosen[m.clarFocus] = map[int]bool{}
		}
		m.clarChosen[m.clarFocus][m.clarCursor] = !m.clarChosen[m.clarFocus][m.clarCursor]
	} else {
		m.clarChosen[m.clarFocus] = map[int]bool{m.clarCursor: true}
	}
	if m.clarFocus < len(m.clarText) {
		m.clarText[m.clarFocus] = "" // choosing an option supersedes a typed answer
	}
}

func (m Model) clarSubmit() (tea.Model, tea.Cmd) {
	s := m.session(m.selectedID)
	if s == nil || s.Clar == nil {
		return m, nil
	}
	qs := s.Clar.Questions
	for i, q := range qs {
		if !m.clarAnswered(i, q) {
			m.clarFocus = i // nudge to the first unanswered rather than submitting partial
			m.clarCursor = 0
			return m, nil
		}
	}
	answers := make([]protocol.ClarificationAnswerDto, 0, len(qs))
	for i, q := range qs {
		answers = append(answers, protocol.ClarificationAnswerDto{
			QuestionID: q.ID,
			Value:      m.clarAnswerValue(i, q),
		})
	}
	m.client.Send(protocol.ClarificationResponse(s.Clar.SessionID, s.Clar.StageID, s.Clar.RequestID, answers))
	s.Clar = nil
	m.clarResetState()
	return m, nil
}

func clampInt(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}

// --- rendering ---

func (m Model) clarificationModal() string {
	t := m.theme
	w := m.modalWidth()
	s := m.session(m.selectedID)
	if s == nil || s.Clar == nil {
		return ""
	}
	qs := s.Clar.Questions

	var b strings.Builder
	b.WriteString(m.titleLine("clarifying questions"))
	b.WriteString(mbg(t, "  — "+s.Clar.StageID, t.P.Dim))
	answered := 0
	for i, q := range qs {
		if m.clarAnswered(i, q) {
			answered++
		}
	}
	b.WriteString(mbg(t, "   ("+itoa(answered)+"/"+itoa(len(qs))+" answered)", t.P.Faint) + "\n\n")

	contentW := w - 6
	for i, q := range qs {
		b.WriteString(m.clarQuestionLines(i, q, i == m.clarFocus, contentW))
		if i < len(qs)-1 {
			b.WriteString("\n")
		}
	}

	b.WriteString("\n" + modalHints(t, [][2]string{
		{"←→", "option"}, {"↑↓", "question"}, {"space", "select"},
		{"e", "custom"}, {"enter", "submit"}, {"esc", "later"},
	}))
	return t.Overlay.Width(w).Render(b.String())
}

// clarQuestionLines renders one question: a focus marker + header chip + prompt, then
// a wrapped row of option chips (cursor highlighted, chosen marked) plus a custom slot.
func (m Model) clarQuestionLines(i int, q ClarQuestion, focused bool, w int) string {
	t := m.theme
	var b strings.Builder

	marker := mbg(t, "  ", t.P.BgPanel)
	promptFg := t.P.Fg
	if focused {
		marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▸ ")
		promptFg = t.P.FgStrong
	}
	promptStyle := lipgloss.NewStyle().Foreground(promptFg).Background(t.P.BgPanel)

	headChip, headW := "", 2 // marker is 2 cols
	if q.Header != "" {
		headChip = lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.BgPanel).Render("[" + q.Header + "] ")
		headW += len(q.Header) + 3 // "[" + header + "] "
	}
	prompt := q.Prompt
	if q.MultiSelect {
		prompt += "  (multi)"
	}
	// Soft-wrap the prompt so long questions (e.g. the [API]/[Architecture] ones)
	// stay fully readable instead of being clipped at the modal edge. The first line
	// flows after the marker + header chip; continuation lines indent under the prompt.
	contIndent := mbg(t, "    ", t.P.BgPanel)
	for li, line := range wrapPrompt(prompt, max(w-headW, 8), max(w-4, 8)) {
		if li == 0 {
			b.WriteString(marker + headChip + promptStyle.Render(line) + "\n")
		} else {
			b.WriteString(contIndent + promptStyle.Render(line) + "\n")
		}
	}

	chips := make([]string, 0, len(q.Options)+1)
	for oi, opt := range q.Options {
		chips = append(chips, m.clarChip(opt, m.clarChosenAt(i, oi), focused && m.clarCursor == oi))
	}
	chips = append(chips, m.clarCustomChip(i, q, focused))

	gap := mbg(t, "  ", t.P.BgPanel)
	for _, line := range wrapStyled(chips, gap, 2, w-4) {
		b.WriteString(mbg(t, "    ", t.P.BgPanel) + line + "\n")
	}
	return b.String()
}

// wrapPrompt greedily word-wraps s, giving the first line firstW columns (it shares
// its row with the marker + header chip) and every continuation line contW. A word
// longer than the limit occupies its own line rather than being truncated.
func wrapPrompt(s string, firstW, contW int) []string {
	words := strings.Fields(s)
	if len(words) == 0 {
		return []string{""}
	}
	var lines []string
	cur, limit := "", firstW
	for _, wd := range words {
		switch {
		case cur == "":
			cur = wd
		case len(cur)+1+len(wd) <= limit:
			cur += " " + wd
		default:
			lines = append(lines, cur)
			cur, limit = wd, contW
		}
	}
	return append(lines, cur)
}

// clarCustomChip renders the free-text slot: "+ custom" when empty, the typed text
// (with a caret while editing) otherwise.
func (m Model) clarCustomChip(i int, q ClarQuestion, focused bool) string {
	text := m.clarTextAt(i)
	chosen := strings.TrimSpace(text) != ""
	cursor := focused && m.clarCursor >= len(q.Options)
	label := "+ custom"
	switch {
	case cursor && m.clarTyping:
		label = "✎ " + text + "▏"
	case chosen:
		label = "✎ " + text
	}
	return m.clarChip(label, chosen, cursor)
}

// clarChip renders one option as a bracketed badge: inverse-highlighted when the
// cursor is on it, marked (●) when chosen.
func (m Model) clarChip(label string, chosen, cursor bool) string {
	t := m.theme
	mark, fg := "○ ", t.P.Dim
	if chosen {
		mark, fg = "● ", t.P.Accent2
	}
	style := lipgloss.NewStyle().Foreground(fg).Background(t.P.BgPanel)
	if cursor {
		style = lipgloss.NewStyle().Foreground(t.P.BgPanel).Background(t.P.Accent).Bold(true)
	}
	return style.Render(" " + mark + label + " ")
}
