// Provider selector: unified model & provider selection overlay.
package input

import (
	"context"
	"fmt"
	"os"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"go.uber.org/zap"

	"github.com/genai-io/san/internal/app/kit"
	"github.com/genai-io/san/internal/core"
	"github.com/genai-io/san/internal/llm"
	"github.com/genai-io/san/internal/log"
	"github.com/genai-io/san/internal/secret"
)

// ── State ──────────────────────────────────────────────────────────────────────

// ProviderState holds provider UI state for the TUI model.
// Domain state (LLM, Store, CurrentModel, tokens, thinking) lives
// on the parent app model, not here.
type ProviderState struct {
	FetchingLimits bool
	Selector       ProviderSelector
	StatusMessage  string // Temporary status shown in status bar
	statusToken    int64
}

// SetStatusMessage sets the temporary status message displayed in the status bar.
func (s *ProviderState) SetStatusMessage(msg string) int64 {
	s.statusToken++
	s.StatusMessage = msg
	return s.statusToken
}

// ProviderStatusExpiredMsg is an alias for kit.StatusExpiredMsg.
type ProviderStatusExpiredMsg = kit.StatusExpiredMsg

// ── Runtime ────────────────────────────────────────────────────────────────────

// UpdateProvider routes provider connection and selection messages.
func UpdateProvider(deps OverlayDeps, state *ProviderState, msg tea.Msg) (tea.Cmd, bool) {
	switch msg := msg.(type) {
	case ProviderConnectingMsg:
		// Keep ticking the spinner while connect/refresh is in flight; stop once
		// the matching ProviderConnectResultMsg lands and IsConnecting goes false.
		if state.Selector.IsConnecting() {
			state.Selector.AdvanceSpinner()
			return providerConnectingTickCmd(), true
		}
		return nil, true
	case ProviderConnectResultMsg:
		return state.Selector.HandleConnectResult(msg), true
	case ProviderModelSelectedMsg:
		return handleProviderModelSelected(deps, state, msg), true
	case ProviderModelsLoadedMsg:
		state.Selector.HandleModelsLoaded(msg)
		return nil, true
	case ProviderStatusExpiredMsg:
		if msg.Token == state.statusToken {
			state.StatusMessage = ""
		}
		return nil, true
	}
	return nil, false
}

func handleProviderModelSelected(deps OverlayDeps, state *ProviderState, msg ProviderModelSelectedMsg) tea.Cmd {
	_, err := state.Selector.SetModel(msg.ModelID, msg.ProviderName, msg.AuthMethod)
	if err != nil {
		deps.Conv.Append(core.ChatMessage{Role: core.RoleNotice, Content: "Error: " + err.Error()})
		return tea.Batch(deps.CommitMessages()...)
	}

	deps.SetCurrentModel(&llm.CurrentModelInfo{
		ModelID:    msg.ModelID,
		Provider:   llm.Name(msg.ProviderName),
		AuthMethod: msg.AuthMethod,
	})
	ctx := context.Background()
	providerRefreshConnection(deps, state, ctx, llm.Name(msg.ProviderName), msg.AuthMethod)
	return deps.PrintWelcome(msg.ModelID)
}

func providerRefreshConnection(deps OverlayDeps, state *ProviderState, ctx context.Context, providerName llm.Name, authMethod llm.AuthMethod) {
	p, err := llm.GetProvider(ctx, providerName, authMethod)
	if err != nil {
		log.Logger().Warn("failed to refresh provider connection",
			zap.String("provider", string(providerName)),
			zap.Error(err))
		return
	}
	deps.SwitchProvider(p)
}

// ── Model types ────────────────────────────────────────────────────────────────

// providerTab represents which tab is active in the kit.
type providerTab int

const (
	providerTabModels    providerTab = iota // model selection tab
	providerTabProviders                    // provider management tab
)

// providerItemKind represents a row type in the visible-items list.
type providerItemKind int

const (
	providerItemProviderHeader providerItemKind = iota // non-selectable provider group header (Models tab)
	providerItemModel                                  // selectable model row (Models tab)
	providerItemProvider                               // provider row (Providers tab)
	providerItemAuthMethod                             // expanded auth-method sub-row (Providers tab)
)

// providerListItem is a single row in the flattened visible-items list.
type providerListItem struct {
	Kind        providerItemKind
	Model       *providerModelItem
	Provider    *providerProviderItem
	AuthMethod  *providerAuthMethodItem
	ProviderIdx int // index into allProviders
}

// providerProviderItem represents a provider with its auth methods.
type providerProviderItem struct {
	Provider    llm.Name
	DisplayName string
	AuthMethods []providerAuthMethodItem
	Connected   bool // whether this provider has at least one connected auth method
}

// providerAuthMethodItem represents an auth method in the second level.
type providerAuthMethodItem struct {
	Provider    llm.Name
	AuthMethod  llm.AuthMethod
	DisplayName string
	Status      llm.Status
	EnvVars     []string
}

// providerModelItem represents a model in the kit.
type providerModelItem struct {
	ID               string
	Name             string
	DisplayName      string
	ProviderName     string
	AuthMethod       llm.AuthMethod
	IsCurrent        bool
	InputTokenLimit  int
	OutputTokenLimit int
}

func newProviderModelItem(mdl llm.ModelInfo, providerName string, authMethod llm.AuthMethod, current *llm.CurrentModelInfo) providerModelItem {
	return providerModelItem{
		ID:               mdl.ID,
		Name:             mdl.Name,
		DisplayName:      mdl.DisplayName,
		ProviderName:     providerName,
		AuthMethod:       authMethod,
		IsCurrent:        current != nil && current.ModelID == mdl.ID && string(current.Provider) == providerName,
		InputTokenLimit:  mdl.InputTokenLimit,
		OutputTokenLimit: mdl.OutputTokenLimit,
	}
}

