package bookmarks

import (
	"fmt"
	"math"
	"slices"
	"strings"

	"charm.land/bubbles/v2/textinput"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/idursun/jjui/internal/jj"
	"github.com/idursun/jjui/internal/ui/actions"
	"github.com/idursun/jjui/internal/ui/common"
	"github.com/idursun/jjui/internal/ui/context"
	"github.com/idursun/jjui/internal/ui/intents"
	"github.com/idursun/jjui/internal/ui/layout"
	"github.com/idursun/jjui/internal/ui/render"
)

type updateItemsMsg struct {
	items []item
}

// SelectRemoteMsg is sent when a remote is clicked
type SelectRemoteMsg struct {
	Index int
}

type itemClickMsg struct {
	Index int
}

type itemScrollMsg struct {
	Delta      int
	Horizontal bool
}

func (m itemScrollMsg) SetDelta(delta int, horizontal bool) tea.Msg {
	m.Delta = delta
	m.Horizontal = horizontal
	return m
}

type filterState int

const (
	filterOff filterState = iota
	filterEditing
	filterApplied
)

var _ common.ImmediateModel = (*Model)(nil)
var _ common.Focusable = (*Model)(nil)
var _ common.Editable = (*Model)(nil)

type Model struct {
	context             *context.MainContext
	current             *jj.Commit
	distanceMap         map[string]int
	remoteNames         []string
	selectedRemoteIdx   int
	allItems            []item
	filteredItems       []item
	cursor              int
	listRenderer        *render.ListRenderer
	filterInput         textinput.Model
	filterState         filterState
	filterText          string
	categoryFilter      string
	ensureCursorVisible bool
	title               string
}

func (m *Model) IsFocused() bool {
	return m.filterState == filterEditing
}

func (m *Model) IsEditing() bool {
	return m.filterState == filterEditing
}

type commandType int

// defines the order of actions in the list
const (
	moveCommand commandType = iota
	deleteCommand
	trackCommand
	untrackCommand
	forgetCommand
)

type item struct {
	name     string
	priority commandType
	dist     int
	args     []string
	key      string
}

func (i item) FilterValue() string {
	return i.name
}

func (i item) Title() string {
	return i.name
}

func (i item) Description() string {
	desc := strings.Join(i.args, " ")
	return desc
}

func (i item) ShortCut() string {
	return i.key
}

func (m *Model) Scopes() []common.Scope {
	if m.IsEditing() {
		return []common.Scope{
			{
				Name:    actions.ScopeBookmarks + ".filter",
				Leak:    common.LeakNone,
				Handler: m,
			},
			{
				Name:    actions.ScopeBookmarks,
				Leak:    common.LeakNone,
				Handler: m,
			},
		}
	}
	return []common.Scope{
		{
			Name:    actions.ScopeBookmarks,
			Leak:    common.LeakAll,
			Handler: m,
		},
	}
}

func (m *Model) Init() tea.Cmd {
	return tea.Batch(m.loadAll, m.loadMovables)
}

func (m *Model) filtered(filter string) tea.Cmd {
	m.categoryFilter = filter
	m.applyFilters(true)
	return nil
}

func (m *Model) cycleRemotes(step int) tea.Cmd {
	if len(m.remoteNames) == 0 {
		return nil
	}

	m.selectedRemoteIdx += step
	if m.selectedRemoteIdx >= len(m.remoteNames) {
		m.selectedRemoteIdx = 0
	} else if m.selectedRemoteIdx < 0 {
		m.selectedRemoteIdx = len(m.remoteNames) - 1
	}

	return m.updateMenuForRemote()
}

func (m *Model) updateMenuForRemote() tea.Cmd {
	m.applyFilters(false)
	return nil
}

