package tui

import (
	"fmt"
	"time"

	"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/huh"
)

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		return m.handleResize(msg)
	case tickMsg:
		return m.handleTick(time.Time(msg))
	case tabDataMsg:
		return m.handleTabData(msg)
	case detailDataMsg:
		// Drop replies for a site the user has already navigated away from,
		// so a slow load can't clobber the panel currently on screen.
		if m.state == stateDetail && m.cursor < len(m.sites) && m.sites[m.cursor].ID != msg.siteID {
			return m, nil
		}
		m.detailChanges = msg.changes
		m.detailChangesSiteID = msg.siteID
		m.detailDailyDays = msg.dailyDays
		return m, nil
	case historyDataMsg:
		if msg.siteID != m.historySiteID {
			return m, nil // stale reply for a previously opened history
		}
		m.historyChanges = msg.changes
		m.historyViewport.SetContent(m.buildHistoryContent())
		m.historyViewport.GotoTop()
		return m, nil
	case slaDataMsg:
		return m.handleSLAData(msg)
	case writeDoneMsg:
		if msg.err != nil {
			m.engine.AddLog(msg.op + " failed: " + msg.err.Error())
		}
		m.refreshLive()
		return m, m.loadTabDataCmd()
	}

	if m.state == stateConfirmDelete {
		return m.handleConfirmDelete(msg)
	}
	if m.state == stateFormSite || m.state == stateFormAlert || m.state == stateFormUser || m.state == stateFormMaint {
		return m.handleFormMsg(msg)
	}
	if m.state == stateLogs {
		return m.handleLogsFullscreen(msg)
	}

	switch msg := msg.(type) {
	case tea.MouseMsg:
		return m.handleMouse(msg)
	case tea.KeyMsg:
		return m.handleKey(msg)
	}
	return m, nil
}

func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "y", "Y":
		// The store delete runs in a Cmd; the in-memory engine/model updates
		// stay here so the row vanishes immediately. If the delete fails, the
		// writeDoneMsg reload converges the UI back to the DB state (and the
		// engine poll loop re-adds a site that is still in the DB).
		st := m.store
		ctx := m.ctx
		id := m.deleteID
		var cmd tea.Cmd
		switch m.deleteTab {
		case tabMonitors:
			cmd = writeCmd("Delete site", func() error { return st.DeleteSite(ctx, id) })
			m.engine.RemoveSite(id)
			m.adjustCursor(len(m.sites) - 1)
		case tabMaint:
			cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(ctx, id) })
			m.adjustCursor(len(m.maintenanceWindows) - 1)
		case tabSettings:
			switch m.settingsSection {
			case sectionAlerts:
				cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(ctx, id) })
				m.adjustCursor(len(m.alerts) - 1)
			case sectionUsers:
				cmd = writeCmd("Delete user", func() error { return st.DeleteUser(ctx, id) })
				m.adjustCursor(len(m.users) - 1)
			}
		}
		m.refreshLive()
		m.state = stateDashboard
		return m, cmd
	case "n", "N", "esc":
		m.state = stateDashboard
	case "ctrl+c":
		return m, tea.Quit
	}
	return m, nil
}

func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		if keyMsg.String() == "ctrl+c" {
			return m, tea.Quit
		}
		if keyMsg.String() == "esc" {
			m.huhForm = nil
			m.state = stateDashboard
			return m, nil
		}
	}
	if m.huhForm != nil {
		form, formCmd := m.huhForm.Update(msg)
		if f, ok := form.(*huh.Form); ok {
			m.huhForm = f
		}
		if m.state == stateFormSite && m.siteFormData != nil &&
			m.siteFormData.SiteType != m.lastSiteType {
			rebuildCmd := m.rebuildSiteForm()
			// Advance to Type select — user just changed it.
			skipName := m.huhForm.NextField()
			return m, tea.Batch(rebuildCmd, skipName)
		}
		if m.huhForm.State == huh.StateCompleted {
			// The store write runs in the returned Cmd; its writeDoneMsg
			// triggers the tab-data reload once the row actually exists.
			cmd := m.submitForm()
			m.refreshLive()
			m.huhForm = nil
			return m, cmd
		}
		return m, formCmd
	}
	return m, nil
}

const detailInlineHeight = 12

