/*
 * ChatCLI - Persona System
 * Copyright (c) 2024 Edilson Freitas
 * License: Apache-2.0
 */
package persona

import (
	"fmt"
	"sort"
	"sync"

	"github.com/diillson/chatcli/pkg/persona/usage"
	"go.uber.org/zap"
)

// Manager handles the active persona state
type Manager struct {
	logger  *zap.Logger
	loader  *Loader
	builder *Builder

	// Changed from single activeAgent to activeAgents map
	activeAgents map[string]*Agent
	activePrompt *ComposedPrompt
	mu           sync.RWMutex

	// usage records which skills get auto-activated, for analytics.
	usage *usage.Store
}

// NewManager creates a new persona manager
func NewManager(logger *zap.Logger) *Manager {
	loader := NewLoader(logger)
	return &Manager{
		logger:       logger,
		loader:       loader,
		builder:      NewBuilder(logger, loader),
		activeAgents: make(map[string]*Agent),
		usage:        usage.Default(),
	}
}

// Initialize sets up the persona system (creates directories if needed)
func (m *Manager) Initialize() error {
	return m.loader.EnsureDirectories()
}

// SetProjectDir sets the project directory for local skills
func (m *Manager) SetProjectDir(dir string) {
	m.loader.SetProjectDir(dir)
}

// LoadAgent (Legacy/Reset Mode): Clears all agents and loads specific one
func (m *Manager) LoadAgent(name string) (*LoadResult, error) {
	m.UnloadAllAgents() // Clear all first
	return m.AttachAgent(name)
}

// AttachAgent adds an agent to the active pool without removing others
func (m *Manager) AttachAgent(name string) (*LoadResult, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	// 1. Check if already attached
	if _, exists := m.activeAgents[name]; exists {
		return nil, fmt.Errorf("agent '%s' is already attached", name)
	}

	// 2. Load agent from disk
	agent, err := m.loader.GetAgent(name)
	if err != nil {
		return nil, err
	}

	// 3. Temporarily add to map to build prompt
	m.activeAgents[name] = agent

	// 4. Rebuild the composite prompt with all agents
	composed, err := m.rebuildPromptInternal()
	if err != nil {
		delete(m.activeAgents, name) // Rollback
		return nil, err
	}

	m.activePrompt = composed

	m.logger.Info("Agent attached", zap.String("name", name), zap.Int("total_active", len(m.activeAgents)))

	return &LoadResult{
		Agent:         agent,
		LoadedSkills:  composed.SkillsLoaded,
		MissingSkills: composed.SkillsMissing,
	}, nil
}

// DetachAgent removes a specific agent
func (m *Manager) DetachAgent(name string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if _, exists := m.activeAgents[name]; !exists {
		return fmt.Errorf("agent '%s' is not active", name)
	}

	delete(m.activeAgents, name)

	// If no agents left, clean prompt
	if len(m.activeAgents) == 0 {
		m.activePrompt = nil
		return nil
	}

	// Rebuild prompt with remaining agents
	composed, err := m.rebuildPromptInternal()
	if err != nil {
		return fmt.Errorf("error rebuilding prompt after detach: %w", err)
	}
	m.activePrompt = composed

	m.logger.Info("Agent detached", zap.String("name", name))
	return nil
}

// UnloadAllAgents (antigo UnloadAgent) clears everything
func (m *Manager) UnloadAllAgents() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.activeAgents = make(map[string]*Agent)
	m.activePrompt = nil
	m.logger.Info("All agents unloaded")
}

// UnloadAgent (legacy) - alias for backward compatibility
func (m *Manager) UnloadAgent() {
	m.UnloadAllAgents()
}

// Helper internal (chame SEMPRE dentro de um Lock)
// Pega todos os agentes em m.activeAgents e chama o Builder
func (m *Manager) rebuildPromptInternal() (*ComposedPrompt, error) {
	// Convert map to slice for the builder
	agents := make([]*Agent, 0, len(m.activeAgents))
	for _, a := range m.activeAgents {
		agents = append(agents, a)
	}

	// Sort by name for deterministic prompts
	sort.Slice(agents, func(i, j int) bool {
		return agents[i].Name < agents[j].Name
	})

	return m.builder.BuildMultiAgentPrompt(agents)
}

// GetActiveAgents returns list of active agents
func (m *Manager) GetActiveAgents() []*Agent {
	m.mu.RLock()
	defer m.mu.RUnlock()

	list := make([]*Agent, 0, len(m.activeAgents))
	for _, a := range m.activeAgents {
		list = append(list, a)
	}
	return list
}

// GetActiveAgent returns the first active agent (legacy compatibility)
func (m *Manager) GetActiveAgent() *Agent {
	m.mu.RLock()
	defer m.mu.RUnlock()
	for _, a := range m.activeAgents {
		return a
	}
	return nil
}

