package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"slices"
	"strings"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/hooks"
	"github.com/steveyegge/beads/internal/rpc"
	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/storage/sqlite"
	"github.com/steveyegge/beads/internal/types"
	"github.com/steveyegge/beads/internal/ui"
	"github.com/steveyegge/beads/internal/utils"
	"github.com/steveyegge/beads/internal/validation"
)

var showCmd = &cobra.Command{
	Use:     "show [id...]",
	GroupID: "issues",
	Short:   "Show issue details",
	Args:    cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		showThread, _ := cmd.Flags().GetBool("thread")
		ctx := rootCtx

		// Check database freshness before reading
		// Skip check when using daemon (daemon auto-imports on staleness)
		if daemonClient == nil {
			if err := ensureDatabaseFresh(ctx); err != nil {
				FatalErrorRespectJSON("%v", err)
			}
		}

		// Resolve partial IDs first (daemon mode only - direct mode uses routed resolution)
		var resolvedIDs []string
		var routedArgs []string // IDs that need cross-repo routing (bypass daemon)
		if daemonClient != nil {
			// In daemon mode, resolve via RPC - but check routing first
			for _, id := range args {
				// Check if this ID needs routing to a different beads directory
				if needsRouting(id) {
					routedArgs = append(routedArgs, id)
					continue
				}
				resolveArgs := &rpc.ResolveIDArgs{ID: id}
				resp, err := daemonClient.ResolveID(resolveArgs)
				if err != nil {
					FatalErrorRespectJSON("resolving ID %s: %v", id, err)
				}
				var resolvedID string
				if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
					FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
				}
				resolvedIDs = append(resolvedIDs, resolvedID)
			}
		}
		// Note: Direct mode uses resolveAndGetIssueWithRouting for prefix-based routing

		// Handle --thread flag: show full conversation thread
		if showThread {
			if daemonClient != nil && len(resolvedIDs) > 0 {
				showMessageThread(ctx, resolvedIDs[0], jsonOutput)
				return
			} else if len(args) > 0 {
				// Direct mode - resolve first arg with routing
				result, err := resolveAndGetIssueWithRouting(ctx, store, args[0])
				if result != nil {
					defer result.Close()
				}
				if err == nil && result != nil && result.ResolvedID != "" {
					showMessageThread(ctx, result.ResolvedID, jsonOutput)
					return
				}
			}
		}

		// If daemon is running, use RPC (but fall back to direct mode for routed IDs)
		if daemonClient != nil {
			allDetails := []interface{}{}
			displayIdx := 0

			// First, handle routed IDs via direct mode
			for _, id := range routedArgs {
				result, err := resolveAndGetIssueWithRouting(ctx, store, id)
				if err != nil {
					if result != nil {
						result.Close()
					}
					fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
					continue
				}
				if result == nil || result.Issue == nil {
					if result != nil {
						result.Close()
					}
					fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
					continue
				}
				issue := result.Issue
				issueStore := result.Store
				if jsonOutput {
					// Get labels and deps for JSON output
					type IssueDetails struct {
						*types.Issue
						Labels       []string                             `json:"labels,omitempty"`
						Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
						Dependents   []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
						Comments     []*types.Comment                     `json:"comments,omitempty"`
						Parent       *string                              `json:"parent,omitempty"`
					}
					details := &IssueDetails{Issue: issue}
					details.Labels, _ = issueStore.GetLabels(ctx, issue.ID)
					if sqliteStore, ok := issueStore.(*sqlite.SQLiteStorage); ok {
						details.Dependencies, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
						details.Dependents, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
					}
					details.Comments, _ = issueStore.GetIssueComments(ctx, issue.ID)
					// Compute parent from dependencies
					for _, dep := range details.Dependencies {
						if dep.DependencyType == types.DepParentChild {
							details.Parent = &dep.ID
							break
						}
					}
					allDetails = append(allDetails, details)
				} else {
					if displayIdx > 0 {
						fmt.Println("\n" + strings.Repeat("─", 60))
					}
					fmt.Printf("\n%s: %s\n", ui.RenderAccent(issue.ID), issue.Title)
					fmt.Printf("Status: %s\n", issue.Status)
					fmt.Printf("Priority: P%d\n", issue.Priority)
					fmt.Printf("Type: %s\n", issue.IssueType)
					if issue.Description != "" {
						fmt.Printf("\nDescription:\n%s\n", issue.Description)
					}
					fmt.Println()
					displayIdx++
				}
				result.Close() // Close immediately after processing each routed ID
			}

			// Then, handle local IDs via daemon
			for _, id := range resolvedIDs {
				showArgs := &rpc.ShowArgs{ID: id}
				resp, err := daemonClient.Show(showArgs)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
					continue
				}

				if jsonOutput {
					type IssueDetails struct {
						types.Issue
						Labels       []string                             `json:"labels,omitempty"`
						Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
						Dependents   []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
						Comments     []*types.Comment                     `json:"comments,omitempty"`
						Parent       *string                              `json:"parent,omitempty"`
					}
					var details IssueDetails
					if err := json.Unmarshal(resp.Data, &details); err == nil {
						// Compute parent from dependencies
						for _, dep := range details.Dependencies {
							if dep.DependencyType == types.DepParentChild {
								details.Parent = &dep.ID
								break
							}
						}
						allDetails = append(allDetails, details)
					}
				} else {
					// Check if issue exists (daemon returns null for non-existent issues)
					if string(resp.Data) == "null" || len(resp.Data) == 0 {
						fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
						continue
					}
					if displayIdx > 0 {
						fmt.Println("\n" + strings.Repeat("─", 60))
					}
					displayIdx++

					// Parse response and use existing formatting code
					type IssueDetails struct {
						types.Issue
						Labels       []string                             `json:"labels,omitempty"`
						Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
						Dependents   []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
						Comments     []*types.Comment                     `json:"comments,omitempty"`
					}
					var details IssueDetails
					if err := json.Unmarshal(resp.Data, &details); err != nil {
						fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
						os.Exit(1)
					}
					issue := &details.Issue

					// Format output (same as direct mode below)
					tierEmoji := ""
					statusSuffix := ""
					switch issue.CompactionLevel {
					case 1:
						tierEmoji = " 🗜️"
						statusSuffix = " (compacted L1)"
					case 2:
						tierEmoji = " 📦"
						statusSuffix = " (compacted L2)"
					}

					fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
					fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
					if issue.CloseReason != "" {
						fmt.Printf("Close reason: %s\n", issue.CloseReason)
					}
					fmt.Printf("Priority: P%d\n", issue.Priority)
					fmt.Printf("Type: %s\n", issue.IssueType)
					if issue.Assignee != "" {
						fmt.Printf("Assignee: %s\n", issue.Assignee)
					}
					if issue.EstimatedMinutes != nil {
						fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
					}
					fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
					if issue.CreatedBy != "" {
						fmt.Printf("Created by: %s\n", issue.CreatedBy)
					}
					fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))

					// Show compaction status
					if issue.CompactionLevel > 0 {
						fmt.Println()
						if issue.OriginalSize > 0 {
							currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
							saved := issue.OriginalSize - currentSize
							if saved > 0 {
								reduction := float64(saved) / float64(issue.OriginalSize) * 100
								fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
									issue.OriginalSize, currentSize, reduction)
							}
						}
						tierEmoji2 := "🗜️"
						if issue.CompactionLevel == 2 {
							tierEmoji2 = "📦"
						}
						compactedDate := ""
						if issue.CompactedAt != nil {
							compactedDate = issue.CompactedAt.Format("2006-01-02")
						}
						fmt.Printf("%s Compacted: %s (Tier %d)\n", tierEmoji2, compactedDate, issue.CompactionLevel)
					}

					if issue.Description != "" {
						fmt.Printf("\nDescription:\n%s\n", issue.Description)
					}
					if issue.Design != "" {
						fmt.Printf("\nDesign:\n%s\n", issue.Design)
					}
					if issue.Notes != "" {
						fmt.Printf("\nNotes:\n%s\n", issue.Notes)
					}
					if issue.AcceptanceCriteria != "" {
						fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
					}

					if len(details.Labels) > 0 {
						fmt.Printf("\nLabels: %v\n", details.Labels)
					}

					if len(details.Dependencies) > 0 {
						fmt.Printf("\nDepends on (%d):\n", len(details.Dependencies))
						for _, dep := range details.Dependencies {
							fmt.Printf("  → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
						}
					}

					if len(details.Dependents) > 0 {
						// Group by dependency type for clarity
						var blocks, children, related, discovered []*types.IssueWithDependencyMetadata
						for _, dep := range details.Dependents {
							switch dep.DependencyType {
							case types.DepBlocks:
								blocks = append(blocks, dep)
							case types.DepParentChild:
								children = append(children, dep)
							case types.DepRelated:
								related = append(related, dep)
							case types.DepDiscoveredFrom:
								discovered = append(discovered, dep)
							default:
								blocks = append(blocks, dep)
							}
						}

						if len(children) > 0 {
							fmt.Printf("\nChildren (%d):\n", len(children))
							for _, dep := range children {
								fmt.Printf("  ↳ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
							}
						}
						if len(blocks) > 0 {
							fmt.Printf("\nBlocks (%d):\n", len(blocks))
							for _, dep := range blocks {
								fmt.Printf("  ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
							}
						}
						if len(related) > 0 {
							fmt.Printf("\nRelated (%d):\n", len(related))
							for _, dep := range related {
								fmt.Printf("  ↔ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
							}
						}
						if len(discovered) > 0 {
							fmt.Printf("\nDiscovered (%d):\n", len(discovered))
							for _, dep := range discovered {
								fmt.Printf("  ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
							}
						}
					}

					if len(details.Comments) > 0 {
						fmt.Printf("\nComments (%d):\n", len(details.Comments))
						for _, comment := range details.Comments {
							fmt.Printf("  [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"))
							commentLines := strings.Split(comment.Text, "\n")
							for _, line := range commentLines {
								fmt.Printf("    %s\n", line)
							}
						}
					}

					fmt.Println()
				}
			}

			if jsonOutput && len(allDetails) > 0 {
				outputJSON(allDetails)
			}
			return
		}

		// Direct mode - use routed resolution for cross-repo lookups
		allDetails := []interface{}{}
		for idx, id := range args {
			// Resolve and get issue with routing (e.g., gt-xyz routes to gastown)
			result, err := resolveAndGetIssueWithRouting(ctx, store, id)
			if err != nil {
				if result != nil {
					result.Close()
				}
				fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
				continue
			}
			if result == nil || result.Issue == nil {
				if result != nil {
					result.Close()
				}
				fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
				continue
			}
			issue := result.Issue
			issueStore := result.Store // Use the store that contains this issue
			// Note: result.Close() called at end of loop iteration

			if jsonOutput {
				// Include labels, dependencies (with metadata), dependents (with metadata), and comments in JSON output
				type IssueDetails struct {
					*types.Issue
					Labels       []string                             `json:"labels,omitempty"`
					Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
					Dependents   []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
					Comments     []*types.Comment                     `json:"comments,omitempty"`
					Parent       *string                              `json:"parent,omitempty"`
				}
				details := &IssueDetails{Issue: issue}
				details.Labels, _ = issueStore.GetLabels(ctx, issue.ID)

				// Get dependencies with metadata (dependency_type field)
				if sqliteStore, ok := issueStore.(*sqlite.SQLiteStorage); ok {
					details.Dependencies, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
					details.Dependents, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
				} else {
					// Fallback to regular methods without metadata for other storage backends
					deps, _ := issueStore.GetDependencies(ctx, issue.ID)
					for _, dep := range deps {
						details.Dependencies = append(details.Dependencies, &types.IssueWithDependencyMetadata{Issue: *dep})
					}
					dependents, _ := issueStore.GetDependents(ctx, issue.ID)
					for _, dependent := range dependents {
						details.Dependents = append(details.Dependents, &types.IssueWithDependencyMetadata{Issue: *dependent})
					}
				}

				details.Comments, _ = issueStore.GetIssueComments(ctx, issue.ID)
				// Compute parent from dependencies
				for _, dep := range details.Dependencies {
					if dep.DependencyType == types.DepParentChild {
						details.Parent = &dep.ID
						break
					}
				}
				allDetails = append(allDetails, details)
				result.Close() // Close before continuing to next iteration
				continue
			}

			if idx > 0 {
				fmt.Println("\n" + strings.Repeat("─", 60))
			}

			// Add compaction emoji to title line
			tierEmoji := ""
			statusSuffix := ""
			switch issue.CompactionLevel {
			case 1:
				tierEmoji = " 🗜️"
				statusSuffix = " (compacted L1)"
			case 2:
				tierEmoji = " 📦"
				statusSuffix = " (compacted L2)"
			}

			fmt.Printf("\n%s: %s%s\n", ui.RenderAccent(issue.ID), issue.Title, tierEmoji)
			fmt.Printf("Status: %s%s\n", issue.Status, statusSuffix)
			if issue.CloseReason != "" {
				fmt.Printf("Close reason: %s\n", issue.CloseReason)
			}
			fmt.Printf("Priority: P%d\n", issue.Priority)
			fmt.Printf("Type: %s\n", issue.IssueType)
			if issue.Assignee != "" {
				fmt.Printf("Assignee: %s\n", issue.Assignee)
			}
			if issue.EstimatedMinutes != nil {
				fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
			}
			fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
			if issue.CreatedBy != "" {
				fmt.Printf("Created by: %s\n", issue.CreatedBy)
			}
			fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))

			// Show compaction status footer
			if issue.CompactionLevel > 0 {
				tierEmoji := "🗜️"
				if issue.CompactionLevel == 2 {
					tierEmoji = "📦"
				}
				tierName := fmt.Sprintf("Tier %d", issue.CompactionLevel)

				fmt.Println()
				if issue.OriginalSize > 0 {
					currentSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria)
					saved := issue.OriginalSize - currentSize
					if saved > 0 {
						reduction := float64(saved) / float64(issue.OriginalSize) * 100
						fmt.Printf("📊 Original: %d bytes | Compressed: %d bytes (%.0f%% reduction)\n",
							issue.OriginalSize, currentSize, reduction)
					}
				}
				compactedDate := ""
				if issue.CompactedAt != nil {
					compactedDate = issue.CompactedAt.Format("2006-01-02")
				}
				fmt.Printf("%s Compacted: %s (%s)\n", tierEmoji, compactedDate, tierName)
			}

			if issue.Description != "" {
				fmt.Printf("\nDescription:\n%s\n", issue.Description)
			}
			if issue.Design != "" {
				fmt.Printf("\nDesign:\n%s\n", issue.Design)
			}
			if issue.Notes != "" {
				fmt.Printf("\nNotes:\n%s\n", issue.Notes)
			}
			if issue.AcceptanceCriteria != "" {
				fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
			}

			// Show labels
			labels, _ := issueStore.GetLabels(ctx, issue.ID)
			if len(labels) > 0 {
				fmt.Printf("\nLabels: %v\n", labels)
			}

			// Show dependencies
			deps, _ := issueStore.GetDependencies(ctx, issue.ID)
			if len(deps) > 0 {
				fmt.Printf("\nDepends on (%d):\n", len(deps))
				for _, dep := range deps {
					fmt.Printf("  → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
				}
			}

			// Show dependents - grouped by dependency type for clarity
			// Use GetDependentsWithMetadata to get the dependency type
			sqliteStore, ok := issueStore.(*sqlite.SQLiteStorage)
			if ok {
				dependentsWithMeta, _ := sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
				if len(dependentsWithMeta) > 0 {
					// Group by dependency type
					var blocks, children, related, discovered []*types.IssueWithDependencyMetadata
					for _, dep := range dependentsWithMeta {
						switch dep.DependencyType {
						case types.DepBlocks:
							blocks = append(blocks, dep)
						case types.DepParentChild:
							children = append(children, dep)
						case types.DepRelated:
							related = append(related, dep)
						case types.DepDiscoveredFrom:
							discovered = append(discovered, dep)
						default:
							blocks = append(blocks, dep) // Default to blocks
						}
					}

					if len(children) > 0 {
						fmt.Printf("\nChildren (%d):\n", len(children))
						for _, dep := range children {
							fmt.Printf("  ↳ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
						}
					}
					if len(blocks) > 0 {
						fmt.Printf("\nBlocks (%d):\n", len(blocks))
						for _, dep := range blocks {
							fmt.Printf("  ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
						}
					}
					if len(related) > 0 {
						fmt.Printf("\nRelated (%d):\n", len(related))
						for _, dep := range related {
							fmt.Printf("  ↔ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
						}
					}
					if len(discovered) > 0 {
						fmt.Printf("\nDiscovered (%d):\n", len(discovered))
						for _, dep := range discovered {
							fmt.Printf("  ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
						}
					}
				}
			} else {
				// Fallback for non-SQLite storage
				dependents, _ := issueStore.GetDependents(ctx, issue.ID)
				if len(dependents) > 0 {
					fmt.Printf("\nBlocks (%d):\n", len(dependents))
					for _, dep := range dependents {
						fmt.Printf("  ← %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
					}
				}
			}

			// Show comments
			comments, _ := issueStore.GetIssueComments(ctx, issue.ID)
			if len(comments) > 0 {
				fmt.Printf("\nComments (%d):\n", len(comments))
				for _, comment := range comments {
					fmt.Printf("  [%s at %s]\n  %s\n\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text)
				}
			}

			fmt.Println()
			result.Close() // Close routed storage after each iteration
		}

		if jsonOutput && len(allDetails) > 0 {
			outputJSON(allDetails)
		} else if len(allDetails) > 0 {
			// Show tip after successful show (non-JSON mode)
			maybeShowTip(store)
		}
	},
}

var updateCmd = &cobra.Command{
	Use:     "update [id...]",
	GroupID: "issues",
	Short:   "Update one or more issues",
	Args:    cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		CheckReadonly("update")
		updates := make(map[string]interface{})

		if cmd.Flags().Changed("status") {
			status, _ := cmd.Flags().GetString("status")
			updates["status"] = status
		}
		if cmd.Flags().Changed("priority") {
			priorityStr, _ := cmd.Flags().GetString("priority")
			priority, err := validation.ValidatePriority(priorityStr)
			if err != nil {
				FatalErrorRespectJSON("%v", err)
			}
			updates["priority"] = priority
		}
		if cmd.Flags().Changed("title") {
			title, _ := cmd.Flags().GetString("title")
			updates["title"] = title
		}
		if cmd.Flags().Changed("assignee") {
			assignee, _ := cmd.Flags().GetString("assignee")
			updates["assignee"] = assignee
		}
		description, descChanged := getDescriptionFlag(cmd)
		if descChanged {
			updates["description"] = description
		}
		if cmd.Flags().Changed("design") {
			design, _ := cmd.Flags().GetString("design")
			updates["design"] = design
		}
		if cmd.Flags().Changed("notes") {
			notes, _ := cmd.Flags().GetString("notes")
			updates["notes"] = notes
		}
		if cmd.Flags().Changed("acceptance") || cmd.Flags().Changed("acceptance-criteria") {
			var acceptanceCriteria string
			if cmd.Flags().Changed("acceptance") {
				acceptanceCriteria, _ = cmd.Flags().GetString("acceptance")
			} else {
				acceptanceCriteria, _ = cmd.Flags().GetString("acceptance-criteria")
			}
			updates["acceptance_criteria"] = acceptanceCriteria
		}
		if cmd.Flags().Changed("external-ref") {
			externalRef, _ := cmd.Flags().GetString("external-ref")
			updates["external_ref"] = externalRef
		}
		if cmd.Flags().Changed("estimate") {
			estimate, _ := cmd.Flags().GetInt("estimate")
			if estimate < 0 {
				FatalErrorRespectJSON("estimate must be a non-negative number of minutes")
			}
			updates["estimated_minutes"] = estimate
		}
		if cmd.Flags().Changed("type") {
			issueType, _ := cmd.Flags().GetString("type")
			// Validate issue type
			if !types.IssueType(issueType).IsValid() {
				FatalErrorRespectJSON("invalid issue type %q. Valid types: bug, feature, task, epic, chore, merge-request, molecule, gate", issueType)
			}
			updates["issue_type"] = issueType
		}
		if cmd.Flags().Changed("add-label") {
			addLabels, _ := cmd.Flags().GetStringSlice("add-label")
			updates["add_labels"] = addLabels
		}
		if cmd.Flags().Changed("remove-label") {
			removeLabels, _ := cmd.Flags().GetStringSlice("remove-label")
			updates["remove_labels"] = removeLabels
		}
		if cmd.Flags().Changed("set-labels") {
			setLabels, _ := cmd.Flags().GetStringSlice("set-labels")
			updates["set_labels"] = setLabels
		}
		if cmd.Flags().Changed("parent") {
			parent, _ := cmd.Flags().GetString("parent")
			updates["parent"] = parent
		}
		if cmd.Flags().Changed("type") {
			issueType, _ := cmd.Flags().GetString("type")
			// Validate issue type
			if _, err := validation.ParseIssueType(issueType); err != nil {
				FatalErrorRespectJSON("%v", err)
			}
			updates["issue_type"] = issueType
		}

		if len(updates) == 0 {
			fmt.Println("No updates specified")
			return
		}

		ctx := rootCtx

		// Resolve partial IDs first
		var resolvedIDs []string
		if daemonClient != nil {
			for _, id := range args {
				resolveArgs := &rpc.ResolveIDArgs{ID: id}
				resp, err := daemonClient.ResolveID(resolveArgs)
				if err != nil {
					FatalErrorRespectJSON("resolving ID %s: %v", id, err)
				}
				var resolvedID string
				if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
					FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
				}
				resolvedIDs = append(resolvedIDs, resolvedID)
			}
		} else {
			var err error
			resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
			if err != nil {
				FatalErrorRespectJSON("%v", err)
			}
		}

		// If daemon is running, use RPC
		if daemonClient != nil {
			updatedIssues := []*types.Issue{}
			for _, id := range resolvedIDs {
				updateArgs := &rpc.UpdateArgs{ID: id}

				// Map updates to RPC args
				if status, ok := updates["status"].(string); ok {
					updateArgs.Status = &status
				}
				if priority, ok := updates["priority"].(int); ok {
					updateArgs.Priority = &priority
				}
				if title, ok := updates["title"].(string); ok {
					updateArgs.Title = &title
				}
				if assignee, ok := updates["assignee"].(string); ok {
					updateArgs.Assignee = &assignee
				}
				if description, ok := updates["description"].(string); ok {
					updateArgs.Description = &description
				}
				if design, ok := updates["design"].(string); ok {
					updateArgs.Design = &design
				}
				if notes, ok := updates["notes"].(string); ok {
					updateArgs.Notes = &notes
				}
				if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
					updateArgs.AcceptanceCriteria = &acceptanceCriteria
				}
				if externalRef, ok := updates["external_ref"].(string); ok {
					updateArgs.ExternalRef = &externalRef
				}
				if estimate, ok := updates["estimated_minutes"].(int); ok {
					updateArgs.EstimatedMinutes = &estimate
				}
				if issueType, ok := updates["issue_type"].(string); ok {
					updateArgs.IssueType = &issueType
				}
				if addLabels, ok := updates["add_labels"].([]string); ok {
					updateArgs.AddLabels = addLabels
				}
				if removeLabels, ok := updates["remove_labels"].([]string); ok {
					updateArgs.RemoveLabels = removeLabels
				}
				if setLabels, ok := updates["set_labels"].([]string); ok {
					updateArgs.SetLabels = setLabels
				}
				if issueType, ok := updates["issue_type"].(string); ok {
					updateArgs.IssueType = &issueType
				}
				if parent, ok := updates["parent"].(string); ok {
					updateArgs.Parent = &parent
				}

				resp, err := daemonClient.Update(updateArgs)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
					continue
				}

				var issue types.Issue
				if err := json.Unmarshal(resp.Data, &issue); err == nil {
					// Run update hook
					if hookRunner != nil {
						hookRunner.Run(hooks.EventUpdate, &issue)
					}
					if jsonOutput {
						updatedIssues = append(updatedIssues, &issue)
					}
				}
				if !jsonOutput {
					fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
				}
			}

			if jsonOutput && len(updatedIssues) > 0 {
				outputJSON(updatedIssues)
			}
			return
		}

		// Direct mode
		updatedIssues := []*types.Issue{}
		for _, id := range resolvedIDs {
			// Check if issue is a template: templates are read-only
			issue, err := store.GetIssue(ctx, id)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error getting %s: %v\n", id, err)
				continue
			}
			if err := validateIssueUpdatable(id, issue); err != nil {
				fmt.Fprintf(os.Stderr, "%s\n", err)
				continue
			}

			// Apply regular field updates if any
			regularUpdates := make(map[string]interface{})
			for k, v := range updates {
				if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
					regularUpdates[k] = v
				}
			}
			if len(regularUpdates) > 0 {
				if err := store.UpdateIssue(ctx, id, regularUpdates, actor); err != nil {
					fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
					continue
				}
			}

			// Handle label operations
			var setLabels, addLabels, removeLabels []string
			if v, ok := updates["set_labels"].([]string); ok {
				setLabels = v
			}
			if v, ok := updates["add_labels"].([]string); ok {
				addLabels = v
			}
			if v, ok := updates["remove_labels"].([]string); ok {
				removeLabels = v
			}
			if len(setLabels) > 0 || len(addLabels) > 0 || len(removeLabels) > 0 {
				if err := applyLabelUpdates(ctx, store, id, actor, setLabels, addLabels, removeLabels); err != nil {
					fmt.Fprintf(os.Stderr, "Error updating labels for %s: %v\n", id, err)
					continue
				}
			}

			// Handle parent reparenting
			if newParent, ok := updates["parent"].(string); ok {
				// Validate new parent exists (unless empty string to remove parent)
				if newParent != "" {
					parentIssue, err := store.GetIssue(ctx, newParent)
					if err != nil {
						fmt.Fprintf(os.Stderr, "Error getting parent %s: %v\n", newParent, err)
						continue
					}
					if parentIssue == nil {
						fmt.Fprintf(os.Stderr, "Error: parent issue %s not found\n", newParent)
						continue
					}
				}

				// Find and remove existing parent-child dependency
				deps, err := store.GetDependencyRecords(ctx, id)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Error getting dependencies for %s: %v\n", id, err)
					continue
				}
				for _, dep := range deps {
					if dep.Type == types.DepParentChild {
						if err := store.RemoveDependency(ctx, id, dep.DependsOnID, actor); err != nil {
							fmt.Fprintf(os.Stderr, "Error removing old parent dependency: %v\n", err)
						}
						break
					}
				}

				// Add new parent-child dependency (if not removing parent)
				if newParent != "" {
					newDep := &types.Dependency{
						IssueID:     id,
						DependsOnID: newParent,
						Type:        types.DepParentChild,
					}
					if err := store.AddDependency(ctx, newDep, actor); err != nil {
						fmt.Fprintf(os.Stderr, "Error adding parent dependency: %v\n", err)
						continue
					}
				}
			}

			// Run update hook
			updatedIssue, _ := store.GetIssue(ctx, id)
			if updatedIssue != nil && hookRunner != nil {
				hookRunner.Run(hooks.EventUpdate, updatedIssue)
			}

			if jsonOutput {
				if updatedIssue != nil {
					updatedIssues = append(updatedIssues, updatedIssue)
				}
			} else {
				fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), id)
			}
		}

		// Schedule auto-flush if any issues were updated
		if len(args) > 0 {
			markDirtyAndScheduleFlush()
		}

		if jsonOutput && len(updatedIssues) > 0 {
			outputJSON(updatedIssues)
		}
	},
}

