package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/steveyegge/gastown/internal/config"
	"github.com/steveyegge/gastown/internal/git"
	"github.com/steveyegge/gastown/internal/polecat"
	"github.com/steveyegge/gastown/internal/rig"
	"github.com/steveyegge/gastown/internal/runtime"
	"github.com/steveyegge/gastown/internal/style"
	"github.com/steveyegge/gastown/internal/swarm"
	"github.com/steveyegge/gastown/internal/tmux"
	"github.com/steveyegge/gastown/internal/workspace"
)

// Swarm command flags
var (
	swarmEpic       string
	swarmTasks      []string
	swarmWorkers    []string
	swarmStart      bool
	swarmStatusJSON bool
	swarmListRig    string
	swarmListStatus string
	swarmListJSON   bool
	swarmTarget     string
)

var swarmCmd = &cobra.Command{
	Use:        "swarm",
	GroupID:    GroupWork,
	Short:      "[DEPRECATED] Use 'gt convoy' instead",
	Deprecated: "Use 'gt convoy' for work tracking. A 'swarm' is now just the ephemeral workers on a convoy.",
	RunE:       requireSubcommand,
	Long: `DEPRECATED: Use 'gt convoy' instead.

The term "swarm" now refers to the ephemeral set of workers on a convoy's issues,
not a persistent tracking unit. Use 'gt convoy' for creating and tracking batched work.

TERMINOLOGY:
  Convoy: Persistent tracking unit (what this command was trying to be)
  Swarm:  Ephemeral workers on a convoy (no separate tracking needed)

MIGRATION:
  gt swarm create  →  gt convoy create
  gt swarm status  →  gt convoy status
  gt swarm list    →  gt convoy list

See 'gt convoy --help' for the new workflow.`,
}

var swarmCreateCmd = &cobra.Command{
	Use:   "create <rig>",
	Short: "Create a new swarm",
	Long: `Create a new swarm in a rig.

Creates a swarm that coordinates multiple polecats working on tasks from
a beads epic. All workers branch from the same base commit.

Examples:
  gt swarm create greenplace --epic gp-abc --worker Toast --worker Nux
  gt swarm create greenplace --epic gp-abc --worker Toast --start`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmCreate,
}

var swarmStatusCmd = &cobra.Command{
	Use:   "status <swarm-id>",
	Short: "Show swarm status",
	Long: `Show detailed status for a swarm.

Displays swarm metadata, task progress, worker assignments, and integration
branch status.`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmStatus,
}

var swarmListCmd = &cobra.Command{
	Use:   "list [rig]",
	Short: "List swarms",
	Long: `List swarms, optionally filtered by rig or status.

Examples:
  gt swarm list
  gt swarm list greenplace
  gt swarm list --status=active
  gt swarm list greenplace --status=landed`,
	Args: cobra.MaximumNArgs(1),
	RunE: runSwarmList,
}

var swarmLandCmd = &cobra.Command{
	Use:   "land <swarm-id>",
	Short: "Land a swarm to main",
	Long: `Manually trigger landing for a completed swarm.

Merges the integration branch to the target branch (usually main).
Normally this is done automatically by the Refinery.`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmLand,
}

var swarmCancelCmd = &cobra.Command{
	Use:   "cancel <swarm-id>",
	Short: "Cancel a swarm",
	Long: `Cancel an active swarm.

Marks the swarm as canceled and optionally cleans up branches.`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmCancel,
}

var swarmStartCmd = &cobra.Command{
	Use:   "start <swarm-id>",
	Short: "Start a created swarm",
	Long: `Start a swarm that was created without --start.

Transitions the swarm from 'created' to 'active' state.`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmStart,
}

var swarmDispatchCmd = &cobra.Command{
	Use:   "dispatch <epic-id>",
	Short: "Assign next ready task to an idle worker",
	Long: `Dispatch the next ready task from an epic to an available worker.

Finds the first unassigned task in the epic's ready front and slings it
to an idle polecat in the rig.

Examples:
  gt swarm dispatch gt-abc         # Dispatch next task from epic gt-abc
  gt swarm dispatch gt-abc --rig greenplace  # Dispatch in specific rig`,
	Args: cobra.ExactArgs(1),
	RunE: runSwarmDispatch,
}