func (m *Model) filterItemsByRemote(allItems []item) []item {
	if len(m.remoteNames) == 0 {
		return allItems
	}

	selectedRemote := m.remoteNames[m.selectedRemoteIdx]
	filtered := make([]item, 0)

	// "local" mode shows local bookmark operations (delete, forget, move, track local, untrack all)
	if selectedRemote == "local" {
		for _, bookmarkItem := range allItems {
			// Include delete, forget, move operations
			if bookmarkItem.priority == deleteCommand || bookmarkItem.priority == forgetCommand || bookmarkItem.priority == moveCommand {
				filtered = append(filtered, bookmarkItem)
				continue
			}

			// Include track items on local bookmarks (no @remote suffix)
			if bookmarkItem.priority == trackCommand && !strings.Contains(bookmarkItem.name, "@") {
				filtered = append(filtered, bookmarkItem)
				continue
			}

			// Include untrack items (bookmarks that are tracked on remotes)
			if bookmarkItem.priority == untrackCommand {
				filtered = append(filtered, bookmarkItem)
				continue
			}
		}
		return filtered
	}

	// Remote mode shows track/untrack items for the selected remote only
	for _, bookmarkItem := range allItems {
		// Only include track/untrack items for the selected remote
		if bookmarkItem.priority == trackCommand || bookmarkItem.priority == untrackCommand {
			if strings.Contains(bookmarkItem.name, "@"+selectedRemote) {
				filtered = append(filtered, bookmarkItem)
			}
		}
	}

	return filtered
}

func (m *Model) loadMovables() tea.Msg {
	output, _ := m.context.RunCommandImmediate(jj.BookmarkListMovable(m.current.GetChangeId()))
	var bookmarkItems []item
	bookmarks := jj.ParseBookmarkListOutput(string(output))
	for _, b := range bookmarks {
		if !b.Conflict && b.CommitId == m.current.CommitId {
			continue
		}

		name := fmt.Sprintf("move '%s' to %s", b.Name, m.current.GetChangeId())
		if b.Conflict {
			name = fmt.Sprintf("move conflicted '%s' to %s", b.Name, m.current.GetChangeId())
		}
		var extraFlags []string
		if b.Backwards {
			name = fmt.Sprintf("move '%s' backwards to %s", b.Name, m.current.GetChangeId())
			extraFlags = append(extraFlags, "--allow-backwards")
		}
		elem := item{
			name:     name,
			priority: moveCommand,
			args:     jj.BookmarkMove(m.current.GetChangeId(), b.Name, extraFlags...),
			dist:     m.distance(b.CommitId),
		}
		if b.Name == "main" || b.Name == "master" {
			elem.key = "m"
		}
		bookmarkItems = append(bookmarkItems, elem)
	}
	return updateItemsMsg{items: bookmarkItems}
}

func (m *Model) loadAll() tea.Msg {
	if output, err := m.context.RunCommandImmediate(jj.BookmarkListAll()); err != nil {
		return nil
	} else {
		bookmarks := jj.ParseBookmarkListOutput(string(output))

		items := make([]item, 0)
		for _, b := range bookmarks {
			distance := m.distance(b.CommitId)
			if b.IsDeletable() {
				items = append(items, item{
					name:     fmt.Sprintf("delete '%s'", b.Name),
					priority: deleteCommand,
					dist:     distance,
					args:     jj.BookmarkDelete(b.Name),
				})
			}

			items = append(items, item{
				name:     fmt.Sprintf("forget '%s'", b.Name),
				priority: forgetCommand,
				dist:     distance,
				args:     jj.BookmarkForget(b.Name),
			})

			// Track local bookmarks as they have no remotes
			if b.IsTrackable() {
				items = append(items, item{
					name:     fmt.Sprintf("track '%s'", b.Name),
					priority: trackCommand,
					dist:     distance,
					args:     jj.BookmarkTrack(b.Name, m.defaultTrackRemote()),
				})
			}

			for _, remote := range b.Remotes {
				nameWithRemote := fmt.Sprintf("%s@%s", b.Name, remote.Remote)
				if remote.Tracked {
					items = append(items, item{
						name:     fmt.Sprintf("untrack '%s'", nameWithRemote),
						priority: untrackCommand,
						dist:     distance,
						args:     jj.BookmarkUntrack(b.Name, remote.Remote),
					})
				} else {
					items = append(items, item{
						name:     fmt.Sprintf("track '%s'", nameWithRemote),
						priority: trackCommand,
						dist:     distance,
						args:     jj.BookmarkTrack(b.Name, remote.Remote),
					})
				}
			}
		}
		return updateItemsMsg{items: items}
	}
}