// ProviderSelector holds the state for the unified model & provider kit.
type ProviderSelector struct {
	active bool
	width  int
	height int
	store  *llm.Store

	// Tab
	activeTab providerTab

	// Data
	connectedProviders []providerProviderItem // providers with models (Models tab headers)
	allProviders       []providerProviderItem // all providers (Providers tab)
	allModels          []providerModelItem

	// Flattened visible-items list (rebuilt on state changes)
	visibleItems []providerListItem
	selectedIdx  int
	scrollOffset int
	maxVisible   int

	// Providers tab: expanded provider
	expandedProviderIdx int // index into allProviders; -1 = none

	// Inline API-key input
	apiKeyInput       textinput.Model
	apiKeyActive      bool
	apiKeyEnvVar      string
	apiKeyProviderIdx int // index into allProviders
	apiKeyAuthIdx     int // index into that provider's AuthMethods

	// Inline confirm-remove prompt
	confirmRemoveActive  bool
	confirmRemoveEnvVar  string
	confirmRemoveItemIdx int // index into visibleItems for the pending remove

	// Models tab: search filter and the two flags that disambiguate keys
	// whose meaning depends on what the user is doing.
	searchQuery    string              // active filter text; "" means no filter
	filteredModels []providerModelItem // allModels narrowed to searchQuery

	// searchFocused routes Space: true while the search box has focus (the user
	// is typing a query) so Space inserts a literal space; false while
	// navigating the list so Space marks the highlighted model instead.
	searchFocused bool

	// modelMarked routes Enter: true once the user has explicitly marked a
	// model with Space, so Enter confirms that mark regardless of cursor; false
	// until then, so Enter acts on the highlighted row. (The active model is
	// rendered [*] on open, but that display state is not a mark.)
	modelMarked bool

	// Provider connection result (shown inline)
	lastConnectResult  string
	lastConnectAuthIdx int // item index that triggered the connection
	lastConnectSuccess bool

	// spinnerTick advances on each ProviderConnectingMsg; used to pick a braille
	// frame while a connect/refresh is in flight.
	spinnerTick int
}

// providerSpinnerInterval is the spin cadence while a connect/refresh runs —
// fast enough to read as a smooth spinner (independent of the slower global
// thinking-spinner tick).
const providerSpinnerInterval = 90 * time.Millisecond

// ProviderConnectingMsg is the periodic "still connecting/refreshing" tick that
// advances the in-flight spinner; the terminal counterpart to
// ProviderConnectResultMsg, which signals the work is done.
type ProviderConnectingMsg struct{}

// providerConnectingTickCmd schedules the next connecting tick (spinner frame).
func providerConnectingTickCmd() tea.Cmd {
	return tea.Tick(providerSpinnerInterval, func(time.Time) tea.Msg {
		return ProviderConnectingMsg{}
	})
}

// AdvanceSpinner moves the in-flight spinner to its next frame.
func (s *ProviderSelector) AdvanceSpinner() { s.spinnerTick++ }

// Transient in-flight result markers. While lastConnectResult equals one of
// these, the row shows an animated spinner instead of static text.
const (
	providerStatusRefreshing = "Refreshing..."
	providerStatusConnecting = "Connecting..."
)

// IsConnecting reports whether a connect/refresh is in flight, so the spinner-tick
// loop keeps ticking and the row renders an animated frame.
func (s *ProviderSelector) IsConnecting() bool {
	return s.active &&
		(s.lastConnectResult == providerStatusRefreshing || s.lastConnectResult == providerStatusConnecting)
}

// providerStatusDisplayInfo contains display information for a provider status.
type providerStatusDisplayInfo struct {
	icon  string
	style lipgloss.Style
	desc  string
}

// providerStatusDisplayMap maps provider status to display information.
var providerStatusDisplayMap = map[llm.Status]providerStatusDisplayInfo{
	llm.StatusConnected: {"●", kit.SelectorStatusConnected(), ""},
	llm.StatusAvailable: {"○", kit.SelectorStatusReady(), "(available)"},
}

// providerGetStatusDisplay returns the icon, style, and description for a provider status.
func providerGetStatusDisplay(status llm.Status) (icon string, style lipgloss.Style, desc string) {
	if info, ok := providerStatusDisplayMap[status]; ok {
		return info.icon, info.style, info.desc
	}
	return "◌", kit.SelectorStatusNone(), ""
}

// NewProviderSelector creates a new provider selector ProviderSelector.
func NewProviderSelector() ProviderSelector {
	return ProviderSelector{
		active:              false,
		selectedIdx:         0,
		maxVisible:          20,
		expandedProviderIdx: -1,
	}
}

// ProviderModelSelectedMsg is sent when a model is selected.
type ProviderModelSelectedMsg struct {
	ModelID      string
	ProviderName string
	AuthMethod   llm.AuthMethod
}

// ProviderConnectResultMsg is sent when inline connection completes.
type ProviderConnectResultMsg struct {
	AuthIdx   int
	Success   bool
	Message   string
	NewStatus llm.Status
}

// ProviderModelsLoadedMsg is sent when async model loading completes.
type ProviderModelsLoadedMsg struct {
	Models []providerModelItem
}

// IsActive returns whether the selector is active.
func (s *ProviderSelector) IsActive() bool {
	return s.active
}

// ── Navigation ─────────────────────────────────────────────────────────────────

func (s *ProviderSelector) ensureVisible() {
	if s.selectedIdx < s.scrollOffset {
		s.scrollOffset = s.selectedIdx
	}
	if s.selectedIdx >= s.scrollOffset+s.maxVisible {
		s.scrollOffset = s.selectedIdx - s.maxVisible + 1
	}
}

func (s *ProviderSelector) MoveUp() {
	for s.selectedIdx > 0 {
		s.selectedIdx--
		if s.visibleItems[s.selectedIdx].Kind != providerItemProviderHeader {
			break
		}
	}
	if s.selectedIdx == 0 {
		s.searchFocused = true
	}
	s.ensureVisible()
}

