package profile

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync/atomic"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
)

// AgentManager provides CRUD operations for agent configurations.
type AgentManager struct {
	mgr *Manager
}

// Get returns the agent with the given ID. Orphan cleanup is an explicit
// lifecycle operation; reads must not mutate the profile while resolving an
// Agent for a live runtime connection.
func (am *AgentManager) Get(id string) (agent.Config, bool) {
	am.mgr.mu.RLock()
	defer am.mgr.mu.RUnlock()
	a, ok := am.mgr.agents[id]
	if !ok {
		return agent.Config{}, false
	}
	return *a, true
}

// All returns all configured agents sorted by ID.
// It also cleans up orphaned agentteam-synced agents whose source channel no
// longer exists.
func (am *AgentManager) All() []agent.Config {
	am.cleanupOrphanAgents()
	am.mgr.mu.RLock()
	defer am.mgr.mu.RUnlock()
	return am.sortedAgents()
}

// Count returns the number of configured agents.
func (am *AgentManager) Count() int {
	am.mgr.mu.RLock()
	defer am.mgr.mu.RUnlock()
	return len(am.mgr.agents)
}

type agentIndexEntry struct {
	ID         string
	SearchText string
}

type agentIndexState struct {
	revision string
	entries  []agentIndexEntry
}

var agentRevisionFallback uint64

func newAgentIndexRevision() string {
	var raw [12]byte
	if _, err := rand.Read(raw[:]); err == nil {
		return hex.EncodeToString(raw[:])
	}
	return fmt.Sprintf("fallback-%d", atomic.AddUint64(&agentRevisionFallback, 1))
}

// rebuildAgentIndexLocked publishes the immutable deterministic ordering and a
// fresh collection revision. Callers hold mgr.mu whenever the manager is
// shared; the constructor calls it before publication.
func (am *AgentManager) rebuildAgentIndexLocked() {
	entries := make([]agentIndexEntry, 0, len(am.mgr.agents))
	for id, configured := range am.mgr.agents {
		entries = append(entries, agentIndexEntry{
			ID:         id,
			SearchText: strings.ToLower(id + " " + configured.Name),
		})
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].ID < entries[j].ID })
	am.mgr.agentIndex = agentIndexState{revision: newAgentIndexRevision(), entries: entries}
}

// PageResult's scan budget bounds index work independently of the number of
// configured Agents. It copies only limit-plus-one matching configs, binds a
// cursor to the normalized query and owning collection revision, and never
// sorts or materializes the complete configured-agent map per request.
const (
	// A substring filter still has to inspect index entries. A call may
	// examine at most this many entries; when the budget is consumed the
	// continuation advances to the last examined entry and reports HasMore.
	agentPageScanBudget   = 256
	agentPageDefaultLimit = 20
	agentPageMaxLimit     = 100
)

// AgentPageResult is the bounded result of one indexed Agent page. NextAnchor
// may be an entry that did not match the filter when a sparse scan consumed the
// work budget; callers must use it rather than the last returned Agent ID.
type AgentPageResult struct {
	Agents     []agent.Config
	HasMore    bool
	Revision   string
	NextAnchor string
	Examined   int
}

// Page is the compatibility wrapper for callers that only need returned
// configs. Exec uses PageResult so sparse continuation can advance over
// non-matching entries without rescanning them indefinitely.
func (am *AgentManager) Page(query, revision, afterID string, limit int) ([]agent.Config, bool, string, error) {
	page, err := am.PageResult(query, revision, afterID, limit)
	if err != nil {
		return nil, false, "", err
	}
	return page.Agents, page.HasMore, page.Revision, nil
}

