package picker

import (
	"fmt"
	"strings"
	"time"

	"github.com/charmbracelet/lipgloss"

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

func (m *model) View() string {
	if m.quitting {
		return ""
	}
	// renderList must run first: it computes the scroll window and sets
	// winFirst/winLast, which the header's "showing" hint reads.
	list := m.renderList()

	var b strings.Builder
	b.WriteString(m.renderHeader())
	b.WriteString("\n\n")
	b.WriteString(list)
	b.WriteString("\n")
	b.WriteString(m.renderPreview())
	b.WriteString("\n")
	b.WriteString(m.renderFooter())
	return b.String()
}

func (m *model) renderHeader() string {
	th := m.th
	sortName := "recently used"
	switch m.sortM {
	case store.SortFrequent:
		sortName = "most used"
	case store.SortAlpha:
		sortName = "name"
	}
	left := th.Title.Render("dial") + th.Meta.Render(
		fmt.Sprintf("   %d hosts · sorted by %s", len(m.rows), sortName))
	if m.grouped {
		left += th.Meta.Render(" · grouped")
	}

	// Orientation hint when the list is scrolled/windowed.
	if m.winFirst >= 0 && (m.winFirst > 0 || m.winLast < len(m.view)-1) {
		left += th.Meta.Render(fmt.Sprintf("  ·  showing %d–%d of %d",
			m.winFirst+1, m.winLast+1, len(m.view)))
	}
	if m.status != "" {
		left += "   " + th.Star.Render(m.status)
	}
	return m.truncate("  " + left)
}

// renderList builds the whole scrollable body as lines (section headers, host
// blocks, and spacers), then windows those lines so the cursor's host — and its
// section header, if any — stays visible. Line-based windowing lets grouped and
// flat modes share one code path and keeps the preview/footer from being pushed
// off-screen regardless of how many headers are on screen.
func (m *model) renderList() string {
	th := m.th
	m.winFirst, m.winLast = -1, -1
	if len(m.view) == 0 {
		msg := "no hosts in ~/.ssh/config"
		if m.query != "" {
			msg = "no hosts match \"" + m.query + "\""
		}
		return "  " + th.Meta.Render(msg)
	}

	var lines []string
	hostLine := make([]int, len(m.view))   // index of a host's first content line
	headerLine := make([]int, len(m.view)) // index of the header line before it, or -1
	for i := range m.view {
		headerLine[i] = -1
		if m.grouped {
			if h, ok := m.headers[i]; ok {
				if len(lines) > 0 {
					lines = append(lines, "") // gap before a new section
				}
				headerLine[i] = len(lines)
				lines = append(lines, m.truncate(m.renderSectionHeader(h)), "")
			}
		}
		hostLine[i] = len(lines)
		l1, l2 := m.renderHostLines(i)
		lines = append(lines, m.truncate(l1), m.truncate(l2), "")
	}

	// Scroll so the cursor block (plus its header, if any) stays visible: start
	// from the previous offset and nudge up or down only as needed.
	budget := m.visibleListLines()
	top := m.top
	blockTop := hostLine[m.cursor]
	if headerLine[m.cursor] >= 0 {
		blockTop = headerLine[m.cursor]
	}
	if blockTop < top {
		top = blockTop // scroll up to reveal the cursor (and its header)
	}
	blockBottom := hostLine[m.cursor] + 1 // the detail line
	if blockBottom >= top+budget {
		top = blockBottom - budget + 1 // scroll down to reveal the cursor
	}
	if maxTop := len(lines) - budget; top > maxTop {
		top = maxTop
	}
	if top < 0 {
		top = 0
	}
	m.top = top
	end := top + budget
	if end > len(lines) {
		end = len(lines)
	}

	for i := range m.view {
		if hostLine[i] >= top && hostLine[i] < end {
			if m.winFirst == -1 {
				m.winFirst = i
			}
			m.winLast = i
		}
	}
	return strings.Join(lines[top:end], "\n")
}

// renderHostLines returns the two rendered lines for the host at view position i.
func (m *model) renderHostLines(i int) (string, string) {
	th := m.th
	r := m.rows[m.view[i]]
	selected := i == m.cursor

	marker := "  "
	if selected {
		marker = th.Cursor.Render("▸ ")
	}
	numStr := th.Number.Render(fmt.Sprintf("%2d", i+1))
	if selected {
		numStr = th.NumberSel.Render(fmt.Sprintf("%2d", i+1))
	}
	star := "  "
	if r.fav {
		star = th.Star.Render("★ ")
	}
	alias := th.Alias.Render(r.host.Alias)
	if selected {
		alias = th.AliasSel.Render(r.host.Alias)
	}
	line1 := fmt.Sprintf("%s%s  %s%s", marker, numStr, star, alias)
	if len(r.host.Tags) > 0 {
		line1 += "   " + m.renderPills(r.host.Tags)
	}

	dstyle := th.Detail
	if selected {
		dstyle = th.DetailSel
	}
	return line1, "       " + dstyle.Render(summary(r.host))
}

// renderPills renders each tag in its own deterministic colour.
func (m *model) renderPills(tags []string) string {
	parts := make([]string, len(tags))
	for i, t := range tags {
		parts[i] = m.th.TagStyle(t).Render(t)
	}
	return strings.Join(parts, " ")
}

// renderSectionHeader renders a grouped-mode section label, coloured to match
// the tag's pill (neutral for the Favourites/untagged sections).
func (m *model) renderSectionHeader(h sectionHead) string {
	return "  " + m.th.TagStyle(h.tag).Bold(true).Render("▊ "+h.title)
}

// summary builds a compact "user@hostname:port (via jump)" line for the list.
func summary(h sshconf.Host) string {
	target := h.HostName
	if target == "" {
		target = h.Alias
	}
	s := target
	if h.User != "" {
		s = h.User + "@" + target
	}
	if h.Port != "" && h.Port != "22" {
		s += ":" + h.Port
	}
	if h.ProxyJump != "" {
		s += "   via " + h.ProxyJump
	}
	return s
}

func (m *model) renderPreview() string {
	th := m.th
	if m.cursor < 0 || m.cursor >= len(m.view) {
		return th.Preview.Render(th.Meta.Render("no selection"))
	}
	r := m.rows[m.view[m.cursor]]
	h := r.host

	rowsKV := [][2]string{
		{"Host", h.Alias},
		{"HostName", orDash(h.HostName)},
		{"User", orDash(h.User)},
		{"Port", orDash(defaultPort(h.Port))},
		{"ProxyJump", orDash(h.ProxyJump)},
		{"Last used", humanizeSince(r.last)},
	}
	var lines []string
	for _, kv := range rowsKV {
		lines = append(lines, fmt.Sprintf("%s  %s",
			th.PreviewK.Render(pad(kv[0], 10)), th.PreviewV.Render(kv[1])))
	}
	box := th.Preview.Width(m.previewWidth()).Render(strings.Join(lines, "\n"))
	return box
}

func (m *model) previewWidth() int {
	// Subtract the box border so the whole box fits the terminal.
	w := m.width - 6
	if w > 100 {
		w = 100 // don't stretch the box uncomfortably wide
	}
	if w < 10 {
		w = 10
	}
	return w
}

func (m *model) renderFooter() string {
	th := m.th
	if m.filtering {
		bar := th.FilterBar.Render("/ " + m.query + "▌")
		hint := th.Help.Render("   type to filter · enter connect · esc cancel")
		return m.truncate("  " + bar + hint)
	}
	hints := []string{
		"↑↓ move", "enter connect", "/ search", "s sort", "g group", "f fav", "q quit",
	}
	return m.truncate("  " + th.Help.Render(strings.Join(hints, " · ")))
}

// truncate clamps a rendered line to the terminal width to avoid wrapping,
// which would break the fixed 2-lines-per-row layout.
func (m *model) truncate(s string) string {
	if m.width <= 0 {
		return s
	}
	return lipgloss.NewStyle().MaxWidth(m.width).Render(s)
}

// humanizeSince renders a connection time as a compact relative phrase for the
// preview pane. A zero time means the host has never been connected to.
func humanizeSince(t time.Time) string {
	if t.IsZero() {
		return "never"
	}
	d := time.Since(t)
	switch {
	case d < 0:
		return "just now"
	case d < time.Minute:
		return "just now"
	case d < time.Hour:
		return plural(int(d.Minutes()), "minute")
	case d < 24*time.Hour:
		return plural(int(d.Hours()), "hour")
	case d < 30*24*time.Hour:
		return plural(int(d.Hours()/24), "day")
	case d < 365*24*time.Hour:
		return plural(int(d.Hours()/(24*30)), "month")
	default:
		return plural(int(d.Hours()/(24*365)), "year")
	}
}

func plural(n int, unit string) string {
	if n <= 1 {
		return fmt.Sprintf("%d %s ago", n, unit)
	}
	return fmt.Sprintf("%d %ss ago", n, unit)
}

func orDash(s string) string {
	if s == "" {
		return "—"
	}
	return s
}

func defaultPort(p string) string {
	if p == "" {
		return "22"
	}
	return p
}

func pad(s string, n int) string {
	if len(s) >= n {
		return s
	}
	return s + strings.Repeat(" ", n-len(s))
}
