package agents

import (
	"context"
	"log/slog"
	"os"
	"path/filepath"
	"time"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/memory"
	"github.com/enmanuel/agents/shell/effects"
	shellknowledge "github.com/enmanuel/agents/shell/knowledge"
	shellmcp "github.com/enmanuel/agents/shell/mcp"
	shellskills "github.com/enmanuel/agents/shell/skills"
	"github.com/enmanuel/agents/shell/ssh"
	"github.com/enmanuel/agents/tools"
	toolbus "github.com/enmanuel/agents/tools/bus"
	toolclock "github.com/enmanuel/agents/tools/clock"
	toolfile "github.com/enmanuel/agents/tools/file"
	toolhttp "github.com/enmanuel/agents/tools/http"
	toolimdb "github.com/enmanuel/agents/tools/imdb"
	toolknowledge "github.com/enmanuel/agents/tools/knowledgetools"
	toolmcp "github.com/enmanuel/agents/tools/mcptools"
	toolmemory "github.com/enmanuel/agents/tools/memorytools"
	toolskills "github.com/enmanuel/agents/tools/skilltools"
	toolssh "github.com/enmanuel/agents/tools/ssh"
	toolweather "github.com/enmanuel/agents/tools/weather"
)

// toolDeps holds external subsystem instances needed by the tool registry.
type toolDeps struct {
	kStore        *shellknowledge.FileStore
	sharedKStore  *shellknowledge.FileStore
	mcpManager    *shellmcp.Manager
	skillLoader   *shellskills.Loader
	skillExecutor *shellskills.Executor
}

// initToolDeps initializes knowledge stores, MCP manager, and skills loader
// based on the agent config. All results are optional (nil when disabled).
func initToolDeps(cfg *config.AgentConfig, dataBase string, logger *slog.Logger) toolDeps {
	var deps toolDeps

	// Knowledge store
	if cfg.Tools.Knowledge.Enabled {
		knowledgeDir := cfg.Tools.Knowledge.Dir
		if knowledgeDir == "" {
			knowledgeDir = filepath.Join("agents", cfg.Agent.ID, "knowledge")
		}
		knowledgeDBPath := filepath.Join(dataBase, "knowledge.db")
		kStore, kErr := shellknowledge.New(knowledgeDir, knowledgeDBPath, logger)
		if kErr != nil {
			logger.Error("knowledge_store_init_failed", "err", kErr)
		} else {
			if syncErr := kStore.Sync(context.Background()); syncErr != nil {
				logger.Error("knowledge_sync_failed", "err", syncErr)
			}
			deps.kStore = kStore
		}
	}

	// Shared knowledge store
	if cfg.Tools.SharedKnowledge.Enabled {
		sharedDir := cfg.Tools.SharedKnowledge.Dir
		if sharedDir == "" {
			sharedDir = "knowledges"
		}
		sharedDBPath := cfg.Tools.SharedKnowledge.DBPath
		if sharedDBPath == "" {
			sharedDBPath = "knowledges/data/knowledge.db"
		}
		sharedKStore, skErr := shellknowledge.New(sharedDir, sharedDBPath, logger)
		if skErr != nil {
			logger.Error("shared_knowledge_store_init_failed", "err", skErr)
		} else {
			if syncErr := sharedKStore.Sync(context.Background()); syncErr != nil {
				logger.Error("shared_knowledge_sync_failed", "err", syncErr)
			}
			logger.Info("shared knowledge enabled", "dir", sharedDir, "db", sharedDBPath)
			deps.sharedKStore = sharedKStore
		}
	}

	// MCP client manager — connects to external MCP servers
	if cfg.Tools.MCP.Enabled && len(cfg.Tools.MCP.Servers) > 0 {
		mcpManager, mcpErr := shellmcp.NewManager(context.Background(), cfg.Tools.MCP.Servers, logger)
		if mcpErr != nil {
			logger.Error("mcp_manager_init_failed", "err", mcpErr)
		} else {
			logger.Info("mcp manager initialized", "servers", len(cfg.Tools.MCP.Servers))
			deps.mcpManager = mcpManager
		}
	}

	// Skills loader
	if cfg.Skills.Enabled {
		skillsPath := cfg.Skills.SkillsPath
		if skillsPath == "" {
			skillsPath = "skills/"
		}
		deps.skillLoader = shellskills.NewLoader(skillsPath)

		// Skills executor for scripts
		allowedInterpreters := cfg.Tools.Skills.AllowedInterpreters
		timeout := cfg.Skills.Timeout
		if timeout == 0 {
			timeout = 60 * time.Second
		}
		deps.skillExecutor = shellskills.NewExecutor(allowedInterpreters, timeout)
		logger.Info("skills enabled", "path", skillsPath, "categories", cfg.Skills.Categories)
	}

	return deps
}

// initRateLimiter configures the rate limiter on the tool registry if enabled.
func initRateLimiter(cfg *config.AgentConfig, toolReg *tools.Registry, logger *slog.Logger) {
	if !cfg.Security.ToolRateLimit.Enabled {
		return
	}
	maxCalls := cfg.Security.ToolRateLimit.MaxCallsPerMin
	if maxCalls <= 0 {
		maxCalls = 10
	}
	rl := tools.NewRateLimiter(maxCalls, time.Minute)
	toolReg.SetRateLimiter(rl)

	cleanupInterval := cfg.Security.ToolRateLimit.CleanupIntervalS
	if cleanupInterval <= 0 {
		cleanupInterval = 60
	}
	go func() {
		ticker := time.NewTicker(time.Duration(cleanupInterval) * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			rl.Cleanup()
		}
	}()
	logger.Info("tool rate limiting enabled", "max_calls_per_min", maxCalls)
}