// PageResult returns a deterministic page from the immutable Agent index. It
// copies only limit-plus-one configs, examines at most agentPageScanBudget
// index entries, binds continuation to the collection revision, and never
// sorts or materializes the complete configured-agent map per request.
func (am *AgentManager) PageResult(query, revision, afterID string, limit int) (AgentPageResult, error) {
	// PageResult is intentionally independent of orphan cleanup: cleanup scans
	// every configured Agent and may perform disk work, which would defeat the
	// bounded scan contract. Lifecycle/configuration paths call cleanup before
	// publishing the immutable index.
	query = strings.ToLower(strings.TrimSpace(query))
	revision = strings.TrimSpace(revision)
	afterID = strings.TrimSpace(afterID)
	am.mgr.mu.RLock()
	defer am.mgr.mu.RUnlock()
	index := am.mgr.agentIndex
	if afterID != "" && revision == "" {
		return AgentPageResult{}, agent.NewPageError(agent.PageErrorMalformed, "agent page cursor is malformed: revision is required")
	}
	if revision != "" && revision != index.revision {
		return AgentPageResult{}, agent.NewPageError(agent.PageErrorStaleRevision, "agent page cursor is stale: collection revision changed")
	}
	if limit <= 0 {
		limit = agentPageDefaultLimit
	}
	if limit > agentPageMaxLimit {
		return AgentPageResult{}, agent.NewPageError(agent.PageErrorLimit, "agent page limit exceeds 100")
	}
	start := 0
	if afterID != "" {
		anchor := sort.Search(len(index.entries), func(i int) bool { return index.entries[i].ID >= afterID })
		if anchor >= len(index.entries) || index.entries[anchor].ID != afterID {
			return AgentPageResult{}, agent.NewPageError(agent.PageErrorStaleAnchor, "agent page cursor is stale: anchor was deleted")
		}
		// The anchor is the last examined entry, not necessarily a member of
		// the query. Query binding is validated by the opaque Exec cursor and
		// the collection revision makes renamed/filter-membership changes stale.
		start = anchor + 1
	}

	result := make([]agent.Config, 0, limit+1)
	lastExamined := ""
	examined := 0
	position := start
	for ; position < len(index.entries) && examined < agentPageScanBudget; position++ {
		entry := index.entries[position]
		examined++
		lastExamined = entry.ID
		if query != "" && !strings.Contains(entry.SearchText, query) {
			continue
		}
		configured, ok := am.mgr.agents[entry.ID]
		if !ok {
			return AgentPageResult{}, agent.NewPageError(agent.PageErrorStaleAnchor, "agent page index is stale")
		}
		result = append(result, *configured)
		if len(result) == limit+1 {
			// Preserve the extra matching entry for the next call. The cursor
			// points at the last returned match, not lastExamined.
			lastExamined = result[limit-1].ID
			result = result[:limit]
			return AgentPageResult{Agents: result, HasMore: true, Revision: index.revision, NextAnchor: lastExamined, Examined: examined}, nil
		}
	}

	hasMore := position < len(index.entries)
	if hasMore && lastExamined == "" {
		return AgentPageResult{}, agent.NewPageError(agent.PageErrorStaleAnchor, "agent page cursor has no examined anchor")
	}
	return AgentPageResult{
		Agents: result, HasMore: hasMore, Revision: index.revision, NextAnchor: lastExamined, Examined: examined,
	}, nil
}

// Add adds a new agent configuration.
// If no default agent is configured, the new agent becomes the default.
// If the agent ID is empty, a unique ID is auto-generated with prefix "agt_".
func (am *AgentManager) Add(a agent.Config) error {
	if strings.TrimSpace(a.ID) == "" {
		a.ID = generateAgentID()
	}
	if err := agent.ValidateAgent(a); err != nil {
		return err
	}
	am.mgr.mu.Lock()
	defer am.mgr.mu.Unlock()
	if _, ok := am.mgr.agents[a.ID]; ok {
		return fmt.Errorf("agent %q already exists", a.ID)
	}
	am.mgr.agents[a.ID] = &a
	am.rebuildAgentIndexLocked()
	if err := am.saveAgentsLocked(); err != nil {
		return err
	}
	if strings.TrimSpace(am.mgr.cfg.DefaultAgent) == "" {
		saved, err := UpdateConfig(am.mgr.configPath(), func(cfg *Config) error {
			if strings.TrimSpace(cfg.DefaultAgent) == "" {
				cfg.DefaultAgent = a.ID
			}
			return nil
		})
		if err != nil {
			log.Printf("profile: save default agent: %v", err)
		} else {
			am.mgr.cfg = saved
		}
	}
	return nil
}