var editCmd = &cobra.Command{
	Use:     "edit [id]",
	GroupID: "issues",
	Short:   "Edit an issue field in $EDITOR",
	Long: `Edit an issue field using your configured $EDITOR.

By default, edits the description. Use flags to edit other fields.

Examples:
  bd edit bd-42                    # Edit description
  bd edit bd-42 --title            # Edit title
  bd edit bd-42 --design           # Edit design notes
  bd edit bd-42 --notes            # Edit notes
  bd edit bd-42 --acceptance       # Edit acceptance criteria`,
	Args: cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		CheckReadonly("edit")
		id := args[0]
		ctx := rootCtx

		// Resolve partial ID if in direct mode
		if daemonClient == nil {
			fullID, err := utils.ResolvePartialID(ctx, store, id)
			if err != nil {
				FatalErrorRespectJSON("resolving %s: %v", id, err)
			}
			id = fullID
		}

		// Determine which field to edit
		fieldToEdit := "description"
		if cmd.Flags().Changed("title") {
			fieldToEdit = "title"
		} else if cmd.Flags().Changed("design") {
			fieldToEdit = "design"
		} else if cmd.Flags().Changed("notes") {
			fieldToEdit = "notes"
		} else if cmd.Flags().Changed("acceptance") {
			fieldToEdit = "acceptance_criteria"
		}

		// Get the editor from environment
		editor := os.Getenv("EDITOR")
		if editor == "" {
			editor = os.Getenv("VISUAL")
		}
		if editor == "" {
			// Try common defaults
			for _, defaultEditor := range []string{"vim", "vi", "nano", "emacs"} {
				if _, err := exec.LookPath(defaultEditor); err == nil {
					editor = defaultEditor
					break
				}
			}
		}
		if editor == "" {
			FatalErrorRespectJSON("no editor found. Set $EDITOR or $VISUAL environment variable")
		}

		// Get the current issue
		var issue *types.Issue
		var err error

		if daemonClient != nil {
			// Daemon mode
			showArgs := &rpc.ShowArgs{ID: id}
			resp, err := daemonClient.Show(showArgs)
			if err != nil {
				FatalErrorRespectJSON("fetching issue %s: %v", id, err)
			}

			issue = &types.Issue{}
			if err := json.Unmarshal(resp.Data, issue); err != nil {
				FatalErrorRespectJSON("parsing issue data: %v", err)
			}
		} else {
			// Direct mode
			issue, err = store.GetIssue(ctx, id)
			if err != nil {
				FatalErrorRespectJSON("fetching issue %s: %v", id, err)
			}
			if issue == nil {
				FatalErrorRespectJSON("issue %s not found", id)
			}
		}

		// Get the current field value
		var currentValue string
		switch fieldToEdit {
		case "title":
			currentValue = issue.Title
		case "description":
			currentValue = issue.Description
		case "design":
			currentValue = issue.Design
		case "notes":
			currentValue = issue.Notes
		case "acceptance_criteria":
			currentValue = issue.AcceptanceCriteria
		}

		// Create a temporary file with the current value
		tmpFile, err := os.CreateTemp("", fmt.Sprintf("bd-edit-%s-*.txt", fieldToEdit))
		if err != nil {
			FatalErrorRespectJSON("creating temp file: %v", err)
		}
		tmpPath := tmpFile.Name()
		defer func() { _ = os.Remove(tmpPath) }()

		// Write current value to temp file
		if _, err := tmpFile.WriteString(currentValue); err != nil {
			_ = tmpFile.Close()
			FatalErrorRespectJSON("writing to temp file: %v", err)
		}
		_ = tmpFile.Close()

		// Open the editor
		editorCmd := exec.Command(editor, tmpPath)
		editorCmd.Stdin = os.Stdin
		editorCmd.Stdout = os.Stdout
		editorCmd.Stderr = os.Stderr

		if err := editorCmd.Run(); err != nil {
			FatalErrorRespectJSON("running editor: %v", err)
		}

		// Read the edited content
		// #nosec G304 -- tmpPath was created earlier in this function
		editedContent, err := os.ReadFile(tmpPath)
		if err != nil {
			FatalErrorRespectJSON("reading edited file: %v", err)
		}

		newValue := string(editedContent)

		// Check if the value changed
		if newValue == currentValue {
			fmt.Println("No changes made")
			return
		}

		// Validate title if editing title
		if fieldToEdit == "title" && strings.TrimSpace(newValue) == "" {
			FatalErrorRespectJSON("title cannot be empty")
		}

		// Update the issue
		updates := map[string]interface{}{
			fieldToEdit: newValue,
		}

		if daemonClient != nil {
			// Daemon mode
			updateArgs := &rpc.UpdateArgs{ID: id}

			switch fieldToEdit {
			case "title":
				updateArgs.Title = &newValue
			case "description":
				updateArgs.Description = &newValue
			case "design":
				updateArgs.Design = &newValue
			case "notes":
				updateArgs.Notes = &newValue
			case "acceptance_criteria":
				updateArgs.AcceptanceCriteria = &newValue
			}

			_, err := daemonClient.Update(updateArgs)
			if err != nil {
				FatalErrorRespectJSON("updating issue: %v", err)
			}
		} else {
			// Direct mode
			if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
				FatalErrorRespectJSON("updating issue: %v", err)
			}
			markDirtyAndScheduleFlush()
		}

		fieldName := strings.ReplaceAll(fieldToEdit, "_", " ")
		fmt.Printf("%s Updated %s for issue: %s\n", ui.RenderPass("✓"), fieldName, id)
	},
}

