package daemon

import (
	"context"
	"log/slog"
	"strings"
	"time"

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

const (
	managedAssetInitialRetryDelay = 2 * time.Minute
	managedAssetRefreshInterval   = 30 * time.Minute
	managedAssetRefreshTimeout    = 30 * time.Second
)

// Daemon manages the agentteam background process lifecycle, including
// log rotation, channel/dispatcher wiring, API server, and optional
// system tray UI.

func (d *Daemon) startManagedAssetRefreshLoop(ctx context.Context) {
	if d == nil || d.dispatcher == nil {
		return
	}
	go func() {
		d.refreshManagedAssetsInBackground(ctx, "startup")
		initialRetry := time.NewTimer(managedAssetInitialRetryDelay)
		ticker := time.NewTicker(managedAssetRefreshInterval)
		defer initialRetry.Stop()
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-initialRetry.C:
				d.refreshManagedAssetsInBackground(ctx, "initial-retry")
			case <-ticker.C:
				d.refreshManagedAssetsInBackground(ctx, "periodic")
			}
		}
	}()
}

func (d *Daemon) refreshManagedAssetsInBackground(ctx context.Context, reason string) {
	agentIDs := d.agentteamAgentIDs()
	if len(agentIDs) == 0 {
		return
	}
	slog.InfoContext(ctx, "managed asset background refresh",
		"reason", reason, "agents", len(agentIDs))

	skillCtx, cancelSkill := context.WithTimeout(ctx, managedAssetRefreshTimeout)
	d.dispatcher.RefreshSkillAccess(skillCtx, agentIDs)
	cancelSkill()

	kbCtx, cancelKB := context.WithTimeout(ctx, managedAssetRefreshTimeout)
	d.dispatcher.RefreshKnowledgeBaseAccess(kbCtx, agentIDs)
	cancelKB()

	memoryCtx, cancelMemory := context.WithTimeout(ctx, managedAssetRefreshTimeout)
	d.dispatcher.RefreshMemoryAccess(memoryCtx, agentIDs)
	cancelMemory()

	// After refreshing every agent's skill/KB bindings, reconcile local managed skill
	// repos: any RepoID no longer bound to any agent (e.g. the skill was
	// hard-deleted server-side) gets its local clone removed. This is the only
	// workspace-wide vantage point, so it doubles as the safety net for any
	// realtime access-change events that were missed or dropped.
	reconcileCtx, cancelReconcile := context.WithTimeout(ctx, managedAssetRefreshTimeout)
	d.reconcileManagedRepositories(reconcileCtx)
	cancelReconcile()
}

// agentteamAgentIDs returns the IDs of every locally-known AgentTeam-sourced agent. These
// are the MeshX-bound agents whose managed asset access the background agentteam
// refreshes; skill access is also reconciled for orphaned clones.
func (d *Daemon) agentteamAgentIDs() []string {
	agents := profile.Default().Agents().All()
	agentIDs := make([]string, 0, len(agents))
	for _, ag := range agents {
		if strings.TrimSpace(ag.Source.Type) != "agentteam" {
			continue
		}
		if id := strings.TrimSpace(ag.ID); id != "" {
			agentIDs = append(agentIDs, id)
		}
	}
	return agentIDs
}

// reconcileManagedRepositories removes local managed skill repositories whose
// RepoID is no longer bound to any AgentTeam-sourced agent. It compares the on-disk
// records (profile.ManagedSkillRepositoryManager.All) against the union of
// cached RepoIDs across all agents and deletes orphans via the manager, which
// removes both the YAML record and the cloned directory. Safe by construction:
// the per-agent sync preserves its cache on transient (Unknown) failures, so a
// stale-but-nonempty cache can only cause under-deletion, never over-deletion.
func (d *Daemon) reconcileManagedRepositories(ctx context.Context) {
	if d == nil || d.dispatcher == nil {
		return
	}
	manager := profile.Default().ManagedSkillRepositories()
	local := manager.All()
	if len(local) == 0 {
		return
	}
	active, complete := d.dispatcher.CachedSkillRepoIDsSnapshot(d.agentteamAgentIDs())
	if !complete {
		slog.InfoContext(ctx, "managed skill repository: skip orphan reconcile until skill cache is complete")
		return
	}
	removedSourceRoots := make([]string, 0)
	for _, repoID := range orphanedManagedRepoIDs(local, active) {
		sourceRoot := managedRepositorySourceRoot(local, repoID, manager.RepositoryPath(repoID))
		if err := manager.Delete(repoID); err != nil {
			slog.WarnContext(ctx, "managed skill repository: reconcile delete failed",
				"repo_id", repoID, "error", err)
			continue
		}
		removedSourceRoots = append(removedSourceRoots, sourceRoot)
		slog.InfoContext(ctx, "managed skill repository: removed orphaned local clone",
			"repo_id", repoID)
	}
	if len(removedSourceRoots) > 0 {
		d.pruneMissingManagedRuntimeSkills(ctx, removedSourceRoots)
	}
}

func (d *Daemon) pruneMissingManagedRuntimeSkills(ctx context.Context, removedSourceRoots []string) {
	for _, ag := range profile.Default().Agents().All() {
		if strings.TrimSpace(ag.Source.Type) != "agentteam" {
			continue
		}
		if err := runtimeconfig.PruneRemovedManagedRuntimeSkillsUnderAgentRoot(profile.AgentWorkDir(ag.ID), removedSourceRoots); err != nil {
			slog.WarnContext(ctx, "managed skill repository: prune runtime copies failed",
				"agent_id", ag.ID, "error", err)
		}
	}
}

func managedRepositorySourceRoot(local []profile.ManagedSkillRepositoryRecord, repoID, fallback string) string {
	repoID = strings.TrimSpace(repoID)
	for _, record := range local {
		if strings.TrimSpace(record.RepoID) != repoID {
			continue
		}
		if path := strings.TrimSpace(record.LocalPath); path != "" {
			return path
		}
	}
	return strings.TrimSpace(fallback)
}

// orphanedManagedRepoIDs returns the RepoIDs present in the local managed-skill
// records but absent from the active set (the union of RepoIDs still bound to
// some agent). Extracted from reconcileManagedRepositories so the set-diff can
// be unit-tested without the profile/dispatcher singletons.
func orphanedManagedRepoIDs(local []profile.ManagedSkillRepositoryRecord, active []string) []string {
	activeSet := make(map[string]struct{}, len(active))
	for _, id := range active {
		if id = strings.TrimSpace(id); id != "" {
			activeSet[id] = struct{}{}
		}
	}
	seen := make(map[string]struct{})
	var orphans []string
	for _, rec := range local {
		repoID := strings.TrimSpace(rec.RepoID)
		if repoID == "" {
			continue
		}
		if _, keep := activeSet[repoID]; keep {
			continue
		}
		if _, dup := seen[repoID]; dup {
			continue
		}
		seen[repoID] = struct{}{}
		orphans = append(orphans, repoID)
	}
	return orphans
}
