package main

import (
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/zzet/gortex/internal/daemon"
)

var osUserHomeDir = os.UserHomeDir

// ---- styles ------------------------------------------------------------

var (
	tuiAccent      = lipgloss.NewStyle().Foreground(lipgloss.Color("#5FD67A"))
	tuiTitle       = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F4F3EF"))
	tuiSub         = lipgloss.NewStyle().Foreground(lipgloss.Color("#969699"))
	tuiHeading     = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#969699"))
	tuiCount       = lipgloss.NewStyle().Foreground(lipgloss.Color("#5A5A60"))
	tuiKey         = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F4F3EF"))
	tuiVal         = lipgloss.NewStyle().Foreground(lipgloss.Color("#969699"))
	tuiSelected    = lipgloss.NewStyle().Foreground(lipgloss.Color("#5FD67A")).Bold(true)
	tuiHint        = lipgloss.NewStyle().Foreground(lipgloss.Color("#5A5A60"))
	tuiErrInline   = lipgloss.NewStyle().Foreground(lipgloss.Color("#F06E64"))
	tuiKeybindKey  = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F4F3EF"))
	tuiKeybindHint = lipgloss.NewStyle().Foreground(lipgloss.Color("#5A5A60"))
)

// ---- repo item / delegate ---------------------------------------------

type repoItem struct {
	name      string
	workspace string
	memory    string
	memBytes  uint64
	files     int
	nodes     int
	edges     int
	path      string
}

func (r repoItem) FilterValue() string {
	// Concatenate every searchable field so users can match by name,
	// workspace, or path fragment via the bubbles list's `/` filter.
	return r.name + " " + r.workspace + " " + r.path
}

type repoDelegate struct{ width int }

func (d repoDelegate) Height() int                         { return 1 }
func (d repoDelegate) Spacing() int                        { return 0 }
func (d repoDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d repoDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
	r, ok := listItem.(repoItem)
	if !ok {
		return
	}

	cursor := "  "
	style := tuiVal
	if index == m.Index() {
		cursor = tuiAccent.Render("▸ ")
		style = tuiSelected
	}

	// Adaptive column widths: drop the path column entirely under ~120 cols.
	nameW := 26
	wsW := 18
	memW := 10
	statsW := 32
	pathOK := d.width >= 120
	pathW := 0
	if pathOK {
		pathW = d.width - (2 + nameW + 2 + wsW + 2 + memW + 3 + statsW + 3)
		if pathW < 20 {
			pathW = 20
		}
	}

	stats := fmt.Sprintf("%4df · %9sn · %9se", r.files, humanizeInt(r.nodes), humanizeInt(r.edges))
	row := fmt.Sprintf("%-*s  %-*s  %*s   %s",
		nameW, truncate(r.name, nameW),
		wsW, truncate(r.workspace, wsW),
		memW, r.memory,
		stats,
	)
	if pathOK {
		row += "   " + tuiHint.Render(truncate(shortenPath(r.path), pathW))
	}

	_, _ = fmt.Fprint(w, cursor+style.Render(row))
}

// ---- model -------------------------------------------------------------

type tuiTickMsg time.Time
type tuiStatusMsg daemon.StatusResponse
type tuiFetchErrMsg struct{ err error }

type statusTUI struct {
	interval time.Duration
	width    int
	height   int

	status     daemon.StatusResponse
	hasStatus  bool
	err        error
	lastUpdate time.Time
	meshTick   int

	repos list.Model
}

func newStatusTUI(interval time.Duration) statusTUI {
	delegate := repoDelegate{}
	l := list.New([]list.Item{}, delegate, 0, 0)
	l.SetShowTitle(false)
	l.SetShowStatusBar(true)
	l.SetShowHelp(false)
	l.SetShowPagination(true)
	l.SetFilteringEnabled(true)
	l.DisableQuitKeybindings()
	l.Styles.NoItems = tuiHint
	l.Styles.StatusBar = tuiHint
	l.Styles.StatusEmpty = tuiHint
	l.Styles.PaginationStyle = tuiHint
	l.Styles.HelpStyle = tuiHint
	l.SetStatusBarItemName("repo", "repos")
	return statusTUI{interval: interval, repos: l}
}

func (m statusTUI) Init() tea.Cmd {
	return tea.Batch(
		tuiFetchCmd(),
		tea.Tick(m.interval, func(t time.Time) tea.Msg { return tuiTickMsg(t) }),
		tea.Tick(150*time.Millisecond, func(t time.Time) tea.Msg { return tuiMeshTickMsg(t) }),
	)
}