func (m *Model) Update(msg tea.Msg) tea.Cmd {
	switch msg := msg.(type) {
	case itemClickMsg:
		items := m.visibleItems()
		if msg.Index >= 0 && msg.Index < len(items) {
			m.cursor = msg.Index
			m.ensureCursorVisible = true
		}
	case itemScrollMsg:
		if msg.Horizontal {
			return nil
		}
		m.ensureCursorVisible = false
		m.listRenderer.StartLine += msg.Delta
		if m.listRenderer.StartLine < 0 {
			m.listRenderer.StartLine = 0
		}
	case SelectRemoteMsg:
		if msg.Index >= 0 && msg.Index < len(m.remoteNames) {
			m.selectedRemoteIdx = msg.Index
			return m.updateMenuForRemote()
		}
		return nil
	case intents.Intent:
		cmd, _ := m.HandleIntent(msg)
		return cmd
	case tea.KeyMsg, tea.PasteMsg:
		if m.filterState == filterEditing {
			updated, cmd := m.filterInput.Update(msg)
			filterChanged := m.filterInput.Value() != updated.Value()
			m.filterInput = updated
			if filterChanged {
				m.applyFilters(false)
			}
			return cmd
		}
		keyMsg, ok := msg.(tea.KeyMsg)
		if !ok {
			return nil
		}
		if cmd, handled := m.HandleIntent(intents.BookmarksApplyShortcut{Key: keyMsg.String()}); handled && cmd != nil {
			return cmd
		}
	case updateItemsMsg:
		m.allItems = append(m.allItems, msg.items...)
		slices.SortFunc(m.allItems, itemSorter)
		return m.updateMenuForRemote()
	}
	return nil
}

func (m *Model) HandleIntent(intent intents.Intent) (tea.Cmd, bool) {
	switch msg := intent.(type) {
	case intents.Apply:
		if m.filterState == filterEditing {
			m.filterText = strings.TrimSpace(m.filterInput.Value())
			if m.filterText == "" {
				m.filterState = filterOff
				m.filterInput.SetValue("")
				m.filterInput.Blur()
			} else {
				m.filterState = filterApplied
				m.filterInput.Blur()
			}
			m.applyFilters(true)
			return nil, true
		}
		selected, ok := m.selectedItem()
		if !ok {
			return nil, true
		}
		return m.context.RunCommand(selected.args, common.Refresh, common.CloseApplied), true
	case intents.BookmarksFilter:
		filter := string(msg.Kind)
		if filter == "" {
			return nil, true
		}
		if m.categoryFilter == filter {
			return m.executeDefaultForFilter(msg.Kind), true
		}
		return m.filtered(filter), true
	case intents.BookmarksCycleRemotes:
		return m.cycleRemotes(msg.Delta), true
	case intents.BookmarksOpenFilter:
		m.filterState = filterEditing
		m.filterInput.Focus()
		m.filterInput.CursorEnd()
		return textinput.Blink, true
	case intents.BookmarksNavigate:
		if msg.IsPage {
			m.ensureCursorVisible = false
			m.listRenderer.StartLine += msg.Delta * m.itemHeight()
			return nil, true
		}
		m.moveCursor(msg.Delta)
		return nil, true
	case intents.Cancel:
		if m.filterState == filterEditing {
			m.resetTextFilter()
			return nil, true
		}
		if m.hasActiveFilter() {
			m.resetAllFilters()
			return nil, true
		}
		return common.Close, true
	case intents.BookmarksApplyShortcut:
		if m.categoryFilter == "" {
			return nil, true
		}
		if cmd := m.executeShortcut(msg.Key); cmd != nil {
			return cmd, true
		}
		return nil, true
	}
	return nil, false
}