var swarmDispatchRig string

func init() {
	// Create flags
	swarmCreateCmd.Flags().StringVar(&swarmEpic, "epic", "", "Beads epic ID for this swarm (required)")
	swarmCreateCmd.Flags().StringSliceVar(&swarmWorkers, "worker", nil, "Polecat names to assign (repeatable)")
	swarmCreateCmd.Flags().BoolVar(&swarmStart, "start", false, "Start swarm immediately after creation")
	swarmCreateCmd.Flags().StringVar(&swarmTarget, "target", "main", "Target branch for landing")
	_ = swarmCreateCmd.MarkFlagRequired("epic") // cobra flags: error only at runtime if missing

	// Status flags
	swarmStatusCmd.Flags().BoolVar(&swarmStatusJSON, "json", false, "Output as JSON")

	// List flags
	swarmListCmd.Flags().StringVar(&swarmListStatus, "status", "", "Filter by status (active, landed, canceled, failed)")
	swarmListCmd.Flags().BoolVar(&swarmListJSON, "json", false, "Output as JSON")

	// Dispatch flags
	swarmDispatchCmd.Flags().StringVar(&swarmDispatchRig, "rig", "", "Rig to dispatch in (auto-detected from epic if not specified)")

	// Add subcommands
	swarmCmd.AddCommand(swarmCreateCmd)
	swarmCmd.AddCommand(swarmStartCmd)
	swarmCmd.AddCommand(swarmStatusCmd)
	swarmCmd.AddCommand(swarmListCmd)
	swarmCmd.AddCommand(swarmLandCmd)
	swarmCmd.AddCommand(swarmCancelCmd)
	swarmCmd.AddCommand(swarmDispatchCmd)

	rootCmd.AddCommand(swarmCmd)
}

// getSwarmRig gets a rig by name.
func getSwarmRig(rigName string) (*rig.Rig, string, error) {
	townRoot, err := workspace.FindFromCwdOrError()
	if err != nil {
		return nil, "", fmt.Errorf("not in a Gas Town workspace: %w", err)
	}

	rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
	if err != nil {
		rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
	}

	g := git.NewGit(townRoot)
	rigMgr := rig.NewManager(townRoot, rigsConfig, g)
	r, err := rigMgr.GetRig(rigName)
	if err != nil {
		return nil, "", fmt.Errorf("rig '%s' not found", rigName)
	}

	return r, townRoot, nil
}

// getAllRigs returns all discovered rigs.
func getAllRigs() ([]*rig.Rig, string, error) {
	townRoot, err := workspace.FindFromCwdOrError()
	if err != nil {
		return nil, "", fmt.Errorf("not in a Gas Town workspace: %w", err)
	}

	rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
	rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
	if err != nil {
		rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
	}

	g := git.NewGit(townRoot)
	rigMgr := rig.NewManager(townRoot, rigsConfig, g)
	rigs, err := rigMgr.DiscoverRigs()
	if err != nil {
		return nil, "", err
	}

	return rigs, townRoot, nil
}