type tuiMeshTickMsg time.Time

func tuiFetchCmd() tea.Cmd {
	return func() tea.Msg {
		st, err := fetchDaemonStatusForCLI()
		if err != nil {
			return tuiFetchErrMsg{err: err}
		}
		return tuiStatusMsg(st)
	}
}

func (m statusTUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {

	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		m.repos.SetSize(m.reposListWidth(), m.reposListHeight())
		// Update the delegate's width so each row knows whether to draw the path column.
		m.repos.SetDelegate(repoDelegate{width: m.reposListWidth()})

	case tuiTickMsg:
		return m, tea.Batch(
			tuiFetchCmd(),
			tea.Tick(m.interval, func(t time.Time) tea.Msg { return tuiTickMsg(t) }),
		)

	case tuiMeshTickMsg:
		m.meshTick++
		return m, tea.Tick(150*time.Millisecond, func(t time.Time) tea.Msg { return tuiMeshTickMsg(t) })

	case tuiStatusMsg:
		m.status = daemon.StatusResponse(msg)
		m.hasStatus = true
		m.err = nil
		m.lastUpdate = time.Now()
		items := make([]list.Item, 0, len(m.status.TrackedRepos))
		for _, r := range m.status.TrackedRepos {
			items = append(items, repoItem{
				name:      firstNonEmpty(r.Prefix, r.Name),
				workspace: workspaceLabel(r),
				memory:    humanizeBytes(r.Memory.TotalBytes),
				memBytes:  r.Memory.TotalBytes,
				files:     r.Files,
				nodes:     r.Nodes,
				edges:     r.Edges,
				path:      r.Path,
			})
		}
		// Sort by total memory desc — biggest repos first, like top.
		sort.Slice(items, func(i, j int) bool {
			a, b := items[i].(repoItem), items[j].(repoItem)
			if a.memBytes != b.memBytes {
				return a.memBytes > b.memBytes
			}
			return a.name < b.name
		})
		m.repos.SetItems(items)
		m.repos.SetSize(m.reposListWidth(), m.reposListHeight())

	case tuiFetchErrMsg:
		m.err = msg.err
		m.lastUpdate = time.Now()

	case tea.KeyMsg:
		// Don't intercept keys while the list is in filter mode — user is
		// typing the query.
		if m.repos.FilterState() == list.Filtering {
			break
		}
		switch msg.String() {
		case "q", "ctrl+c", "esc":
			return m, tea.Quit
		}
	}

	var cmd tea.Cmd
	m.repos, cmd = m.repos.Update(msg)
	return m, cmd
}

func (m statusTUI) View() string {
	if m.width == 0 {
		return ""
	}
	// Inset every section by tuiPadding columns and weave explicit blank
	// lines between sections so the mesh logo and section blocks have
	// clear breathing room. Empty sections (e.g. no sessions yet) are
	// elided along with their trailing blank.
	var lines []string
	push := func(s string) {
		if s != "" {
			lines = append(lines, indentBlock(s, tuiPadding))
		}
	}
	gap := func(n int) {
		for i := 0; i < n; i++ {
			lines = append(lines, "")
		}
	}

	gap(1)               // top breathing room
	push(m.renderHeader())
	gap(2)               // logo gets extra space below
	if w := m.renderWorkspaces(); w != "" {
		push(w)
		gap(1)
	}
	push(m.renderRepos())
	gap(1)
	if s := m.renderSessions(); s != "" {
		push(s)
		gap(1)
	}
	if s := m.renderServers(); s != "" {
		push(s)
		gap(1)
	}
	if s := m.renderLSPRouter(); s != "" {
		push(s)
		gap(1)
	}
	push(m.renderFooter())
	return strings.Join(lines, "\n")
}

// tuiPadding is the left inset (in columns) applied to every section.
const tuiPadding = 2

// indentBlock prefixes every non-empty line of s with n spaces.
func indentBlock(s string, n int) string {
	if s == "" {
		return s
	}
	pad := strings.Repeat(" ", n)
	lines := strings.Split(s, "\n")
	for i, line := range lines {
		lines[i] = pad + line
	}
	return strings.Join(lines, "\n")
}

// ---- layout sections ---------------------------------------------------

func (m statusTUI) renderHeader() string {
	mesh := miniMesh(m.meshTick)
	right := m.headerRightCol()
	// Give the logo room to breathe — 4 columns of gap, vertically centered
	// on the right column so the title line sits on the second mesh row.
	right = "\n" + right // shift down one row so "gortex daemon" lines up with the mesh middle
	return lipgloss.JoinHorizontal(lipgloss.Top, mesh, "    ", right)
}