func (m *Model) recalcLayout() {
	chrome := chromeBase
	if m.filterMode || m.filterText != "" {
		chrome++
	}
	if m.detailOpen && m.currentTab == tabMonitors {
		chrome += detailInlineHeight
	}
	m.maxTableRows = m.termHeight - chrome
	if m.maxTableRows < 1 {
		m.maxTableRows = 1
	}
}

func (m *Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
	m.termWidth = msg.Width
	m.termHeight = msg.Height
	m.recalcLayout()
	m.logViewport.Width = msg.Width - chromePadH
	m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeFooter + 2)
	m.historyViewport.Width = msg.Width - chromePadH
	m.historyViewport.Height = msg.Height - 10
	m.slaViewport.Width = msg.Width - chromePadH
	m.slaViewport.Height = msg.Height - 16
	if m.huhForm != nil {
		formHeight := msg.Height - 7
		if formHeight < 5 {
			formHeight = 5
		}
		m.huhForm.WithHeight(formHeight)
	}
	return m, nil
}

func (m *Model) handleTick(t time.Time) (tea.Model, tea.Cmd) {
	m.refreshLive()
	m.tickCount++
	target := sinApprox(float64(m.tickCount)*0.3)*0.5 + 0.5
	m.pulsePos, m.pulseVel = m.pulseSpring.Update(m.pulsePos, m.pulseVel, target)

	cmds := []tea.Cmd{tickCmd()}
	if t.Sub(m.lastTabLoad) > tabRefreshTTL {
		m.lastTabLoad = t
		cmds = append(cmds, m.loadTabDataCmd())
		if dc := m.detailRefreshCmd(); dc != nil {
			cmds = append(cmds, dc)
		}
	}
	return m, tea.Batch(cmds...)
}

// detailRefreshCmd reloads the open detail panel's state-change list on the
// tab-data cadence, so a flap that happens while the panel is on screen shows
// up without leaving and re-entering. Nil when no detail panel is open.
func (m *Model) detailRefreshCmd() tea.Cmd {
	if m.state != stateDetail || m.cursor >= len(m.sites) {
		return nil
	}
	return m.loadDetailCmd(m.sites[m.cursor].ID)
}

// handleTabData folds an async tab-data load into the model. Replies older
// than the newest issued load are dropped so out-of-order completions can't
// overwrite fresher data. On error the previous data is kept and the failure
// logged, so a transient store error never blanks the view.
func (m *Model) handleTabData(msg tabDataMsg) (tea.Model, tea.Cmd) {
	if msg.seq != m.tabSeq {
		return m, nil
	}
	if msg.err != nil {
		m.engine.AddLog("Tab data refresh failed: " + msg.err.Error())
		return m, nil
	}
	m.alerts = msg.alerts
	if m.isAdmin {
		m.users = msg.users
	}
	m.nodes = msg.nodes
	m.maintenanceWindows = msg.maint
	m.clampCursor()
	return m, nil
}

// testAlertCmd sends a test notification off the UI goroutine; the outcome
// surfaces through the engine log (picked up by the next refreshLive).
func (m *Model) testAlertCmd(id int, name string) tea.Cmd {
	eng := m.engine
	ctx := m.ctx
	return func() tea.Msg {
		if err := eng.TestAlert(ctx, id); err != nil {
			eng.AddLog(fmt.Sprintf("Test alert failed (%s): %v", name, err))
		}
		return nil
	}
}

func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc", "q":
			m.state = stateDashboard
			m.focusedPanel = panelLogs
		case "ctrl+c":
			return m, tea.Quit
		case "f":
			m.logFilterImportant = !m.logFilterImportant
			m.refreshLogContent()
		case "up", "k":
			m.logViewport.ScrollUp(1)
		case "down", "j":
			m.logViewport.ScrollDown(1)
		case "pgup":
			m.logViewport.ScrollUp(m.logViewport.Height)
		case "pgdown":
			m.logViewport.ScrollDown(m.logViewport.Height)
		}
	case tea.MouseMsg:
		switch msg.Button {
		case tea.MouseButtonWheelUp:
			m.logViewport.ScrollUp(3)
		case tea.MouseButtonWheelDown:
			m.logViewport.ScrollDown(3)
		}
	case tickMsg:
		m.refreshLogContent()
	}
	return m, nil
}