func runSwarmCreate(cmd *cobra.Command, args []string) error {
	rigName := args[0]

	r, townRoot, err := getSwarmRig(rigName)
	if err != nil {
		return err
	}

	// Use beads to create the swarm molecule
	// First check if the epic already exists (it may be pre-created)
	// Use BeadsPath() to ensure we read from git-synced beads location
	beadsPath := r.BeadsPath()
	checkCmd := exec.Command("bd", "show", swarmEpic, "--json")
	checkCmd.Dir = beadsPath
	if err := checkCmd.Run(); err != nil {
		// Epic doesn't exist, create it as a swarm molecule
		createArgs := []string{
			"create",
			"--type=epic",
			"--mol-type=swarm",
			"--title", swarmEpic,
			"--silent",
		}
		createCmd := exec.Command("bd", createArgs...)
		createCmd.Dir = beadsPath
		var stdout bytes.Buffer
		createCmd.Stdout = &stdout
		if err := createCmd.Run(); err != nil {
			return fmt.Errorf("creating swarm epic: %w", err)
		}
	}

	// Get current git commit as base
	baseCommit := "unknown"
	gitCmd := exec.Command("git", "rev-parse", "HEAD")
	gitCmd.Dir = r.Path
	if out, err := gitCmd.Output(); err == nil {
		baseCommit = strings.TrimSpace(string(out))
	}

	integration := fmt.Sprintf("swarm/%s", swarmEpic)

	// Output
	fmt.Printf("%s Created swarm %s\n\n", style.Bold.Render("✓"), swarmEpic)
	fmt.Printf("  Epic:        %s\n", swarmEpic)
	fmt.Printf("  Rig:         %s\n", rigName)
	fmt.Printf("  Base commit: %s\n", truncate(baseCommit, 8))
	fmt.Printf("  Integration: %s\n", integration)
	fmt.Printf("  Target:      %s\n", swarmTarget)
	fmt.Printf("  Workers:     %s\n", strings.Join(swarmWorkers, ", "))

	// If workers specified, assign them to tasks
	if len(swarmWorkers) > 0 {
		fmt.Printf("\nNote: Worker assignment to tasks is handled during swarm start\n")
	}

	// Start if requested
	if swarmStart {
		// Get swarm status to find ready tasks
		statusCmd := exec.Command("bd", "swarm", "status", swarmEpic, "--json")
		statusCmd.Dir = beadsPath
		var statusOut bytes.Buffer
		statusCmd.Stdout = &statusOut
		if err := statusCmd.Run(); err != nil {
			return fmt.Errorf("getting swarm status: %w", err)
		}

		// Parse status to dispatch workers
		var status struct {
			Ready []struct {
				ID    string `json:"id"`
				Title string `json:"title"`
			} `json:"ready"`
		}
		if err := json.Unmarshal(statusOut.Bytes(), &status); err == nil && len(status.Ready) > 0 {
			fmt.Printf("\nReady front has %d tasks available\n", len(status.Ready))
			if len(swarmWorkers) > 0 {
				// Spawn workers for ready tasks
				fmt.Printf("Spawning workers...\n")
				_ = spawnSwarmWorkersFromBeads(r, townRoot, swarmEpic, swarmWorkers, status.Ready)
			}
		}
	} else {
		fmt.Printf("\n  %s\n", style.Dim.Render("Use --start or 'gt swarm start' to activate"))
	}

	return nil
}

