package profile

import (
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// ManagedKnowledgeBaseRepositoryRecord describes one KB source repository that
// Desktop has synced into an agent workspace under kb/managed/. The server is
// the authority for access; this YAML is a local sync/index cache only.
type ManagedKnowledgeBaseRepositoryRecord struct {
	AgentID                  string    `yaml:"agent_id"`
	ScopeKey                 string    `yaml:"scope_key"`
	KnowledgeBaseID          string    `yaml:"knowledge_base_id"`
	KnowledgeBaseName        string    `yaml:"knowledge_base_name"`
	KnowledgeBaseDescription string    `yaml:"knowledge_base_description,omitempty"`
	SourceID                 string    `yaml:"source_id"`
	SourceName               string    `yaml:"source_name"`
	SourceDescription        string    `yaml:"source_description,omitempty"`
	SourceType               string    `yaml:"source_type,omitempty"`
	RepoID                   string    `yaml:"repo_id"`
	RepoRole                 string    `yaml:"repo_role"`
	CloneURL                 string    `yaml:"clone_url"`
	Branch                   string    `yaml:"branch"`
	Revision                 string    `yaml:"revision,omitempty"`
	BarePath                 string    `yaml:"bare_path"`
	WorktreePath             string    `yaml:"worktree_path"`
	AccessProfile            string    `yaml:"access_profile,omitempty"`
	Writable                 bool      `yaml:"writable,omitempty"`
	LastSyncAt               time.Time `yaml:"last_sync_at,omitempty"`
	LastSyncState            string    `yaml:"last_sync_state,omitempty"`
	LastError                string    `yaml:"last_error,omitempty"`
	LastAttemptAt            time.Time `yaml:"last_attempt_at,omitempty"`
	RetryCount               int       `yaml:"retry_count,omitempty"`
}

type managedKnowledgeBaseRepositoriesFile struct {
	Repositories []ManagedKnowledgeBaseRepositoryRecord `yaml:"repositories"`
	ActiveScopes map[string]string                      `yaml:"active_scopes,omitempty"`
}

type ManagedKnowledgeBaseRepositoryManager struct {
	mgr *Manager
}

func (m *Manager) ManagedKnowledgeBaseRepositories() *ManagedKnowledgeBaseRepositoryManager {
	return &ManagedKnowledgeBaseRepositoryManager{mgr: m}
}

func (m *ManagedKnowledgeBaseRepositoryManager) Dir() string {
	root := filepath.Join(m.mgr.configDir, "managed-kbs")
	return root
}

func (m *ManagedKnowledgeBaseRepositoryManager) BareRepositoryPath(repoID string) string {
	safeRepoID := sanitizeRepoIDForPath(repoID)
	return filepath.Join(m.Dir(), "bare", safeRepoID+".git")
}

func (m *ManagedKnowledgeBaseRepositoryManager) AgentWorkDir(agentID string) string {
	trimmedAgentID := strings.TrimSpace(agentID)
	return filepath.Join(m.mgr.configDir, "agents", trimmedAgentID)
}

func (m *ManagedKnowledgeBaseRepositoryManager) WorktreePath(agentID, kbID, sourceID, repoID, repoRole string) string {
	safeRepoDir := sanitizeRepoIDForPath(repoID + "-" + repoRole)
	return filepath.Join(m.AgentWorkDir(agentID), "kb", "managed", sanitizeRepoIDForPath(kbID), sanitizeRepoIDForPath(sourceID), safeRepoDir)
}

func (m *ManagedKnowledgeBaseRepositoryManager) filePath() string {
	fileName := "managed_kbs.yaml"
	return m.mgr.filePath(fileName)
}

func (m *ManagedKnowledgeBaseRepositoryManager) All() []ManagedKnowledgeBaseRepositoryRecord {
	m.mgr.mu.RLock()
	defer m.mgr.mu.RUnlock()
	sf := &managedKnowledgeBaseRepositoriesFile{}
	if err := loadYAMLFile(m.filePath(), sf); err != nil {
		slog.Debug("profile: load managed_kbs.yaml", "error", err)
		return nil
	}
	return sf.Repositories
}

func (m *ManagedKnowledgeBaseRepositoryManager) RecordsForAgent(agentID string) []ManagedKnowledgeBaseRepositoryRecord {
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return nil
	}
	all := m.All()
	out := make([]ManagedKnowledgeBaseRepositoryRecord, 0, len(all))
	for _, rec := range all {
		if strings.TrimSpace(rec.AgentID) == agentID {
			out = append(out, rec)
		}
	}
	return out
}

