package tui

import (
	"albedo/cli/internal/config"
	"albedo/cli/internal/daemon"
	"context"
	"errors"
	"fmt"
	"regexp"
	"slices"
	"strings"

	tea "charm.land/bubbletea/v2"
	"github.com/charmbracelet/x/ansi"
)

// CapabilityPageModel owns a dedicated session page for one source of agent
// context. MCP servers are added and edited through one form (mcp_form.go).
type CapabilityPageModel struct {
	Prefs           config.CapabilityPrefs
	Conn            *daemon.Connection
	Form            *mcpForm
	SessionID, Kind string
	Items           []capabilityItem
	Revision        string
	Diagnostics     []string
	page
	Global, ExtensionEnabled, ConfirmExtension bool
	ConfirmDelete                              bool
}
type capabilityItem struct {
	ID, Title, Detail string
	Candidate         daemon.CatalogCandidate
	Secrets           daemon.MCPSecretNames
	Server            config.MCPServer
}
type capabilityLoadedMsg struct {
	Prefs            config.CapabilityPrefs
	Revision         string
	Diagnostics      []string
	Err              error
	Items            []capabilityItem
	Gen              int
	ExtensionEnabled bool
}
type capabilitySavedMsg struct {
	Err     error
	Warning string
	Gen     int
}
type CapabilityPageDoneMsg struct{}
type CapabilityPageChangedMsg struct{}

type ChatOpenCapabilityPageMsg struct{ Kind string }

func NewCapabilityPageModel(conn *daemon.Connection, sessionID, kind string) CapabilityPageModel {
	// Pages open on the global defaults; s scopes changes to this session.
	return CapabilityPageModel{Conn: conn, SessionID: sessionID, Kind: kind, Global: true, page: page{Loading: true, Generation: nextPageGeneration()}}
}
func (m CapabilityPageModel) Init() tea.Cmd { return m.loadCmd(m.Generation) }
func (m CapabilityPageModel) loadCmd(gen int) tea.Cmd {
	kind := m.Kind
	return func() tea.Msg {
		if kind == "skills" || kind == "instructions" {
			catalog, err := daemon.GetCapabilityCatalog(context.Background(), m.Conn, m.SessionID)
			items := make([]capabilityItem, 0, len(catalog.Candidates))
			for _, candidate := range catalog.Candidates {
				if candidate.Kind == kind {
					items = append(items, capabilityItem{ID: candidate.ID, Title: candidate.Title, Detail: candidate.Source, Candidate: candidate})
				}
			}
			return capabilityLoadedMsg{Items: items, Revision: catalog.Revision, Diagnostics: catalog.Diagnostics, ExtensionEnabled: catalog.Extensions[kind], Gen: gen, Err: err}
		}
		if kind != "mcp" {
			return capabilityLoadedMsg{Gen: gen, Err: errors.New("unknown capability page")}
		}
		settings, err := daemon.GetSettings(context.Background(), m.Conn)
		if err != nil {
			return capabilityLoadedMsg{Gen: gen, Err: err}
		}
		items := mcpItems(settings)
		slices.SortFunc(items, func(a, b capabilityItem) int { return strings.Compare(a.ID, b.ID) })
		enabled := true
		if m.Conn != nil {
			var extensions []ExtensionItem
			extensions, err = daemon.ListExtensions(context.Background(), m.Conn, m.SessionID)
			if i := slices.IndexFunc(extensions, func(ext ExtensionItem) bool { return ext.Name == kind }); err == nil && i >= 0 {
				enabled = extensions[i].Enabled
			}
		}
		return capabilityLoadedMsg{Items: items, Prefs: settings.Capabilities, ExtensionEnabled: enabled, Gen: gen, Err: err}
	}
}

// mcpItems lists the configured servers with the names of the secrets the
// daemon holds for them (never their contents).
func mcpItems(settings daemon.Settings) []capabilityItem {
	items := make([]capabilityItem, 0, len(settings.MCP))
	for name, server := range settings.MCP {
		address := server.URL
		if server.Type == "stdio" {
			address = server.Command
		}
		items = append(items, capabilityItem{ID: name, Title: name, Detail: server.Type + " · " + address, Server: server, Secrets: settings.Credentials.MCP[name]})
	}
	return items
}

func (m CapabilityPageModel) enableExtensionCmd(gen int) tea.Cmd {
	return func() tea.Msg {
		_, err := daemon.SelectExtension(context.Background(), m.Conn, m.SessionID, daemon.ExtensionSelectionRequest{Name: m.Kind, Enabled: new(true)})
		return capabilitySavedMsg{Gen: gen, Err: err}
	}
}

func (m CapabilityPageModel) toggleCmd(item capabilityItem, gen int) tea.Cmd {
	next := !m.selectedEnabled(item)
	return m.choiceCmd(item, &next, gen)
}