var closeCmd = &cobra.Command{
	Use:     "close [id...]",
	GroupID: "issues",
	Short:   "Close one or more issues",
	Args:    cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		CheckReadonly("close")
		reason, _ := cmd.Flags().GetString("reason")
		if reason == "" {
			// Check --resolution alias (Jira CLI convention)
			reason, _ = cmd.Flags().GetString("resolution")
		}
		if reason == "" {
			reason = "Closed"
		}
		force, _ := cmd.Flags().GetBool("force")
		continueFlag, _ := cmd.Flags().GetBool("continue")
		noAuto, _ := cmd.Flags().GetBool("no-auto")
		suggestNext, _ := cmd.Flags().GetBool("suggest-next")

		ctx := rootCtx

		// --continue only works with a single issue
		if continueFlag && len(args) > 1 {
			FatalErrorRespectJSON("--continue only works when closing a single issue")
		}

		// --suggest-next only works with a single issue
		if suggestNext && len(args) > 1 {
			FatalErrorRespectJSON("--suggest-next only works when closing a single issue")
		}

		// Resolve partial IDs first
		var resolvedIDs []string
		if daemonClient != nil {
			for _, id := range args {
				resolveArgs := &rpc.ResolveIDArgs{ID: id}
				resp, err := daemonClient.ResolveID(resolveArgs)
				if err != nil {
					FatalErrorRespectJSON("resolving ID %s: %v", id, err)
				}
				var resolvedID string
				if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
					FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
				}
				resolvedIDs = append(resolvedIDs, resolvedID)
			}
		} else {
			var err error
			resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
			if err != nil {
				FatalErrorRespectJSON("%v", err)
			}
		}

		// If daemon is running, use RPC
		if daemonClient != nil {
			closedIssues := []*types.Issue{}
			for _, id := range resolvedIDs {
				// Get issue for template and pinned checks
				showArgs := &rpc.ShowArgs{ID: id}
				showResp, showErr := daemonClient.Show(showArgs)
				if showErr == nil {
					var issue types.Issue
					if json.Unmarshal(showResp.Data, &issue) == nil {
						if err := validateIssueClosable(id, &issue, force); err != nil {
							fmt.Fprintf(os.Stderr, "%s\n", err)
							continue
						}
					}
				}

				closeArgs := &rpc.CloseArgs{
					ID:          id,
					Reason:      reason,
					SuggestNext: suggestNext,
				}
				resp, err := daemonClient.CloseIssue(closeArgs)
				if err != nil {
					fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
					continue
				}

				// Handle response based on whether SuggestNext was requested
				if suggestNext {
					var result rpc.CloseResult
					if err := json.Unmarshal(resp.Data, &result); err == nil {
						if result.Closed != nil {
							// Run close hook
							if hookRunner != nil {
								hookRunner.Run(hooks.EventClose, result.Closed)
							}
							if jsonOutput {
								closedIssues = append(closedIssues, result.Closed)
							}
						}
						if !jsonOutput {
							fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
							// Display newly unblocked issues
							if len(result.Unblocked) > 0 {
								fmt.Printf("\nNewly unblocked:\n")
								for _, issue := range result.Unblocked {
									fmt.Printf("  • %s %q (P%d)\n", issue.ID, issue.Title, issue.Priority)
								}
							}
						}
					}
				} else {
					var issue types.Issue
					if err := json.Unmarshal(resp.Data, &issue); err == nil {
						// Run close hook
						if hookRunner != nil {
							hookRunner.Run(hooks.EventClose, &issue)
						}
						if jsonOutput {
							closedIssues = append(closedIssues, &issue)
						}
					}
					if !jsonOutput {
						fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
					}
				}
			}

			// Handle --continue flag in daemon mode
			// Note: --continue requires direct database access to walk parent-child chain
			if continueFlag && len(closedIssues) > 0 {
				fmt.Fprintf(os.Stderr, "\nNote: --continue requires direct database access\n")
				fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon close %s --continue\n", resolvedIDs[0])
			}

			if jsonOutput && len(closedIssues) > 0 {
				outputJSON(closedIssues)
			}
			return
		}

		// Direct mode
		closedIssues := []*types.Issue{}
		closedCount := 0
		for _, id := range resolvedIDs {
			// Get issue for checks
			issue, _ := store.GetIssue(ctx, id)

			if err := validateIssueClosable(id, issue, force); err != nil {
				fmt.Fprintf(os.Stderr, "%s\n", err)
				continue
			}

			if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
				fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
				continue
			}

			closedCount++

			// Run close hook
			closedIssue, _ := store.GetIssue(ctx, id)
			if closedIssue != nil && hookRunner != nil {
				hookRunner.Run(hooks.EventClose, closedIssue)
			}

			if jsonOutput {
				if closedIssue != nil {
					closedIssues = append(closedIssues, closedIssue)
				}
			} else {
				fmt.Printf("%s Closed %s: %s\n", ui.RenderPass("✓"), id, reason)
			}
		}

		// Handle --suggest-next flag in direct mode
		if suggestNext && len(resolvedIDs) == 1 && closedCount > 0 {
			unblocked, err := store.GetNewlyUnblockedByClose(ctx, resolvedIDs[0])
			if err == nil && len(unblocked) > 0 {
				if jsonOutput {
					outputJSON(map[string]interface{}{
						"closed":    closedIssues,
						"unblocked": unblocked,
					})
					return
				}
				fmt.Printf("\nNewly unblocked:\n")
				for _, issue := range unblocked {
					fmt.Printf("  • %s %q (P%d)\n", issue.ID, issue.Title, issue.Priority)
				}
			}
		}

		// Schedule auto-flush if any issues were closed
		if len(args) > 0 {
			markDirtyAndScheduleFlush()
		}

		// Handle --continue flag
		if continueFlag && len(resolvedIDs) == 1 && closedCount > 0 {
			autoClaim := !noAuto
			result, err := AdvanceToNextStep(ctx, store, resolvedIDs[0], autoClaim, actor)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Warning: could not advance to next step: %v\n", err)
			} else if result != nil {
				if jsonOutput {
					// Include continue result in JSON output
					outputJSON(map[string]interface{}{
						"closed":   closedIssues,
						"continue": result,
					})
					return
				}
				PrintContinueResult(result)
			}
		}

		if jsonOutput && len(closedIssues) > 0 {
			outputJSON(closedIssues)
		}
	},
}

