// Command agentctl manages Matrix agents: list, start, stop, remove.
//
// Usage:
//
//	agentctl list                  # all agents with their status
//	agentctl start                 # start all enabled agents
//	agentctl start assistant-bot   # start a specific agent
//	agentctl stop                  # stop all running agents
//	agentctl stop assistant-bot    # stop a specific agent
//	agentctl remove assistant-bot  # disable agent (keeps data)
package main

import (
	"fmt"
	"os"
	"strings"
	"syscall"

	"github.com/spf13/cobra"

	"github.com/enmanuel/agents/shell/process"
)

const (
	runDir     = "run"
	agentsGlob = "agents/*/config.yaml"
)

// ── entry point ───────────────────────────────────────────────────────────

func main() {
	var binPath string

	mgr := process.NewManager(runDir, agentsGlob, "")

	root := &cobra.Command{
		Use:   "agentctl",
		Short: "Manage Matrix agents",
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return os.MkdirAll(runDir, 0o755)
		},
	}

	root.PersistentFlags().StringVar(&binPath, "bin", "",
		"Launcher binary path. Defaults to ./bin/launcher, falls back to 'go run ./cmd/launcher'")

	root.AddCommand(
		listCmd(mgr),
		startCmd(mgr, &binPath),
		stopCmd(mgr),
		reloadCmd(mgr),
		removeCmd(mgr),
		avatarCmd(),
		uploadMediaCmd(),
		setAvatarURLCmd(),
		autoAvatarCmd(),
		displaynameCmd(),
	)

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

// ── list ──────────────────────────────────────────────────────────────────

func listCmd(mgr *process.Manager) *cobra.Command {
	return &cobra.Command{
		Use:     "list",
		Short:   "List all agents and their current status",
		Aliases: []string{"ls"},
		RunE: func(cmd *cobra.Command, args []string) error {
			statuses, err := mgr.StatusAll()
			if err != nil {
				return err
			}
			if len(statuses) == 0 {
				fmt.Println("No agents found under agents/*/config.yaml")
				return nil
			}

			fmt.Printf("%-20s  %-14s  %-8s  %-4s  %s\n", "ID", "STATUS", "VERSION", "INST", "DESCRIPTION")
			fmt.Println(strings.Repeat("─", 78))
			for _, s := range statuses {
				fmt.Printf("%-20s  %-14s  %-8s  %-4d  %s\n",
					s.ID,
					statusLabel(s),
					s.Version,
					s.Instances,
					truncate(s.Desc, 32),
				)
			}
			return nil
		},
	}
}

// ── start ─────────────────────────────────────────────────────────────────

func startCmd(mgr *process.Manager, binPath *string) *cobra.Command {
	return &cobra.Command{
		Use:   "start [agent-id...]",
		Short: "Start one or all enabled agents",
		RunE: func(cmd *cobra.Command, args []string) error {
			statuses, err := mgr.StatusAll()
			if err != nil {
				return err
			}

			targets := filterTargets(statuses, args)
			if len(targets) == 0 {
				return fmt.Errorf("no matching agents found")
			}

			started := 0
			for _, s := range targets {
				if !s.Enabled {
					fmt.Printf("skip  %-20s  (disabled in config)\n", s.ID)
					continue
				}
				if err := mgr.Start(s.AgentInfo); err != nil {
					fmt.Fprintf(os.Stderr, "fail  %-20s  %v\n", s.ID, err)
					continue
				}

				fmt.Printf("start %-20s  PID %d  (instances: %d)  log → %s\n",
					s.ID, mgr.ReadPID(s.ID), mgr.InstanceCount(s.ID), mgr.LogPath(s.ID))
				started++
			}

			if started == 0 {
				fmt.Println("Nothing started.")
			}
			return nil
		},
	}
}

// ── stop ──────────────────────────────────────────────────────────────────

func stopCmd(mgr *process.Manager) *cobra.Command {
	return &cobra.Command{
		Use:   "stop [agent-id...]",
		Short: "Stop one or all running agents",
		RunE: func(cmd *cobra.Command, args []string) error {
			statuses, err := mgr.StatusAll()
			if err != nil {
				return err
			}

			targets := filterTargets(statuses, args)
			if len(targets) == 0 {
				return fmt.Errorf("no matching agents found")
			}

			stopped := 0
			for _, s := range targets {
				if !s.Running {
					fmt.Printf("skip  %-20s  (not running)\n", s.ID)
					continue
				}
				pid := s.PID
				if err := mgr.Stop(s.ID); err != nil {
					fmt.Fprintf(os.Stderr, "fail  %-20s  %v\n", s.ID, err)
					continue
				}
				fmt.Printf("stop  %-20s  stopped PID %d\n", s.ID, pid)
				stopped++
			}

			if stopped == 0 {
				fmt.Println("Nothing stopped.")
			}
			return nil
		},
	}
}