// ActivateAgentScope switches the runtime-visible managed KB snapshot for one
// agent. A scope change removes the prior managed worktrees before the new
// scope is allowed to query or materialize access. Bare repositories remain
// available for a later authorized sync.
func (m *ManagedKnowledgeBaseRepositoryManager) ActivateAgentScope(agentID, scopeKey string) (bool, error) {
	agentID = strings.TrimSpace(agentID)
	scopeKey = strings.TrimSpace(scopeKey)
	if agentID == "" || scopeKey == "" {
		return false, errors.New("managed kb repository: agent_id and scope_key are required")
	}

	m.mgr.mu.Lock()
	defer m.mgr.mu.Unlock()
	sf := &managedKnowledgeBaseRepositoriesFile{}
	if err := loadYAMLFile(m.filePath(), sf); err != nil {
		return false, fmt.Errorf("load managed_kbs.yaml: %w", err)
	}
	if sf.ActiveScopes != nil && strings.TrimSpace(sf.ActiveScopes[agentID]) == scopeKey {
		return false, nil
	}

	if err := os.RemoveAll(filepath.Join(m.AgentWorkDir(agentID), "kb", "managed")); err != nil {
		return false, fmt.Errorf("remove prior scope managed worktrees for agent %s: %w", agentID, err)
	}
	kept := sf.Repositories[:0]
	for _, rec := range sf.Repositories {
		if strings.TrimSpace(rec.AgentID) != agentID {
			kept = append(kept, rec)
		}
	}
	sf.Repositories = kept
	if sf.ActiveScopes == nil {
		sf.ActiveScopes = make(map[string]string)
	}
	sf.ActiveScopes[agentID] = scopeKey
	if err := saveYAMLFileLocked(sf, m.filePath(), m.mgr); err != nil {
		return true, err
	}
	return true, nil
}

// ReplaceAgentRecords atomically replaces the local record set for one agent
// and removes worktree directories that no longer belong to that agent. Bare
// clones are intentionally retained because other agents may reuse them.
func (m *ManagedKnowledgeBaseRepositoryManager) ReplaceAgentRecords(agentID, scopeKey string, records []ManagedKnowledgeBaseRepositoryRecord) error {
	agentID = strings.TrimSpace(agentID)
	scopeKey = strings.TrimSpace(scopeKey)
	if agentID == "" || scopeKey == "" {
		return errors.New("managed kb repository: agent_id and scope_key are required")
	}
	for i := range records {
		records[i].AgentID = agentID
		records[i].ScopeKey = scopeKey
		if err := validateManagedKnowledgeBaseRepositoryRecord(records[i]); err != nil {
			return err
		}
	}
	m.mgr.mu.Lock()
	defer m.mgr.mu.Unlock()
	sf := &managedKnowledgeBaseRepositoriesFile{}
	if err := loadYAMLFile(m.filePath(), sf); err != nil {
		return fmt.Errorf("load managed_kbs.yaml: %w", err)
	}
	if sf.ActiveScopes == nil || strings.TrimSpace(sf.ActiveScopes[agentID]) != scopeKey {
		return fmt.Errorf("managed kb repository: scope %q is not active for agent %s", scopeKey, agentID)
	}
	newKeys := make(map[string]struct{}, len(records))
	for _, rec := range records {
		newKeys[managedKBRecordKey(rec)] = struct{}{}
	}
	kept := sf.Repositories[:0]
	for _, rec := range sf.Repositories {
		if strings.TrimSpace(rec.AgentID) != agentID {
			kept = append(kept, rec)
			continue
		}
		if _, keep := newKeys[managedKBRecordKey(rec)]; keep {
			continue
		}
		if err := removeManagedKBWorktree(rec); err != nil {
			return fmt.Errorf("remove stale worktree for agent %s: %w", agentID, err)
		}
	}
	kept = append(kept, records...)
	sf.Repositories = kept
	return saveYAMLFileLocked(sf, m.filePath(), m.mgr)
}

func validateManagedKnowledgeBaseRepositoryRecord(rec ManagedKnowledgeBaseRepositoryRecord) error {
	if strings.TrimSpace(rec.AgentID) == "" {
		return errors.New("managed kb repository: agent_id is required")
	}
	if strings.TrimSpace(rec.ScopeKey) == "" {
		return errors.New("managed kb repository: scope_key is required")
	}
	if strings.TrimSpace(rec.KnowledgeBaseID) == "" {
		return errors.New("managed kb repository: knowledge_base_id is required")
	}
	if strings.TrimSpace(rec.SourceID) == "" {
		return errors.New("managed kb repository: source_id is required")
	}
	if strings.TrimSpace(rec.RepoID) == "" {
		return errors.New("managed kb repository: repo_id is required")
	}
	if strings.TrimSpace(rec.RepoRole) == "" {
		return errors.New("managed kb repository: repo_role is required")
	}
	if strings.TrimSpace(rec.CloneURL) == "" {
		return errors.New("managed kb repository: clone_url is required")
	}
	if strings.TrimSpace(rec.Branch) == "" {
		return errors.New("managed kb repository: branch is required")
	}
	if strings.TrimSpace(rec.BarePath) == "" || strings.TrimSpace(rec.WorktreePath) == "" {
		return errors.New("managed kb repository: local paths are required")
	}
	return nil
}

func managedKBRecordKey(rec ManagedKnowledgeBaseRepositoryRecord) string {
	return strings.Join([]string{
		strings.TrimSpace(rec.AgentID),
		strings.TrimSpace(rec.ScopeKey),
		strings.TrimSpace(rec.KnowledgeBaseID),
		strings.TrimSpace(rec.SourceID),
		strings.TrimSpace(rec.RepoID),
		strings.TrimSpace(rec.RepoRole),
	}, "\x00")
}

func removeManagedKBWorktree(rec ManagedKnowledgeBaseRepositoryRecord) error {
	path := strings.TrimSpace(rec.WorktreePath)
	if path == "" {
		return nil
	}
	if err := os.RemoveAll(path); err != nil {
		slog.Warn("managed kb repository: remove stale worktree", "path", path, "error", err)
		return err
	}
	return nil
}