// buildToolRegistry creates a Registry with tools enabled in the agent's config.
func buildToolRegistry(
	cfg *config.AgentConfig,
	sshExec *ssh.Executor,
	sender effects.Sender,
	memStore memory.Store,
	kStore *shellknowledge.FileStore,
	sharedKStore *shellknowledge.FileStore,
	mcpManager *shellmcp.Manager,
	skillLoader *shellskills.Loader,
	skillExecutor *shellskills.Executor,
	roomCtx *toolmemory.RoomContext,
	logger *slog.Logger,
) *tools.Registry {
	reg := tools.NewRegistry(logger)

	if cfg.Tools.HTTP.Enabled {
		reg.Register(toolhttp.NewHTTPGet(cfg.Tools.HTTP))
		reg.Register(toolhttp.NewHTTPPost(cfg.Tools.HTTP))
		logger.Debug("registered http tools")
	}

	if cfg.Tools.SSH.Enabled {
		reg.Register(toolssh.NewSSHCommand(cfg.Tools.SSH, sshExec))
		logger.Debug("registered ssh tool")
	}

	if cfg.Tools.FileOps.Enabled {
		reg.Register(toolfile.NewReadFile(cfg.Tools.FileOps))
		reg.Register(toolfile.NewListDirectory(cfg.Tools.FileOps))
		if !cfg.Tools.FileOps.ReadOnly {
			reg.Register(toolfile.NewWriteFile(cfg.Tools.FileOps))
			reg.Register(toolfile.NewAppendFile(cfg.Tools.FileOps))
			reg.Register(toolfile.NewDeleteFile(cfg.Tools.FileOps))
		}
		logger.Debug("registered file tools")
	}

	// current_time is always available
	reg.Register(toolclock.NewCurrentTime())
	logger.Debug("registered current_time tool")

	// weather tool is always available
	reg.Register(toolweather.NewWeather())
	logger.Debug("registered weather tool")

	// imdb tool (enabled via config)
	if cfg.Tools.IMDb.Enabled {
		reg.Register(toolimdb.NewIMDbSearch(cfg.Tools.IMDb))
		logger.Debug("registered imdb tool")
	}

	// bus_send is always available
	reg.Register(toolbus.NewBusSend(sender, cfg.Tools.Bus))
	logger.Debug("registered bus tool")

	// Memory tools (memory_clear_context registered later since it needs the Agent)
	if cfg.Tools.Memory.Enabled && memStore != nil {
		reg.Register(toolmemory.NewMemorySave(cfg.Agent.ID, memStore))
		reg.Register(toolmemory.NewMemoryRecall(cfg.Agent.ID, memStore))
		reg.Register(toolmemory.NewMemoryForget(cfg.Agent.ID, memStore))
		reg.Register(toolmemory.NewMemorySummary(cfg.Agent.ID, memStore))
		logger.Debug("registered memory tools")
	}

	// Knowledge tools
	if cfg.Tools.Knowledge.Enabled && kStore != nil {
		reg.Register(toolknowledge.NewKnowledgeSearch(kStore))
		reg.Register(toolknowledge.NewKnowledgeRead(kStore))
		reg.Register(toolknowledge.NewKnowledgeWrite(kStore))
		reg.Register(toolknowledge.NewKnowledgeList(kStore))
		logger.Debug("registered knowledge tools")
	}

	// Shared knowledge tools
	if cfg.Tools.SharedKnowledge.Enabled && sharedKStore != nil {
		sharedTools := toolknowledge.NewSharedKnowledgeTools(sharedKStore)
		for _, tool := range sharedTools {
			reg.Register(tool)
		}
		logger.Debug("registered shared knowledge tools", "count", len(sharedTools))
	}

	// MCP tools — register tools from all connected MCP servers
	if mcpManager != nil {
		for serverName, mcpClient := range mcpManager.AllClients() {
			// Find the config for this server to get prefix, filter, timeout
			var serverCfg *config.MCPServerCfg
			for i := range cfg.Tools.MCP.Servers {
				if cfg.Tools.MCP.Servers[i].Name == serverName {
					serverCfg = &cfg.Tools.MCP.Servers[i]
					break
				}
			}
			if serverCfg == nil {
				logger.Warn("no config found for MCP server", "name", serverName)
				continue
			}

			// Convert and register MCP tools
			mcpTools := toolmcp.FromMCPServer(mcpClient, serverCfg.Prefix, serverCfg.Tools, serverCfg.Timeout, logger)
			for _, tool := range mcpTools {
				reg.Register(tool)
			}
			logger.Debug("registered MCP tools", "server", serverName, "count", len(mcpTools))
		}
	}

	// Skills tools — register skill search, load, read, and run tools
	if skillLoader != nil {
		reg.Register(toolskills.NewSkillSearch(skillLoader, cfg.Skills.Categories))
		reg.Register(toolskills.NewSkillLoad(skillLoader))
		reg.Register(toolskills.NewSkillReadResource(skillLoader))
		if skillExecutor != nil {
			reg.Register(toolskills.NewSkillRunScript(skillLoader, skillExecutor))
		}
		logger.Debug("registered skills tools")
	}

	return reg
}

// resolveDataBase returns the base directory for agent runtime data.
// Priority: config storage.base_path > $AGENTS_DATA_DIR/<id> > agents/<id>/data
func resolveDataBase(cfg *config.AgentConfig) string {
	if cfg.Storage.BasePath != "" {
		return cfg.Storage.BasePath
	}
	if envDir := os.Getenv("AGENTS_DATA_DIR"); envDir != "" {
		return filepath.Join(envDir, cfg.Agent.ID)
	}
	return filepath.Join("agents", cfg.Agent.ID, "data")
}