func (s *ProviderSelector) MoveDown() {
	for s.selectedIdx < len(s.visibleItems)-1 {
		s.selectedIdx++
		if s.visibleItems[s.selectedIdx].Kind != providerItemProviderHeader {
			break
		}
	}
	s.searchFocused = false
	s.ensureVisible()
}

func (s *ProviderSelector) switchTab(t providerTab) {
	if t == s.activeTab {
		return
	}
	s.activeTab = t
	s.resetNavigation()
	s.resetModelSearch()
	s.resetConnectionResult()
	s.expandedProviderIdx = -1
	s.apiKeyActive = false
	s.rebuildVisibleItems()
}

func (s *ProviderSelector) NextTab() { s.switchTab((s.activeTab + 1) % 2) }
func (s *ProviderSelector) PrevTab() { s.switchTab((s.activeTab + 1 + 2) % 2) }

func (s *ProviderSelector) GoBack() bool {
	if s.apiKeyActive {
		s.apiKeyActive = false
		return true
	}
	if s.expandedProviderIdx >= 0 {
		s.expandedProviderIdx = -1
		s.resetConnectionResult()
		s.rebuildVisibleItems()
		return true
	}
	return false
}

func (s *ProviderSelector) clearModelSearch() bool {
	if s.searchQuery == "" {
		return false
	}
	s.searchQuery = ""
	s.searchFocused = false
	s.rebuildVisibleItems()
	return true
}

func (s *ProviderSelector) trimModelSearch() {
	if len(s.searchQuery) == 0 {
		return
	}
	s.searchQuery = s.searchQuery[:len(s.searchQuery)-1]
	if s.searchQuery == "" {
		// Empty query means we're no longer typing in the search box, so Space
		// returns to marking models rather than inserting a literal space.
		s.searchFocused = false
	}
	s.rebuildVisibleItems()
}

func (s *ProviderSelector) appendModelSearch(text string) {
	s.searchQuery += text
	s.searchFocused = true
	s.rebuildVisibleItems()
}

func (s *ProviderSelector) HandleKeypress(key tea.KeyMsg) tea.Cmd {
	// Route to API key input if active
	if s.apiKeyActive {
		return s.handleAPIKeyInput(key)
	}

	// Route to confirm-remove if active
	if s.confirmRemoveActive {
		return s.handleConfirmRemove(key)
	}

	switch key.Type {
	case tea.KeyTab:
		if s.searchQuery == "" {
			s.NextTab()
		}
		return nil

	case tea.KeyShiftTab:
		if s.searchQuery == "" {
			s.PrevTab()
		}
		return nil

	case tea.KeyUp, tea.KeyCtrlP:
		s.MoveUp()
		return nil

	case tea.KeyDown, tea.KeyCtrlN:
		s.MoveDown()
		return nil

	case tea.KeyEnter:
		return s.Select()

	case tea.KeyRight:
		if s.searchQuery == "" {
			s.NextTab()
		}
		return nil

	case tea.KeyLeft:
		if s.searchQuery == "" && !s.GoBack() {
			s.PrevTab()
		}
		return nil

	case tea.KeyEsc:
		if s.clearModelSearch() {
			return nil
		}
		if s.GoBack() {
			return nil
		}
		s.Cancel()
		return func() tea.Msg { return kit.DismissedMsg{} }

	case tea.KeyBackspace:
		s.trimModelSearch()
		return nil

	case tea.KeySpace:
		if s.activeTab == providerTabModels && !s.searchFocused {
			return s.toggleModel()
		}
		s.appendModelSearch(" ")
		return nil

	case tea.KeyRunes:
		s.appendModelSearch(string(key.Runes))
		return nil

	case tea.KeyCtrlE:
		return s.handleCredentialEdit()

	case tea.KeyCtrlD:
		return s.handleCredentialRemove()
	}

	// Vim navigation (only when search query is empty)
	if s.searchQuery == "" {
		switch key.String() {
		case "j":
			s.MoveDown()
		case "k":
			s.MoveUp()
		case "l":
			s.NextTab()
		case "h":
			if !s.GoBack() {
				s.PrevTab()
			}
		}
	}

	return nil
}

func (s *ProviderSelector) handleAPIKeyInput(key tea.KeyMsg) tea.Cmd {
	switch key.Type {
	case tea.KeyEnter:
		value := strings.TrimSpace(s.apiKeyInput.Value())
		if value == "" {
			return nil
		}
		if store := secret.Default(); store != nil {
			_ = store.Set(s.apiKeyEnvVar, value)
		}
		os.Setenv(s.apiKeyEnvVar, value)
		s.apiKeyActive = false

		// Find the auth method and trigger connection
		if s.apiKeyProviderIdx >= 0 && s.apiKeyProviderIdx < len(s.allProviders) {
			dp := &s.allProviders[s.apiKeyProviderIdx]
			if s.apiKeyAuthIdx >= 0 && s.apiKeyAuthIdx < len(dp.AuthMethods) {
				am := dp.AuthMethods[s.apiKeyAuthIdx]
				return s.connectAuthMethod(am, s.selectedIdx)
			}
		}
		return nil

	case tea.KeyEsc:
		s.apiKeyActive = false
		return nil

	default:
		var cmd tea.Cmd
		s.apiKeyInput, cmd = s.apiKeyInput.Update(key)
		return cmd
	}
}

// ── Selection ──────────────────────────────────────────────────────────────────

func (s *ProviderSelector) Select() tea.Cmd {
	// On the Models tab: once the user has explicitly marked a model with
	// Space, Enter confirms that marked model regardless of cursor position.
	// Without an explicit mark, fall through to the highlighted row so that
	// plain navigation + Enter and search + Enter still select what the cursor
	// is on (the active model is shown [*] on open, but that is not a mark).
	if s.activeTab == providerTabModels && s.modelMarked {
		if cmd := s.selectMarkedModel(); cmd != nil {
			return cmd
		}
	}

	if s.selectedIdx < 0 || s.selectedIdx >= len(s.visibleItems) {
		return nil
	}

	item := s.visibleItems[s.selectedIdx]
	switch item.Kind {
	case providerItemModel:
		return s.selectModel(item.Model)
	case providerItemProvider:
		return s.selectProvider(item)
	case providerItemAuthMethod:
		return s.selectAuthMethod(item)
	default:
		return nil
	}
}