func runSwarmStart(cmd *cobra.Command, args []string) error {
	swarmID := args[0]

	// Find the swarm's rig
	rigs, townRoot, err := getAllRigs()
	if err != nil {
		return err
	}

	var foundRig *rig.Rig
	for _, r := range rigs {
		// Check if swarm exists in this rig by querying beads
		// Use BeadsPath() to ensure we read from git-synced location
		checkCmd := exec.Command("bd", "show", swarmID, "--json")
		checkCmd.Dir = r.BeadsPath()
		if err := checkCmd.Run(); err == nil {
			foundRig = r
			break
		}
	}

	if foundRig == nil {
		return fmt.Errorf("swarm '%s' not found", swarmID)
	}

	// Get swarm status from beads
	statusCmd := exec.Command("bd", "swarm", "status", swarmID, "--json")
	statusCmd.Dir = foundRig.BeadsPath()
	var stdout bytes.Buffer
	statusCmd.Stdout = &stdout

	if err := statusCmd.Run(); err != nil {
		return fmt.Errorf("getting swarm status: %w", err)
	}

	var status struct {
		EpicID string `json:"epic_id"`
		Ready  []struct {
			ID    string `json:"id"`
			Title string `json:"title"`
		} `json:"ready"`
		Active []struct {
			ID       string `json:"id"`
			Assignee string `json:"assignee"`
		} `json:"active"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &status); err != nil {
		return fmt.Errorf("parsing swarm status: %w", err)
	}

	if len(status.Active) > 0 {
		fmt.Printf("Swarm already has %d active tasks\n", len(status.Active))
	}

	if len(status.Ready) == 0 {
		fmt.Println("No ready tasks to dispatch")
		return nil
	}

	fmt.Printf("%s Swarm %s starting with %d ready tasks\n", style.Bold.Render("✓"), swarmID, len(status.Ready))

	// If workers were specified in create, use them; otherwise prompt user
	if len(swarmWorkers) > 0 {
		fmt.Printf("\nSpawning workers...\n")
		_ = spawnSwarmWorkersFromBeads(foundRig, townRoot, swarmID, swarmWorkers, status.Ready)
	} else {
		fmt.Printf("\nReady tasks:\n")
		for _, task := range status.Ready {
			fmt.Printf("  ○ %s: %s\n", task.ID, task.Title)
		}
		fmt.Printf("\nUse 'gt sling <task-id> <rig>/<worker>' to assign tasks\n")
	}

	return nil
}

func runSwarmDispatch(cmd *cobra.Command, args []string) error {
	epicID := args[0]

	// Find the epic's rig by trying to show it in each rig
	rigs, townRoot, err := getAllRigs()
	if err != nil {
		return err
	}

	var foundRig *rig.Rig
	for _, r := range rigs {
		// If --rig specified, only check that rig
		if swarmDispatchRig != "" && r.Name != swarmDispatchRig {
			continue
		}
		// Use BeadsPath() to ensure we read from git-synced location
		checkCmd := exec.Command("bd", "show", epicID, "--json")
		checkCmd.Dir = r.BeadsPath()
		if err := checkCmd.Run(); err == nil {
			foundRig = r
			break
		}
	}

	if foundRig == nil {
		if swarmDispatchRig != "" {
			return fmt.Errorf("epic '%s' not found in rig '%s'", epicID, swarmDispatchRig)
		}
		return fmt.Errorf("epic '%s' not found in any rig", epicID)
	}

	// Get swarm/epic status to find ready tasks
	statusCmd := exec.Command("bd", "swarm", "status", epicID, "--json")
	statusCmd.Dir = foundRig.BeadsPath()
	var stdout bytes.Buffer
	statusCmd.Stdout = &stdout

	if err := statusCmd.Run(); err != nil {
		return fmt.Errorf("getting epic status: %w", err)
	}

	var status struct {
		Ready []struct {
			ID       string `json:"id"`
			Title    string `json:"title"`
			Assignee string `json:"assignee"`
		} `json:"ready"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &status); err != nil {
		return fmt.Errorf("parsing epic status: %w", err)
	}

	// Filter to unassigned ready tasks
	var unassigned []struct {
		ID    string
		Title string
	}
	for _, task := range status.Ready {
		if task.Assignee == "" {
			unassigned = append(unassigned, struct {
				ID    string
				Title string
			}{task.ID, task.Title})
		}
	}

	if len(unassigned) == 0 {
		fmt.Println("No unassigned ready tasks to dispatch")
		return nil
	}

	// Find idle polecats (no hooked work)
	polecatGit := git.NewGit(foundRig.Path)
	polecatMgr := polecat.NewManager(foundRig, polecatGit)
	polecats, err := polecatMgr.List()
	if err != nil {
		return fmt.Errorf("listing polecats: %w", err)
	}

	// Check which polecats have no hooked work
	var idlePolecats []string
	for _, p := range polecats {
		// Check if polecat has hooked work by querying beads
		hookCheckCmd := exec.Command("bd", "list", "--status=hooked", "--assignee", fmt.Sprintf("%s/polecats/%s", foundRig.Name, p.Name), "--json")
		hookCheckCmd.Dir = foundRig.BeadsPath()
		var hookOut bytes.Buffer
		hookCheckCmd.Stdout = &hookOut
		if err := hookCheckCmd.Run(); err == nil {
			var hooked []interface{}
			if err := json.Unmarshal(hookOut.Bytes(), &hooked); err == nil && len(hooked) == 0 {
				idlePolecats = append(idlePolecats, p.Name)
			}
		}
	}

	if len(idlePolecats) == 0 {
		fmt.Println("No idle polecats available")
		fmt.Printf("\nUnassigned ready tasks:\n")
		for _, task := range unassigned {
			fmt.Printf("  ○ %s: %s\n", task.ID, task.Title)
		}
		fmt.Printf("\nCreate a new polecat or wait for one to become idle.\n")
		return nil
	}

	// Dispatch first unassigned task to first idle polecat
	task := unassigned[0]
	worker := idlePolecats[0]
	target := fmt.Sprintf("%s/%s", foundRig.Name, worker)

	fmt.Printf("Dispatching %s to %s...\n", task.ID, target)

	// Use gt sling to assign the task
	slingCmd := exec.Command("gt", "sling", task.ID, target)
	slingCmd.Dir = townRoot
	slingCmd.Stdout = os.Stdout
	slingCmd.Stderr = os.Stderr

	if err := slingCmd.Run(); err != nil {
		return fmt.Errorf("slinging task: %w", err)
	}

	fmt.Printf("%s Dispatched %s: %s → %s\n", style.Bold.Render("✓"), task.ID, task.Title, target)

	// Show remaining tasks and workers
	if len(unassigned) > 1 {
		fmt.Printf("\n%d more ready tasks available\n", len(unassigned)-1)
	}
	if len(idlePolecats) > 1 {
		fmt.Printf("%d more idle polecats available\n", len(idlePolecats)-1)
	}

	return nil
}