func (m CapabilityPageModel) choiceCmd(item capabilityItem, enabled *bool, gen int) tea.Cmd {
	scope := "session"
	if m.Global {
		scope = "global"
	}
	return func() tea.Msg {
		var result daemon.ReloadResult
		var err error
		if m.Kind == "mcp" {
			result, err = daemon.SetCapability(context.Background(), m.Conn, m.SessionID, daemon.CapabilitySelectionRequest{Kind: m.Kind, Name: item.ID, Scope: scope, Enabled: enabled})
		} else {
			result, err = daemon.SetCatalogCapability(context.Background(), m.Conn, m.SessionID, daemon.CatalogCapabilityRequest{Revision: m.Revision, ID: item.ID, Scope: scope, Enabled: enabled})
		}
		return capabilitySavedMsg{Gen: gen, Err: err, Warning: result.Warning}
	}
}

var mcpHeaderName = regexp.MustCompile(`^[!#$%&'*+.^_` + "`" + `|~0-9A-Za-z-]+$`)
var mcpEnvName = regexp.MustCompile(`^[A-Za-z_][A-Za-z_0-9]*$`)

// saveMCPCmd writes a server and its credentials together and reloads the
// session once. If the reload fails (the server cannot connect), both are
// restored, so a half-configured server is never left behind.
func (m CapabilityPageModel) saveMCPCmd(sub mcpSubmission, gen int) tea.Cmd {
	return func() tea.Msg {
		result, err := daemon.SaveMCP(context.Background(), m.Conn, m.SessionID, daemon.MCPUpdateRequest{Name: sub.Name, Server: &sub.Server, Secrets: sub.Secrets})
		return capabilitySavedMsg{Gen: gen, Err: err, Warning: result.Warning}
	}
}

func (m CapabilityPageModel) deleteMCPCmd(name string, gen int) tea.Cmd {
	return func() tea.Msg {
		result, err := daemon.SaveMCP(context.Background(), m.Conn, m.SessionID, daemon.MCPUpdateRequest{Name: name, Server: nil, Secrets: daemon.MCPSecretsPatch{}})
		return capabilitySavedMsg{Gen: gen, Err: err, Warning: result.Warning}
	}
}