func (s *ProviderSelector) selectModel(m *providerModelItem) tea.Cmd {
	if m == nil {
		return nil
	}
	s.active = false
	return func() tea.Msg {
		return ProviderModelSelectedMsg{
			ModelID:      m.ID,
			ProviderName: m.ProviderName,
			AuthMethod:   m.AuthMethod,
		}
	}
}

// selectModelFromIDs is like selectModel but takes the model identity as strings
// and constructs the message directly, without requiring a model pointer.
func (s *ProviderSelector) selectModelFromIDs(id, provider string, auth llm.AuthMethod) tea.Cmd {
	s.active = false
	return func() tea.Msg {
		return ProviderModelSelectedMsg{
			ModelID:      id,
			ProviderName: provider,
			AuthMethod:   auth,
		}
	}
}

// selectMarkedModel confirms the model the user marked with Space (the one
// rendered [*]). Used by Select() when an explicit mark exists, so the choice
// does not depend on cursor position. Returns nil if nothing is marked.
func (s *ProviderSelector) selectMarkedModel() tea.Cmd {
	for _, m := range s.allModels {
		if m.IsCurrent {
			return s.selectModelFromIDs(m.ID, m.ProviderName, m.AuthMethod)
		}
	}
	return nil
}

// toggleModel marks the currently highlighted model item (radio-style: marking
// one clears the others). Unlike Select (Enter), it only updates the IsCurrent
// flag visually and does NOT activate the model or close the overlay; the mark
// is what a subsequent Enter confirms.
func (s *ProviderSelector) toggleModel() tea.Cmd {
	if s.selectedIdx < 0 || s.selectedIdx >= len(s.visibleItems) {
		return nil
	}
	item := s.visibleItems[s.selectedIdx]
	if item.Kind != providerItemModel || item.Model == nil {
		return nil
	}
	m := item.Model
	for i := range s.allModels {
		s.allModels[i].IsCurrent = s.allModels[i].ID == m.ID && s.allModels[i].ProviderName == m.ProviderName
	}
	for i := range s.filteredModels {
		s.filteredModels[i].IsCurrent = s.filteredModels[i].ID == m.ID && s.filteredModels[i].ProviderName == m.ProviderName
	}
	for i := range s.visibleItems {
		if s.visibleItems[i].Kind == providerItemModel && s.visibleItems[i].Model != nil {
			vi := s.visibleItems[i].Model
			vi.IsCurrent = vi.ID == m.ID && vi.ProviderName == m.ProviderName
		}
	}
	s.modelMarked = true
	return nil
}

// selectProvider handles Enter on a provider row (Providers tab).
// Connected single auth method: refresh models.
// Disconnected single auth method: auto-connect or show API key input.
// Multiple auth methods: expand inline to show auth method list.
func (s *ProviderSelector) selectProvider(item providerListItem) tea.Cmd {
	if item.Provider == nil {
		return nil
	}
	p := item.Provider

	if len(p.AuthMethods) == 1 {
		am := p.AuthMethods[0]
		if am.Status == llm.StatusConnected {
			// Refresh: re-fetch models for this connected provider
			return s.refreshAuthMethod(am, s.selectedIdx)
		}
		return s.tryConnectOrPromptKey(am, item.ProviderIdx, 0)
	}

	if len(p.AuthMethods) == 0 {
		return nil
	}

	// Multiple auth methods: toggle inline expansion
	if s.expandedProviderIdx == item.ProviderIdx {
		s.expandedProviderIdx = -1
	} else {
		s.expandedProviderIdx = item.ProviderIdx
	}
	s.resetConnectionResult()
	s.rebuildVisibleItems()
	return nil
}

func (s *ProviderSelector) selectAuthMethod(item providerListItem) tea.Cmd {
	if item.AuthMethod == nil {
		return nil
	}
	am := item.AuthMethod

	if am.Status == llm.StatusConnected {
		// Refresh: re-fetch models for this connected auth method
		return s.refreshAuthMethod(*am, s.selectedIdx)
	}

	return s.tryConnectOrPromptKey(*am, item.ProviderIdx, s.findAuthMethodIndex(item))
}

// handleCredentialEdit handles the 'e' key for editing credentials on connected providers.
// For providers with a single auth method: activates API key input directly.
// For providers with multiple auth methods: expands the provider first, then allows editing.
func (s *ProviderSelector) handleCredentialEdit() tea.Cmd {
	if s.selectedIdx < 0 || s.selectedIdx >= len(s.visibleItems) {
		return nil
	}

	item := s.visibleItems[s.selectedIdx]

	switch item.Kind {
	case providerItemProvider:
		return s.handleCredentialEditForProvider(item)
	case providerItemAuthMethod:
		return s.handleCredentialEditForAuthMethod(item)
	default:
		return nil
	}
}

// handleCredentialEditForProvider handles credential edit for a provider row.
func (s *ProviderSelector) handleCredentialEditForProvider(item providerListItem) tea.Cmd {
	if item.Provider == nil {
		return nil
	}
	p := item.Provider

	// Single auth method: activate API key input directly
	if len(p.AuthMethods) == 1 {
		am := p.AuthMethods[0]
		envVar := providerFirstEnvVar(am.EnvVars)
		if envVar == "" {
			return nil
		}
		s.apiKeyProviderIdx = item.ProviderIdx
		s.apiKeyAuthIdx = 0
		s.initAPIKeyInput(envVar)
		return nil
	}

	// Multiple auth methods: expand if not already expanded
	if len(p.AuthMethods) == 0 {
		return nil
	}

	if s.expandedProviderIdx != item.ProviderIdx {
		s.expandedProviderIdx = item.ProviderIdx
		s.resetConnectionResult()
		s.rebuildVisibleItems()
	}

	return nil
}