func (m *Model) executeDefaultForFilter(kind intents.BookmarksFilterKind) tea.Cmd {
	for _, listItem := range m.visibleItems() {
		if listItem.key != "" {
			return m.context.RunCommand(jj.Args(listItem.args...), common.Refresh, common.CloseApplied)
		}
	}
	if selected, ok := m.selectedItem(); ok {
		return m.context.RunCommand(jj.Args(selected.args...), common.Refresh, common.CloseApplied)
	}
	for _, listItem := range m.visibleItems() {
		if strings.HasPrefix(listItem.FilterValue(), string(kind)) {
			return m.context.RunCommand(jj.Args(listItem.args...), common.Refresh, common.CloseApplied)
		}
	}
	return nil
}

func (m *Model) executeShortcut(key string) tea.Cmd {
	for _, listItem := range m.visibleItems() {
		if listItem.key == key {
			return m.context.RunCommand(jj.Args(listItem.args...), common.Refresh, common.CloseApplied)
		}
	}
	return nil
}

func itemSorter(a item, b item) int {
	if a.priority != b.priority {
		return int(a.priority) - int(b.priority)
	}
	if a.dist == b.dist {
		return strings.Compare(a.name, b.name)
	}
	if a.dist >= 0 && b.dist >= 0 {
		return a.dist - b.dist
	}
	if a.dist < 0 && b.dist < 0 {
		return b.dist - a.dist
	}
	return b.dist - a.dist
}

func (m *Model) ViewRect(dl *render.DisplayContext, box layout.Box) {
	pw, ph := box.R.Dx(), box.R.Dy()
	contentWidth := max(min(pw, 80)-4, 0)
	contentHeight := max(min(ph, 40)-4, 0)
	menuWidth := max(contentWidth+2, 0)
	menuHeight := max(contentHeight+2, 0)
	frame := box.Center(menuWidth, menuHeight)
	if frame.R.Dx() <= 0 || frame.R.Dy() <= 0 {
		return
	}

	menuTitleStyle := common.DefaultPalette.Get("bookmarks menu title")
	menuTextStyle := common.DefaultPalette.Get("bookmarks menu text")
	menuMatchedStyle := common.DefaultPalette.Get("bookmarks menu matched")
	borderStyle := common.DefaultPalette.GetBorder("bookmarks menu border", lipgloss.NormalBorder())

	dl.AddBackdrop(box.R, render.ZMenuBorder-1)
	contentBox := frame.Inset(1)
	if contentBox.R.Dx() <= 0 || contentBox.R.Dy() <= 0 {
		return
	}
	dl.AddFill(contentBox.R, ' ', menuTextStyle, render.ZMenuContent)

	borderBase := lipgloss.NewStyle().Width(contentBox.R.Dx()).Height(contentBox.R.Dy()).Render("")
	dl.AddDraw(frame.R, borderStyle.Render(borderBase), render.ZMenuBorder)

	titleBox, contentBox := contentBox.CutTop(1)
	dl.
		Text(titleBox.R.Min.X, titleBox.R.Min.Y, render.ZMenuContent).
		Styled(m.title, menuTitleStyle).
		Done()

	_, contentBox = contentBox.CutTop(1)
	remoteBox, contentBox := contentBox.CutTop(1)
	m.renderRemotes(dl, remoteBox)

	_, contentBox = contentBox.CutTop(1)
	filterBox, contentBox := contentBox.CutTop(1)
	if m.filterState == filterEditing {
		fis := m.filterInput.Styles()
		fis.Focused.Prompt = menuMatchedStyle.PaddingLeft(1)
		fis.Focused.Text = menuTextStyle
		fis.Blurred.Prompt = menuMatchedStyle.PaddingLeft(1)
		fis.Blurred.Text = menuTextStyle
		m.filterInput.SetStyles(fis)
		m.filterInput.SetWidth(max(contentBox.R.Dx()-2, 0))
		dl.AddDraw(filterBox.R, m.filterInput.View(), render.ZMenuContent)
		dl.SetCursorInRect(m.filterInput.Cursor(), filterBox.R, 0, 0)
	} else {
		m.renderFilterView(dl, filterBox)
	}

	_, listBox := contentBox.CutTop(1)
	m.renderList(dl, listBox)
}

