// Package profile provides a thread-safe, in-memory singleton Manager that
// centralizes all configuration file management (agents, channels, runtimes,
// skills, apps, DBaaS/databases, external MCP servers, and default LLM
// settings). It loads YAML files from ~/.agentteam/ at initialization and persists
// mutations back to disk.
package profile

import (
	"context"
	"log"
	"log/slog"
	"os"
	"strings"
	"sync"

	"github.com/pingcap/failpoint"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/crypto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/dbaas"
)

// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------

var (
	defaultManager *Manager
	defaultDir     string
	defaultMu      sync.Mutex
)

// Default returns the global singleton Manager.
func Default() *Manager {
	cfgDir := ConfigDir()
	fpDir := ""
	if val, err := failpoint.Eval("agentteam-config-dir"); err == nil && val != nil {
		fpDir, _ = val.(string)
	}
	if fpDir != "" {
		cfgDir = fpDir
	}

	defaultMu.Lock()
	defer defaultMu.Unlock()
	if defaultManager == nil || defaultDir != cfgDir {
		defaultManager = New(cfgDir)
		defaultDir = cfgDir
	}
	return defaultManager
}

// ---------------------------------------------------------------------------
// YAML file wrappers
// ---------------------------------------------------------------------------

type agentsFile struct {
	Agents []agent.Config `yaml:"agents"`
}

type serversFile struct {
	Servers []AgentTeamServer `yaml:"servers"`
}

const configFileName = "config.yaml"

// ---------------------------------------------------------------------------
// Manager
// ---------------------------------------------------------------------------

// Manager is a thread-safe, in-memory cache backed by YAML files on disk.
// It provides sub-managers for each entity type via Agents(), Servers(),
// Runtimes(), Skills(), DBaaS(), Databases(), and MCPServers().
type Manager struct {
	mu        sync.RWMutex
	configDir string

	cfg           *Config
	agents        map[string]*agent.Config
	agentIndex    agentIndexState
	servers       []AgentTeamServer
	runtimes      map[string]*runtimeconfig.Runtime
	skills        map[string]*Skill
	dbaasServices map[string]*dbaas.Service
	databases     map[string]*dbaas.Database
	mcpServers    map[string]*MCPServer

	agentMgr     *AgentManager
	serverMgr    *ServerManager
	runtimeMgr   *RuntimeManager
	skillMgr     *SkillManager
	dbaasMgr     *DBaaSManager
	databaseMgr  *DatabaseManager
	mcpServerMgr *MCPServerManager

	runtimeDetectCancel              context.CancelFunc
	runtimeDetectChangeHandlers      map[int]RuntimeDetectChangeHandler
	nextRuntimeDetectChangeHandlerID int
	cachedDetectResults              []runtimeconfig.DetectResult
	watcher                          *FileWatcher
}

// New creates a Manager that reads all YAML files from configDir.
func New(cfgDir string) *Manager {
	m := &Manager{
		configDir:     cfgDir,
		agents:        make(map[string]*agent.Config),
		runtimes:      make(map[string]*runtimeconfig.Runtime),
		skills:        make(map[string]*Skill),
		dbaasServices: make(map[string]*dbaas.Service),
		databases:     make(map[string]*dbaas.Database),
		mcpServers:    make(map[string]*MCPServer),
	}
	m.agentMgr = &AgentManager{mgr: m}
	m.agentMgr.rebuildAgentIndexLocked()
	m.serverMgr = &ServerManager{
		mgr:     m,
		runtime: newServerRuntimeControl(),
		locks:   make(map[string]*sync.Mutex),
	}
	m.runtimeMgr = &RuntimeManager{mgr: m}
	m.skillMgr = &SkillManager{mgr: m}
	m.dbaasMgr = &DBaaSManager{mgr: m}
	m.databaseMgr = &DatabaseManager{mgr: m}
	m.mcpServerMgr = &MCPServerManager{mgr: m}

	if err := os.MkdirAll(cfgDir, 0755); err != nil {
		slog.Warn("create config directory", "dir", cfgDir, "error", err)
	}
	m.Reload()
	m.runtimeMgr.detectRuntimes()
	return m
}