// handleCredentialEditForAuthMethod handles credential edit for an auth method row.
func (s *ProviderSelector) handleCredentialEditForAuthMethod(item providerListItem) tea.Cmd {
	if item.AuthMethod == nil {
		return nil
	}
	am := item.AuthMethod

	envVar := providerFirstEnvVar(am.EnvVars)
	if envVar == "" {
		return nil
	}

	s.apiKeyProviderIdx = item.ProviderIdx
	s.apiKeyAuthIdx = s.findAuthMethodIndex(item)
	s.initAPIKeyInput(envVar)
	return nil
}

// handleCredentialRemove handles Ctrl+D: shows a confirmation prompt before removing.
func (s *ProviderSelector) handleCredentialRemove() tea.Cmd {
	if s.activeTab != providerTabProviders {
		return nil
	}
	if s.selectedIdx < 0 || s.selectedIdx >= len(s.visibleItems) {
		return nil
	}

	item := s.visibleItems[s.selectedIdx]

	var envVars []string
	switch item.Kind {
	case providerItemProvider:
		if item.Provider == nil || len(item.Provider.AuthMethods) != 1 {
			return nil
		}
		envVars = item.Provider.AuthMethods[0].EnvVars
	case providerItemAuthMethod:
		if item.AuthMethod == nil {
			return nil
		}
		envVars = item.AuthMethod.EnvVars
	default:
		return nil
	}

	envVar := providerFirstEnvVar(envVars)
	if envVar == "" {
		return nil
	}

	s.confirmRemoveActive = true
	s.confirmRemoveEnvVar = envVar
	s.confirmRemoveItemIdx = s.selectedIdx
	return nil
}

// handleConfirmRemove handles keypresses while the confirm-remove prompt is active.
func (s *ProviderSelector) handleConfirmRemove(key tea.KeyMsg) tea.Cmd {
	s.confirmRemoveActive = false
	switch key.String() {
	case "y", "Y":
		return s.executeCredentialRemove()
	default:
		return nil
	}
}

// executeCredentialRemove performs the actual credential removal.
func (s *ProviderSelector) executeCredentialRemove() tea.Cmd {
	envVar := s.confirmRemoveEnvVar

	// Resolve the provider and auth method from the item
	item := s.visibleItems[s.confirmRemoveItemIdx]
	var providerName llm.Name
	var authMethod llm.AuthMethod
	switch item.Kind {
	case providerItemProvider:
		if item.Provider == nil || len(item.Provider.AuthMethods) != 1 {
			return nil
		}
		providerName = item.Provider.AuthMethods[0].Provider
		authMethod = item.Provider.AuthMethods[0].AuthMethod
	case providerItemAuthMethod:
		if item.AuthMethod == nil {
			return nil
		}
		providerName = item.AuthMethod.Provider
		authMethod = item.AuthMethod.AuthMethod
	default:
		return nil
	}

	// Remove from secret store and unset env var
	if store := secret.Default(); store != nil {
		_ = store.Delete(envVar)
	}
	os.Unsetenv(envVar)

	// Disconnect provider and remove cached models from the llm store
	if s.store != nil {
		_ = s.store.Disconnect(providerName)
		_ = s.store.RemoveCachedModels(providerName, authMethod)

		// Clear current model if it belongs to the disconnected provider
		if cur := s.store.GetCurrentModel(); cur != nil && cur.Provider == providerName {
			_ = s.store.ClearCurrentModel()
			llm.Default().SetCurrentModel(nil)
		}

		// If no connections remain, clear the runtime provider too
		if len(s.store.GetConnections()) == 0 {
			llm.Default().SetProvider(nil)
		}
	}

	// Reload provider data to reflect the removed credential
	s.resetConnectionResult()
	_, _ = s.loadProviderData()
	s.rebuildVisibleItems()
	return nil
}

// tryConnectOrPromptKey connects if env vars are available, otherwise shows API key input.
func (s *ProviderSelector) tryConnectOrPromptKey(am providerAuthMethodItem, providerIdx, authIdx int) tea.Cmd {
	if am.Status == llm.StatusAvailable || providerIsEnvReady(am.EnvVars) {
		return s.connectAuthMethod(am, s.selectedIdx)
	}

	// Show inline API key input
	envVar := providerFirstEnvVar(am.EnvVars)
	if envVar == "" {
		return nil
	}
	s.apiKeyProviderIdx = providerIdx
	s.apiKeyAuthIdx = authIdx
	s.initAPIKeyInput(envVar)
	return nil
}

func (s *ProviderSelector) findAuthMethodIndex(item providerListItem) int {
	if item.AuthMethod == nil || item.ProviderIdx < 0 || item.ProviderIdx >= len(s.allProviders) {
		return 0
	}
	p := &s.allProviders[item.ProviderIdx]
	for i, am := range p.AuthMethods {
		if am.Provider == item.AuthMethod.Provider && am.AuthMethod == item.AuthMethod.AuthMethod {
			return i
		}
	}
	return 0
}

func providerIsEnvReady(envVars []string) bool {
	for _, v := range envVars {
		if v != "" && secret.Resolve(v) != "" {
			return true
		}
	}
	return false
}

func providerFirstEnvVar(envVars []string) string {
	for _, v := range envVars {
		if v != "" {
			return v
		}
	}
	return ""
}

// ── Loading ────────────────────────────────────────────────────────────────────

// Enter opens the unified model & provider kit.
func (s *ProviderSelector) Enter(ctx context.Context, width, height int) (tea.Cmd, error) {
	s.resetNavigation()
	s.resetModelSearch()
	s.resetConnectionResult()
	s.expandedProviderIdx = -1
	s.apiKeyActive = false
	s.active = true
	s.activeTab = providerTabModels
	s.width = width
	s.height = height

	cmd, err := s.loadProviderData()
	if err != nil {
		return nil, err
	}
	s.rebuildVisibleItems()
	return cmd, nil
}