func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
	if m.state == stateHistory {
		switch msg.Button {
		case tea.MouseButtonWheelUp:
			m.historyViewport.ScrollUp(3)
		case tea.MouseButtonWheelDown:
			m.historyViewport.ScrollDown(3)
		}
		return m, nil
	}
	if m.state == stateSLA {
		switch msg.Button {
		case tea.MouseButtonWheelUp:
			m.slaViewport.ScrollUp(3)
		case tea.MouseButtonWheelDown:
			m.slaViewport.ScrollDown(3)
		}
		return m, nil
	}
	if m.state == stateDetail {
		if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
			return m.handleSparklineClick(msg)
		}
		return m, nil
	}
	if m.state != stateDashboard {
		return m, nil
	}
	if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
		return m.handleClick(msg)
	}
	if msg.Button != tea.MouseButtonWheelUp && msg.Button != tea.MouseButtonWheelDown {
		return m, nil
	}

	if m.currentTab == tabMonitors && m.focusedPanel == panelLogs {
		if msg.Button == tea.MouseButtonWheelUp {
			m.scrollLogs(-3)
		} else {
			m.scrollLogs(3)
		}
		return m, nil
	}

	listLen := m.currentListLen()
	if msg.Button == tea.MouseButtonWheelUp {
		if m.cursor > 0 {
			m.cursor--
			if m.cursor < m.tableOffset {
				m.tableOffset = m.cursor
			}
		}
	} else {
		if m.cursor < listLen-1 {
			m.cursor++
			if m.cursor >= m.tableOffset+m.maxTableRows {
				m.tableOffset++
			}
		}
	}
	m.syncSelectedID()
	if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) {
		return m, m.loadDetailCmd(m.sites[m.cursor].ID)
	}
	return m, nil
}

func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	if msg.String() == "ctrl+c" {
		return m, tea.Quit
	}
	if msg.String() == "ctrl+l" {
		return m, tea.ClearScreen
	}

	if m.filterMode {
		return m.handleFilterKey(msg)
	}

	switch m.state {
	case stateDetail:
		return m.handleDetailKey(msg)
	case stateHistory:
		return m.handleHistoryKey(msg)
	case stateSLA:
		return m.handleSLAKey(msg)
	case stateAlertDetail:
		return m.handleAlertDetailKey(msg)
	case stateDashboard:
		return m.handleDashboardKey(msg)
	}
	return m, nil
}

func (m *Model) handleFilterKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "esc":
		m.filterMode = false
		m.filterText = ""
		m.cursor = 0
		m.tableOffset = 0
		m.recalcLayout()
		m.refreshLive()
	case "enter":
		m.filterMode = false
		m.recalcLayout()
	case "backspace":
		if len(m.filterText) > 0 {
			m.filterText = m.filterText[:len(m.filterText)-1]
			m.cursor = 0
			m.tableOffset = 0
			m.refreshLive()
		}
	case "ctrl+c":
		return m, tea.Quit
	default:
		if len(msg.Runes) == 1 {
			m.filterText += string(msg.Runes)
			m.cursor = 0
			m.tableOffset = 0
			m.refreshLive()
		}
	}
	return m, nil
}

func (m *Model) handleDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "up", "k":
		m.detailViewport.ScrollUp(1)
		return m, nil
	case "down", "j":
		m.detailViewport.ScrollDown(1)
		return m, nil
	case "pgup":
		m.detailViewport.ScrollUp(m.detailViewport.Height / 2)
		return m, nil
	case "pgdown":
		m.detailViewport.ScrollDown(m.detailViewport.Height / 2)
		return m, nil
	case "esc":
		if m.sparkTooltipIdx >= 0 {
			m.sparkTooltipIdx = -1
			return m, nil
		}
		m.sparkTooltipIdx = -1
		m.state = stateDashboard
	case "i":
		m.sparkTooltipIdx = -1
		m.state = stateDashboard
	case "e":
		return m.handleEditItem()
	case "h":
		if m.cursor < len(m.sites) {
			site := m.sites[m.cursor]
			m.historySiteName = site.Name
			m.historySiteID = site.ID
			m.historyChanges = nil
			m.historyViewport = viewport.New(
				m.termWidth-chromePadH,
				m.termHeight-10,
			)
			m.historyViewport.SetContent("\n  Loading state history...")
			m.state = stateHistory
			return m, m.loadHistoryCmd(site.ID)
		}
	case "s":
		if m.cursor < len(m.sites) {
			return m, m.openSLAView(m.sites[m.cursor])
		}
	case "q":
		m.state = stateDashboard
	}
	return m, nil
}