// spawnSwarmWorkersFromBeads spawns sessions for swarm workers using beads task list.
func spawnSwarmWorkersFromBeads(r *rig.Rig, townRoot string, swarmID string, workers []string, tasks []struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}) error { //nolint:unparam // error return kept for future use
	t := tmux.NewTmux()
	polecatSessMgr := polecat.NewSessionManager(t, r)
	polecatGit := git.NewGit(r.Path)
	polecatMgr := polecat.NewManager(r, polecatGit)

	// Pair workers with tasks (round-robin if more tasks than workers)
	workerIdx := 0
	for _, task := range tasks {
		if workerIdx >= len(workers) {
			break // No more workers
		}

		worker := workers[workerIdx]
		workerIdx++

		// Use gt sling to assign task to worker (this updates beads)
		slingCmd := exec.Command("gt", "sling", task.ID, fmt.Sprintf("%s/%s", r.Name, worker))
		slingCmd.Dir = townRoot
		if err := slingCmd.Run(); err != nil {
			style.PrintWarning("  couldn't sling %s to %s: %v", task.ID, worker, err)

			// Fallback: update polecat state directly
			if err := polecatMgr.AssignIssue(worker, task.ID); err != nil {
				style.PrintWarning("  couldn't assign %s to %s: %v", task.ID, worker, err)
				continue
			}
		}

		// Check if already running
		running, _ := polecatSessMgr.IsRunning(worker)
		if running {
			fmt.Printf("  %s already running, injecting task...\n", worker)
		} else {
			fmt.Printf("  Starting %s...\n", worker)
			if err := polecatSessMgr.Start(worker, polecat.SessionStartOptions{}); err != nil {
				style.PrintWarning("  couldn't start %s: %v", worker, err)
				continue
			}
			// Wait for Claude to initialize
			time.Sleep(5 * time.Second)
		}

		// Inject work assignment
		context := fmt.Sprintf("[SWARM] You are part of swarm %s.\n\nAssigned task: %s\nTitle: %s\n\nWork on this task. When complete, commit and signal DONE.",
			swarmID, task.ID, task.Title)
		if err := polecatSessMgr.Inject(worker, context); err != nil {
			style.PrintWarning("  couldn't inject to %s: %v", worker, err)
		} else {
			fmt.Printf("  %s → %s ✓\n", worker, task.ID)
		}
	}

	return nil
}

func runSwarmStatus(cmd *cobra.Command, args []string) error {
	swarmID := args[0]

	// Find the swarm's rig by trying to show it in each rig
	rigs, _, err := getAllRigs()
	if err != nil {
		return err
	}
	if len(rigs) == 0 {
		return fmt.Errorf("no rigs found")
	}

	// Find which rig has this swarm
	var foundRig *rig.Rig
	for _, r := range rigs {
		// Use BeadsPath() to ensure we read from git-synced location
		checkCmd := exec.Command("bd", "show", swarmID, "--json")
		checkCmd.Dir = r.BeadsPath()
		if err := checkCmd.Run(); err == nil {
			foundRig = r
			break
		}
	}

	if foundRig == nil {
		return fmt.Errorf("swarm '%s' not found in any rig", swarmID)
	}

	// Use bd swarm status to get swarm info from beads
	bdArgs := []string{"swarm", "status", swarmID}
	if swarmStatusJSON {
		bdArgs = append(bdArgs, "--json")
	}

	bdCmd := exec.Command("bd", bdArgs...)
	bdCmd.Dir = foundRig.BeadsPath()
	bdCmd.Stdout = os.Stdout
	bdCmd.Stderr = os.Stderr

	return bdCmd.Run()
}