// Update updates an existing agent configuration.
func (am *AgentManager) Update(a agent.Config) error {
	if err := agent.ValidateAgent(a); err != nil {
		return err
	}
	am.mgr.mu.Lock()
	defer am.mgr.mu.Unlock()
	if _, ok := am.mgr.agents[a.ID]; !ok {
		return fmt.Errorf("agent %q not found", a.ID)
	}
	am.mgr.agents[a.ID] = &a
	am.rebuildAgentIndexLocked()
	return am.saveAgentsLocked()
}

// Delete removes an agent by ID.
// If the persisted default was removed, a new default is elected.
func (am *AgentManager) Delete(id string) error {
	am.mgr.mu.Lock()
	defer am.mgr.mu.Unlock()
	if _, ok := am.mgr.agents[id]; !ok {
		return fmt.Errorf("agent %q not found", id)
	}
	delete(am.mgr.agents, id)
	am.rebuildAgentIndexLocked()
	if err := am.saveAgentsLocked(); err != nil {
		return err
	}
	am.persistDefaultAfterAgentRemovalLocked(
		[]string{id},
		UpdateConfig,
		"elected new default agent after removal",
		"failed to save default agent after removal",
	)
	return nil
}

// Default returns the ID of the configured default agent.
func (am *AgentManager) Default() string {
	am.mgr.mu.RLock()
	defer am.mgr.mu.RUnlock()
	return am.mgr.cfg.DefaultAgent
}

// SetDefault sets the default agent ID and persists the config.
func (am *AgentManager) SetDefault(id string) error {
	am.mgr.mu.Lock()
	defer am.mgr.mu.Unlock()
	if id != "" {
		if _, ok := am.mgr.agents[id]; !ok {
			return fmt.Errorf("agent %q not found", id)
		}
	}
	saved, err := UpdateConfig(am.mgr.configPath(), func(cfg *Config) error {
		cfg.DefaultAgent = id
		return nil
	})
	if err != nil {
		return err
	}
	am.mgr.cfg = saved
	return nil
}

func (am *AgentManager) sortedAgents() []agent.Config {
	result := make([]agent.Config, 0, len(am.mgr.agents))
	for _, a := range am.mgr.agents {
		result = append(result, *a)
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i].ID < result[j].ID
	})
	return result
}

func (am *AgentManager) saveAgentsLocked() error {
	return saveYAMLFile(agentsFileFromMap(am.mgr.agents), am.mgr.filePath("agents.yaml"), am.mgr)
}

func (am *AgentManager) saveAgentsMapLocked(agents map[string]*agent.Config) error {
	return saveYAMLFileLocked(agentsFileFromMap(agents), am.mgr.filePath("agents.yaml"), am.mgr)
}

type configUpdateFunc func(string, func(*Config) error) (*Config, error)

func (am *AgentManager) persistDefaultAfterAgentRemovalLocked(removed []string, update configUpdateFunc, electedLogMessage, warnLogMessage string) {
	if len(removed) == 0 {
		return
	}

	removedIDs := make(map[string]struct{}, len(removed))
	for _, id := range removed {
		removedIDs[id] = struct{}{}
	}
	nextDefault := ""
	sorted := am.sortedAgents()
	if len(sorted) > 0 {
		nextDefault = sorted[0].ID
	}
	changed := false

	saved, err := update(am.mgr.configPath(), func(cfg *Config) error {
		if !defaultAgentNeedsReelection(cfg.DefaultAgent, removedIDs, am.mgr.agents) {
			return nil
		}
		cfg.DefaultAgent = nextDefault
		changed = true
		return nil
	})
	if err != nil {
		slog.Warn(warnLogMessage, "error", err)
		return
	}
	am.mgr.cfg = saved
	if changed && saved.DefaultAgent != "" {
		slog.Info(electedLogMessage, "agent", saved.DefaultAgent)
	}
}

func defaultAgentNeedsReelection(defaultAgent string, removedIDs map[string]struct{}, agents map[string]*agent.Config) bool {
	defaultID := strings.TrimSpace(defaultAgent)
	if defaultID == "" {
		return defaultAgent != ""
	}
	if _, removed := removedIDs[defaultID]; removed {
		return true
	}
	if _, exists := agents[defaultID]; !exists {
		return true
	}
	return false
}