func (m *Model) handleSparklineClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
	if m.cursor >= len(m.sites) {
		return m, nil
	}
	site := m.sites[m.cursor]
	hist, _ := m.engine.GetHistory(site.ID)

	if zi := m.zones.Get("spark-latency"); zi != nil && !zi.IsZero() && zi.InBounds(msg) {
		x, _ := zi.Pos(msg)
		m.sparkTooltipIdx = resolveSparklineIndex(x, detailSparkWidth, len(hist.Latencies))
		return m, nil
	}
	if zi := m.zones.Get("spark-heartbeat"); zi != nil && !zi.IsZero() && zi.InBounds(msg) {
		x, _ := zi.Pos(msg)
		m.sparkTooltipIdx = resolveSparklineIndex(x, detailSparkWidth, len(hist.Statuses))
		return m, nil
	}

	m.sparkTooltipIdx = -1
	return m, nil
}

func (m *Model) handleSLAKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "esc":
		m.state = stateDetail
	case "1", "2", "3", "4":
		idx := int(msg.String()[0]-'0') - 1
		if idx >= 0 && idx < len(slaPeriods) {
			m.slaPeriodIdx = idx
			return m, m.loadSLACmd(m.slaSiteID, idx)
		}
	case "up", "k":
		m.slaViewport.ScrollUp(1)
	case "down", "j":
		m.slaViewport.ScrollDown(1)
	case "pgup":
		m.slaViewport.HalfPageUp()
	case "pgdown":
		m.slaViewport.HalfPageDown()
	case "ctrl+c":
		return m, tea.Quit
	}
	return m, nil
}

func (m *Model) openSLAView(site models.Site) tea.Cmd {
	m.slaSiteName = site.Name
	m.slaSiteID = site.ID
	m.slaPeriodIdx = 2 // default 30d
	m.slaViewport = viewport.New(
		m.termWidth-chromePadH,
		m.termHeight-16,
	)
	m.slaViewport.SetContent("\n  Loading SLA report...")
	m.state = stateSLA
	return m.loadSLACmd(site.ID, m.slaPeriodIdx)
}

// handleSLAData folds an async SLA load into the model. The SLA math itself is
// pure CPU and cheap, so it runs here; only the state-change read happens in
// the Cmd. Replies for a different site or period than currently selected are
// stale and dropped.
func (m *Model) handleSLAData(msg slaDataMsg) (tea.Model, tea.Cmd) {
	if msg.siteID != m.slaSiteID || msg.periodIdx != m.slaPeriodIdx {
		return m, nil
	}
	period := slaPeriods[msg.periodIdx]

	var currentStatus models.Status
	for _, s := range m.sites {
		if s.ID == msg.siteID {
			currentStatus = s.Status
			break
		}
	}

	m.slaReport = monitor.ComputeSLA(msg.changes, currentStatus, period.duration)
	m.slaDailyBreakdown = monitor.ComputeDailyBreakdown(msg.changes, currentStatus, period.days, time.Now())

	m.slaViewport = viewport.New(
		m.termWidth-chromePadH,
		m.termHeight-16,
	)
	m.slaViewport.SetContent(m.buildSLADailyContent())
	m.slaViewport.GotoTop()
	return m, nil
}

func (m *Model) handleHistoryKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "esc":
		m.state = stateDetail
	case "up", "k":
		m.historyViewport.ScrollUp(1)
	case "down", "j":
		m.historyViewport.ScrollDown(1)
	case "pgup":
		m.historyViewport.HalfPageUp()
	case "pgdown":
		m.historyViewport.HalfPageDown()
	case "home", "g":
		m.historyViewport.GotoTop()
	case "end", "G":
		m.historyViewport.GotoBottom()
	case "ctrl+c":
		return m, tea.Quit
	}
	return m, nil
}

func (m *Model) handleAlertDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "i", "esc":
		m.state = stateDashboard
	}
	return m, nil
}

