// Command launcher starts one or more agents from their config files.
//
// Usage:
//
//	go run ./cmd/launcher                              # auto-discovers agents/*/config.yaml
//	go run ./cmd/launcher -c agents/assistant/config.yaml
package main

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"

	"github.com/spf13/cobra"

	"github.com/enmanuel/agents/agents"
	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/decision"
	pksecurity "github.com/enmanuel/agents/pkg/security"
	"github.com/enmanuel/agents/shell/bus"
	agentlog "github.com/enmanuel/agents/shell/logger"
	shellsecurity "github.com/enmanuel/agents/shell/security"

	// Blank imports: each agent self-registers its rules via init().
	_ "github.com/enmanuel/agents/agents/asistente-2"
	_ "github.com/enmanuel/agents/agents/assistant-bot"
	_ "github.com/enmanuel/agents/agents/meteorologo"
)

func main() {
	var (
		configPaths []string
		logLevel    string
		logDir      string
	)

	root := &cobra.Command{
		Use:   "launcher",
		Short: "Start Matrix agents from config files",
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			if len(configPaths) == 0 {
				matches, _ := filepath.Glob("agents/*/config.yaml")
				configPaths = matches
			}
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			lvl := parseLogLevel(logLevel)

			// ── Launcher-level logger ──
			logger, launcherCleanup, err := agentlog.NewAgentLogger(agentlog.LoggerConfig{
				BaseDir: logDir,
				AgentID: "launcher",
				Level:   lvl,
			})
			if err != nil {
				// Fallback to stdout if file logger fails.
				logger = newLogger(logLevel)
				logger.Warn("could not create file logger, falling back to stdout", "err", err)
				launcherCleanup = func() {}
			}
			defer launcherCleanup()

			if len(configPaths) == 0 {
				logger.Warn("no agent configs found — nothing to start")
				return nil
			}

			ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
			defer stop()

			// ── Load centralized security policy ──
			secPolicy, secErr := shellsecurity.Load("security/")
			if secErr != nil {
				logger.Warn("security policy load failed, using empty policy (open access)", "err", secErr)
				secPolicy = pksecurity.SecurityPolicy{}
			} else {
				logger.Info("security policy loaded",
					"user_groups", len(secPolicy.UserGroups),
					"agent_groups", len(secPolicy.AgentGroups),
					"policies", len(secPolicy.Policies),
				)
			}

			// ── Shared bus for inter-agent communication ──
			agentBus := bus.New(logger)

			// NOTE: the multi-bot orchestrator is parked (Matrix-out). Its room
			// discovery was Matrix-intrinsic and has been removed; it is no longer
			// wired into the launcher. Re-introducing it over unibus is a later step.

			// ── Shared dependencies for agent registry ──
			deps := &launchDeps{
				agentBus:  agentBus,
				logDir:    logDir,
				logLevel:  lvl,
				parentCtx: ctx,
				secPolicy: secPolicy,
			}
			registry := newAgentRegistry(deps)

			// ── SIGHUP: hot-reload individual agent or all agents ──
			sighup := make(chan os.Signal, 1)
			signal.Notify(sighup, syscall.SIGHUP)
			go func() {
				for {
					select {
					case <-ctx.Done():
						return
					case _, ok := <-sighup:
						if !ok {
							return
						}
						id := readReloadTarget("run/reload.txt")
						// Remove the target file after reading so it doesn't
						// affect the next SIGHUP.
						_ = os.Remove("run/reload.txt")
						if id == "" {
							logger.Info("sighup: reloading all agents")
							registry.reloadAll(rulesFor)
						} else {
							logger.Info("sighup: reloading agent", "id", id)
							registry.reload(id, rulesFor)
						}
					}
				}
			}()

			// ── Start normal agents ──
			for _, path := range configPaths {
				path := path
				cfg, err := config.Load(path)
				if err != nil {
					logger.Error("failed to load config", "path", path, "err", err)
					continue
				}
				if !cfg.Agent.Enabled {
					logger.Info("agent disabled, skipping", "id", cfg.Agent.ID)
					continue
				}
				if cfg.Agent.Template {
					logger.Info("agent is template, skipping", "id", cfg.Agent.ID)
					continue
				}

				// Per-agent logger → writes to logs/<agent-id>/YYYY-MM-DD.jsonl
				agentLogger, agentCleanup, aErr := agentlog.NewAgentLogger(agentlog.LoggerConfig{
					BaseDir: logDir,
					AgentID: cfg.Agent.ID,
					Level:   lvl,
				})
				if aErr != nil {
					logger.Warn("agent file logger failed, using launcher logger", "agent", cfg.Agent.ID, "err", aErr)
					agentLogger = logger.With("agent", cfg.Agent.ID)
					agentCleanup = func() {}
				}

				// Branch: robot (command-only, lightweight) vs agent (full runtime).
				var runner agents.Runner

				if cfg.Agent.Type == "robot" {
					robot, rErr := agents.NewRobot(cfg, agentLogger)
					if rErr != nil {
						logger.Error("failed to create robot", "id", cfg.Agent.ID, "err", rErr)
						agentCleanup()
						continue
					}
					runner = robot
					agentLogger.Info("created robot", "id", cfg.Agent.ID)
				} else {
					rules := rulesFor(cfg.Agent.ID, logger)

					// Resolve centralized ACL for this agent
					agentACL := pksecurity.ResolveACL(cfg.Agent.ID, deps.secPolicy)
					agentLogger.Debug("resolved acl for agent",
						"agent", cfg.Agent.ID,
						"acl_empty", agentACL.Empty(),
					)

					a, cErr := agents.New(cfg, rules, agentACL, agentLogger)
					if cErr != nil {
						logger.Error("failed to create agent", "id", cfg.Agent.ID, "err", cErr)
						agentCleanup()
						continue
					}

					// Connect agent to the inter-agent bus.
					a.SetBus(agentBus)

					runner = a
				}

				registry.register(&runningAgent{
					runner:     runner,
					cfg:        cfg,
					cfgPath:    path,
					logger:     agentLogger,
					logCleanup: agentCleanup,
				})
			}

			registry.waitAll()
			registry.cleanupLogs()
			logger.Info("all agents stopped")
			return nil
		},
	}

	root.Flags().StringSliceVarP(&configPaths, "config", "c", nil,
		"Agent config file(s). If omitted, discovers all agents/*/config.yaml")
	root.Flags().StringVar(&logLevel, "log-level", "info",
		"Log level: debug | info | warn | error")
	root.Flags().StringVar(&logDir, "log-dir", "logs",
		`Log directory (logs/<agent>/YYYY-MM-DD.jsonl). Use "stdout" for console only`)

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

// rulesFor retrieves the rule factory for the given agent ID from the
// global registry (populated by init() in each agent package).
// Returns nil if no rules are registered (command-only bot).
func rulesFor(agentID string, logger *slog.Logger) []decision.Rule {
	factory := agents.GetRules(agentID)
	if factory == nil {
		logger.Warn("no rules registered for agent, using empty ruleset (command-only)", "id", agentID)
		return nil
	}
	return factory()
}

func parseLogLevel(level string) slog.Level {
	switch level {
	case "debug":
		return slog.LevelDebug
	case "warn":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

// newLogger creates a stdout-only JSON logger (fallback when file logger fails).
func newLogger(level string) *slog.Logger {
	return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: parseLogLevel(level)}))
}