// ── reload ────────────────────────────────────────────────────────────────

func reloadCmd(mgr *process.Manager) *cobra.Command {
	return &cobra.Command{
		Use:   "reload [agent-id]",
		Short: "Hot-reload an agent (or all agents) without stopping the launcher",
		Long: `Sends SIGHUP to the running launcher, which triggers a hot-reload.
If an agent-id is given, only that agent is reloaded.
If no agent-id is given, all agents are reloaded.

The launcher must be running. Use 'agentctl start' first if needed.`,
		Args: cobra.MaximumNArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			pid := mgr.UnifiedPID()
			if pid <= 0 {
				return fmt.Errorf("launcher is not running")
			}

			target := ""
			if len(args) == 1 {
				target = args[0]
			}

			if target != "" {
				if err := os.WriteFile("run/reload.txt", []byte(target), 0o644); err != nil {
					return fmt.Errorf("write reload target: %w", err)
				}
				fmt.Printf("reload %-20s  sending SIGHUP to PID %d\n", target, pid)
			} else {
				// Remove any stale reload.txt so SIGHUP reloads all agents.
				_ = os.Remove("run/reload.txt")
				fmt.Printf("reload %-20s  sending SIGHUP to PID %d\n", "(all)", pid)
			}

			if err := syscall.Kill(pid, syscall.SIGHUP); err != nil {
				return fmt.Errorf("kill -HUP %d: %w", pid, err)
			}
			return nil
		},
	}
}

// ── remove ────────────────────────────────────────────────────────────────

func removeCmd(mgr *process.Manager) *cobra.Command {
	return &cobra.Command{
		Use:   "remove <agent-id>",
		Short: "Disable an agent (sets enabled: false). Does not delete data.",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			id := args[0]

			statuses, err := mgr.StatusAll()
			if err != nil {
				return err
			}

			var target *process.AgentStatus
			for i := range statuses {
				if statuses[i].ID == id {
					target = &statuses[i]
					break
				}
			}
			if target == nil {
				return fmt.Errorf("agent %q not found", id)
			}

			if target.Running {
				if err := mgr.Stop(id); err != nil {
					fmt.Fprintf(os.Stderr, "warn  stop failed: %v\n", err)
				} else {
					fmt.Printf("stop  %-20s  stopped PID %d\n", id, target.PID)
				}
			}

			if err := setEnabled(target.ConfigPath, false); err != nil {
				return fmt.Errorf("update config: %w", err)
			}

			fmt.Printf("ok    %-20s  marked as disabled in %s\n", id, target.ConfigPath)
			fmt.Printf("      Data preserved at agents/%s/data/\n", id)
			return nil
		},
	}
}

// ── helpers ───────────────────────────────────────────────────────────────

func filterTargets(statuses []process.AgentStatus, ids []string) []process.AgentStatus {
	if len(ids) == 0 {
		return statuses
	}
	set := make(map[string]bool, len(ids))
	for _, id := range ids {
		set[id] = true
	}
	var out []process.AgentStatus
	for _, s := range statuses {
		if set[s.ID] {
			out = append(out, s)
		}
	}
	return out
}

func statusLabel(s process.AgentStatus) string {
	switch {
	case !s.Enabled:
		return "disabled"
	case s.Running:
		if s.Instances > 1 {
			return fmt.Sprintf("● running(%d)", s.Instances)
		}
		return "● running"
	default:
		return "○ stopped"
	}
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max-1] + "…"
}

// setEnabled flips `enabled: true/false` in the agent section of the YAML.
func setEnabled(configPath string, enabled bool) error {
	raw, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}

	current := "enabled: true"
	replacement := "enabled: false"
	if enabled {
		current = "enabled: false"
		replacement = "enabled: true"
	}

	updated := strings.Replace(string(raw), current, replacement, 1)
	if updated == string(raw) {
		return nil
	}

	return os.WriteFile(configPath, []byte(updated), 0o644)
}