func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q":
		return m, tea.Quit
	case "/":
		if m.currentTab == tabMonitors {
			m.filterMode = true
			m.recalcLayout()
			return m, nil
		}
	case ">", ".":
		if m.currentTab == tabMonitors {
			m.sortColumn = (m.sortColumn + 1) % sortMax
			m.sortAsc = false
			m.refreshLive()
		}
	case "<", ",":
		if m.currentTab == tabMonitors {
			m.sortColumn = (m.sortColumn - 1 + sortMax) % sortMax
			m.sortAsc = false
			m.refreshLive()
		}
	case "r":
		if m.currentTab == tabMonitors {
			m.sortAsc = !m.sortAsc
			m.refreshLive()
		}
	case "tab":
		m.switchTab(m.currentTab + 1)
	case "left":
		if m.currentTab == tabSettings {
			m.switchSettingsSection(m.settingsSection - 1)
		}
	case "right":
		if m.currentTab == tabSettings {
			m.switchSettingsSection(m.settingsSection + 1)
		}
	case "l":
		switch m.currentTab {
		case tabSettings:
			m.switchSettingsSection(m.settingsSection + 1)
		case tabMonitors:
			if m.focusedPanel == panelLogs {
				m.logsOpen = false
				m.focusedPanel = panelMonitors
			} else {
				m.logsOpen = true
				m.focusedPanel = panelLogs
			}
			m.recalcLayout()
		}
	case "up", "k":
		if m.currentTab == tabMonitors && m.focusedPanel == panelLogs {
			m.scrollLogs(-1)
			return m, nil
		}
		if m.cursor > 0 {
			m.cursor--
			if m.cursor < m.tableOffset {
				m.tableOffset = m.cursor
			}
			m.syncSelectedID()
			if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) {
				return m, m.loadDetailCmd(m.sites[m.cursor].ID)
			}
		}
	case "down", "j":
		if m.currentTab == tabMonitors && m.focusedPanel == panelLogs {
			m.scrollLogs(1)
			return m, nil
		}
		max := m.currentListLen() - 1
		if m.cursor < max {
			m.cursor++
			if m.cursor >= m.tableOffset+m.maxTableRows {
				m.tableOffset++
			}
			m.syncSelectedID()
			if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) {
				return m, m.loadDetailCmd(m.sites[m.cursor].ID)
			}
		}
	case "n":
		return m.handleNewItem()
	case "enter":
		if m.currentTab == tabMonitors && m.focusedPanel == panelLogs {
			m.refreshLogContent()
			m.logViewport.GotoTop()
			m.state = stateLogs
			return m, nil
		}
		if m.currentTab == tabMonitors && len(m.sites) > 0 {
			m.state = stateDetail
			return m, m.loadDetailCmd(m.sites[m.cursor].ID)
		}
		return m.handleEditItem()
	case "e":
		return m.handleEditItem()
	case "t":
		if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
			a := m.alerts[m.cursor]
			return m, m.testAlertCmd(a.ID, a.Name)
		}
	case " ":
		if m.currentTab == tabMonitors && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
			gid := m.sites[m.cursor].ID
			m.collapsed[gid] = !m.collapsed[gid]
			payload := collapsedJSON(m.collapsed)
			st := m.store
			ctx := m.ctx
			m.refreshLive()
			return m, writeCmd("Save collapsed groups", func() error {
				return st.SetPreference(ctx, "collapsed_groups", payload)
			})
		}
	case "p":
		if m.currentTab == tabMonitors && len(m.sites) > 0 {
			id := m.sites[m.cursor].ID
			paused := m.engine.ToggleSitePause(id)
			st := m.store
			ctx := m.ctx
			m.refreshLive()
			return m, writeCmd("Update pause state", func() error {
				return st.UpdateSitePaused(ctx, id, paused)
			})
		}
	case "i":
		if m.currentTab == tabMonitors && len(m.sites) > 0 {
			m.detailOpen = !m.detailOpen
			m.recalcLayout()
			st := m.store
			ctx := m.ctx
			open := m.detailOpen
			var cmd tea.Cmd
			if m.detailOpen {
				cmd = m.loadDetailCmd(m.sites[m.cursor].ID)
			}
			saveCmd := writeCmd("Save detail preference", func() error {
				v := "false"
				if open {
					v = "true"
				}
				return st.SetPreference(ctx, "detail_open", v)
			})
			if cmd != nil {
				return m, tea.Batch(cmd, saveCmd)
			}
			return m, saveCmd
		} else if m.currentTab == tabSettings && m.settingsSection == sectionAlerts && len(m.alerts) > 0 {
			m.state = stateAlertDetail
		}
	case "esc":
		if m.currentTab == tabMonitors {
			if m.focusedPanel != panelMonitors {
				m.focusedPanel = panelMonitors
			} else if m.detailOpen {
				m.detailOpen = false
				m.recalcLayout()
				st := m.store
				ctx := m.ctx
				return m, writeCmd("Save detail preference", func() error {
					return st.SetPreference(ctx, "detail_open", "false")
				})
			}
		}
	case "h":
		if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) {
			site := m.sites[m.cursor]
			m.historySiteName = site.Name
			m.historySiteID = site.ID
			m.historyChanges = nil
			m.historyViewport = viewport.New(
				m.termWidth-chromePadH,
				m.termHeight-10,
			)
			m.historyViewport.SetContent("\n  Loading state history...")
			m.state = stateHistory
			return m, m.loadHistoryCmd(site.ID)
		}
	case "s":
		if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) {
			return m, m.openSLAView(m.sites[m.cursor])
		}
	case "x":
		if m.currentTab == tabMaint && len(m.maintenanceWindows) > 0 {
			mw := m.maintenanceWindows[m.cursor]
			now := time.Now()
			isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
			if isActive {
				st := m.store
				ctx := m.ctx
				id := mw.ID
				m.refreshLive()
				return m, writeCmd("End maintenance", func() error {
					return st.EndMaintenanceWindow(ctx, id)
				})
			}
		}
	case "T":
		m.themeIndex = (m.themeIndex + 1) % len(themes)
		m.theme = themes[m.themeIndex]
		m.st = newStyles(m.theme)
		st := m.store
		ctx := m.ctx
		name := m.theme.Name
		return m, writeCmd("Save theme", func() error {
			return st.SetPreference(ctx, "theme", name)
		})
	case "d":
		return m.handleDeleteItem()
	}
	return m, nil
}

