// Package agents defines the Agent runtime that ties core and shell together.
package agents

import (
	"context"
	"fmt"
	"log/slog"
	"sync"
	"time"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/acl"
	"github.com/enmanuel/agents/pkg/command"
	"github.com/enmanuel/agents/pkg/decision"
	coretypes "github.com/enmanuel/agents/pkg/llm"
	"github.com/enmanuel/agents/pkg/memory"
	"github.com/enmanuel/agents/pkg/personality"
	"github.com/enmanuel/agents/pkg/sanitize"
	"github.com/enmanuel/agents/shell/bus"
	shellcron "github.com/enmanuel/agents/shell/cron"
	"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/shell/transportunibus"
	"github.com/enmanuel/agents/tools"
	toolmemory "github.com/enmanuel/agents/tools/memorytools"
)

const (
	defaultMaxToolIterations = 5
	defaultWindowSize        = 20
)

// CommandHandler executes a built-in command and returns the response text.
type CommandHandler func(ctx context.Context, msgCtx decision.MessageContext) string

// Agent is the assembled runtime: pure core + impure shell.
type Agent struct {
	cfg         *config.AgentConfig
	personality personality.Personality
	rules       []decision.Rule
	llm         coretypes.CompleteFunc // nil when no LLM configured (simple_bot)
	transport   *transportunibus.Transport
	sender      effects.Sender // bus-backed sender used for replies and tools
	runner      *effects.Runner
	toolReg     *tools.Registry
	logger      *slog.Logger
	mcpManager  *shellmcp.Manager // nil when MCP client is disabled

	// Lifecycle — cancel stops this agent individually; done is closed when Run returns.
	cancel context.CancelFunc
	done   chan struct{}

	// Access control
	acl acl.ACL

	// Commands — handlers keyed by canonical name; cmdAliases maps alias → canonical
	commands    map[string]CommandHandler
	cmdAliases  map[string]string // alias → canonical name
	customSpecs []command.Spec    // specs from RegisterCommand (for !help)
	startTime   time.Time

	// Memory
	windows    map[string]memory.Window
	windowsMu  sync.RWMutex
	memStore   memory.Store // nil when memory is disabled
	windowSize int
	roomCtx    *toolmemory.RoomContext

	// Prompt-commands — loaded from prompts/*.md at startup
	promptCmds map[string]string // name → prompt content

	// Knowledge store — non-nil when knowledge is enabled
	knowledgeStore *shellknowledge.FileStore

	// Shared knowledge store — non-nil when shared_knowledge is enabled
	sharedKnowledgeStore *shellknowledge.FileStore

	// Skills loader — non-nil when skills are enabled
	skillLoader *shellskills.Loader

	// Sanitization options — nil when sanitization is disabled
	sanitizeOpts *sanitize.Options

	// Bus — set via SetBus() when running under the unified launcher
	agentBus *bus.Bus

	// Scheduler — nil when no schedules are configured
	scheduler *shellcron.Scheduler
}