// loadProviderData refreshes provider and model data from a fresh store.
// Does NOT reset UI state (tabs, selection, expansion) or call rebuildVisibleItems.
func (s *ProviderSelector) loadProviderData() (tea.Cmd, error) {
	store, err := llm.NewStore()
	if err != nil {
		return nil, fmt.Errorf("failed to load store: %w", err)
	}
	s.store = store

	providersWithStatus := llm.GetProvidersWithStatus(store)

	s.connectedProviders = nil
	s.allProviders = nil

	for _, p := range llm.ProvidersByOrder() {
		infos, ok := providersWithStatus[p]
		if !ok || len(infos) == 0 {
			continue
		}

		item := providerProviderItem{
			Provider:    p,
			DisplayName: llm.ProviderDisplayName(p),
			AuthMethods: make([]providerAuthMethodItem, 0, len(infos)),
		}

		connected := false
		for _, info := range infos {
			item.AuthMethods = append(item.AuthMethods, providerAuthMethodItem{
				Provider:    info.Meta.Provider,
				AuthMethod:  info.Meta.AuthMethod,
				DisplayName: info.Meta.DisplayName,
				Status:      info.Status,
				EnvVars:     info.Meta.EnvVars,
			})
			if info.Status == llm.StatusConnected {
				connected = true
			}
		}

		item.Connected = connected
		s.allProviders = append(s.allProviders, item)
		if connected {
			s.connectedProviders = append(s.connectedProviders, item)
		}
	}

	current := store.GetCurrentModel()

	s.allModels = nil
	allCached := store.GetAllCachedModels()
	if len(allCached) == 0 {
		allCached = store.GetAllCachedModelsIncludeExpired()
	}

	var asyncCmd tea.Cmd
	if len(allCached) > 0 {
		s.loadModelsCached(allCached, current)
	} else {
		asyncCmd = s.loadModelsAsync(store, current)
	}

	s.ensureModelProvidersExist()
	s.sortConnectedProviders(current)

	return asyncCmd, nil
}

// ensureModelProvidersExist ensures every provider that has cached models
// is represented in connectedProviders (handles cases where registry doesn't
// have the provider registered but models exist in cache).
func (s *ProviderSelector) ensureModelProvidersExist() {
	existing := make(map[string]bool)
	for _, cp := range s.connectedProviders {
		existing[string(cp.Provider)] = true
	}

	// Collect unique provider names from models
	seen := make(map[string]bool)
	for _, m := range s.allModels {
		if existing[m.ProviderName] || seen[m.ProviderName] {
			continue
		}
		seen[m.ProviderName] = true

		displayName := llm.ProviderDisplayName(llm.Name(m.ProviderName))
		if displayName == "" {
			displayName = m.ProviderName
		}

		s.connectedProviders = append(s.connectedProviders, providerProviderItem{
			Provider:    llm.Name(m.ProviderName),
			DisplayName: displayName,
			Connected:   true,
		})
	}
}

// loadModelsAsync returns a tea.Cmd that fetches models from all connected
// providers concurrently, sending a ProviderModelsLoadedMsg when done.
func (s *ProviderSelector) loadModelsAsync(store *llm.Store, current *llm.CurrentModelInfo) tea.Cmd {
	connections := store.GetConnections()
	return func() tea.Msg {
		ctx := context.Background()

		type providerResult struct {
			providerName string
			authMethod   llm.AuthMethod
			models       []llm.ModelInfo
		}

		ch := make(chan providerResult, len(connections))
		var wg sync.WaitGroup

		for name, conn := range connections {
			wg.Add(1)
			go func(providerName string, authMethod llm.AuthMethod) {
				defer wg.Done()
				p, err := llm.GetProvider(ctx, llm.Name(providerName), authMethod)
				if err != nil {
					return
				}
				mdls, err := p.ListModels(ctx)
				if err != nil {
					return
				}
				ch <- providerResult{providerName, authMethod, mdls}
			}(name, conn.AuthMethod)
		}

		go func() { wg.Wait(); close(ch) }()

		var models []providerModelItem
		for r := range ch {
			prov := llm.Name(r.providerName)
			_ = store.CacheModels(prov, r.authMethod, r.models)

			for _, mdl := range r.models {
				models = append(models, newProviderModelItem(mdl, r.providerName, r.authMethod, current))
			}
		}
		return ProviderModelsLoadedMsg{Models: models}
	}
}

// HandleModelsLoaded updates the panel with asynchronously loaded models.
func (s *ProviderSelector) HandleModelsLoaded(msg ProviderModelsLoadedMsg) {
	s.allModels = msg.Models
	s.ensureModelProvidersExist()

	var current *llm.CurrentModelInfo
	if s.store != nil {
		current = s.store.GetCurrentModel()
	}
	s.sortConnectedProviders(current)
	s.rebuildVisibleItems()
}

// loadModelsCached loads models from the store cache.
func (s *ProviderSelector) loadModelsCached(allCached map[string][]llm.ModelInfo, current *llm.CurrentModelInfo) {
	for key, models := range allCached {
		parts := strings.SplitN(key, ":", 2)
		providerName := key
		var authMethod llm.AuthMethod
		if len(parts) >= 2 {
			providerName = parts[0]
			authMethod = llm.AuthMethod(parts[1])
		}

		for _, mdl := range models {
			s.allModels = append(s.allModels, newProviderModelItem(mdl, providerName, authMethod, current))
		}
	}
}

// sortConnectedProviders sorts connected providers so that the current
// selection's provider comes first, then alphabetical.
func (s *ProviderSelector) sortConnectedProviders(current *llm.CurrentModelInfo) {
	if current == nil {
		return
	}
	currentProvider := current.Provider
	sort.SliceStable(s.connectedProviders, func(i, j int) bool {
		iMatch := s.connectedProviders[i].Provider == currentProvider
		jMatch := s.connectedProviders[j].Provider == currentProvider
		if iMatch != jMatch {
			return iMatch
		}
		return false
	})
}