func (m *Model) handleNewItem() (tea.Model, tea.Cmd) {
	m.editID = 0
	m.editToken = ""
	switch m.currentTab {
	case tabMonitors:
		m.state = stateFormSite
		return m, m.initSiteHuhForm()
	case tabMaint:
		m.state = stateFormMaint
		return m, m.initMaintHuhForm()
	case tabSettings:
		switch m.settingsSection {
		case sectionAlerts:
			m.state = stateFormAlert
			return m, m.initAlertHuhForm()
		case sectionUsers:
			if m.isAdmin {
				m.state = stateFormUser
				return m, m.initUserHuhForm()
			}
		}
	}
	return m, nil
}

func (m *Model) handleEditItem() (tea.Model, tea.Cmd) {
	switch m.currentTab {
	case tabMonitors:
		if len(m.sites) > 0 {
			m.editID = m.sites[m.cursor].ID
			m.editToken = m.sites[m.cursor].Token
			m.state = stateFormSite
			return m, m.initSiteHuhForm()
		}
	case tabSettings:
		switch m.settingsSection {
		case sectionAlerts:
			if len(m.alerts) > 0 {
				m.editID = m.alerts[m.cursor].ID
				m.state = stateFormAlert
				return m, m.initAlertHuhForm()
			}
		case sectionUsers:
			if m.isAdmin && len(m.users) > 0 {
				m.editID = m.users[m.cursor].ID
				m.state = stateFormUser
				return m, m.initUserHuhForm()
			}
		}
	}
	return m, nil
}

func (m *Model) handleDeleteItem() (tea.Model, tea.Cmd) {
	switch m.currentTab {
	case tabMonitors:
		if len(m.sites) > 0 {
			m.deleteID = m.sites[m.cursor].ID
			m.deleteName = m.sites[m.cursor].Name
			m.deleteTab = tabMonitors
			m.state = stateConfirmDelete
		}
	case tabMaint:
		if len(m.maintenanceWindows) > 0 {
			m.deleteID = m.maintenanceWindows[m.cursor].ID
			m.deleteName = m.maintenanceWindows[m.cursor].Title
			m.deleteTab = tabMaint
			m.state = stateConfirmDelete
		}
	case tabSettings:
		switch m.settingsSection {
		case sectionAlerts:
			if len(m.alerts) > 0 {
				m.deleteID = m.alerts[m.cursor].ID
				m.deleteName = m.alerts[m.cursor].Name
				m.deleteTab = tabSettings
				m.state = stateConfirmDelete
			}
		case sectionUsers:
			if m.isAdmin && len(m.users) > 0 {
				m.deleteID = m.users[m.cursor].ID
				m.deleteName = m.users[m.cursor].Username
				m.deleteTab = tabSettings
				m.state = stateConfirmDelete
			}
		}
	}
	return m, nil
}