func (m statusTUI) headerRightCol() string {
	if m.err != nil && !m.hasStatus {
		return lipgloss.JoinVertical(
			lipgloss.Left,
			tuiTitle.Render("gortex daemon"),
			"",
			tuiErrInline.Render("✗ "+m.err.Error()),
			tuiHint.Render("retrying every "+m.interval.String()),
		)
	}
	if !m.hasStatus {
		return lipgloss.JoinVertical(
			lipgloss.Left,
			tuiTitle.Render("gortex daemon"),
			"",
			tuiHint.Render("connecting…"),
		)
	}
	st := m.status
	state := tuiAccent.Render("ready")
	if !st.Ready {
		state = lipgloss.NewStyle().Foreground(lipgloss.Color("#E8C66B")).Render("warming")
	}
	row1 := tuiTitle.Render("gortex daemon")
	row2 := strings.Join([]string{
		tuiSub.Render(versionString(st.Version)),
		tuiSub.Render("pid " + fmt.Sprintf("%d", st.PID)),
		state,
		tuiSub.Render("up " + humanizeDur(time.Duration(st.UptimeSeconds)*time.Second)),
	}, tuiHint.Render(" · "))
	row3 := strings.Join([]string{
		tuiKey.Render(humanizeBytes(st.Runtime.Alloc)) + tuiSub.Render(" alloc"),
		tuiKey.Render(humanizeInt(st.SearchBackend.DocCount)) + tuiSub.Render(" docs"),
		tuiKey.Render(fmt.Sprintf("%d", st.Sessions)) + tuiSub.Render(" sessions"),
		tuiKey.Render(fmt.Sprintf("%d", st.Runtime.NumGoroutine)) + tuiSub.Render(" goroutines"),
	}, tuiHint.Render(" · "))
	return lipgloss.JoinVertical(lipgloss.Left, row1, "", row2, row3)
}

func (m statusTUI) renderWorkspaces() string {
	if !m.hasStatus || len(m.status.Workspaces) == 0 {
		return ""
	}
	var lines []string
	lines = append(lines, tuiHeading.Render("workspaces")+"   "+tuiCount.Render(fmt.Sprintf("· %d", len(m.status.Workspaces))))
	for _, ws := range m.status.Workspaces {
		projects := ""
		if len(ws.Projects) > 0 {
			projects = "  " + tuiHint.Render(strings.Join(ws.Projects, " · "))
		}
		stats := fmt.Sprintf("%2d %s · %s files · %s nodes · %s edges",
			len(ws.Repos), pluralize(len(ws.Repos), "repo", "repos"),
			humanizeInt(ws.Files), humanizeInt(ws.Nodes), humanizeInt(ws.Edges))
		line := "  " + tuiKey.Render(padRight(ws.Slug, 12)) + "  " + tuiVal.Render(stats) + projects
		lines = append(lines, line)
	}
	return strings.Join(lines, "\n")
}

func (m statusTUI) renderRepos() string {
	if !m.hasStatus {
		return ""
	}
	header := tuiHeading.Render("repos") + "   " + tuiCount.Render(fmt.Sprintf("· %d", len(m.status.TrackedRepos)))
	hint := tuiKeybindKey.Render("/") + tuiKeybindHint.Render(" filter   ") +
		tuiKeybindKey.Render("↑↓") + tuiKeybindHint.Render(" navigate")
	row := lipgloss.JoinHorizontal(
		lipgloss.Top,
		header,
		strings.Repeat(" ", maxInt(2, m.width-lipgloss.Width(header)-lipgloss.Width(hint))),
		hint,
	)
	return row + "\n" + m.repos.View()
}

func (m statusTUI) renderSessions() string {
	if !m.hasStatus || len(m.status.MCPSessions) == 0 {
		return ""
	}
	var lines []string
	lines = append(lines, tuiHeading.Render("sessions")+"   "+tuiCount.Render(fmt.Sprintf("· %d", len(m.status.MCPSessions))))
	for _, s := range m.status.MCPSessions {
		client := s.ClientName
		if client == "" {
			client = "(unknown)"
		}
		if s.ClientVersion != "" {
			client += " " + tuiHint.Render(s.ClientVersion)
		}
		dur := humanizeDur(time.Duration(s.ConnectedSecs) * time.Second)
		shortID := truncate(s.ID, 16)
		cwd := shortenPath(s.Cwd)
		line := "  " + tuiKey.Render(padRight(shortID, 16)) + "  " +
			tuiVal.Render(padRight(client, 22)) + "  " +
			tuiSub.Render(padRight(dur, 7)) + "  " +
			tuiHint.Render(cwd)
		lines = append(lines, line)
	}
	return strings.Join(lines, "\n")
}

