// Package picker is dial's interactive host list. Per the brief it is a static,
// numbered, high-contrast list — NOT a fuzzy-filter-as-you-type UI. The resting
// state is always a clean, well-spaced, readable list; a search/filter mode is
// available on demand via "/" but is never the default view.
//
// The picker only *selects* a host. Connecting is the caller's job (see package
// connect), keeping selection decoupled from what's done with the chosen host.
package picker

import (
	"sort"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"

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

// Action is what the user chose to do with the selected host. The type leaves
// room for additional actions to slot in without reworking the result plumbing.
type Action int

const (
	ActionNone Action = iota
	ActionConnect
)

// Result reports the outcome of a picker session.
type Result struct {
	Alias  string // selected host alias; empty if the user quit
	Action Action
}

// row is a host enriched with its usage stats and favourite flag.
type row struct {
	host  sshconf.Host
	count int
	last  time.Time
	fav   bool
}

type model struct {
	th    theme.Theme
	rows  []row // immutable set of all hosts
	sortM store.SortMode

	sorted []int // indices into rows, in sorted order (favourites first)
	view   []int // indices into rows currently shown (sorted, filtered, grouped)

	grouped bool                // section the list by tag
	headers map[int]sectionHead // view position -> section header shown before it

	cursor int // position within view (a host row)
	top    int // scroll offset, in rendered lines

	// winFirst/winLast are the first/last host positions visible after the last
	// render, used for the "showing X–Y of N" header hint.
	winFirst, winLast int

	filtering bool
	query     string

	width, height int
	status        string // transient one-line message

	result   Result
	quitting bool
}

// sectionHead labels a group section in grouped mode. tag is the colour key
// (empty for the Favourites/untagged sections, which render neutral).
type sectionHead struct {
	title string
	tag   string
}

// Run displays the picker and blocks until the user connects or quits. It
// persists sort-mode and favourite changes via the store as they happen. The
// returned Result carries the chosen alias (empty if quit).
func Run(hosts []sshconf.Host, data store.Data, settings store.Settings) (Result, error) {
	m, _ := buildModel(hosts, data, settings)
	p := tea.NewProgram(m, tea.WithAltScreen())
	fm, err := p.Run()
	if err != nil {
		return Result{}, err
	}
	return fm.(*model).result, nil
}

// buildModel constructs an initialised model (rows enriched, sorted, filtered)
// without starting a tea program, so tests can render it directly.
func buildModel(hosts []sshconf.Host, data store.Data, settings store.Settings) (*model, error) {
	fav := map[string]bool{}
	for _, f := range data.Favorites {
		fav[f] = true
	}
	rows := make([]row, len(hosts))
	for i, h := range hosts {
		r := row{host: h, fav: fav[h.Alias]}
		if st := data.Stats[h.Alias]; st != nil {
			r.count = st.Count
			r.last = st.LastUsed
		}
		rows[i] = r
	}

	m := &model{
		th:      theme.New(settings.Theme),
		rows:    rows,
		sortM:   settings.Sort,
		grouped: settings.GroupByTag,
	}
	m.sortAll()
	m.applyFilter()
	return m, nil
}

func (m *model) Init() tea.Cmd { return nil }

// sortAll rebuilds m.sorted: favourites first, then by the active sort mode,
// ties broken by alias for stable, predictable ordering.
func (m *model) sortAll() {
	idx := make([]int, len(m.rows))
	for i := range idx {
		idx[i] = i
	}
	less := func(a, b int) bool {
		ra, rb := m.rows[a], m.rows[b]
		if ra.fav != rb.fav {
			return ra.fav // favourites float to the top
		}
		switch m.sortM {
		case store.SortFrequent:
			if ra.count != rb.count {
				return ra.count > rb.count
			}
		case store.SortAlpha:
			// Alias-only ordering; handled by the shared tiebreaker below.
		default: // SortRecent
			if !ra.last.Equal(rb.last) {
				return ra.last.After(rb.last)
			}
		}
		return strings.ToLower(ra.host.Alias) < strings.ToLower(rb.host.Alias)
	}
	sort.SliceStable(idx, func(i, j int) bool { return less(idx[i], idx[j]) })
	m.sorted = idx
}

// applyFilter recomputes m.view from m.sorted using the current query. An empty
// query shows everything (the resting state). Matching is a case-insensitive
// substring over alias, hostname, user, and tags — predictable, not fuzzy.
func (m *model) applyFilter() {
	q := strings.ToLower(strings.TrimSpace(m.query))
	filtered := make([]int, 0, len(m.sorted))
	for _, i := range m.sorted {
		if q == "" || rowMatches(m.rows[i], q) {
			filtered = append(filtered, i)
		}
	}
	if m.grouped {
		m.view, m.headers = m.regroup(filtered)
	} else {
		m.view, m.headers = filtered, nil
	}
	if m.cursor >= len(m.view) {
		m.cursor = len(m.view) - 1
	}
	if m.cursor < 0 {
		m.cursor = 0
	}
	m.ensureVisible()
}

// regroup reorders the filtered hosts into sections: a Favourites section
// first (if any), then one section per tag (tags sorted alphabetically), then
// an "untagged" section. A host is placed under its first tag only, so the view
// stays a clean permutation with no duplicated rows. Within each section, the
// active sort order is preserved (favourites are pulled out, so their tag
// sections don't repeat them).
func (m *model) regroup(filtered []int) ([]int, map[int]sectionHead) {
	var favs, untagged []int
	buckets := map[string][]int{}
	var tagOrder []string
	seenTag := map[string]bool{}

	for _, vi := range filtered {
		r := m.rows[vi]
		if r.fav {
			favs = append(favs, vi)
			continue
		}
		if len(r.host.Tags) == 0 {
			untagged = append(untagged, vi)
			continue
		}
		t := r.host.Tags[0]
		if !seenTag[t] {
			seenTag[t] = true
			tagOrder = append(tagOrder, t)
		}
		buckets[t] = append(buckets[t], vi)
	}
	sort.Strings(tagOrder)

	view := make([]int, 0, len(filtered))
	headers := map[int]sectionHead{}
	addSection := func(h sectionHead, hosts []int) {
		if len(hosts) == 0 {
			return
		}
		headers[len(view)] = h
		view = append(view, hosts...)
	}

	addSection(sectionHead{title: "★ favourites"}, favs)
	for _, t := range tagOrder {
		addSection(sectionHead{title: t, tag: t}, buckets[t])
	}
	addSection(sectionHead{title: "untagged"}, untagged)
	return view, headers
}

func rowMatches(r row, q string) bool {
	h := r.host
	if strings.Contains(strings.ToLower(h.Alias), q) ||
		strings.Contains(strings.ToLower(h.HostName), q) ||
		strings.Contains(strings.ToLower(h.User), q) {
		return true
	}
	for _, t := range h.Tags {
		if strings.Contains(strings.ToLower(t), q) {
			return true
		}
	}
	return false
}
