package ui

import (
	"strings"

	"charm.land/bubbles/v2/help"
	"charm.land/bubbles/v2/key"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"

	"git.secluded.site/keld/internal/theme"
)

// chromeLines is the number of terminal lines reserved for the
// session's visual frame. Screens receive the terminal height minus
// this value so they can size themselves without overflowing.
//
//	breadcrumb:  1 (0 on first screen, but we reserve 1 for consistency)
//	title + gap: 2
//	gap + help:  2
const chromeLines = 5

// keys defines the global key bindings that the session handles
// regardless of which screen is active.
var keys = struct {
	Back key.Binding
	Quit key.Binding
}{
	Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
	Quit: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit")),
}

// Session is the top-level tea.Model for keld's interactive mode. It
// manages a stack of screens, renders chrome, and handles global
// navigation (back/cancel).
type Session struct {
	// screens holds every screen in the flow. The session keeps them
	// all so back navigation can re-activate earlier screens with
	// their state intact.
	screens []Screen

	// cursor points to the currently active screen in the screens
	// slice.
	cursor int

	// styles holds the pre-computed theme.
	styles *theme.Styles

	// help renders the key binding bar at the bottom.
	help help.Model

	// width and height store the last known terminal dimensions.
	width, height int

	// lastSize caches the most recent WindowSizeMsg so it can be
	// replayed to screens activated via advance or back navigation.
	lastSize *tea.WindowSizeMsg

	// done is set when the session should exit (cancel or complete).
	done bool

	// completed distinguishes successful flow completion from
	// cancellation. The caller checks this via Completed() to decide
	// whether to execute restic.
	completed bool
}

// New creates a session with the given screens and shared styles.
// The caller allocates a [theme.Styles] and passes the same pointer
// to both screen constructors and this function, so everyone shares
// a single theme that the session updates on background detection.
//
// Screens that should be skipped (because their value is already
// resolved) should simply not be included in the slice — the caller
// is responsible for building only the screens that need user input.
func New(screens []Screen, styles *theme.Styles) Session {
	h := help.New()
	h.Styles = styles.Help

	// Copy the slice so mutations during forwardToScreen don't
	// affect the caller's original.
	owned := make([]Screen, len(screens))
	copy(owned, screens)

	return Session{
		screens: owned,
		styles:  styles,
		help:    h,
	}
}

// Completed reports whether the user finished the entire flow
// (as opposed to cancelling with Ctrl+C or Esc on the first screen).
func (s Session) Completed() bool {
	return s.completed
}

// Init requests the terminal background colour and window size, then
// initialises the first screen.
func (s Session) Init() tea.Cmd {
	cmds := []tea.Cmd{
		tea.RequestBackgroundColor,
		tea.RequestWindowSize,
	}
	if len(s.screens) > 0 {
		cmds = append(cmds, s.screens[s.cursor].Init())
	}
	return tea.Batch(cmds...)
}

// Update handles messages. Global concerns (background detection,
// resize, Ctrl+C) are handled here. Esc and all other messages are
// forwarded to the active screen. The session navigates back when
// it receives a [BackMsg] and advances when it receives a [DoneMsg].
func (s Session) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.BackgroundColorMsg:
		*s.styles = theme.New(msg.IsDark())
		s.help.Styles = s.styles.Help
		// Forward to the active screen so it can rebuild any
		// cached themed state (e.g. huh form themes).
		return s.forwardToScreen(msg)

	case tea.WindowSizeMsg:
		s.width = msg.Width
		s.height = msg.Height
		s.lastSize = &msg
		s.help.SetWidth(msg.Width)
		// Forward an adjusted size to the active screen so it knows
		// how much vertical space is available after chrome.
		return s.forwardToScreen(s.adjustedSizeMsg())

	case tea.KeyPressMsg:
		if key.Matches(msg, keys.Quit) {
			s.done = true
			return s, tea.Quit
		}
		// All other keys, including Esc, are forwarded to the screen.
		// Screens that don't handle Esc return BackCmd.
		return s.forwardToScreen(msg)

	case BackMsg:
		return s.navigateBack()

	case DoneMsg:
		return s.advance()

	case ExtendMsg:
		return s.extend(msg)
	}

	return s.forwardToScreen(msg)
}

