package kb

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/dispatcher"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/workspace"
	"golang.org/x/sync/singleflight"
)

type GitRunner interface {
	profile.KnowledgeBaseGitRunner
}

type ProviderFunc func(ctx context.Context, agentID string) ([]KnowledgeBaseAccess, dispatcher.SkillLookupState, error)

// Sync mirrors skill.Sync's authority semantics but materializes KB
// source repositories as linked worktrees under each agent's kb/managed/ tree.
type Sync struct {
	provider ProviderFunc
	git      GitRunner
	manager  *profile.ManagedKnowledgeBaseRepositoryManager

	sf         singleflight.Group
	agentLocks sync.Map
}

func NewSync(provider ProviderFunc, git GitRunner, manager *profile.ManagedKnowledgeBaseRepositoryManager) *Sync {
	return &Sync{provider: provider, git: git, manager: manager}
}

func (s *Sync) SyncAgent(ctx context.Context, agentID, scopeKey string) (dispatcher.SkillLookupState, error) {
	agentID = strings.TrimSpace(agentID)
	scopeKey = strings.TrimSpace(scopeKey)
	if agentID == "" || scopeKey == "" {
		return LookupUnknown, nil
	}
	lockValue, _ := s.agentLocks.LoadOrStore(agentID, &sync.Mutex{})
	agentLock := lockValue.(*sync.Mutex)
	agentLock.Lock()
	defer agentLock.Unlock()
	if s.manager != nil {
		changed, err := s.manager.ActivateAgentScope(agentID, scopeKey)
		if changed {
			if clearErr := s.writeIndex(agentID, nil); clearErr != nil {
				return LookupUnknown, fmt.Errorf("kb sync: clear prior scope index: %w", clearErr)
			}
		}
		if err != nil {
			return LookupUnknown, fmt.Errorf("kb sync: activate scope: %w", err)
		}
	}
	if s.provider == nil {
		return LookupUnknown, errors.New("kb sync: nil provider")
	}
	access, state, err := s.provider(ctx, agentID)
	if err != nil {
		slog.WarnContext(ctx, "kb sync: lookup failed; preserving local records", "agent_id", agentID, "error", err)
		return LookupUnknown, fmt.Errorf("kb sync: lookup: %w", err)
	}
	switch state {
	case LookupBound:
		if len(access) == 0 {
			return LookupUnknown, errors.New("kb sync: bound state with empty access")
		}
		records := s.syncRepositories(ctx, agentID, scopeKey, access)
		if s.manager != nil {
			if err := s.manager.ReplaceAgentRecords(agentID, scopeKey, records); err != nil {
				return LookupUnknown, err
			}
		}
		if err := s.writeIndex(agentID, records); err != nil {
			return LookupUnknown, err
		}
		return LookupBound, nil
	case LookupUnbound:
		if s.manager != nil {
			if err := s.manager.ReplaceAgentRecords(agentID, scopeKey, nil); err != nil {
				return LookupUnknown, err
			}
		}
		if err := s.writeIndex(agentID, nil); err != nil {
			return LookupUnknown, err
		}
		return LookupUnbound, nil
	default:
		return LookupUnknown, nil
	}
}

func (s *Sync) syncRepositories(ctx context.Context, agentID, scopeKey string, access []KnowledgeBaseAccess) []profile.ManagedKnowledgeBaseRepositoryRecord {
	records := make([]profile.ManagedKnowledgeBaseRepositoryRecord, 0, len(access))
	for _, kb := range access {
		if strings.TrimSpace(kb.RepoID) == "" {
			continue
		}
		rec, err := s.syncOneRepository(ctx, agentID, scopeKey, kb)
		if err != nil {
			slog.WarnContext(ctx, "kb sync: repository sync failed; recording failure", "agent_id", agentID, "repo_id", kb.RepoID, "error", err)
		}
		if strings.TrimSpace(rec.RepoID) != "" {
			records = append(records, rec)
		}
	}
	return records
}