// GetActivePrompt returns the composed prompt for the active agent
func (m *Manager) GetActivePrompt() *ComposedPrompt {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.activePrompt
}

// GetSystemPrompt returns the full system prompt string for the active agent
func (m *Manager) GetSystemPrompt() string {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.activePrompt == nil {
		return ""
	}
	return m.activePrompt.FullPrompt
}

// HasActiveAgent returns true if an agent is currently loaded
func (m *Manager) HasActiveAgent() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return len(m.activeAgents) > 0
}

// ListAgents returns all available agents
func (m *Manager) ListAgents() ([]*Agent, error) {
	return m.loader.ListAgents()
}

// ListSkills returns all available skills
func (m *Manager) ListSkills() ([]*Skill, error) {
	return m.loader.ListSkills()
}

// GetAgentsDir returns the path to the agents directory
func (m *Manager) GetAgentsDir() string {
	return m.loader.GetAgentsDir()
}

// GetSkillsDir returns the path to the skills directory
func (m *Manager) GetSkillsDir() string {
	return m.loader.GetSkillsDir()
}

// GetLoader returns the underlying loader for advanced callers (e.g., the worker system).
func (m *Manager) GetLoader() *Loader {
	return m.loader
}

// RefreshSkills re-scans skill directories to pick up newly installed or removed skills.
func (m *Manager) RefreshSkills() ([]*Skill, error) {
	return m.loader.ListSkills()
}

// GetSkill loads a skill by name, delegating to the loader.
func (m *Manager) GetSkill(name string) (*Skill, error) {
	return m.loader.GetSkill(name)
}

// FindTriggeredSkills returns skills whose triggers match the given text.
// Skills with DisableModelInvocation=true are excluded.
func (m *Manager) FindTriggeredSkills(text string) []*Skill {
	allSkills, err := m.loader.ListSkills()
	if err != nil {
		return nil
	}
	var matched []*Skill
	for _, skill := range allSkills {
		if skill.DisableModelInvocation {
			continue
		}
		if skill.MatchesTrigger(text) {
			matched = append(matched, skill)
		}
	}
	return matched
}

// FindPathMatchedSkills returns skills whose `paths:` globs match any of the
// given file paths. Skills with DisableModelInvocation=true are excluded.
// filePaths should be forward-slash normalized; MatchesPath re-normalizes
// defensively so callers may pass OS paths too.
func (m *Manager) FindPathMatchedSkills(filePaths []string) []*Skill {
	if len(filePaths) == 0 {
		return nil
	}
	allSkills, err := m.loader.ListSkills()
	if err != nil {
		return nil
	}
	var matched []*Skill
	for _, skill := range allSkills {
		if skill.DisableModelInvocation {
			continue
		}
		if skill.MatchesPath(filePaths) {
			matched = append(matched, skill)
		}
	}
	return matched
}

// FindAutoActivatedSkills merges trigger-based and path-based activation,
// deduplicating by skill name. Returned slice is safe to iterate in any order.
func (m *Manager) FindAutoActivatedSkills(text string, filePaths []string) []*Skill {
	seen := make(map[string]bool)
	var out []*Skill
	for _, s := range m.FindTriggeredSkills(text) {
		if !seen[s.Name] {
			seen[s.Name] = true
			out = append(out, s)
		}
	}
	for _, s := range m.FindPathMatchedSkills(filePaths) {
		if !seen[s.Name] {
			seen[s.Name] = true
			out = append(out, s)
		}
	}
	// Record activations asynchronously so analytics never slow a turn.
	if m.usage != nil && len(out) > 0 {
		names := make([]string, 0, len(out))
		for _, s := range out {
			names = append(names, s.Name)
		}
		go m.usage.Record(names...)
	}
	return out
}

// UsageRanking returns skill activation analytics, most-used first.
func (m *Manager) UsageRanking() []usage.Ranked {
	if m.usage == nil {
		return nil
	}
	return m.usage.Ranking()
}

// GetSkillByName resolves a skill by its exact name, preferring project-local
// over global sources (delegates to loader.GetSkill).
func (m *Manager) GetSkillByName(name string) (*Skill, error) {
	return m.loader.GetSkill(name)
}

// ListAllSkills returns every loadable skill (installed or built-in), for
// use by completers and the /<skill-name> router. Skills without a Name set
// are filtered out.
func (m *Manager) ListAllSkills() []*Skill {
	skills, err := m.loader.ListSkills()
	if err != nil {
		return nil
	}
	out := skills[:0:len(skills)]
	for _, s := range skills {
		if s != nil && s.Name != "" {
			out = append(out, s)
		}
	}
	return out
}

// ValidateAgent checks if an agent configuration is valid
func (m *Manager) ValidateAgent(name string) ([]string, []string, error) {
	return m.builder.ValidateAgent(name)
}