// rebuildVisibleItems constructs the flat visible-items list from current state.
func (s *ProviderSelector) rebuildVisibleItems() {
	s.visibleItems = nil

	switch s.activeTab {
	case providerTabModels:
		s.rebuildModelsTab()
	case providerTabProviders:
		s.rebuildProvidersTab()
	}

	s.clampSelection()
}

// rebuildModelsTab builds visible items for the Models tab.
func (s *ProviderSelector) rebuildModelsTab() {
	s.updateFilter()

	// Group filtered models by provider
	providerModels := make(map[string][]providerModelItem)
	for i := range s.filteredModels {
		m := &s.filteredModels[i]
		providerModels[m.ProviderName] = append(providerModels[m.ProviderName], *m)
	}

	for i := range s.connectedProviders {
		cp := &s.connectedProviders[i]
		models := providerModels[string(cp.Provider)]
		if len(models) == 0 && s.searchQuery != "" {
			continue
		}

		s.visibleItems = append(s.visibleItems, providerListItem{
			Kind:        providerItemProviderHeader,
			Provider:    cp,
			ProviderIdx: i,
		})

		// Sort models: current first
		sort.SliceStable(models, func(a, b int) bool {
			return models[a].IsCurrent && !models[b].IsCurrent
		})

		for j := range models {
			s.visibleItems = append(s.visibleItems, providerListItem{
				Kind:        providerItemModel,
				Model:       &models[j],
				ProviderIdx: i,
			})
		}
	}
}

// rebuildProvidersTab builds visible items for the Providers tab.
func (s *ProviderSelector) rebuildProvidersTab() {
	for i := range s.allProviders {
		p := &s.allProviders[i]

		// Apply search filter on provider name
		if s.searchQuery != "" {
			query := strings.ToLower(s.searchQuery)
			if !kit.FuzzyMatch(strings.ToLower(p.DisplayName), query) &&
				!kit.FuzzyMatch(strings.ToLower(string(p.Provider)), query) {
				continue
			}
		}

		s.visibleItems = append(s.visibleItems, providerListItem{
			Kind:        providerItemProvider,
			Provider:    p,
			ProviderIdx: i,
		})

		// Show expanded auth methods
		if s.expandedProviderIdx == i {
			for j := range p.AuthMethods {
				s.visibleItems = append(s.visibleItems, providerListItem{
					Kind:        providerItemAuthMethod,
					AuthMethod:  &p.AuthMethods[j],
					ProviderIdx: i,
				})
			}
		}
	}
}

func (s *ProviderSelector) updateFilter() {
	if s.searchQuery == "" {
		s.filteredModels = s.allModels
		return
	}
	query := strings.ToLower(s.searchQuery)
	s.filteredModels = nil
	for _, m := range s.allModels {
		if kit.FuzzyMatch(strings.ToLower(m.ID), query) ||
			kit.FuzzyMatch(strings.ToLower(m.DisplayName), query) ||
			kit.FuzzyMatch(strings.ToLower(m.ProviderName), query) {
			s.filteredModels = append(s.filteredModels, m)
		}
	}
}

func (s *ProviderSelector) clampSelection() {
	if len(s.visibleItems) == 0 {
		s.selectedIdx = 0
		return
	}
	if s.selectedIdx >= len(s.visibleItems) {
		s.selectedIdx = len(s.visibleItems) - 1
	}
	if s.selectedIdx < 0 {
		s.selectedIdx = 0
	}
	// Skip non-selectable items forward
	if s.visibleItems[s.selectedIdx].Kind == providerItemProviderHeader {
		for s.selectedIdx < len(s.visibleItems)-1 {
			s.selectedIdx++
			if s.visibleItems[s.selectedIdx].Kind != providerItemProviderHeader {
				break
			}
		}
	}
}

// refreshAuthMethod re-fetches models for an already connected provider auth method.
func (s *ProviderSelector) refreshAuthMethod(item providerAuthMethodItem, authIdx int) tea.Cmd {
	if s.IsConnecting() {
		// A connect/refresh is already in flight; ignore re-entry so we don't
		// start a second spinner-tick loop or a concurrent store write.
		return nil
	}
	s.lastConnectResult = providerStatusRefreshing
	s.lastConnectAuthIdx = authIdx
	s.lastConnectSuccess = false

	work := func() tea.Msg {
		ctx := context.Background()

		llmProvider, err := llm.GetProvider(ctx, item.Provider, item.AuthMethod)
		if err != nil {
			return ProviderConnectResultMsg{
				AuthIdx: authIdx,
				Success: false,
				Message: fmt.Sprintf("failed to load models for %s: %s", item.Provider, err.Error()),
			}
		}

		models, err := llmProvider.ListModels(ctx)

		store, _ := llm.NewStore()
		if store != nil && len(models) > 0 {
			_ = store.CacheModels(item.Provider, item.AuthMethod, models)
		}

		if err != nil && len(models) == 0 {
			return ProviderConnectResultMsg{
				AuthIdx: authIdx,
				Success: false,
				Message: fmt.Sprintf("failed to load models for %s: %s", item.Provider, err.Error()),
			}
		}

		if err != nil {
			return ProviderConnectResultMsg{
				AuthIdx:   authIdx,
				Success:   true,
				Message:   fmt.Sprintf("⚠ %d models loaded with refresh warning", len(models)),
				NewStatus: llm.StatusConnected,
			}
		}

		return ProviderConnectResultMsg{
			AuthIdx:   authIdx,
			Success:   true,
			Message:   fmt.Sprintf("● %d models", len(models)),
			NewStatus: llm.StatusConnected,
		}
	}
	// Start the spinner alongside the async work.
	return tea.Batch(providerConnectingTickCmd(), work)
}

