package picker

import (
	tea "github.com/charmbracelet/bubbletea"

	"gitea.apointless.space/bsncubed/dial/internal/store"
)

// Layout budget (lines) reserved around the scrolling list.
const (
	headerLines  = 2 // title + blank spacer
	footerLines  = 1 // key hints
	previewLines = 8 // bordered preview box (2 border + 6 fields)
	linesPerRow  = 3 // alias line + detail line + blank spacer
)

// visibleListLines returns how many rendered lines the scrolling list may
// occupy, reserving space for the header, preview box, and footer.
func (m *model) visibleListLines() int {
	reserved := headerLines + previewLines + footerLines + 1 // +1 slack for separators
	if m.filtering {
		reserved++ // filter input line
	}
	n := m.height - reserved
	if n < linesPerRow {
		return linesPerRow // always show at least one host block
	}
	return n
}

// visibleCount estimates how many host rows fit on screen, used only as the
// page size for PgUp/PgDn.
func (m *model) visibleCount() int {
	n := m.visibleListLines() / linesPerRow
	if n < 1 {
		return 1
	}
	return n
}

// ensureVisible clamps the cursor to a valid host position. The actual line
// scrolling (m.top) is computed during render, where header/host line heights
// are known.
func (m *model) ensureVisible() {
	if m.cursor >= len(m.view) {
		m.cursor = len(m.view) - 1
	}
	if m.cursor < 0 {
		m.cursor = 0
	}
}

func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width, m.height = msg.Width, msg.Height
		m.ensureVisible()
		return m, nil
	case tea.KeyMsg:
		if m.filtering {
			return m.updateFiltering(msg)
		}
		return m.updateNormal(msg)
	}
	return m, nil
}

// updateNormal handles keys in the resting (static list) state.
func (m *model) updateNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	m.status = ""
	switch msg.String() {
	case "ctrl+c", "q", "esc":
		m.quitting = true
		return m, tea.Quit

	case "up", "k", "ctrl+p":
		m.move(-1)
	case "down", "j", "ctrl+n":
		m.move(1)
	case "home":
		m.cursor = 0
		m.ensureVisible()
	case "end", "G":
		m.cursor = len(m.view) - 1
		m.ensureVisible()
	case "pgup":
		m.move(-m.visibleCount())
	case "pgdown":
		m.move(m.visibleCount())

	case "1", "2", "3", "4", "5", "6", "7", "8", "9":
		// Jump the cursor to the row bearing that number (1-based, absolute).
		n := int(msg.String()[0] - '0')
		if n >= 1 && n <= len(m.view) {
			m.cursor = n - 1
			m.ensureVisible()
		}

	case "/":
		m.filtering = true
		m.query = ""
		return m, nil

	case "s":
		// Cycle sort mode (recent → frequent → alpha) and persist the preference.
		m.sortM = m.sortM.Next()
		m.persistSort()
		m.reindex()

	case "g":
		// Toggle tag grouping (orthogonal to sort) and persist.
		m.grouped = !m.grouped
		m.persistGroup()
		if m.grouped {
			m.status = "grouped by tag"
		} else {
			m.status = "grouping off"
		}
		m.reindex()

	case "f":
		m.toggleFavorite()

	case "enter":
		return m.selectWith(ActionConnect)
	}
	return m, nil
}

// updateFiltering handles keys while the on-demand search box is open.
func (m *model) updateFiltering(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "esc", "ctrl+c":
		// Leave filter mode and return to the clean, static resting list.
		m.filtering = false
		m.query = ""
		m.applyFilter()
		return m, nil
	case "enter":
		return m.selectWith(ActionConnect)
	case "up", "ctrl+p":
		m.move(-1)
		return m, nil
	case "down", "ctrl+n":
		m.move(1)
		return m, nil
	case "backspace":
		if len(m.query) > 0 {
			// Trim one rune from the query.
			r := []rune(m.query)
			m.query = string(r[:len(r)-1])
			m.applyFilter()
		}
		return m, nil
	default:
		if msg.Type == tea.KeyRunes {
			m.query += string(msg.Runes)
			m.applyFilter()
		}
		return m, nil
	}
}

func (m *model) move(delta int) {
	if len(m.view) == 0 {
		return
	}
	m.cursor += delta
	if m.cursor < 0 {
		m.cursor = 0
	}
	if m.cursor >= len(m.view) {
		m.cursor = len(m.view) - 1
	}
	m.ensureVisible()
}

// reindex re-sorts and re-filters while keeping the highlighted host under the
// cursor where possible.
func (m *model) reindex() {
	var keepAlias string
	if m.cursor >= 0 && m.cursor < len(m.view) {
		keepAlias = m.rows[m.view[m.cursor]].host.Alias
	}
	m.sortAll()
	m.applyFilter()
	if keepAlias != "" {
		for i, vi := range m.view {
			if m.rows[vi].host.Alias == keepAlias {
				m.cursor = i
				break
			}
		}
	}
	m.ensureVisible()
}

func (m *model) selectWith(action Action) (tea.Model, tea.Cmd) {
	if m.cursor < 0 || m.cursor >= len(m.view) {
		return m, nil
	}
	m.result = Result{Alias: m.rows[m.view[m.cursor]].host.Alias, Action: action}
	m.quitting = true
	return m, tea.Quit
}

func (m *model) toggleFavorite() {
	if m.cursor < 0 || m.cursor >= len(m.view) {
		return
	}
	ri := m.view[m.cursor]
	alias := m.rows[ri].host.Alias
	on, err := store.ToggleFavorite(alias)
	if err != nil {
		m.status = "could not save favourite: " + err.Error()
		return
	}
	m.rows[ri].fav = on
	if on {
		m.status = "★ favourited " + alias
	} else {
		m.status = "removed favourite " + alias
	}
	m.reindex()
}

func (m *model) persistSort() {
	s := store.LoadSettings()
	s.Sort = m.sortM
	_ = store.SaveSettings(s) // non-fatal: preference only
}

func (m *model) persistGroup() {
	s := store.LoadSettings()
	s.GroupByTag = m.grouped
	_ = store.SaveSettings(s) // non-fatal: preference only
}