func (s *Sync) syncOneRepository(ctx context.Context, agentID, scopeKey string, kb KnowledgeBaseAccess) (profile.ManagedKnowledgeBaseRepositoryRecord, error) {
	if s.manager == nil {
		return profile.ManagedKnowledgeBaseRepositoryRecord{}, errors.New("kb sync: nil repository manager")
	}
	barePath := s.manager.BareRepositoryPath(kb.RepoID)
	worktreePath := s.manager.WorktreePath(agentID, kb.KnowledgeBaseID, kb.SourceID, kb.RepoID, kb.RepoRole)
	now := time.Now().UTC()
	if s.git == nil {
		err := errors.New("kb sync: nil git runner")
		return s.failureRecord(agentID, scopeKey, kb, barePath, worktreePath, now, err), err
	}
	key := strings.TrimSpace(kb.RepoID) + "@" + strings.TrimSpace(kb.Branch)
	v, err, _ := s.sf.Do(key, func() (any, error) {
		revision, syncErr := s.git.FetchBare(ctx, barePath, kb.Branch, kb.Auth)
		if syncErr != nil {
			revision, syncErr = s.git.CloneBare(ctx, kb.CloneURL, kb.Branch, barePath, kb.Auth)
		}
		return revision, syncErr
	})
	if err != nil {
		return s.failureRecord(agentID, scopeKey, kb, barePath, worktreePath, now, err), fmt.Errorf("sync bare KB repository %s: %w", kb.RepoID, err)
	}
	revision, _ := v.(string)
	if err := s.git.WorktreeCheckout(ctx, barePath, worktreePath, revision); err != nil {
		return s.failureRecord(agentID, scopeKey, kb, barePath, worktreePath, now, err), err
	}
	rec := s.readyRecord(agentID, scopeKey, kb, barePath, worktreePath, revision, now)
	slog.InfoContext(ctx, "kb sync: repository worktree ready", "agent_id", agentID, "repo_id", kb.RepoID, "revision", revision, "path", worktreePath)
	return rec, nil
}

func (s *Sync) readyRecord(agentID, scopeKey string, kb KnowledgeBaseAccess, barePath, worktreePath, revision string, now time.Time) profile.ManagedKnowledgeBaseRepositoryRecord {
	return profile.ManagedKnowledgeBaseRepositoryRecord{
		AgentID:                  agentID,
		ScopeKey:                 scopeKey,
		KnowledgeBaseID:          kb.KnowledgeBaseID,
		KnowledgeBaseName:        kb.KnowledgeBaseName,
		KnowledgeBaseDescription: kb.KnowledgeBaseDescription,
		SourceID:                 kb.SourceID,
		SourceName:               kb.SourceName,
		SourceDescription:        kb.SourceDescription,
		SourceType:               kb.SourceType,
		RepoID:                   kb.RepoID,
		RepoRole:                 kb.RepoRole,
		CloneURL:                 kb.CloneURL,
		Branch:                   kb.Branch,
		Revision:                 revision,
		BarePath:                 barePath,
		WorktreePath:             worktreePath,
		AccessProfile:            kb.AccessProfile,
		Writable:                 kb.Writable,
		LastSyncAt:               now,
		LastSyncState:            profile.SkillSyncStateReady,
		LastAttemptAt:            now,
	}
}

func (s *Sync) failureRecord(agentID, scopeKey string, kb KnowledgeBaseAccess, barePath, worktreePath string, now time.Time, err error) profile.ManagedKnowledgeBaseRepositoryRecord {
	message := ""
	if err != nil {
		message = truncateErr(err.Error())
	}
	rec := s.readyRecord(agentID, scopeKey, kb, barePath, worktreePath, "", now)
	rec.LastSyncState = profile.SkillSyncStateFailed
	rec.LastError = message
	rec.RetryCount = 1
	return rec
}

func (s *Sync) writeIndex(agentID string, records []profile.ManagedKnowledgeBaseRepositoryRecord) error {
	workDir := profile.AgentWorkDir(agentID)
	if s.manager != nil {
		workDir = s.manager.AgentWorkDir(agentID)
	}
	entries := make([]workspace.ManagedKnowledgeBaseIndexEntry, 0, len(records))
	for _, rec := range records {
		if strings.TrimSpace(rec.LastSyncState) == profile.SkillSyncStateFailed {
			continue
		}
		rel, err := filepath.Rel(workDir, rec.WorktreePath)
		if err != nil || strings.HasPrefix(rel, "..") {
			rel = rec.WorktreePath
		}
		entries = append(entries, workspace.ManagedKnowledgeBaseIndexEntry{Name: rec.KnowledgeBaseName, Description: rec.KnowledgeBaseDescription, SourceName: rec.SourceName, RepoRole: rec.RepoRole, Path: rel, Branch: rec.Branch, Revision: rec.Revision})
	}
	return workspace.WriteManagedKnowledgeBaseIndex(workDir, entries)
}

func truncateErr(s string) string {
	s = strings.TrimSpace(s)
	if len(s) <= 500 {
		return s
	}
	return s[:500]
}