func (m *Model) renderRemotes(dl *render.DisplayContext, lineBox layout.Box) {
	if lineBox.R.Dx() <= 0 || lineBox.R.Dy() <= 0 {
		return
	}

	remotePromptStyle := common.DefaultPalette.Get("bookmarks title")
	menuTextStyle := common.DefaultPalette.Get("bookmarks menu text")
	remoteTextStyle := common.DefaultPalette.Get("bookmarks dimmed")
	remoteSelectedStyle := common.DefaultPalette.Get("bookmarks menu selected")
	noRemoteStyle := common.DefaultPalette.Get("bookmarks error")

	dl.AddFill(lineBox.R, ' ', menuTextStyle, render.ZMenuContent)

	// Render above menu content
	tb := dl.Text(lineBox.R.Min.X, lineBox.R.Min.Y, render.ZMenuContent+1).
		Styled(" ", menuTextStyle).
		Styled("Remotes: ", remotePromptStyle)

	if len(m.remoteNames) == 0 {
		tb.Styled("NO REMOTE FOUND", noRemoteStyle).Done()
		return
	}

	for idx, remoteName := range m.remoteNames {
		style := remoteTextStyle
		if idx == m.selectedRemoteIdx {
			style = remoteSelectedStyle
		}
		tb.Clickable(remoteName, style, SelectRemoteMsg{Index: idx}).Styled(" ", menuTextStyle)
	}

	tb.Done()
}

func (m *Model) distance(commitId string) int {
	if dist, ok := m.distanceMap[commitId]; ok {
		return dist
	}
	return math.MinInt32
}

func loadRemoteNames(c context.CommandRunner) []string {
	bytes, _ := c.RunCommandImmediate(jj.GitRemoteList())
	remotes := jj.ParseRemoteListOutput(string(bytes))
	return remotes
}

func (m *Model) defaultTrackRemote() string {
	for _, remote := range m.remoteNames {
		if remote == "origin" {
			return remote
		}
	}
	for _, remote := range m.remoteNames {
		if remote != "local" {
			return remote
		}
	}
	return "origin"
}

func NewModel(c *context.MainContext, current *jj.Commit, commitIds []string) *Model {
	remotes := loadRemoteNames(c)
	// Add "local" as the first option to view local bookmark operations
	remotes = append([]string{"local"}, remotes...)

	m := &Model{
		context:           c,
		current:           current,
		distanceMap:       calcDistanceMap(current.CommitId, commitIds),
		remoteNames:       remotes,
		selectedRemoteIdx: 0,
		listRenderer:      render.NewListRenderer(itemScrollMsg{}),
		title:             "Bookmark Operations",
		allItems:          make([]item, 0),
	}
	m.listRenderer.Z = render.ZMenuContent

	m.filterInput = textinput.New()
	m.filterInput.Prompt = "Filter: "
	m.filterInput.SetVirtualCursor(false)
	m.applyFilters(true)

	return m
}

func (m *Model) visibleItems() []item {
	return m.filteredItems
}

func (m *Model) selectedItem() (item, bool) {
	items := m.visibleItems()
	if m.cursor < 0 || m.cursor >= len(items) {
		return item{}, false
	}
	return items[m.cursor], true
}

func (m *Model) itemHeight() int {
	return 3
}

func (m *Model) moveCursor(delta int) {
	items := m.visibleItems()
	if len(items) == 0 {
		m.cursor = 0
		return
	}
	next := m.cursor + delta
	if next < 0 {
		next = 0
	} else if next >= len(items) {
		next = len(items) - 1
	}
	if next != m.cursor {
		m.cursor = next
		m.ensureCursorVisible = true
	}
}