// showMessageThread displays a full conversation thread for a message
func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
	// Get the starting message
	var startMsg *types.Issue
	var err error

	if daemonClient != nil {
		resp, err := daemonClient.Show(&rpc.ShowArgs{ID: messageID})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error fetching message %s: %v\n", messageID, err)
			os.Exit(1)
		}
		if err := json.Unmarshal(resp.Data, &startMsg); err != nil {
			fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
			os.Exit(1)
		}
	} else {
		startMsg, err = store.GetIssue(ctx, messageID)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error fetching message %s: %v\n", messageID, err)
			os.Exit(1)
		}
	}

	if startMsg == nil {
		fmt.Fprintf(os.Stderr, "Message %s not found\n", messageID)
		os.Exit(1)
	}

	// Find the root of the thread by following replies-to dependencies upward
	// Per Decision 004, RepliesTo is now stored as a dependency, not an Issue field
	rootMsg := startMsg
	seen := make(map[string]bool)
	seen[rootMsg.ID] = true

	for {
		// Find parent via replies-to dependency
		parentID := findRepliesTo(ctx, rootMsg.ID, daemonClient, store)
		if parentID == "" {
			break // No parent, this is the root
		}
		if seen[parentID] {
			break // Avoid infinite loops
		}
		seen[parentID] = true

		var parentMsg *types.Issue
		if daemonClient != nil {
			resp, err := daemonClient.Show(&rpc.ShowArgs{ID: parentID})
			if err != nil {
				break // Parent not found, use current as root
			}
			if err := json.Unmarshal(resp.Data, &parentMsg); err != nil {
				break
			}
		} else {
			parentMsg, _ = store.GetIssue(ctx, parentID)
		}
		if parentMsg == nil {
			break
		}
		rootMsg = parentMsg
	}

	// Now collect all messages in the thread
	// Start from root and find all replies
	// Build a map of child ID -> parent ID for display purposes
	threadMessages := []*types.Issue{rootMsg}
	threadIDs := map[string]bool{rootMsg.ID: true}
	repliesTo := map[string]string{} // child ID -> parent ID
	queue := []string{rootMsg.ID}

	// BFS to find all replies
	for len(queue) > 0 {
		currentID := queue[0]
		queue = queue[1:]

		// Find all messages that reply to currentID via replies-to dependency
		// Per Decision 004, replies are found via dependents with type replies-to
		replies := findReplies(ctx, currentID, daemonClient, store)

		for _, reply := range replies {
			if threadIDs[reply.ID] {
				continue // Already seen
			}
			threadMessages = append(threadMessages, reply)
			threadIDs[reply.ID] = true
			repliesTo[reply.ID] = currentID // Track parent for display
			queue = append(queue, reply.ID)
		}
	}

	// Sort by creation time
	slices.SortFunc(threadMessages, func(a, b *types.Issue) int {
		return a.CreatedAt.Compare(b.CreatedAt)
	})

	if jsonOutput {
		encoder := json.NewEncoder(os.Stdout)
		encoder.SetIndent("", "  ")
		_ = encoder.Encode(threadMessages)
		return
	}

	// Display the thread
	fmt.Printf("\n%s Thread: %s\n", ui.RenderAccent("📬"), rootMsg.Title)
	fmt.Println(strings.Repeat("─", 66))

	for _, msg := range threadMessages {
		// Show indent based on depth (count replies_to chain using our map)
		depth := 0
		parent := repliesTo[msg.ID]
		for parent != "" && depth < 5 {
			depth++
			parent = repliesTo[parent]
		}
		indent := strings.Repeat("  ", depth)

		// Format timestamp
		timeStr := msg.CreatedAt.Format("2006-01-02 15:04")

		// Status indicator
		statusIcon := "📧"
		if msg.Status == types.StatusClosed {
			statusIcon = "✓"
		}

		fmt.Printf("%s%s %s %s\n", indent, statusIcon, ui.RenderAccent(msg.ID), ui.RenderMuted(timeStr))
		fmt.Printf("%s  From: %s  To: %s\n", indent, msg.Sender, msg.Assignee)
		if parentID := repliesTo[msg.ID]; parentID != "" {
			fmt.Printf("%s  Re: %s\n", indent, parentID)
		}
		fmt.Printf("%s  %s: %s\n", indent, ui.RenderMuted("Subject"), msg.Title)
		if msg.Description != "" {
			// Indent the body
			bodyLines := strings.Split(msg.Description, "\n")
			for _, line := range bodyLines {
				fmt.Printf("%s  %s\n", indent, line)
			}
		}
		fmt.Println()
	}

	fmt.Printf("Total: %d messages in thread\n\n", len(threadMessages))
}

