package tui

import (
	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/textarea"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
)

// This file implements the prompt input field of the full-screen TUI (US-007,
// FR-11/13/14). It wraps charm.land/bubbles/v2/textarea into a small `input`
// component so the model can embed a real multi-line editor instead of the
// throwaway string buffer the skeleton shipped with.
//
// Why textarea rather than a hand-rolled buffer: textarea edits by grapheme /
// rune, so CJK and emoji are inserted and deleted whole. This is exactly the
// class of bug the old REPL input had — it keyed on byte length (len==1) and
// silently dropped the trailing bytes of every multi-byte rune. We deliberately
// delegate all character handling to textarea and never touch bytes ourselves.
//
// Shift+Enter inserts a newline so the editor is a true multi-line composer;
// plain Enter submits (intercepted by the model, never reaching textarea). The
// default InsertNewline binding (Enter) is therefore rebound to Shift+Enter. See
// model.handleKey.

// maxInputRows caps how tall the editor grows as the user adds lines. Past this
// the buffer keeps growing but textarea scrolls its own viewport, so the shell's
// row accounting stays bounded and the transcript never collapses to nothing.
const maxInputRows = 6

// input is the prompt editor. It embeds a textarea.Model and exposes just the
// surface the root model needs: value/clear, focus/blur (input is blurred while
// a run is in flight so keystrokes never corrupt an in-flight prompt), a width
// setter driven by tea.WindowSizeMsg, and a render string for View.
type input struct {
	ta textarea.Model
	// width is the full editor width (terminal columns) last set via SetWidth. It
	// is the span of the top/bottom rules drawn around the editor (Claude-Code
	// style), kept separately because textarea's own Width() reports only the
	// inner text area (prompt column excluded).
	width int
}

// newInput builds a focused editor. It starts one row tall and grows with the
// buffer (up to maxInputRows) as the user inserts newlines with Enter. The
// buffer itself is unbounded; beyond maxInputRows textarea scrolls internally.
func newInput() input {
	ta := textarea.New()
	ta.Prompt = "> "
	ta.Placeholder = "Type a message… (Enter to send, Shift+Enter for newline)"
	ta.ShowLineNumbers = false
	ta.CharLimit = 0
	// Let the textarea own its own height: DynamicHeight grows/shrinks it to the
	// content between MinHeight (1) and MaxHeight (maxInputRows), and — critically
	// — fixes the viewport scroll offset in the same pass. Doing it manually (an
	// after-the-fact SetHeight in syncHeight) left a stale scroll offset: inserting
	// a newline scrolled the cursor into view while the editor was still 1 row
	// tall, pushing the first line off the top, and the later SetHeight never
	// scrolled it back — so a two-line buffer rendered as two blank lines.
	ta.MinHeight = 1
	ta.MaxHeight = maxInputRows
	ta.DynamicHeight = true
	// textarea.New starts at defaultHeight (6). DynamicHeight only recomputes on
	// edits, so pin the empty editor to one row up front — otherwise the shell
	// would reserve six rows before the user has typed anything.
	ta.SetHeight(1)
	// Rebind InsertNewline from its default (Enter) to the newline keys, since
	// plain Enter is the model's submit key (handleKey intercepts it before
	// textarea sees it). Shift+Enter is the primary, advertised binding: Bubble
	// Tea v2 already enables the Kitty keyboard protocol's disambiguate flag
	// (flag 1) on every View, so capable terminals (kitty, ghostty, wezterm,
	// recent iTerm2) report Shift+Enter as a distinct CSI-u sequence rather than
	// a bare CR. Crucially this is flag 1, NOT flag 8 (ReportAllKeysAsEscapeCodes)
	// — flag 8 broke IME / CJK input because it strips associated text, whereas
	// flag 1 only disambiguates special keys and leaves text entry untouched.
	// On terminals without the protocol (macOS Terminal.app, tmux by default)
	// Shift+Enter arrives byte-identical to Enter and would submit, so Ctrl+J (a
	// literal LF, always distinct from Enter's CR) and Alt+Enter (ESC-prefixed,
	// always distinct) are kept as silent fallbacks — a newline is guaranteed to
	// work everywhere. All three split the line at the cursor and keep typed text.
	ta.KeyMap.InsertNewline = key.NewBinding(
		key.WithKeys("shift+enter", "ctrl+j", "alt+enter"),
		key.WithHelp("shift+enter", "insert newline"),
	)
	// Draw the cursor into the rendered string: the model composes View as a
	// plain string rather than driving textarea's real cursor reporting.
	ta.SetVirtualCursor(true)
	// Drop the default cursor-line background highlight so the composer is framed
	// only by the top/bottom rules (see View), matching Claude Code — no fill.
	styles := ta.Styles()
	styles.Focused.CursorLine = lipgloss.NewStyle()
	styles.Blurred.CursorLine = lipgloss.NewStyle()
	ta.SetStyles(styles)
	ta.Focus()
	return input{ta: ta}
}

// Update forwards a message (typically a key press) to the underlying textarea
// and returns the updated component. The model calls this only for keys it does
// not intercept itself (submit / interrupt / quit), so textarea sees ordinary
// editing keys — including Enter (newline) and CJK / emoji runes, which it
// inserts whole. Height is owned by textarea's DynamicHeight (see newInput), so
// there is nothing to re-sync here.
func (in input) Update(msg tea.Msg) (input, tea.Cmd) {
	var cmd tea.Cmd
	in.ta, cmd = in.ta.Update(msg)
	return in, cmd
}

// Height reports the current visible row count of the editor so the model can
// reserve that many rows in its View layout. It includes the two rule rows (top
// and bottom) drawn around the textarea.
func (in input) Height() int { return in.ta.Height() + 2 }

// Value returns the current buffer contents, including any embedded newlines.
func (in input) Value() string { return in.ta.Value() }

// SetValue replaces the buffer contents and moves the cursor to the end. It is
// used by slash autocomplete (Tab) to complete the buffer to the chosen command.
func (in *input) SetValue(s string) {
	in.ta.SetValue(s)
}

// Clear empties the buffer and resets the cursor to the start.
func (in *input) Clear() {
	in.ta.Reset()
}

// Focus enables editing and returns the cursor-blink Cmd.
func (in *input) Focus() tea.Cmd { return in.ta.Focus() }

// Blur disables editing (used while a run is in flight).
func (in *input) Blur() { in.ta.Blur() }

// Focused reports whether the editor currently accepts input.
func (in input) Focused() bool { return in.ta.Focused() }

// Line reports the zero-based index of the line the cursor is on, and LineCount
// the total number of lines in the buffer. The model uses them to decide whether
// ↑/↓ should walk the prompt history (caret on the first / last line) or move the
// caret within a multi-line draft.
func (in input) Line() int      { return in.ta.Line() }
func (in input) LineCount() int { return in.ta.LineCount() }

// SetWidth resizes the editor to the terminal width so wrapping and the prompt
// column line up with the rest of the shell.
func (in *input) SetWidth(w int) {
	if w < 0 {
		w = 0
	}
	in.width = w
	in.ta.SetWidth(w)
}

// View renders the editor to a string for embedding in the model's View. The
// textarea is framed with a top and bottom rule (no side borders) in the muted
// gray, mirroring Claude Code's composer — a pair of horizontal lines rather
// than a background fill. The rules span the full editor width.
func (in input) View() string {
	style := lipgloss.NewStyle().
		Border(lipgloss.NormalBorder(), true, false, true, false).
		BorderForeground(lipgloss.Color(colorGray))
	if in.width > 0 {
		style = style.Width(in.width)
	}
	return style.Render(in.ta.View())
}