// New assembles an Agent from its config, rules, pre-resolved ACL, and logger.
// The ACL is resolved externally (e.g. from security/ YAML files) and injected here.
// Pass acl.ACL{} (empty) for open access (no restrictions).
func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logger *slog.Logger) (*Agent, error) {
	// unibus transport — discovers rooms, receives messages, sends replies.
	tr, err := transportunibus.New(cfg.Bus, logger)
	if err != nil {
		return nil, fmt.Errorf("unibus transport: %w", err)
	}
	sender := tr.Sender()

	// SSH executor
	sshExec := ssh.NewExecutor(cfg.SSH, logger)

	// LLM client — optional; if no provider is configured, the agent runs as simple_bot
	llmFunc, err := initLLM(cfg, logger)
	if err != nil {
		return nil, err
	}

	// Effects runner
	runner := effects.NewRunner(sender, sshExec, logger)

	// Resolve base data path for this agent
	dataBase := resolveDataBase(cfg)
	logger.Debug("data base path", "path", dataBase)

	// Memory subsystem
	memInit, err := initMemoryStore(cfg.Memory.Enabled, cfg.Memory.WindowSize, cfg.Memory.DBPath, dataBase, logger)
	if err != nil {
		return nil, err
	}

	// Tool dependencies (knowledge, MCP, skills)
	deps := initToolDeps(cfg, dataBase, logger)

	if !agentACL.Empty() {
		logger.Info("acl enabled (centralized security policy)")
	}

	// Tool registry — register tools enabled in config
	roomCtx := &toolmemory.RoomContext{}
	toolReg := buildToolRegistry(cfg, sshExec, sender, memInit.store, deps.kStore, deps.sharedKStore, deps.mcpManager, deps.skillLoader, deps.skillExecutor, roomCtx, logger)

	// Rate limiting for tools
	initRateLimiter(cfg, toolReg, logger)

	a := &Agent{
		cfg:                  cfg,
		acl:                  agentACL,
		personality:          personality.FromConfig(cfg.Personality),
		rules:                rules,
		llm:                  llmFunc,
		transport:            tr,
		sender:               sender,
		runner:               runner,
		toolReg:              toolReg,
		logger:               logger,
		mcpManager:           deps.mcpManager,
		done:                 make(chan struct{}),
		commands:             make(map[string]CommandHandler),
		cmdAliases:           command.BuiltinNames(),
		startTime:            time.Now(),
		windows:              make(map[string]memory.Window),
		memStore:             memInit.store,
		knowledgeStore:       deps.kStore,
		sharedKnowledgeStore: deps.sharedKStore,
		skillLoader:          deps.skillLoader,
		windowSize:           memInit.windowSize,
		roomCtx:              roomCtx,
	}

	// Configure sanitization if enabled
	if cfg.Security.Sanitize.Enabled {
		minSev := parseSeverity(cfg.Security.Sanitize.MinSeverity)
		a.sanitizeOpts = &sanitize.Options{
			Mode:             sanitize.ParseMode(cfg.Security.Sanitize.Mode),
			MinSeverity:      minSev,
			DisabledPatterns: cfg.Security.Sanitize.DisabledPatterns,
		}
		logger.Info("input sanitization enabled",
			"mode", a.sanitizeOpts.Mode,
			"min_severity", minSev,
		)
	}

	// Register built-in command handlers
	a.registerBuiltinCommands()

	// Load prompt-commands from prompts/ directory
	a.loadPromptCommands()

	// Register memory_clear_context with self as WindowClearer (after a is created)
	if cfg.Tools.Memory.Enabled && memInit.store != nil {
		toolReg.Register(toolmemory.NewMemoryClearContext(a, roomCtx))
	}

	// Cron scheduler — only when schedules are configured
	if len(cfg.Schedules) > 0 {
		a.scheduler = shellcron.New(cfg.Schedules, sender, llmFunc, cfg.LLM.Primary.Model, logger)
		logger.Info("cron scheduler configured", "schedules", len(cfg.Schedules))
	}

	return a, nil
}

// RegisterCommand adds a custom command handler for this agent.
// The spec provides metadata (aliases, description, usage) for !help.
// Must be called before Run().
func (a *Agent) RegisterCommand(spec command.Spec, handler CommandHandler) {
	a.commands[spec.Name] = handler
	a.cmdAliases[spec.Name] = spec.Name
	for _, alias := range spec.Aliases {
		a.cmdAliases[alias] = spec.Name
	}
	a.customSpecs = append(a.customSpecs, spec)
	a.logger.Info("command_registered", "command", spec.Name, "aliases", spec.Aliases)
}

// SetBus attaches the agent to the inter-agent bus for orchestration.
// Must be called before Run().
func (a *Agent) SetBus(b *bus.Bus) {
	a.agentBus = b
}

// Stop cancels this agent's individual context, causing Run to return.
// Safe to call multiple times.
func (a *Agent) Stop() {
	if a.cancel != nil {
		a.cancel()
	}
}

// Done returns a channel that is closed when Run has returned.
func (a *Agent) Done() <-chan struct{} {
	return a.done
}

// Run starts the agent's transport loop. Blocks until ctx is cancelled.
func (a *Agent) Run(ctx context.Context) error {
	ctx, a.cancel = context.WithCancel(ctx)
	defer close(a.done)

	if a.transport != nil {
		defer a.transport.Close()
	}
	if a.memStore != nil {
		defer a.memStore.Close()
	}
	if a.knowledgeStore != nil {
		defer a.knowledgeStore.Close()
	}
	if a.sharedKnowledgeStore != nil {
		defer a.sharedKnowledgeStore.Close()
	}
	if a.mcpManager != nil {
		defer a.mcpManager.Close()
	}
	a.logger.Info("agent starting",
		"id", a.cfg.Agent.ID,
		"name", a.cfg.Agent.Name,
		"endpoint", a.transport.Endpoint(),
		"tools", a.toolReg.Names(),
	)

	// Start bus listener if connected to the orchestration bus
	if a.agentBus != nil {
		ch := a.agentBus.Subscribe(bus.AgentID(a.cfg.Agent.ID))
		go a.listenBus(ctx, ch)
		a.logger.Info("bus listener started")
	}

	// Start cron scheduler in background goroutine (blocks until ctx cancelled)
	if a.scheduler != nil {
		go a.scheduler.Start(ctx)
	}

	return a.transport.Run(ctx, a.handleInbound)
}