// findRepliesTo finds the parent ID that this issue replies to via replies-to dependency.
// Returns empty string if no parent found.
func findRepliesTo(ctx context.Context, issueID string, daemonClient *rpc.Client, store storage.Storage) string {
	if daemonClient != nil {
		// In daemon mode, use Show to get dependencies with metadata
		resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
		if err != nil {
			return ""
		}
		// Parse the full show response to get dependencies
		type showResponse struct {
			Dependencies []struct {
				ID             string `json:"id"`
				DependencyType string `json:"dependency_type"`
			} `json:"dependencies"`
		}
		var details showResponse
		if err := json.Unmarshal(resp.Data, &details); err != nil {
			return ""
		}
		for _, dep := range details.Dependencies {
			if dep.DependencyType == string(types.DepRepliesTo) {
				return dep.ID
			}
		}
		return ""
	}
	// Direct mode - query storage
	deps, err := store.GetDependencyRecords(ctx, issueID)
	if err != nil {
		return ""
	}
	for _, dep := range deps {
		if dep.Type == types.DepRepliesTo {
			return dep.DependsOnID
		}
	}
	return ""
}

// findReplies finds all issues that reply to this issue via replies-to dependency.
func findReplies(ctx context.Context, issueID string, daemonClient *rpc.Client, store storage.Storage) []*types.Issue {
	if daemonClient != nil {
		// In daemon mode, use Show to get dependents with metadata
		resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
		if err != nil {
			return nil
		}
		// Parse the full show response to get dependents
		type showResponse struct {
			Dependents []struct {
				types.Issue
				DependencyType string `json:"dependency_type"`
			} `json:"dependents"`
		}
		var details showResponse
		if err := json.Unmarshal(resp.Data, &details); err != nil {
			return nil
		}
		var replies []*types.Issue
		for _, dep := range details.Dependents {
			if dep.DependencyType == string(types.DepRepliesTo) {
				issue := dep.Issue // Copy to avoid aliasing
				replies = append(replies, &issue)
			}
		}
		return replies
	}
	// Direct mode - query storage
	if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
		deps, err := sqliteStore.GetDependentsWithMetadata(ctx, issueID)
		if err != nil {
			return nil
		}
		var replies []*types.Issue
		for _, dep := range deps {
			if dep.DependencyType == types.DepRepliesTo {
				issue := dep.Issue // Copy to avoid aliasing
				replies = append(replies, &issue)
			}
		}
		return replies
	}

	allDeps, err := store.GetAllDependencyRecords(ctx)
	if err != nil {
		return nil
	}

	var replies []*types.Issue
	for childID, deps := range allDeps {
		for _, dep := range deps {
			if dep.Type == types.DepRepliesTo && dep.DependsOnID == issueID {
				issue, _ := store.GetIssue(ctx, childID)
				if issue != nil {
					replies = append(replies, issue)
				}
			}
		}
	}

	return replies
}