// deleteBySourceLocked removes all agents whose Source.Type == "agentteam" and
// Source.ID == serverID. Must be called with the mutex held.
func (am *AgentManager) deleteBySourceLocked(serverID string) error {
	agents, err := loadAgentsMap(am.mgr.filePath("agents.yaml"))
	if err != nil {
		return err
	}

	var removed []string
	for id, a := range agents {
		if a.Source.Type == "agentteam" && a.Source.ID == serverID {
			removed = append(removed, id)
		}
	}
	if len(removed) == 0 {
		am.mgr.agents = agents
		am.rebuildAgentIndexLocked()
		return nil
	}
	for _, id := range removed {
		delete(agents, id)
		slog.Info("removed agent from deleted server", "agent_id", id, "server_id", serverID)
	}
	if err := am.saveAgentsMapLocked(agents); err != nil {
		return err
	}
	am.mgr.agents = agents
	am.rebuildAgentIndexLocked()

	am.persistDefaultAfterAgentRemovalLocked(
		removed,
		updateConfigLocked,
		"elected new default agent after channel deletion",
		"failed to save default agent after channel deletion",
	)
	return nil
}

// CleanupOrphans removes agentteam-synced agents whose source channel no
// longer exists. This handles the case where a channel was removed outside
// of the normal delete flow (e.g., manual config file edit). It also deletes
// the orphaned agents' workspace directories under ~/.agentteam/agents/<id>/.
func (am *AgentManager) CleanupOrphans() {
	am.cleanupOrphanAgents()
}

func (am *AgentManager) cleanupOrphanAgents() {
	am.mgr.mu.Lock()
	defer am.mgr.mu.Unlock()

	// Build set of existing AgentTeam server IDs.
	serverIDs := make(map[string]struct{}, len(am.mgr.servers))
	for _, server := range am.mgr.servers {
		serverIDs[server.ID] = struct{}{}
	}

	var orphans []string
	for id, a := range am.mgr.agents {
		if a.Source.Type == "agentteam" && a.Source.ID != "" {
			if _, exists := serverIDs[a.Source.ID]; !exists {
				orphans = append(orphans, id)
			}
		}
	}

	if len(orphans) == 0 {
		return
	}

	sort.Strings(orphans)
	for _, id := range orphans {
		delete(am.mgr.agents, id)
		slog.Info("removed orphan agent whose source channel no longer exists", "agent_id", id)
		if err := os.RemoveAll(AgentWorkDir(id)); err != nil {
			slog.Warn("failed to remove orphan agent workspace", "agent_id", id, "error", err)
		} else {
			slog.Info("removed orphan agent workspace", "agent_id", id, "dir", filepath.Base(AgentWorkDir(id)))
		}
	}
	am.rebuildAgentIndexLocked()
	if err := am.saveAgentsLocked(); err != nil {
		slog.Warn("failed to save orphan cleanup agent state", "error", err)
	}

	am.persistDefaultAfterAgentRemovalLocked(
		orphans,
		UpdateConfig,
		"elected new default agent after orphan cleanup",
		"failed to save default agent after orphan cleanup",
	)
}

// reloadAgents loads agents.yaml into the Manager's agents map.
func reloadAgents(m *Manager) {
	agents, err := loadAgentsMap(m.filePath("agents.yaml"))
	if err != nil {
		log.Printf("profile: load agents: %v", err)
		return
	}
	m.agents = agents
	m.agentMgr.rebuildAgentIndexLocked()
}

func loadAgentsMap(path string) (map[string]*agent.Config, error) {
	af := &agentsFile{}
	if err := loadYAMLFile(path, af); err != nil {
		return nil, err
	}
	agents := make(map[string]*agent.Config, len(af.Agents))
	for i := range af.Agents {
		a := af.Agents[i]
		if a.Source.IsZero() {
			a.Source = agent.NewLocalSource()
		}
		agents[a.ID] = &a
	}
	return agents, nil
}

func agentsFileFromMap(agents map[string]*agent.Config) agentsFile {
	af := agentsFile{Agents: make([]agent.Config, 0, len(agents))}
	for _, a := range agents {
		af.Agents = append(af.Agents, *a)
	}
	return af
}