// Reload refreshes all in-memory caches from disk.
func (m *Manager) Reload() {
	serverMode := m.Servers().LifecycleMode()
	if serverMode == ServerLifecycleActive || serverMode == ServerLifecyclePreparing {
		_ = m.ReloadServersChecked()
		m.mu.Lock()
		defer m.mu.Unlock()
		m.reloadLockedWithoutServers()
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	m.reloadLocked()
}

// ---------------------------------------------------------------------------
// Sub-managers
// ---------------------------------------------------------------------------

// Agents returns the agent configuration manager.
func (m *Manager) Agents() *AgentManager { return m.agentMgr }

// Servers returns the AgentTeam server auth configuration manager.
func (m *Manager) Servers() *ServerManager { return m.serverMgr }

// Runtimes returns the runtime detection/configuration manager.
func (m *Manager) Runtimes() *RuntimeManager { return m.runtimeMgr }

// Skills returns the local skill configuration manager.
func (m *Manager) Skills() *SkillManager { return m.skillMgr }

// DBaaS returns the managed database service configuration manager.
func (m *Manager) DBaaS() *DBaaSManager { return m.dbaasMgr }

// Databases returns the database configuration manager.
func (m *Manager) Databases() *DatabaseManager { return m.databaseMgr }

// MCPServers returns the external MCP server configuration manager.
func (m *Manager) MCPServers() *MCPServerManager { return m.mcpServerMgr }

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

// Config returns a snapshot of the currently loaded top-level configuration.
func (m *Manager) Config() *Config {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return cloneConfig(m.cfg)
}

// SetLLMConfig updates the LLM configuration and persists it to config.yaml.
func (m *Manager) SetLLMConfig(llm LLM) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	saved, err := UpdateConfig(m.configPath(), func(cfg *Config) error {
		cfg.LLM = llm
		return nil
	})
	if err != nil {
		return err
	}
	m.cfg = saved
	return nil
}

// ConfigDir returns the configuration directory path.
func (m *Manager) ConfigDir() string {
	return m.configDir
}

// ---------------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------------

func (m *Manager) configPath() string {
	return m.filePath(configFileName)
}

func (m *Manager) filePath(name string) string {
	return m.configDir + "/" + name
}

func (m *Manager) reloadLocked() {
	cfg, err := Load(m.configPath())
	if err != nil {
		log.Printf("profile: load config: %v", err)
		cfg = DefaultConfig()
	}
	m.cfg = cfg

	if len(cfg.Security.PublicKey) > 0 && len(cfg.Security.PrivateKey) > 0 {
		if err := crypto.SetKeys(
			[]byte(cfg.Security.PublicKey),
			[]byte(cfg.Security.PrivateKey),
		); err != nil {
			log.Printf("profile: init crypto keys: %v", err)
		}
	}

	reloadAgents(m)
	reloadServers(m)
	reloadSkills(m)
	reloadDBaaS(m)
	reloadDatabases(m)
	reloadMCPServers(m)

	m.ensureDefaultAgent()
}

func (m *Manager) reloadLockedWithoutServers() {
	cfg, err := Load(m.configPath())
	if err != nil {
		log.Printf("profile: load config: %v", err)
		cfg = DefaultConfig()
	}
	m.cfg = cfg

	if len(cfg.Security.PublicKey) > 0 && len(cfg.Security.PrivateKey) > 0 {
		if err := crypto.SetKeys(
			[]byte(cfg.Security.PublicKey),
			[]byte(cfg.Security.PrivateKey),
		); err != nil {
			log.Printf("profile: init crypto keys: %v", err)
		}
	}

	reloadAgents(m)
	reloadSkills(m)
	reloadDBaaS(m)
	reloadDatabases(m)
	reloadMCPServers(m)
	m.ensureDefaultAgent()
}

func (m *Manager) ensureDefaultAgent() {
	defaultID := strings.TrimSpace(m.cfg.DefaultAgent)

	if defaultID != "" {
		if _, ok := m.agents[defaultID]; ok {
			return
		}
		slog.Info("default agent no longer exists, re-electing", "old_default", defaultID)
	}

	m.cfg.DefaultAgent = ""
	sorted := m.agentMgr.sortedAgents()
	if len(sorted) > 0 {
		m.cfg.DefaultAgent = sorted[0].ID
	}

	if m.cfg.DefaultAgent != "" {
		slog.Info("elected default agent", "agent", m.cfg.DefaultAgent)
	}

	desiredDefault := m.cfg.DefaultAgent
	saved, err := UpdateConfig(m.configPath(), func(cfg *Config) error {
		cfg.DefaultAgent = desiredDefault
		return nil
	})
	if err != nil {
		log.Printf("profile: save default agent: %v", err)
	} else {
		m.cfg = saved
	}
}

func (m *Manager) startDetectContext() (context.Context, context.CancelFunc) {
	return context.WithCancel(context.Background())
}