func (m *Model) hasActiveFilter() bool {
	return m.categoryFilter != "" || m.currentFilterText() != ""
}

func (m *Model) currentFilterText() string {
	if m.filterState == filterEditing {
		return strings.TrimSpace(m.filterInput.Value())
	}
	return strings.TrimSpace(m.filterText)
}

func (m *Model) resetTextFilter() {
	m.filterInput.SetValue("")
	m.filterText = ""
	m.filterState = filterOff
	m.filterInput.Blur()
	m.applyFilters(true)
}

func (m *Model) resetAllFilters() {
	m.categoryFilter = ""
	m.resetTextFilter()
}

func (m *Model) applyFilters(resetCursor bool) {
	items := m.filterItemsByRemote(m.allItems)

	if m.categoryFilter != "" {
		filtered := make([]item, 0, len(items))
		for _, item := range items {
			if m.categoryFilterMatch(item, m.categoryFilter) {
				filtered = append(filtered, item)
			}
		}
		items = filtered
	}

	filterText := m.currentFilterText()
	if filterText != "" {
		filtered := make([]item, 0, len(items))
		for _, item := range items {
			if m.textFilterMatch(item, filterText) {
				filtered = append(filtered, item)
			}
		}
		items = filtered
	}

	m.filteredItems = items
	if resetCursor || m.cursor >= len(m.filteredItems) {
		m.cursor = 0
	}
	m.listRenderer.StartLine = 0
}

func (m *Model) categoryFilterMatch(item item, filter string) bool {
	if !strings.HasPrefix(item.FilterValue(), filter) {
		return false
	}
	if strings.HasPrefix(filter, "track") || strings.HasPrefix(filter, "untrack") {
		return m.remoteFilterMatch(item, filter)
	}
	return true
}

func (m *Model) textFilterMatch(item item, filter string) bool {
	filter = strings.TrimSpace(filter)
	if filter == "" {
		return true
	}
	lowerFilter := strings.ToLower(filter)
	if !strings.Contains(strings.ToLower(item.FilterValue()), lowerFilter) {
		return false
	}
	if strings.HasPrefix(lowerFilter, "track") || strings.HasPrefix(lowerFilter, "untrack") {
		return m.remoteFilterMatch(item, lowerFilter)
	}
	return true
}

func (m *Model) remoteFilterMatch(item item, filter string) bool {
	if len(m.remoteNames) == 0 || m.selectedRemoteIdx >= len(m.remoteNames) {
		return true
	}
	selectedRemote := m.remoteNames[m.selectedRemoteIdx]
	if selectedRemote == "local" {
		if strings.HasPrefix(filter, "untrack") {
			return true
		}
		return !strings.Contains(item.FilterValue(), "@")
	}
	if strings.Contains(item.FilterValue(), "@") {
		return strings.Contains(item.FilterValue(), "@"+selectedRemote)
	}
	return false
}

func (m *Model) renderFilterView(dl *render.DisplayContext, box layout.Box) {
	if box.R.Dx() <= 0 || box.R.Dy() <= 0 {
		return
	}
	width := box.R.Dx()
	menuTextStyle := common.DefaultPalette.Get("bookmarks menu text")
	menuMatchedStyle := common.DefaultPalette.Get("bookmarks menu matched")
	labelStyle := menuTextStyle.PaddingLeft(1).PaddingRight(1)
	valueStyle := menuMatchedStyle

	action := "all actions"
	if m.categoryFilter != "" {
		action = m.categoryFilter
	}

	remote := "all remotes"
	if len(m.remoteNames) > 0 && m.selectedRemoteIdx < len(m.remoteNames) {
		remote = m.remoteNames[m.selectedRemoteIdx]
	}

	parts := []string{
		labelStyle.Render("Showing:"),
		valueStyle.Render(action),
		labelStyle.Render("for"),
		valueStyle.Render(remote),
		labelStyle.Render("remote"),
	}

	filterText := m.currentFilterText()
	if filterText != "" {
		parts = append(parts,
			labelStyle.Render("containing"),
			valueStyle.Render("\""+filterText+"\""),
		)
	}

	dl.AddDraw(box.R, menuTextStyle.Width(width).Render(lipgloss.JoinHorizontal(0, parts...)), render.ZMenuContent)
}