func (m statusTUI) renderServers() string {
	if !m.hasStatus || len(m.status.ConfiguredServers) == 0 {
		return ""
	}
	var lines []string
	lines = append(lines, tuiHeading.Render("servers")+"   "+tuiCount.Render(fmt.Sprintf("· %d", len(m.status.ConfiguredServers))))
	for _, s := range m.status.ConfiguredServers {
		var flags []string
		if s.Local {
			flags = append(flags, "local")
		}
		if s.Default {
			flags = append(flags, "default")
		}
		if s.HasAuth {
			flags = append(flags, "auth")
		}
		if len(s.Workspaces) > 0 {
			flags = append(flags, "ws="+strings.Join(s.Workspaces, ","))
		}
		flagLine := ""
		if len(flags) > 0 {
			flagLine = "   " + tuiHint.Render(strings.Join(flags, " · "))
		}
		line := "  " + tuiKey.Render(padRight(s.Slug, 12)) + "  " + tuiVal.Render(s.URL) + flagLine
		lines = append(lines, line)
	}
	return strings.Join(lines, "\n")
}

func (m statusTUI) renderLSPRouter() string {
	if !m.hasStatus || m.status.LSPRouter == nil {
		return ""
	}
	r := m.status.LSPRouter
	specs := r.EnabledSpecs
	active := r.ActiveProviders
	if len(specs) == 0 && len(active) == 0 {
		return ""
	}
	var lines []string
	header := tuiHeading.Render("lsp router")
	header += "   " + tuiCount.Render(fmt.Sprintf("· %d enabled · %d alive", len(specs), len(active)))
	lines = append(lines, header)
	for _, s := range specs {
		state := "·"
		stateRender := tuiSub
		if s.Available {
			state = "✓"
			stateRender = tuiAccent
		}
		line := "  " + stateRender.Render(state) + " " +
			tuiKey.Render(padRight(s.Name, 28)) + "  " +
			tuiVal.Render(padRight(s.Languages, 28))
		lines = append(lines, line)
	}
	for _, a := range active {
		lines = append(lines, "  "+tuiHint.Render("alive: "+a.Spec+"@"+a.Workspace+"  last_used="+a.LastUsed))
	}
	return strings.Join(lines, "\n")
}

func (m statusTUI) renderFooter() string {
	left := tuiHint.Render(time.Now().Format("15:04:05") + " · refreshing every " + m.interval.String())
	right := strings.Join([]string{
		tuiKeybindKey.Render("/") + tuiKeybindHint.Render(" filter"),
		tuiKeybindKey.Render("↑↓") + tuiKeybindHint.Render(" scroll"),
		tuiKeybindKey.Render("q") + tuiKeybindHint.Render(" quit"),
	}, tuiKeybindHint.Render("  "))
	pad := m.width - lipgloss.Width(left) - lipgloss.Width(right)
	if pad < 1 {
		pad = 1
	}
	return left + strings.Repeat(" ", pad) + right
}

// ---- sizing -----------------------------------------------------------

func (m statusTUI) reposListWidth() int {
	w := m.width - 2
	if w < 40 {
		w = 40
	}
	return w
}

func (m statusTUI) reposListHeight() int {
	if m.height == 0 {
		return 10
	}
	// Mirror the layout in View(): top gap (1) + header (5) + gap-below-logo
	// (2) + each non-empty section's heading + rows + 1 gap line + repos
	// heading (1) + list status/pagination (2) + footer (1) + small slack.
	reserved := 1 + 5 + 2
	if m.hasStatus {
		if len(m.status.Workspaces) > 0 {
			reserved += 1 + len(m.status.Workspaces) + 1
		}
		if len(m.status.MCPSessions) > 0 {
			reserved += 1 + len(m.status.MCPSessions) + 1
		}
		if len(m.status.ConfiguredServers) > 0 {
			reserved += 1 + len(m.status.ConfiguredServers) + 1
		}
	}
	reserved += 1 // repos heading row
	reserved += 2 // bubbles list status bar + pagination
	reserved += 1 // gap below repos
	reserved += 1 // footer
	h := m.height - reserved
	if h < 6 {
		h = 6
	}
	return h
}

// ---- helpers ----------------------------------------------------------