var mcpName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`)

// openForm starts adding a server, or editing the selected one with its
// stored credentials summarized (never shown).
func (m *CapabilityPageModel) openForm(edit bool) {
	m.Error, m.Notice = "", ""
	if !edit {
		m.Form = newMCPForm("", config.MCPServer{Type: "http"}, daemon.MCPSecretNames{})
		return
	}
	item := m.Items[m.Cursor]
	m.Form = newMCPForm(item.ID, item.Server, item.Secrets)
}

func (m CapabilityPageModel) save(cmd func(int) tea.Cmd) (CapabilityPageModel, tea.Cmd) {
	m.Saving = true
	m.Generation = nextPageGeneration()
	return m, cmd(m.Generation)
}

func (m CapabilityPageModel) Update(msg tea.Msg) (CapabilityPageModel, tea.Cmd) {
	switch msg := msg.(type) {
	case capabilityLoadedMsg:
		if !m.settle(msg.Gen, msg.Err, &m.Loading) {
			return m, nil
		}
		m.Cursor = reselect(m.Cursor, m.Items, msg.Items, func(item capabilityItem) string { return item.ID })
		m.Items, m.Prefs, m.ExtensionEnabled, m.Error = msg.Items, msg.Prefs, msg.ExtensionEnabled, ""
		if m.Kind != "mcp" {
			m.Revision, m.Diagnostics = msg.Revision, msg.Diagnostics
		}
		return m, nil
	case capabilitySavedMsg:
		if msg.Gen == m.Generation {
			if apiErr, ok := errors.AsType[*daemon.APIError](msg.Err); ok && apiErr.Code == "stale_catalog" {
				m.Saving, m.Loading, m.Error = false, true, ""
				m.Notice = "The list changed. Review it before trying again."
				m.Generation = nextPageGeneration()
				return m, m.loadCmd(m.Generation)
			}
		}
		if !m.settle(msg.Gen, msg.Err, &m.Saving) {
			return m, nil
		}
		m.Form = nil
		m.ConfirmDelete = false
		m.Notice = "Saved. Session reloaded."
		if msg.Warning != "" {
			m.Notice = msg.Warning
		}
		m.Loading, m.Generation = true, nextPageGeneration()
		return m, tea.Batch(m.loadCmd(m.Generation), func() tea.Msg { return CapabilityPageChangedMsg{} })
	case tea.PasteMsg:
		if m.Form == nil || m.Saving {
			return m, nil
		}
		_, cmd := m.Form.update(msg)
		return m, cmd
	case tea.KeyPressMsg:
		if m.Saving {
			return m, nil
		}
		if m.ConfirmExtension {
			switch msg.String() {
			case "esc":
				m.ConfirmExtension = false
			case "enter":
				m.ConfirmExtension = false
				return m.save(m.enableExtensionCmd)
			}
			return m, nil
		}
		if m.Form != nil {
			if msg.String() == "esc" {
				m.Form = nil
				m.Error = ""
				return m, nil
			}
			submit, cmd := m.Form.update(msg)
			if !submit {
				return m, cmd
			}
			sub, err := m.Form.submission(m.Items)
			if err != nil {
				m.Error = err.Error()
				return m, nil
			}
			m.Error = ""
			return m.save(func(gen int) tea.Cmd { return m.saveMCPCmd(sub, gen) })
		}
		if m.ConfirmDelete {
			m.ConfirmDelete = false
			if msg.String() == "enter" && len(m.Items) > 0 {
				return m.save(func(gen int) tea.Cmd { return m.deleteMCPCmd(m.Items[m.Cursor].ID, gen) })
			}
			return m, nil
		}
		if msg.String() == "esc" || msg.String() == "ctrl+c" {
			return m, func() tea.Msg { return CapabilityPageDoneMsg{} }
		}
		if m.Loading {
			return m, nil
		}
		if m.step(msg.String(), len(m.Items)) {
			return m, nil
		}
		mcp := m.Kind == "mcp"
		hasItems := len(m.Items) > 0
		switch msg.String() {
		case "r":
			m.Loading = true
			m.Generation = nextPageGeneration()
			return m, m.loadCmd(m.Generation)
		case "g", "s":
			m.Global = msg.String() == "g"
		case "E":
			if !m.ExtensionEnabled {
				m.ConfirmExtension = true
			}
		case "n":
			if mcp {
				m.openForm(false)
			}
		case "d":
			if mcp && hasItems {
				m.ConfirmDelete = true
			}
		case "e":
			// Re-enables a server switched off in extensions.json.
			if mcp && hasItems {
				if item := m.Items[m.Cursor]; item.Server.Enabled != nil && !*item.Server.Enabled {
					server := item.Server
					enabled := true
					server.Enabled = &enabled
					return m.save(func(gen int) tea.Cmd {
						return m.saveMCPCmd(mcpSubmission{Name: item.ID, Server: server, Secrets: daemon.MCPSecretsPatch{}}, gen)
					})
				}
			}
		case "enter":
			if mcp && hasItems {
				m.openForm(true)
			}
		case "space":
			if hasItems && m.canToggle(m.Items[m.Cursor]) {
				return m.save(func(gen int) tea.Cmd { return m.toggleCmd(m.Items[m.Cursor], gen) })
			}
		case "x":
			if !mcp && hasItems && m.canClear(m.Items[m.Cursor]) {
				return m.save(func(gen int) tea.Cmd { return m.choiceCmd(m.Items[m.Cursor], nil, gen) })
			}
		}
	}
	return m, nil
}
func (m CapabilityPageModel) canToggle(item capabilityItem) bool {
	if m.Kind == "mcp" {
		return true
	}
	candidate := item.Candidate
	return candidate.PreferenceKey != nil && candidate.ShadowedBy == nil && (candidate.Valid || m.selectedEnabled(item))
}

func (m CapabilityPageModel) canClear(item capabilityItem) bool {
	candidate := item.Candidate
	if candidate.PreferenceKey == nil || candidate.ShadowedBy != nil {
		return false
	}
	if m.Global {
		return candidate.GlobalPreference != nil
	}
	return candidate.SessionOverride != nil
}

func (m CapabilityPageModel) selectedEnabled(item capabilityItem) bool {
	if m.Kind != "mcp" {
		if m.Global {
			return item.Candidate.GlobalPreference == nil || *item.Candidate.GlobalPreference
		}
		return item.Candidate.EffectiveEnabled
	}
	if m.Global {
		if value, ok := m.Prefs.Global[m.Kind][item.ID]; ok {
			return value
		}
		return true
	}
	return m.Prefs.Enabled(m.SessionID, m.Kind, item.ID)
}

func enabledLabel(enabled bool) string {
	if enabled {
		return "on"
	}
	return "off"
}

func (m CapabilityPageModel) View() string {
	width := max(1, m.Width)
	mcp := m.Kind == "mcp"
	heading := map[string]string{"skills": "Skills", "instructions": "Instruction files", "mcp": "MCP servers"}[m.Kind]
	scope := "this session"
	if m.Global {
		scope = "global default"
	}
	rows := m.header("/"+m.Kind, scope)
	if m.Loading {
		rows = append(rows, DefaultStyles.Faint.Render("loading "+heading+"…"))
		return strings.Join(rows, "\n")
	}
	if m.ConfirmExtension || (m.ConfirmDelete && len(m.Items) > 0) {
		question := "This will reload this session. Enable the extension?"
		if m.ConfirmDelete {
			question = "Its stored credentials will also be removed. Delete MCP server " + m.Items[m.Cursor].ID + "?"
		}
		rows = append(rows, "")
		for line := range strings.SplitSeq(ansi.Wrap(question, width, " "), "\n") {
			rows = append(rows, DefaultStyles.Warning.Render(line))
		}
		cancel := hint{"esc", "cancel"}
		if m.ConfirmDelete {
			cancel = hint{"any other key", "cancels"}
		}
		rows = append(rows, keyHints(hint{"enter", "confirm"}, cancel))
		return m.fit(rows)
	}
	if !m.ExtensionEnabled {
		rows = append(rows, DefaultStyles.Warning.Render("Extension is off")+DefaultStyles.Decor.Render(" · ")+keyHints(hint{"E", "enable (reloads this session)"}))
	}
	if len(m.Diagnostics) > 0 {
		diagnostic := m.Diagnostics[0]
		if len(m.Diagnostics) > 1 {
			diagnostic = fmt.Sprintf("%d discovery diagnostics: %s", len(m.Diagnostics), diagnostic)
		}
		rows = append(rows, DefaultStyles.Warning.Render(ansi.Truncate(diagnostic, width, "…")))
	}
	if len(m.Items) == 0 {
		rows = append(rows, DefaultStyles.Faint.Render("No "+strings.ToLower(heading)+" found."))
	}
	list := make([]string, len(m.Items))
	for i, item := range m.Items {
		label := DefaultStyles.Success.Render("on ")
		if !m.selectedEnabled(item) || (mcp && item.Server.Enabled != nil && !*item.Server.Enabled) {
			label = DefaultStyles.Faint.Render("off")
		}
		if !mcp {
			switch {
			case !item.Candidate.Valid:
				label = DefaultStyles.Warning.Render("invalid")
			case item.Candidate.ShadowedBy != nil:
				label = DefaultStyles.Faint.Render("shadowed")
			}
		}
		detail := ""
		if mcp {
			detail = DefaultStyles.Faint.Render(" · " + item.Detail)
		}
		list[i] = listRow(i == m.Cursor, label+"  "+item.Title+detail, width)
	}
	rows = append(rows, scrolled(list, m.Cursor, max(1, m.Height-12))...)
	switch {
	case m.Form != nil:
		rows = append(rows, "")
		rows = append(rows, m.Form.view(width)...)
		if m.Saving {
			rows = append(rows, "connecting and reloading…")
		}
	case m.Saving:
		rows = append(rows, "saving and reloading…")
	default:
		if len(m.Items) > 0 {
			selected := m.Items[m.Cursor]
			detail := selected.Detail
			if mcp {
				auth := "no credentials"
				if selected.Secrets.Any() {
					auth = "credentials stored privately"
				}
				detail += " · " + auth
				if selected.Server.Enabled != nil && !*selected.Server.Enabled {
					detail += " · disabled in extensions.json · press e to enable"
				}
			}
			rows = append(rows, "", DefaultStyles.Faint.Render(ansi.Truncate(detail, width, "…")))
			if !mcp {
				candidate := selected.Candidate
				if candidate.Diagnostic != nil {
					rows = append(rows, DefaultStyles.Warning.Render(ansi.Truncate(*candidate.Diagnostic, width, "…")))
				}
				global, session := "default", "inherit"
				if candidate.GlobalPreference != nil {
					global = enabledLabel(*candidate.GlobalPreference)
				}
				if candidate.SessionOverride != nil {
					session = enabledLabel(*candidate.SessionOverride)
				}
				state := fmt.Sprintf("global: %s · session: %s · enabled: %s · eligible on reload: %s", global, session, enabledLabel(candidate.EffectiveEnabled), enabledLabel(candidate.Eligible))
				rows = append(rows, DefaultStyles.Faint.Render(ansi.Truncate(state, width, "…")))
			}
		}
		hints := []hint{{"↑↓", "select"}}
		if len(m.Items) > 0 && m.canToggle(m.Items[m.Cursor]) {
			hints = append(hints, hint{"space", "toggle"})
		}
		if !mcp && len(m.Items) > 0 && m.canClear(m.Items[m.Cursor]) {
			hints = append(hints, hint{"x", "inherit"})
		}
		hints = append(hints, hint{"s", "session"}, hint{"g", "global"}, hint{"r", "refresh"}, hint{"esc", "back"})
		rows = append(rows, "", keyHints(hints...))
		if mcp {
			rows = append(rows, keyHints(hint{"n", "add server"}, hint{"enter", "edit"}, hint{"d", "delete"}))
		}
	}
	return m.fit(rows)
}