func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
	tabCount := tabSettings + 1
	for i := 0; i < tabCount; i++ {
		if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
			m.switchTab(i)
			return m, nil
		}
	}

	if m.currentTab == tabSettings {
		maxSec := 1
		if m.isAdmin {
			maxSec = 2
		}
		for i := 0; i <= maxSec; i++ {
			if m.zones.Get(fmt.Sprintf("section-%d", i)).InBounds(msg) {
				m.switchSettingsSection(i)
				return m, nil
			}
		}
	}

	if m.currentTab == tabMonitors {
		sortZones := []struct {
			zone string
			col  int
		}{
			{"sort-status", sortStatus},
			{"sort-name", sortName},
			{"sort-latency", sortLatency},
		}
		for _, sz := range sortZones {
			if m.zones.Get(sz.zone).InBounds(msg) {
				if m.sortColumn == sz.col {
					m.sortAsc = !m.sortAsc
				} else {
					m.sortColumn = sz.col
					m.sortAsc = false
				}
				m.refreshLive()
				return m, nil
			}
		}

		if m.zones.Get("panel-monitors").InBounds(msg) {
			m.focusedPanel = panelMonitors
		} else if m.zones.Get("panel-logs").InBounds(msg) {
			m.focusedPanel = panelLogs
			return m, nil
		} else if m.detailOpen && m.zones.Get("panel-detail").InBounds(msg) {
			m.focusedPanel = panelDetail
			return m, nil
		}
	}

	prefix, listLen := m.currentZonePrefix()
	end := m.tableOffset + m.maxTableRows
	if end > listLen {
		end = listLen
	}
	for i := m.tableOffset; i < end; i++ {
		if m.zones.Get(fmt.Sprintf("%s-%d", prefix, i)).InBounds(msg) {
			m.cursor = i
			m.syncSelectedID()
			if m.detailOpen && m.currentTab == tabMonitors {
				return m, m.loadDetailCmd(m.sites[m.cursor].ID)
			}
			return m, nil
		}
	}

	return m, nil
}

func (m *Model) switchTab(idx int) {
	maxTabs := tabSettings
	if idx > maxTabs {
		idx = 0
	}
	m.currentTab = idx
	m.cursor = 0
	m.tableOffset = 0
	m.state = stateDashboard
}

func (m *Model) adjustCursor(_ int) {
	m.clampCursor()
}

func (m *Model) submitForm() tea.Cmd {
	switch m.state {
	case stateFormSite:
		if m.siteFormData != nil {
			return m.submitSiteForm()
		}
	case stateFormAlert:
		if m.alertFormData != nil {
			return m.submitAlertForm()
		}
	case stateFormUser:
		if m.userFormData != nil {
			return m.submitUserForm()
		}
	case stateFormMaint:
		if m.maintFormData != nil {
			return m.submitMaintForm()
		}
	}
	return nil
}

func (m Model) currentListLen() int {
	switch m.currentTab {
	case tabMonitors:
		return len(m.sites)
	case tabMaint:
		return len(m.maintenanceWindows)
	case tabSettings:
		switch m.settingsSection {
		case sectionAlerts:
			return len(m.alerts)
		case sectionNodes:
			return len(m.nodes)
		case sectionUsers:
			return len(m.users)
		}
	}
	return 0
}

func (m Model) currentZonePrefix() (string, int) {
	switch m.currentTab {
	case tabMonitors:
		return "site", len(m.sites)
	case tabMaint:
		return "maint", len(m.maintenanceWindows)
	case tabSettings:
		switch m.settingsSection {
		case sectionAlerts:
			return "alert", len(m.alerts)
		case sectionUsers:
			return "user", len(m.users)
		}
	}
	return "site", 0
}