func runSwarmList(cmd *cobra.Command, args []string) error {
	rigs, _, err := getAllRigs()
	if err != nil {
		return err
	}

	// Filter by rig if specified
	if len(args) > 0 {
		rigName := args[0]
		var filtered []*rig.Rig
		for _, r := range rigs {
			if r.Name == rigName {
				filtered = append(filtered, r)
			}
		}
		if len(filtered) == 0 {
			return fmt.Errorf("rig '%s' not found", rigName)
		}
		rigs = filtered
	}

	if len(rigs) == 0 {
		fmt.Println("No rigs found.")
		return nil
	}

	// Use bd list --mol-type=swarm to find swarm molecules
	bdArgs := []string{"list", "--mol-type=swarm", "--type=epic"}
	if swarmListJSON {
		bdArgs = append(bdArgs, "--json")
	}

	// Collect swarms from all rigs
	type swarmListEntry struct {
		ID     string `json:"id"`
		Title  string `json:"title"`
		Status string `json:"status"`
		Rig    string `json:"rig"`
	}
	var allSwarms []swarmListEntry

	for _, r := range rigs {
		bdCmd := exec.Command("bd", bdArgs...)
		bdCmd.Dir = r.BeadsPath() // Use BeadsPath() for git-synced beads
		var stdout bytes.Buffer
		bdCmd.Stdout = &stdout

		if err := bdCmd.Run(); err != nil {
			continue
		}

		if swarmListJSON {
			// Parse JSON output
			var issues []struct {
				ID     string `json:"id"`
				Title  string `json:"title"`
				Status string `json:"status"`
			}
			if err := json.Unmarshal(stdout.Bytes(), &issues); err == nil {
				for _, issue := range issues {
					allSwarms = append(allSwarms, swarmListEntry{
						ID:     issue.ID,
						Title:  issue.Title,
						Status: issue.Status,
						Rig:    r.Name,
					})
				}
			}
		} else {
			// Parse line output - each line is an issue
			lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
			for _, line := range lines {
				if line == "" {
					continue
				}
				// Filter by status if specified
				if swarmListStatus != "" && !strings.Contains(strings.ToLower(line), swarmListStatus) {
					continue
				}
				allSwarms = append(allSwarms, swarmListEntry{
					ID:  line,
					Rig: r.Name,
				})
			}
		}
	}

	// JSON output
	if swarmListJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(allSwarms)
	}

	// Human-readable output
	if len(allSwarms) == 0 {
		fmt.Println("No swarms found.")
		fmt.Println("Create a swarm with: gt swarm create <rig> --epic <epic-id>")
		return nil
	}

	fmt.Printf("%s\n\n", style.Bold.Render("Swarms"))
	for _, entry := range allSwarms {
		fmt.Printf("  %s [%s]\n", entry.ID, entry.Rig)
	}
	fmt.Printf("\nUse 'gt swarm status <id>' for detailed status.\n")

	return nil
}