func (m *Model) renderList(dl *render.DisplayContext, listBox layout.Box) {
	if listBox.R.Dx() <= 0 || listBox.R.Dy() <= 0 {
		return
	}

	listWidth := max(listBox.R.Dx()-2, 0)
	items := m.visibleItems()
	itemCount := len(items)
	if itemCount == 0 {
		return
	}

	itemHeight := m.itemHeight()
	m.listRenderer.StartLine = render.ClampStartLine(m.listRenderer.StartLine, listBox.R.Dy(), itemCount*itemHeight)
	m.listRenderer.Render(
		dl,
		listBox,
		itemCount,
		m.cursor,
		m.ensureCursorVisible,
		func(_ int) int { return itemHeight },
		func(dl *render.DisplayContext, index int, rect layout.Rectangle) {
			if index < 0 || index >= itemCount {
				return
			}
			renderItem(dl, rect, listWidth, m.categoryFilter != "", m.cursor, index, items[index])
		},
		func(index int, _ tea.Mouse) tea.Msg { return itemClickMsg{Index: index} },
	)
	m.listRenderer.RegisterScroll(dl, listBox)
	m.ensureCursorVisible = false
}

func renderItem(dl *render.DisplayContext, rect layout.Rectangle, width int, showShortcuts bool, cursor int, index int, item item) {
	var (
		title string
		desc  string
	)
	title = item.Title()
	desc = item.Description()
	shortcut := ""
	if showShortcuts {
		shortcut = item.ShortCut()
	}
	if width <= 0 {
		return
	}

	if len(title) > width {
		title = title[:width-1] + "…"
	}

	if len(desc) > width {
		desc = desc[:width-1] + "…"
	}

	textStyle := common.DefaultPalette.Get("bookmarks menu text")
	descStyle := common.DefaultPalette.Get("bookmarks menu dimmed")
	shortcutStyle := common.DefaultPalette.Get("bookmarks menu shortcut")

	if index == cursor {
		textStyle = common.DefaultPalette.Get("bookmarks menu selected text")
		descStyle = common.DefaultPalette.Get("bookmarks menu selected dimmed")
		shortcutStyle = common.DefaultPalette.Get("bookmarks menu selected shortcut")
	}

	titleLine := ""
	if shortcut != "" {
		titleLine = lipgloss.JoinHorizontal(0, shortcutStyle.PaddingLeft(1).Render(shortcut), textStyle.PaddingLeft(1).Render(title))
	} else {
		titleLine = textStyle.PaddingLeft(1).Render(title)
	}
	titleLine = lipgloss.PlaceHorizontal(width+2, 0, titleLine, lipgloss.WithWhitespaceStyle(textStyle))

	descStyle = descStyle.PaddingLeft(1).PaddingRight(1).Width(width + 2)
	descLine := descStyle.Render(desc)
	descLine = lipgloss.PlaceHorizontal(width+2, 0, descLine, lipgloss.WithWhitespaceStyle(textStyle))

	spacerLine := textStyle.Width(width + 2).Render("")
	content := lipgloss.JoinVertical(lipgloss.Left, titleLine, descLine, spacerLine)
	if content == "" {
		return
	}
	dl.AddDraw(rect, content, render.ZMenuContent)
}

func calcDistanceMap(current string, commitIds []string) map[string]int {
	distanceMap := make(map[string]int)
	currentPos := -1
	for i, id := range commitIds {
		if id == current {
			currentPos = i
			break
		}
	}
	for i, id := range commitIds {
		dist := i - currentPos
		distanceMap[id] = dist
	}
	return distanceMap
}