// miniMesh returns a 5-line mesh logo with the active node walking once per
// tick, sized for the header (no label/sub — just the mark).
func miniMesh(tick int) string {
	const reset = "\x1b[0m"
	cAccent := "\x1b[38;2;95;214;122m"
	cPerim := "\x1b[38;2;240;240;240m"
	cInner := "\x1b[38;2;70;70;78m"

	perimeter := [12][2]int{
		{0, 1}, {0, 2}, {0, 3},
		{1, 4}, {2, 4}, {3, 4},
		{4, 3}, {4, 2}, {4, 1},
		{3, 0}, {2, 0}, {1, 0},
	}
	innerAccents := map[[2]int]bool{
		{2, 2}: true,
		{2, 3}: true,
	}
	cellAt := func(r, c int) int {
		corner := (r == 0 || r == 4) && (c == 0 || c == 4)
		if corner {
			return 0
		}
		if r == 0 || r == 4 || c == 0 || c == 4 {
			return 1
		}
		if innerAccents[[2]int{r, c}] {
			return 1
		}
		return 2
	}

	active := perimeter[tick%len(perimeter)]
	var lines [5]string
	for r := 0; r < 5; r++ {
		var b strings.Builder
		for c := 0; c < 5; c++ {
			switch cellAt(r, c) {
			case 0:
				b.WriteString(" ")
			case 1:
				if r == active[0] && c == active[1] {
					b.WriteString(cAccent + "●" + reset)
				} else {
					b.WriteString(cPerim + "●" + reset)
				}
			case 2:
				b.WriteString(cInner + "·" + reset)
			}
			if c < 4 {
				b.WriteString(" ")
			}
		}
		lines[r] = b.String()
	}
	return strings.Join(lines[:], "\n")
}

func humanizeBytes(b uint64) string {
	const unit = 1024
	if b < unit {
		return fmt.Sprintf("%d B", b)
	}
	div, exp := uint64(unit), 0
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}
	v := float64(b) / float64(div)
	suffix := []string{"KiB", "MiB", "GiB", "TiB"}[exp]
	return fmt.Sprintf("%.1f %s", v, suffix)
}

func humanizeDur(d time.Duration) string {
	if d < time.Second {
		return "0s"
	}
	if d < time.Minute {
		return fmt.Sprintf("%ds", int(d.Seconds()))
	}
	if d < time.Hour {
		return fmt.Sprintf("%dm", int(d.Minutes()))
	}
	if d < 24*time.Hour {
		h := int(d.Hours())
		m := int(d.Minutes()) - h*60
		if m == 0 {
			return fmt.Sprintf("%dh", h)
		}
		return fmt.Sprintf("%dh%dm", h, m)
	}
	days := int(d.Hours()) / 24
	h := int(d.Hours()) - days*24
	if h == 0 {
		return fmt.Sprintf("%dd", days)
	}
	return fmt.Sprintf("%dd%dh", days, h)
}

func truncate(s string, n int) string {
	if lipgloss.Width(s) <= n {
		return s
	}
	if n <= 1 {
		return "…"
	}
	// Naive byte truncation works fine for the ASCII-heavy fields here;
	// when content is multibyte (paths, names with Unicode), the trailing
	// ellipsis covers any visual jitter.
	return s[:n-1] + "…"
}

func padRight(s string, n int) string {
	w := lipgloss.Width(s)
	if w >= n {
		return s
	}
	return s + strings.Repeat(" ", n-w)
}

func shortenPath(p string) string {
	home := homeDir()
	if home != "" && strings.HasPrefix(p, home) {
		return "~" + p[len(home):]
	}
	return p
}

func homeDir() string {
	if h, err := userHomeDir(); err == nil {
		return h
	}
	return ""
}

// userHomeDir is a thin alias so tests can stub it without pulling in
// os in this file's import set.
var userHomeDir = osUserHomeDir

func pluralize(n int, singular, plural string) string {
	if n == 1 {
		return singular
	}
	return plural
}

// versionString prefixes the version with "v" only when the string isn't
// already prefixed; the daemon may report "0.19.1" or "v0.19.1" depending on
// how the binary was built.
func versionString(v string) string {
	if v == "" {
		return ""
	}
	if strings.HasPrefix(v, "v") || strings.HasPrefix(v, "V") {
		return v
	}
	return "v" + v
}

func firstNonEmpty(s ...string) string {
	for _, v := range s {
		if v != "" {
			return v
		}
	}
	return ""
}

func workspaceLabel(r daemon.TrackedRepoStatus) string {
	if r.Workspace == "" {
		return ""
	}
	if r.WorkspaceProject != "" && r.WorkspaceProject != r.Workspace {
		return r.Workspace + "/" + r.WorkspaceProject
	}
	return r.Workspace
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}