// connectAuthMethod initiates an async connection to a provider auth method.
func (s *ProviderSelector) connectAuthMethod(item providerAuthMethodItem, authIdx int) tea.Cmd {
	if s.IsConnecting() {
		// A connect/refresh is already in flight; ignore re-entry so we don't
		// start a second spinner-tick loop or a concurrent store write.
		return nil
	}
	s.lastConnectResult = providerStatusConnecting
	s.lastConnectAuthIdx = authIdx
	s.lastConnectSuccess = false

	work := func() tea.Msg {
		ctx := context.Background()
		result, err := s.ConnectProvider(ctx, item.Provider, item.AuthMethod)
		if err != nil {
			return ProviderConnectResultMsg{
				AuthIdx: authIdx,
				Success: false,
				Message: err.Error(),
			}
		}

		return ProviderConnectResultMsg{
			AuthIdx:   authIdx,
			Success:   true,
			Message:   result,
			NewStatus: llm.StatusConnected,
		}
	}
	return tea.Batch(providerConnectingTickCmd(), work)
}

// HandleConnectResult updates the selector state with connection result.
func (s *ProviderSelector) HandleConnectResult(msg ProviderConnectResultMsg) tea.Cmd {
	s.lastConnectAuthIdx = msg.AuthIdx
	s.lastConnectResult = msg.Message
	s.lastConnectSuccess = msg.Success

	if !msg.Success {
		return nil
	}

	// Reload provider/model data, preserving UI state (tab, expansion, result).
	cmd, _ := s.loadProviderData()
	s.rebuildVisibleItems()
	return cmd
}

// ConnectProvider connects to a provider and verifies the connection.
func (s *ProviderSelector) ConnectProvider(ctx context.Context, p llm.Name, authMethod llm.AuthMethod) (string, error) {
	if s.store == nil {
		store, err := llm.NewStore()
		if err != nil {
			return "", fmt.Errorf("failed to load store: %w", err)
		}
		s.store = store
	}

	meta, ok := llm.GetMeta(p, authMethod)
	if !ok {
		return "", fmt.Errorf("provider not found: %s:%s", p, authMethod)
	}

	if !llm.IsReady(meta) {
		missingVars := []string{}
		for _, envVar := range meta.EnvVars {
			if envVar == "" {
				continue
			}
			missingVars = append(missingVars, envVar)
		}
		return "", fmt.Errorf("missing required environment variables: %s", strings.Join(missingVars, ", "))
	}

	llmProvider, err := llm.GetProvider(ctx, p, authMethod)
	if err != nil {
		return "", fmt.Errorf("failed to create provider: %w", err)
	}

	models, listErr := llmProvider.ListModels(ctx)
	if listErr != nil && len(models) == 0 {
		return "", fmt.Errorf("failed to load models for %s: %w", meta.DisplayName, listErr)
	}
	if len(models) > 0 {
		_ = s.store.CacheModels(p, authMethod, models)
	}

	if err := s.store.Connect(p, authMethod); err != nil {
		return "", fmt.Errorf("failed to save connection: %w", err)
	}

	if listErr != nil {
		return fmt.Sprintf("Connected to %s via %s (%d models; refresh warning: %v)", meta.DisplayName, authMethod, len(models), listErr), nil
	}

	return fmt.Sprintf("Connected to %s via %s (%d models)", meta.DisplayName, authMethod, len(models)), nil
}

// SetModel sets the current model.
func (s *ProviderSelector) SetModel(modelID string, providerName string, authMethod llm.AuthMethod) (string, error) {
	if s.store == nil {
		store, err := llm.NewStore()
		if err != nil {
			return "", fmt.Errorf("failed to load store: %w", err)
		}
		s.store = store
	}

	if err := s.store.SetCurrentModel(modelID, llm.Name(providerName), authMethod); err != nil {
		return "", fmt.Errorf("failed to set model: %w", err)
	}

	return fmt.Sprintf("Model set to: %s (%s)", modelID, providerName), nil
}

// initAPIKeyInput initializes the textinput for API key entry.
func (s *ProviderSelector) initAPIKeyInput(envVar string) {
	ti := textinput.New()
	ti.Placeholder = envVar
	ti.Focus()
	ti.CharLimit = 256
	ti.Width = 40
	ti.EchoMode = textinput.EchoPassword
	s.apiKeyInput = ti
	s.apiKeyActive = true
	s.apiKeyEnvVar = envVar
}

// ── Reset ──────────────────────────────────────────────────────────────────────

func (s *ProviderSelector) resetConnectionResult() {
	s.lastConnectResult = ""
	s.lastConnectAuthIdx = 0
	s.lastConnectSuccess = false
}

func (s *ProviderSelector) resetModelSearch() {
	s.searchQuery = ""
	s.filteredModels = nil
	s.scrollOffset = 0
}

func (s *ProviderSelector) resetNavigation() {
	s.selectedIdx = 0
	s.scrollOffset = 0
	s.searchFocused = false
	s.modelMarked = false
}

// Cancel cancels the selector and clears transient state so the next open starts cleanly.
func (s *ProviderSelector) Cancel() {
	s.active = false
	s.connectedProviders = nil
	s.allProviders = nil
	s.allModels = nil
	s.filteredModels = nil
	s.visibleItems = nil
	s.expandedProviderIdx = -1
	s.apiKeyActive = false
	s.store = nil
	s.resetNavigation()
	s.resetModelSearch()
	s.resetConnectionResult()
}

// ── Helpers ────────────────────────────────────────────────────────────────────

func providerBestAuthMethodStatus(methods []providerAuthMethodItem) llm.Status {
	for _, m := range methods {
		if m.Status == llm.StatusConnected {
			return llm.StatusConnected
		}
	}
	for _, m := range methods {
		if m.Status == llm.StatusAvailable {
			return llm.StatusAvailable
		}
	}
	return llm.StatusNotConfigured
}