func runSwarmLand(cmd *cobra.Command, args []string) error {
	swarmID := args[0]

	// Find the swarm's rig
	rigs, townRoot, err := getAllRigs()
	if err != nil {
		return err
	}

	var foundRig *rig.Rig
	for _, r := range rigs {
		// Use BeadsPath() for git-synced beads
		checkCmd := exec.Command("bd", "show", swarmID, "--json")
		checkCmd.Dir = r.BeadsPath()
		if err := checkCmd.Run(); err == nil {
			foundRig = r
			break
		}
	}

	if foundRig == nil {
		return fmt.Errorf("swarm '%s' not found", swarmID)
	}

	// Check swarm status - all children should be closed
	statusCmd := exec.Command("bd", "swarm", "status", swarmID, "--json")
	statusCmd.Dir = foundRig.BeadsPath()
	var stdout bytes.Buffer
	statusCmd.Stdout = &stdout

	if err := statusCmd.Run(); err != nil {
		return fmt.Errorf("getting swarm status: %w", err)
	}

	var status struct {
		Ready       []struct{ ID string } `json:"ready"`
		Active      []struct{ ID string } `json:"active"`
		Blocked     []struct{ ID string } `json:"blocked"`
		Completed   []struct{ ID string } `json:"completed"`
		TotalIssues int                   `json:"total_issues"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &status); err != nil {
		return fmt.Errorf("parsing swarm status: %w", err)
	}

	// Check if all tasks are complete
	if len(status.Ready) > 0 || len(status.Active) > 0 || len(status.Blocked) > 0 {
		return fmt.Errorf("swarm has incomplete tasks: %d ready, %d active, %d blocked",
			len(status.Ready), len(status.Active), len(status.Blocked))
	}

	fmt.Printf("Landing swarm %s to main...\n", swarmID)

	// Use swarm manager for the actual landing (git operations)
	mgr := swarm.NewManager(foundRig)
	sw, err := mgr.LoadSwarm(swarmID)
	if err != nil {
		return fmt.Errorf("loading swarm from beads: %w", err)
	}

	// Execute full landing protocol
	config := swarm.LandingConfig{
		TownRoot: townRoot,
	}
	result, err := mgr.ExecuteLanding(swarmID, config)
	if err != nil {
		return fmt.Errorf("landing protocol: %w", err)
	}

	if !result.Success {
		return fmt.Errorf("landing failed: %s", result.Error)
	}

	// Close the swarm epic in beads
	closeArgs := []string{"close", swarmID, "--reason", "Swarm landed to main"}
	if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
		closeArgs = append(closeArgs, "--session="+sessionID)
	}
	closeCmd := exec.Command("bd", closeArgs...)
	closeCmd.Dir = foundRig.BeadsPath()
	if err := closeCmd.Run(); err != nil {
		style.PrintWarning("couldn't close swarm epic in beads: %v", err)
	}

	fmt.Printf("%s Swarm %s landed to main\n", style.Bold.Render("✓"), sw.ID)
	fmt.Printf("  Sessions stopped: %d\n", result.SessionsStopped)
	fmt.Printf("  Branches cleaned: %d\n", result.BranchesCleaned)
	return nil
}

func runSwarmCancel(cmd *cobra.Command, args []string) error {
	swarmID := args[0]

	// Find the swarm's rig
	rigs, _, err := getAllRigs()
	if err != nil {
		return err
	}

	var foundRig *rig.Rig
	for _, r := range rigs {
		// Use BeadsPath() for git-synced beads
		checkCmd := exec.Command("bd", "show", swarmID, "--json")
		checkCmd.Dir = r.BeadsPath()
		if err := checkCmd.Run(); err == nil {
			foundRig = r
			break
		}
	}

	if foundRig == nil {
		return fmt.Errorf("swarm '%s' not found", swarmID)
	}

	// Check if swarm is already closed
	checkCmd := exec.Command("bd", "show", swarmID, "--json")
	checkCmd.Dir = foundRig.BeadsPath()
	var stdout bytes.Buffer
	checkCmd.Stdout = &stdout
	if err := checkCmd.Run(); err != nil {
		return fmt.Errorf("checking swarm status: %w", err)
	}

	var issue struct {
		Status string `json:"status"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &issue); err == nil {
		if issue.Status == "closed" {
			return fmt.Errorf("swarm already closed")
		}
	}

	// Close the swarm epic in beads with canceled reason
	closeArgs := []string{"close", swarmID, "--reason", "Swarm canceled"}
	if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
		closeArgs = append(closeArgs, "--session="+sessionID)
	}
	closeCmd := exec.Command("bd", closeArgs...)
	closeCmd.Dir = foundRig.BeadsPath()
	if err := closeCmd.Run(); err != nil {
		return fmt.Errorf("closing swarm: %w", err)
	}

	fmt.Printf("%s Swarm %s canceled\n", style.Bold.Render("✓"), swarmID)
	return nil
}

// Helper functions

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n]
}