func init() {
	showCmd.Flags().Bool("thread", false, "Show full conversation thread (for messages)")
	rootCmd.AddCommand(showCmd)

	updateCmd.Flags().StringP("status", "s", "", "New status")
	registerPriorityFlag(updateCmd, "")
	updateCmd.Flags().String("title", "", "New title")
	updateCmd.Flags().StringP("type", "t", "", "New type (bug|feature|task|epic|chore|merge-request|molecule|gate)")
	registerCommonIssueFlags(updateCmd)
	updateCmd.Flags().String("notes", "", "Additional notes")
	updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
	_ = updateCmd.Flags().MarkHidden("acceptance-criteria") // Only fails if flag missing (caught in tests)
	updateCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
	updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)")
	updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)")
	updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
	updateCmd.Flags().String("parent", "", "New parent issue ID (reparents the issue, use empty string to remove parent)")
	rootCmd.AddCommand(updateCmd)

	editCmd.Flags().Bool("title", false, "Edit the title")
	editCmd.Flags().Bool("description", false, "Edit the description (default)")
	editCmd.Flags().Bool("design", false, "Edit the design notes")
	editCmd.Flags().Bool("notes", false, "Edit the notes")
	editCmd.Flags().Bool("acceptance", false, "Edit the acceptance criteria")
	rootCmd.AddCommand(editCmd)

	closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
	closeCmd.Flags().String("resolution", "", "Alias for --reason (Jira CLI convention)")
	_ = closeCmd.Flags().MarkHidden("resolution") // Hidden alias for agent/CLI ergonomics
	closeCmd.Flags().BoolP("force", "f", false, "Force close pinned issues")
	closeCmd.Flags().Bool("continue", false, "Auto-advance to next step in molecule")
	closeCmd.Flags().Bool("no-auto", false, "With --continue, show next step but don't claim it")
	closeCmd.Flags().Bool("suggest-next", false, "Show newly unblocked issues after closing")
	rootCmd.AddCommand(closeCmd)
}