// View renders the chrome (breadcrumb, title, help bar) around the
// active screen's content.
func (s Session) View() tea.View {
	if s.done || len(s.screens) == 0 {
		return tea.NewView("")
	}

	screen := s.screens[s.cursor]
	sty := s.styles

	var b strings.Builder

	// Breadcrumb: show selections from completed screens before the
	// current one.
	if crumbs := s.breadcrumb(); crumbs != "" {
		b.WriteString(sty.Breadcrumb.Render(crumbs))
		b.WriteString("\n")
	}

	// Title of the current screen.
	if title := screen.Title(); title != "" {
		b.WriteString(sty.Title.Render(title))
		b.WriteString("\n\n")
	}

	// Screen content.
	b.WriteString(screen.View())

	// Help bar: screen-specific bindings + global bindings. Copy
	// the slice to avoid mutating the screen's backing array.
	screenBindings := screen.KeyBindings()
	bindings := make([]key.Binding, 0, len(screenBindings)+2)
	bindings = append(bindings, screenBindings...)
	bindings = append(bindings, keys.Back, keys.Quit)
	b.WriteString("\n")
	b.WriteString(s.help.ShortHelpView(bindings))
	b.WriteString("\n")

	return tea.NewView(b.String())
}

// breadcrumb builds the breadcrumb string from selections of all
// completed screens before the current one.
func (s Session) breadcrumb() string {
	var parts []string
	for i := 0; i < s.cursor; i++ {
		if sel := s.screens[i].Selection(); sel != "" {
			parts = append(parts, sel)
		}
	}
	if len(parts) == 0 {
		return ""
	}
	return strings.Join(parts, " "+lipgloss.NewStyle().Foreground(s.styles.Accent).Render("▸")+" ")
}

// navigateBack moves to the previous screen, or exits if already on
// the first screen.
func (s Session) navigateBack() (tea.Model, tea.Cmd) {
	if s.cursor <= 0 {
		// On the first screen: back exits.
		s.done = true
		return s, tea.Quit
	}
	s.cursor--
	return s, s.activateScreen()
}

// advance moves to the next screen. If the current screen is the last
// one, the flow is complete — the session marks itself done and quits.
func (s Session) advance() (tea.Model, tea.Cmd) {
	if s.cursor >= len(s.screens)-1 {
		// Last screen completed: flow is done.
		s.done = true
		s.completed = true
		return s, tea.Quit
	}
	s.cursor++
	return s, s.activateScreen()
}

// extend appends screens after the current cursor position, replacing
// any screens that were already queued beyond it. This lets the flow
// be built dynamically — e.g. after resolving which command-specific
// screens are needed.
func (s Session) extend(msg ExtendMsg) (tea.Model, tea.Cmd) {
	if len(msg.Screens) == 0 {
		return s, nil
	}
	// Truncate to current position + 1 (keep the active screen and
	// all completed screens before it), then append the new screens.
	s.screens = append(s.screens[:s.cursor+1], msg.Screens...)
	return s, nil
}

// activateScreen initialises the screen at the current cursor and
// sends it the adjusted window size so it has correct dimensions
// immediately. Used by both navigateBack and advance.
func (s Session) activateScreen() tea.Cmd {
	cmd := s.screens[s.cursor].Init()
	if s.lastSize != nil {
		updated, sizeCmd := s.screens[s.cursor].Update(s.adjustedSizeMsg())
		s.screens[s.cursor] = updated
		cmd = tea.Batch(cmd, sizeCmd)
	}
	return cmd
}

// forwardToScreen sends a message to the active screen and updates it.
func (s Session) forwardToScreen(msg tea.Msg) (tea.Model, tea.Cmd) {
	if len(s.screens) == 0 {
		return s, nil
	}

	updated, cmd := s.screens[s.cursor].Update(msg)
	s.screens[s.cursor] = updated
	return s, cmd
}

// adjustedSizeMsg returns a WindowSizeMsg with the height reduced by
// the chrome lines so screens can size themselves correctly.
func (s Session) adjustedSizeMsg() tea.WindowSizeMsg {
	return tea.WindowSizeMsg{Width: s.width, Height: max(1, s.height-chromeLines)}
}
