package task

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/cline/cli/pkg/cli/display"
	"github.com/cline/cli/pkg/cli/global"
	"github.com/cline/cli/pkg/cli/handlers"
	"github.com/cline/cli/pkg/cli/slash"
	"github.com/cline/cli/pkg/cli/types"
	"github.com/cline/grpc-go/client"
	"github.com/cline/grpc-go/cline"
)

// Sentinel errors for CheckSendEnabled
var (
	ErrNoActiveTask = fmt.Errorf("no active task")
	ErrTaskBusy     = fmt.Errorf("task is currently busy")
)

// Manager handles task execution and message display
type Manager struct {
	mu               sync.RWMutex
	client           *client.ClineClient
	clientAddress    string
	state            *types.ConversationState
	renderer         *display.Renderer
	hookRenderer     *display.HookRenderer
	toolRenderer     *display.ToolRenderer
	systemRenderer   *display.SystemMessageRenderer
	streamingDisplay *display.StreamingDisplay
	handlerRegistry  *handlers.HandlerRegistry
	slashRegistry    *slash.Registry
	isStreamingMode  bool
	isInteractive    bool
	currentMode      string // "plan" or "act"
}

// NewManager creates a new task manager
func NewManager(client *client.ClineClient) *Manager {
	state := types.NewConversationState()
	renderer := display.NewRenderer(global.Config.OutputFormat)
	toolRenderer := display.NewToolRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
	hookRenderer := display.NewHookRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
	systemRenderer := display.NewSystemMessageRenderer(renderer, renderer.GetMdRenderer(), global.Config.OutputFormat)
	streamingDisplay := display.NewStreamingDisplay(state, renderer)

	// Create handler registry and register handlers
	registry := handlers.NewHandlerRegistry()
	registry.Register(handlers.NewAskHandler())
	registry.Register(handlers.NewSayHandler())

	return &Manager{
		client:           client,
		clientAddress:    "", // Will be set when client is provided
		state:            state,
		renderer:         renderer,
		hookRenderer:     hookRenderer,
		toolRenderer:     toolRenderer,
		systemRenderer:   systemRenderer,
		streamingDisplay: streamingDisplay,
		handlerRegistry:  registry,
		slashRegistry:    slash.NewRegistry(),
		currentMode:      "plan", // Default mode
	}
}

// NewManagerForAddress creates a new task manager for a specific instance address
func NewManagerForAddress(ctx context.Context, address string) (*Manager, error) {
	client, err := global.GetClientForAddress(ctx, address)
	if err != nil {
		return nil, fmt.Errorf("failed to get client for address %s: %w", address, err)
	}

	manager := NewManager(client)
	manager.clientAddress = address

	// Fetch slash commands from backend (non-blocking, errors are logged)
	manager.fetchSlashCommands(ctx)

	return manager, nil
}

// NewManagerForDefault creates a new task manager using the default instance
func NewManagerForDefault(ctx context.Context) (*Manager, error) {
	client, err := global.GetDefaultClient(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get default client: %w", err)
	}

	manager := NewManager(client)

	// Get the default instance address
	if global.Clients != nil {
		manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
	}

	// Fetch slash commands from backend (non-blocking, errors are logged)
	manager.fetchSlashCommands(ctx)

	return manager, nil
}

// fetchSlashCommands fetches available slash commands from the backend
// This is non-blocking and errors are logged but don't prevent manager creation
func (m *Manager) fetchSlashCommands(ctx context.Context) {
	if err := m.slashRegistry.FetchFromBackend(ctx, m.client); err != nil {
		if global.Config.Verbose {
			m.renderer.RenderDebug("Failed to fetch slash commands: %v", err)
		}
		// Non-fatal: CLI-local commands are still available
	} else if global.Config.Verbose {
		m.renderer.RenderDebug("Loaded %d slash commands", len(m.slashRegistry.GetCommands()))
	}
}

// SwitchToInstance switches the manager to use a different Cline instance
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	// Get client for the new address
	newClient, err := global.GetClientForAddress(ctx, address)
	if err != nil {
		return fmt.Errorf("failed to get client for address %s: %w", address, err)
	}

	// Update the client and address
	m.client = newClient
	m.clientAddress = address

	if global.Config.Verbose {
		m.renderer.RenderDebug("Switched to instance: %s", address)
	}

	return nil
}

// GetCurrentInstance returns the address of the current instance
func (m *Manager) GetCurrentInstance() string {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.clientAddress
}

// CreateTask creates a new task
func (m *Manager) CreateTask(ctx context.Context, prompt string, images, files []string, settingsFlags []string) (string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if global.Config.Verbose {
		m.renderer.RenderDebug("Creating task: %s", prompt)
		if len(files) > 0 {
			m.renderer.RenderDebug("Files: %v", files)
		}
		if len(images) > 0 {
			m.renderer.RenderDebug("Images: %v", images)
		}
		if len(settingsFlags) > 0 {
			m.renderer.RenderDebug("Settings: %v", settingsFlags)
		}
	}

	// Check if there's an active task and cancel it first
	if err := m.cancelExistingTaskIfNeeded(ctx); err != nil {
		return "", fmt.Errorf("failed to cancel existing task: %w", err)
	}

	// Parse task settings if provided
	var taskSettings *cline.Settings
	if len(settingsFlags) > 0 {
		var err error
		taskSettings, _, err = ParseTaskSettings(settingsFlags)
		if err != nil {
			return "", fmt.Errorf("failed to parse task settings: %w", err)
		}
	}

	// Create task request
	req := &cline.NewTaskRequest{
		Text:         prompt,
		Images:       images,
		Files:        files,
		TaskSettings: taskSettings,
	}

	resp, err := m.client.Task.NewTask(ctx, req)
	if err != nil {
		return "", fmt.Errorf("failed to create task: %w", err)
	}

	taskID := resp.Value

	return taskID, nil
}

// cancelExistingTaskIfNeeded checks if there's an active task and cancels it
func (m *Manager) cancelExistingTaskIfNeeded(ctx context.Context) error {
	// Try to get the current state to check if there's an active task
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		// If we can't get state, assume no active task and continue
		if global.Config.Verbose {
			m.renderer.RenderDebug("Could not get state to check for active task: %v", err)
		}
		return nil
	}

	// Properly parse the state to check if there's actually an active task
	if state.StateJson != "" {
		var stateData types.ExtensionState
		if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
			// If we can't parse state, assume no active task
			if global.Config.Verbose {
				m.renderer.RenderDebug("Could not parse state JSON: %v", err)
			}
			return nil
		}

		// Check if there's actually an active task
		if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
			if global.Config.Verbose {
				m.renderer.RenderDebug("Found active task %s, cancelling...", stateData.CurrentTaskItem.Id)
			}

			// Cancel the existing task
			_, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
			if err != nil {
				if global.Config.Verbose {
					m.renderer.RenderDebug("Cancel task returned error: %v", err)
				}
			} else {
				fmt.Println("Cancelled existing task to start new one")
			}
		}
	}

	return nil
}

// ValidateCheckpointExists checks if a checkpoint ID is valid
func (m *Manager) ValidateCheckpointExists(ctx context.Context, checkpointID int64) error {
	// Get current state
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return fmt.Errorf("failed to get state: %w", err)
	}

	// Extract messages
	messages, err := m.extractMessagesFromState(state.StateJson)
	if err != nil {
		return fmt.Errorf("failed to extract messages: %w", err)
	}

	// Find and validate the checkpoint message
	for _, msg := range messages {
		if msg.Timestamp == checkpointID {
			if msg.Say != string(types.SayTypeCheckpointCreated) {
				return fmt.Errorf("timestamp %d is not a checkpoint (type: %s)", checkpointID, msg.Type)
			}
			return nil // Valid checkpoint
		}
	}

	return fmt.Errorf("checkpoint ID %d not found in task history", checkpointID)
}

// CheckSendEnabled checks if we can send a message to the current task
// Returns nil if sending is allowed, or an error indicating why it's not allowed
// We duplicate the logic from buttonConfig::getButtonConfig
func (m *Manager) CheckSendEnabled(ctx context.Context) error {
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return fmt.Errorf("failed to get latest state: %w", err)
	}

	var stateData types.ExtensionState
	if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
		return fmt.Errorf("failed to parse state: %w", err)
	}

	// Check if there is an active task
	if stateData.CurrentTaskItem == nil {
		return ErrNoActiveTask
	}

	messages, err := m.extractMessagesFromState(state.StateJson)
	if err != nil {
		return fmt.Errorf("failed to extract messages: %w", err)
	}

	if len(messages) == 0 {
		return nil
	}

	// Use final message to perform validation
	lastMessage := messages[len(messages)-1]

	// Error types which we allow sending on
	errorTypes := []string{
		string(types.AskTypeAPIReqFailed),        // "api_req_failed"
		string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
	}

	isError := false

	// Check if message is an error type
	if lastMessage.Type == types.MessageTypeAsk {
		for _, errType := range errorTypes {
			if lastMessage.Ask == errType {
				isError = true
				break
			}
		}
	}

	// Streaming and error check
	if lastMessage.Partial && !isError {
		if global.Config.Verbose {
			m.renderer.RenderDebug("Send disabled: task is streaming and non-error")
		}
		return ErrTaskBusy
	}

	// All ask messages allow sending, EXCEPT command_output
	if lastMessage.Type == types.MessageTypeAsk {
		// Special case: command_output means command is actively streaming
		// In the CLI, we don't want to show input during streaming output (too messy)
		// The webview can show "Proceed While Running" button, but CLI should wait
		if lastMessage.Ask == string(types.AskTypeCommandOutput) {
			if global.Config.Verbose {
				m.renderer.RenderDebug("Send disabled: command output is streaming")
			}
			return ErrTaskBusy
		}

		if global.Config.Verbose {
			m.renderer.RenderDebug("Send enabled: ask message")
		}
		return nil
	}

	// Technically unnecessary but implements getButtonConfig 1-1
	if lastMessage.Type == types.MessageTypeSay && lastMessage.Say == string(types.SayTypeAPIReqStarted) {
		if global.Config.Verbose {
			m.renderer.RenderDebug("Send disabled: API request is active")
		}
		return ErrTaskBusy
	}

	if global.Config.Verbose {
		m.renderer.RenderDebug("Send disabled: default fallback")
	}

	return ErrTaskBusy
}

// CheckNeedsApproval determines if the current task is waiting for approval
// Returns (needsApproval, lastMessage, error)
func (m *Manager) CheckNeedsApproval(ctx context.Context) (bool, *types.ClineMessage, error) {
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return false, nil, fmt.Errorf("failed to get latest state: %w", err)
	}

	messages, err := m.extractMessagesFromState(state.StateJson)
	if err != nil {
		return false, nil, fmt.Errorf("failed to extract messages: %w", err)
	}

	if len(messages) == 0 {
		return false, nil, nil
	}

	// Use final message to check if approval is needed
	lastMessage := messages[len(messages)-1]

	// Only check non-partial ask messages
	if lastMessage.Partial {
		return false, nil, nil
	}

	// Check if this is an approval-required ask type
	if lastMessage.Type == types.MessageTypeAsk {
		approvalTypes := []string{
			string(types.AskTypeTool),
			string(types.AskTypeCommand),
			string(types.AskTypeBrowserActionLaunch),
			string(types.AskTypeUseMcpServer),
		}

		for _, approvalType := range approvalTypes {
			if lastMessage.Ask == approvalType {
				return true, lastMessage, nil
			}
		}
	}

	return false, nil, nil
}

// SendMessage sends a followup message to the current task
func (m *Manager) SendMessage(ctx context.Context, message string, images, files []string, approve string) error {
	responseType := "messageResponse"

	if approve == "true" {
		responseType = "yesButtonClicked"
	}

	if approve == "false" {
		responseType = "noButtonClicked"
	}

	if global.Config.Verbose {
		m.renderer.RenderDebug("Sending message: %s", message)
		if len(files) > 0 {
			m.renderer.RenderDebug("Files: %v", files)
		}
		if len(images) > 0 {
			m.renderer.RenderDebug("Images: %v", images)
		}
	}

	// Send the followup message using AskResponse
	req := &cline.AskResponseRequest{
		ResponseType: responseType,
		Text:         message,
		Images:       images,
		Files:        files,
	}

	_, err := m.client.Task.AskResponse(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to send message: %w", err)
	}

	return nil
}

// SetMode sets the Plan/Act mode for the current Cline instance and optionally sends message
func (m *Manager) SetMode(ctx context.Context, mode string, message *string, images, files []string) error {
	if mode != "act" && mode != "plan" {
		return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
	}

	var protoMode cline.PlanActMode
	if mode == "plan" {
		protoMode = cline.PlanActMode_PLAN
	} else {
		protoMode = cline.PlanActMode_ACT
	}

	req := &cline.TogglePlanActModeRequest{
		Metadata: &cline.Metadata{},
		Mode:     protoMode,
	}

	if message != nil {
		req.ChatContent = &cline.ChatContent{
			Message: message,
			Images:  images,
			Files:   files,
		}
	}

	_, err := m.client.State.TogglePlanActModeProto(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
	}

	return nil
}

// SetModeAndSendMessage sets the mode and sends a message in one operation
// Handles task restoration internally if the mode switch cancels the current task
func (m *Manager) SetModeAndSendMessage(ctx context.Context, mode, message string, images, files []string) error {
	if mode != "act" && mode != "plan" {
		return fmt.Errorf("invalid mode '%s': must be 'act' or 'plan'", mode)
	}

	taskId, err := m.getCurrentTaskId(ctx)
	if err != nil {
		return fmt.Errorf("failed to get current task ID: %w", err)
	}
	fmt.Printf("Current task ID: %s\n", taskId)

	var protoMode cline.PlanActMode
	if mode == "plan" {
		protoMode = cline.PlanActMode_PLAN
	} else {
		protoMode = cline.PlanActMode_ACT
	}

	req := &cline.TogglePlanActModeRequest{
		Metadata: &cline.Metadata{},
		Mode:     protoMode,
		ChatContent: &cline.ChatContent{
			Message: &message,
			Images:  images,
			Files:   files,
		},
	}

	result, err := m.client.State.TogglePlanActModeProto(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to set mode to '%s': %w", mode, err)
	}

	taskPreserved := result.Value

	if taskPreserved {
		fmt.Printf("Message sent as part of mode change\n")
		return nil
	} else {
		if message != "" || len(images) > 0 || len(files) > 0 {
			fmt.Printf("Task was cancelled, restoring task ID: %s\n", taskId)

			err = m.ReinitExistingTaskFromId(ctx, taskId)
			if err != nil {
				return fmt.Errorf("Failed to restore task: %w", err)
			}
			fmt.Printf("Task restored successfully\n")

			// Hardcoded sleep should be replaced with a way to fetch whether task is ready algorithmically
			time.Sleep(1 * time.Second)

			err = m.SendMessage(ctx, message, images, files, "")
			if err != nil {
				return fmt.Errorf("Failed to send message: %w", err)
			}
			fmt.Printf("Message sent to restored task\n")
		}
	}

	return nil
}

// getCurrentTaskId extracts the current task ID from the server state
func (m *Manager) getCurrentTaskId(ctx context.Context) (string, error) {
	// Get the latest state
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return "", fmt.Errorf("failed to get state: %w", err)
	}

	// Parse the server state JSON
	var stateData types.ExtensionState
	if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
		return "", fmt.Errorf("failed to parse state JSON: %w", err)
	}

	// Extract current task ID
	if stateData.CurrentTaskItem != nil && stateData.CurrentTaskItem.Id != "" {
		return stateData.CurrentTaskItem.Id, nil
	}

	return "", fmt.Errorf("no current task found in state")
}

// ReinitExistingTaskFromId reinitializes an existing task from the given task ID
func (m *Manager) ReinitExistingTaskFromId(ctx context.Context, taskId string) error {
	req := &cline.StringRequest{Value: taskId}
	resp, err := m.client.Task.ShowTaskWithId(ctx, req)
	if err != nil {
		return fmt.Errorf("Failed to reinitialize task %s: %w", taskId, err)
	}

	fmt.Printf("Successfully reinitialized task: %s (ID: %s)\n", taskId, resp.Id)

	return nil
}

// ResumeTask resumes an existing task by ID
func (m *Manager) ResumeTask(ctx context.Context, taskID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if global.Config.Verbose {
		m.renderer.RenderDebug("Resuming task: %s", taskID)
	}

	// This call handles cancellation of any active task
	if err := m.ReinitExistingTaskFromId(ctx, taskID); err != nil {
		return fmt.Errorf("failed to resume task %s: %w", taskID, err)
	}

	fmt.Printf("Task %s resumed successfully\n", taskID)

	return nil
}

// RestoreCheckpoint restores the task to a specific checkpoint
func (m *Manager) RestoreCheckpoint(ctx context.Context, checkpointID int64, restoreType string) error {
	if global.Config.Verbose {
		m.renderer.RenderDebug("Restoring checkpoint: %d (type: %s)", checkpointID, restoreType)
	}

	// Create the checkpoint restore request
	req := &cline.CheckpointRestoreRequest{
		Metadata:    &cline.Metadata{},
		Number:      checkpointID,
		RestoreType: restoreType,
	}

	_, err := m.client.Checkpoints.CheckpointRestore(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to restore checkpoint %d: %w", checkpointID, err)
	}

	return nil
}

// CancelTask cancels the current task
func (m *Manager) CancelTask(ctx context.Context) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	_, err := m.client.Task.CancelTask(ctx, &cline.EmptyRequest{})
	if err != nil {
		return fmt.Errorf("failed to cancel task: %w", err)
	}

	return nil
}

// ShowConversation displays the current conversation
func (m *Manager) ShowConversation(ctx context.Context) error {
	// Check if there's an active task before showing conversation
	err := m.CheckSendEnabled(ctx)
	if err != nil {
		// Handle specific error cases
		if errors.Is(err, ErrNoActiveTask) {
			fmt.Println("No active task found. Use 'cline task new' to create a task first.")
			return nil
		}
		// For other errors (like task busy), we can still show the conversation
	}

	// Disable streaming mode for static view
	m.mu.Lock()
	m.isStreamingMode = false
	m.mu.Unlock()

	m.mu.RLock()
	defer m.mu.RUnlock()

	// Get the latest state which contains messages
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return fmt.Errorf("failed to get state: %w", err)
	}

	// Parse the state JSON to extract messages
	messages, err := m.extractMessagesFromState(state.StateJson)
	if err != nil {
		return fmt.Errorf("failed to extract messages: %w", err)
	}

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

	for i, msg := range messages {
		if msg.Partial {
			continue
		}
		m.displayMessage(msg, false, false, i)
	}

	return nil
}

func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string, interactive bool) error {
	// Enable streaming mode
	m.mu.Lock()
	m.isStreamingMode = true
	m.isInteractive = interactive
	m.mu.Unlock()

	if global.Config.OutputFormat != "plain" {
		markdown := fmt.Sprintf("*Using instance: %s*\n*Press Ctrl+C to exit*", instanceAddress)
		rendered := m.renderer.RenderMarkdown(markdown)
		fmt.Printf("%s", rendered)
	} else {
		fmt.Printf("Using instance: %s\n", instanceAddress)
		if interactive {
			fmt.Println("Following task conversation in interactive mode... (Press Ctrl+C to exit)")
		} else {
			fmt.Println("Following task conversation... (Press Ctrl+C to exit)")
		}
	}

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create stream coordinator
	coordinator := NewStreamCoordinator()

	// Load history first
	totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
	if err != nil {
		m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
		totalMessageCount = 0
	}
	coordinator.SetConversationTurnStartIndex(totalMessageCount)

	// Start both streams concurrently
	errChan := make(chan error, 3)

	if global.Config.OutputFormat == "json" {
		go m.handleStateStream(ctx, coordinator, errChan, nil)
	} else {
		go m.handleStateStream(ctx, coordinator, errChan, nil)
		go m.handlePartialMessageStream(ctx, coordinator, errChan)

		// Start input handler if interactive mode is enabled
		if interactive {
			inputHandler := NewInputHandler(m, coordinator, cancel)
			go inputHandler.Start(ctx, errChan)
		}
	}

	// Handle Ctrl+C signals
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		defer signal.Stop(sigChan) // Clean up signal handler when goroutine exits
		for {
			select {
			case <-ctx.Done():
				return
			case <-sigChan:
				if interactive {
					// Interactive mode (task chat)
					// Check if input is currently being shown
					if coordinator.IsInputAllowed() {
						// Input form is showing - huh will handle the signal via ErrUserAborted
						// Do nothing here, let the input handler deal with it
					} else {
						// Streaming mode - cancel the task and stay in follow mode
						m.renderer.RenderTaskCancelled()
						if err := m.CancelTask(context.Background()); err != nil {
							fmt.Printf("Error cancelling task: %v\n", err)
						}
						// Don't cancel main context - stay in follow mode
					}
				} else {
					// Non-interactive mode (task view --follow)
					// Just exit without canceling the task
					cancel()
					return // Exit the loop after canceling in non-interactive mode
				}
			}
		}
	}()

	// Wait for either stream to error or context cancellation
	select {
	case <-ctx.Done():
		// Check if this was a user-initiated cancellation (Ctrl+C)
		// Return nil for clean exit instead of context.Canceled error
		if ctx.Err() == context.Canceled {
			return nil
		}
		return ctx.Err()
	case err := <-errChan:
		cancel()
		return err
	}
}

// FollowConversationUntilCompletion streams conversation updates until task completion
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context, opts FollowOptions) error {
	// Check if there's an active task before entering follow mode
	// Skip this check if we just created a task (to avoid race condition where task isn't active yet)
	if !opts.SkipActiveTaskCheck {
		err := m.CheckSendEnabled(ctx)
		if err != nil {
			if errors.Is(err, ErrNoActiveTask) {
				fmt.Println("No task is currently running.")
				return nil
			}
			// For other errors (like task busy), we can still enter follow mode
			// as the user may want to observe the task
		}
	}

	// Enable streaming mode
	m.mu.Lock()
	m.isStreamingMode = true
	m.mu.Unlock()

	fmt.Println("Following task conversation until completion... (Press Ctrl+C to exit)")

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Create stream coordinator
	coordinator := NewStreamCoordinator()

	// Load history first
	totalMessageCount, err := m.loadAndDisplayRecentHistory(ctx)
	if err != nil {
		m.renderer.RenderDebug("Warning: Failed to load conversation history: %v", err)
		totalMessageCount = 0
	}
	coordinator.SetConversationTurnStartIndex(totalMessageCount)

	// Start both streams concurrently
	errChan := make(chan error, 2)
	completionChan := make(chan bool, 1)

	if global.Config.OutputFormat == "json" {
		go m.handleStateStream(ctx, coordinator, errChan, completionChan)
	} else {
		go m.handleStateStream(ctx, coordinator, errChan, completionChan)
		go m.handlePartialMessageStream(ctx, coordinator, errChan)
	}

	// Wait for completion, error, or context cancellation
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-completionChan:
		cancel()
		return nil
	case err := <-errChan:
		cancel()
		return err
	}
}

// handleStateStream handles the SubscribeToState stream
func (m *Manager) handleStateStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error, completionChan chan bool) {
	stateStream, err := m.client.State.SubscribeToState(ctx, &cline.EmptyRequest{})
	if err != nil {
		errChan <- fmt.Errorf("failed to subscribe to state: %w", err)
		return
	}

	for {
		select {
		case <-ctx.Done():
			return
		default:
			stateUpdate, err := stateStream.Recv()
			if err != nil {
				m.renderer.RenderDebug("State stream receive error: %v", err)
				errChan <- fmt.Errorf("failed to receive state update: %w", err)
				return
			}

			var pErr error

			if global.Config.OutputFormat == "json" {
				pErr = m.processStateUpdateJsonMode(stateUpdate, coordinator, completionChan)
			} else {
				pErr = m.processStateUpdate(stateUpdate, coordinator, completionChan)
			}

			if pErr != nil {
				m.renderer.RenderDebug("State processing error: %v", pErr)
			}
		}
	}
}

func (m *Manager) processStateUpdateJsonMode(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
	messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
	if err != nil {
		return err
	}

	// Process messages from current conversation turn onwards
	startIndex := coordinator.GetConversationTurnStartIndex()

	var foundCompletion bool
	var displayedUsage bool

	for i := startIndex; i < len(messages); i++ {
		msg := messages[i]

		if global.Config.Verbose {
			m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
		}

		// Exit after we've seen a task completion & printed out the usage info
		if msg.Say == string(types.SayTypeCompletionResult) {
			foundCompletion = true
		}

		// Determine if message is ready to be displayed now
		shouldDisplay := true

		switch {
		case msg.Say == string(types.SayTypeAPIReqStarted):
			shouldDisplay = false
			apiInfo := types.APIRequestInfo{Cost: -1}
			if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
				shouldDisplay = true
				displayedUsage = true
			}
		}

		// Skip if message is partial, except for a specific edge case
		if msg.Partial {
			// Exception: display if type=say, text="", say="text"
			if msg.IsSay() && msg.Text == "" && msg.Say == string(types.SayTypeText) {
				shouldDisplay = true
			} else {
				shouldDisplay = false
			}
		}

		// Display valid messages, exit as soon as we hit a non-valid message
		if shouldDisplay {
			coordinator.CompleteTurn(i + 1) // Mark the message as complete as soon as we print it
			m.displayMessage(msg, false, false, i)
		} else {
			break
		}
	}

	// We only want to exit after we've displayed the usage, for the case of seeing completion result
	if completionChan != nil && foundCompletion && displayedUsage {
		completionChan <- true
	}

	return nil
}

// processStateUpdate processes state updates and supports logic for handling task competion markers
func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *StreamCoordinator, completionChan chan bool) error {
	// Update current mode from state
	m.updateMode(stateUpdate.StateJson)

	messages, err := m.extractMessagesFromState(stateUpdate.StateJson)
	if err != nil {
		return err
	}

	// Process messages from current conversation turn onwards
	startIndex := coordinator.GetConversationTurnStartIndex()

	var foundCompletion bool
	var displayedUsage bool

	for i := startIndex; i < len(messages); i++ {
		msg := messages[i]

		if global.Config.Verbose {
			m.renderer.RenderDebug("State message %d: type=%s, say=%s", i, msg.Type, msg.Say)
		}

		// Exit after we've seen a task completion & printed out the usage info
		if msg.Say == string(types.SayTypeCompletionResult) {
			foundCompletion = true
		}

		switch {
		case msg.Say == string(types.SayTypeUserFeedback):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)
				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeCommand):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeCommandOutput):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeCommandPermissionDenied):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeBrowserActionLaunch):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeMcpServerRequestStarted):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeMcpServerResponse):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeMcpNotification):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeUseMcpServer):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeCheckpointCreated):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeHookStatus):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeHookOutputStream):
			// Hook stdout/stderr streaming arrives as hook_output_stream messages.
			// These are intentionally suppressed unless verbose (see SayHandler.handleHookOutputStream),
			// but we still need to route them through the normal handler pipeline in streaming/follow
			// mode so verbose users actually see `HOOK> ...` lines.
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				m.displayMessage(msg, false, false, i)
				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Say == string(types.SayTypeAPIReqStarted):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			apiInfo := types.APIRequestInfo{Cost: -1}
			if err := json.Unmarshal([]byte(msg.Text), &apiInfo); err == nil && apiInfo.Cost >= 0 {
				if !coordinator.IsProcessedInCurrentTurn(msgKey) {
					fmt.Println() // adds a separator between cline message and usage message
					m.displayMessage(msg, false, false, i)

					coordinator.MarkProcessedInCurrentTurn(msgKey)
					coordinator.CompleteTurn(len(messages))
					displayedUsage = true
				}
			}

		case msg.Say == string(types.SayTypeCompletionResult):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
				fmt.Println()
				m.displayMessage(msg, false, false, i)
				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Ask == string(types.AskTypeCommandOutput):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			if !coordinator.IsProcessedInCurrentTurn(msgKey) {
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Ask == string(types.AskTypePlanModeRespond):
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			// Non-streaming mode: render normally when message is complete
			if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
				m.displayMessage(msg, false, false, i)

				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}

		case msg.Type == types.MessageTypeAsk:
			msgKey := fmt.Sprintf("%d", msg.Timestamp)
			// Only render if not already handled by partial stream
			if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
				m.displayMessage(msg, false, false, i)
				coordinator.MarkProcessedInCurrentTurn(msgKey)
			}
		}
	}

	// We only want to exit after we've displayed the usage, for the case of seeing completion result
	if completionChan != nil && foundCompletion && displayedUsage {
		completionChan <- true
	}

	return nil
}

// handlePartialMessageStream handles the SubscribeToPartialMessage stream for streaming assistant text
func (m *Manager) handlePartialMessageStream(ctx context.Context, coordinator *StreamCoordinator, errChan chan error) {
	partialStream, err := m.client.Ui.SubscribeToPartialMessage(ctx, &cline.EmptyRequest{})
	if err != nil {
		errChan <- fmt.Errorf("failed to subscribe to partial messages: %w", err)
		return
	}

	defer func() {
		m.streamingDisplay.FreezeActiveSegment()
	}()

	for {
		select {
		case <-ctx.Done():
			return
		default:
			protoMsg, err := partialStream.Recv()
			if err != nil {
				m.renderer.RenderDebug("Partial stream receive error: %v", err)
				errChan <- fmt.Errorf("failed to receive partial message: %w", err)
				return
			}

			// Convert proto message to our Message struct
			msg := types.ConvertProtoToMessage(protoMsg)

			// Debug: Log received message (always show for debugging)
			m.renderer.RenderDebug("Received streaming message: type=%s, partial=%v, text_len=%d",
				msg.Type, msg.Partial, len(msg.Text))

			// Handle the message with streaming support for de-dupping
			if err := m.handleStreamingMessage(msg, coordinator); err != nil {
				m.renderer.RenderDebug("Error handling streaming message: %v", err)
			}
		}
	}
}

// handleStreamingMessage handles a streaming message
func (m *Manager) handleStreamingMessage(msg *types.ClineMessage, coordinator *StreamCoordinator) error {
	// Debug: Always log what we're processing
	m.renderer.RenderDebug("Processing message: timestamp=%d, partial=%v, type=%s, text_preview=%s",
		msg.Timestamp, msg.Partial, msg.Type, m.truncateText(msg.Text, 50))

	// Use streaming display which handles deduplication internally
	if err := m.streamingDisplay.HandlePartialMessage(msg); err != nil {
		m.renderer.RenderDebug("Streaming display failed, using fallback: %v", err)
		// Fallback to regular display
		m.displayMessage(msg, true, false, -1)
	}

	return nil
}

// truncateText truncates text for debug display
func (m *Manager) truncateText(text string, maxLen int) string {
	if len(text) <= maxLen {
		return text
	}
	return text[:maxLen] + "..."
}

// displayMessage displays a single message using the handler system
func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool, messageIndex int) error {
	if global.Config.OutputFormat == "json" {
		return m.outputMessageAsJSON(msg)
	} else {
		m.mu.RLock()
		isStreaming := m.isStreamingMode
		isInteractive := m.isInteractive
		m.mu.RUnlock()

		dc := &handlers.DisplayContext{
			State:          m.state,
			Renderer:       m.renderer,
			ToolRenderer:   m.toolRenderer,
			HookRenderer:   m.hookRenderer,
			SystemRenderer: m.systemRenderer,
			IsLast:          isLast,
			IsPartial:       isPartial,
			Verbose:         global.Config.Verbose,
			MessageIndex:    messageIndex,
			IsStreamingMode: isStreaming,
			IsInteractive:   isInteractive,
		}

		return m.handlerRegistry.Handle(msg, dc)
	}
}

// outputMessageAsJSON prints a single cline message as json
func (m *Manager) outputMessageAsJSON(msg *types.ClineMessage) error {
	jsonBytes, err := json.MarshalIndent(msg, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal message as JSON: %w", err)
	}

	fmt.Println(string(jsonBytes))
	return nil
}

// loadAndDisplayRecentHistory loads and displays recent conversation history and returns the total number of existing messages
func (m *Manager) loadAndDisplayRecentHistory(ctx context.Context) (int, error) {
	// Get the latest state which contains messages
	state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
	if err != nil {
		return 0, fmt.Errorf("failed to get state: %w", err)
	}

	// Parse the state JSON to extract messages
	messages, err := m.extractMessagesFromState(state.StateJson)
	if err != nil {
		return 0, fmt.Errorf("failed to extract messages: %w", err)
	}

	if len(messages) == 0 {
		fmt.Println("No conversation history found.")
		return 0, nil
	}

	// Show only the last 100 messages by default
	const maxHistoryMessages = 100
	totalMessages := len(messages)
	startIndex := 0

	if totalMessages > maxHistoryMessages {
		startIndex = totalMessages - maxHistoryMessages
		if global.Config.OutputFormat != "plain" {
			markdown := fmt.Sprintf("*Conversation history (%d of %d messages)*", maxHistoryMessages, totalMessages)
			rendered := m.renderer.RenderMarkdown(markdown)
			fmt.Printf("\n%s\n\n", rendered)
		} else {
			fmt.Printf("--- Conversation history (%d of %d messages) ---\n", maxHistoryMessages, totalMessages)
		}
	} else {
		if global.Config.OutputFormat != "plain" {
			markdown := fmt.Sprintf("*Conversation history (%d messages)*", totalMessages)
			rendered := m.renderer.RenderMarkdown(markdown)
			fmt.Printf("\n%s\n\n", rendered)
		} else {
			fmt.Printf("--- Conversation history (%d messages) ---\n", totalMessages)
		}
	}

	for i := startIndex; i < len(messages); i++ {
		msg := messages[i]

		if msg.Partial {
			continue
		}

		m.displayMessage(msg, false, false, i)
	}

	// Return the total number of messages in the conversation
	return totalMessages, nil
}

// extractMessagesFromState parses the state JSON and extracts messages
func (m *Manager) extractMessagesFromState(stateJson string) ([]*types.ClineMessage, error) {
	return types.ExtractMessagesFromStateJSON(stateJson)
}

// GetState returns the current conversation state
func (m *Manager) GetState() *types.ConversationState {
	return m.state
}

// GetClient returns the underlying ClineClient for direct gRPC calls
func (m *Manager) GetClient() *client.ClineClient {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.client
}

// GetRenderer returns the renderer for formatting output
func (m *Manager) GetRenderer() *display.Renderer {
	return m.renderer
}

// GetCurrentMode returns the current plan/act mode
func (m *Manager) GetCurrentMode() string {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.currentMode
}

// GetSlashRegistry returns the slash command registry
func (m *Manager) GetSlashRegistry() *slash.Registry {
	return m.slashRegistry
}

// extractModeFromState extracts the current mode from state JSON
func (m *Manager) extractModeFromState(stateJson string) string {
	var rawState map[string]interface{}
	if err := json.Unmarshal([]byte(stateJson), &rawState); err != nil {
		return m.currentMode // Return current mode if parsing fails
	}

	if mode, ok := rawState["mode"].(string); ok {
		return mode
	}

	return m.currentMode // Return current mode if not found in state
}

// updateMode updates the current mode from state
func (m *Manager) updateMode(stateJson string) {
	mode := m.extractModeFromState(stateJson)
	m.mu.Lock()
	m.currentMode = mode
	m.mu.Unlock()
}

// UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
	boolPtr := func(b bool) *bool { return &b }

	settings := &cline.Settings{
		AutoApprovalSettings: &cline.AutoApprovalSettings{
			Actions: &cline.AutoApprovalActions{},
		},
	}

	// Set the specific action to true based on actionKey
	truePtr := boolPtr(true)

	switch actionKey {
	case "read_files":
		settings.AutoApprovalSettings.Actions.ReadFiles = truePtr
	case "edit_files":
		settings.AutoApprovalSettings.Actions.EditFiles = truePtr
	case "execute_all_commands":
		settings.AutoApprovalSettings.Actions.ExecuteAllCommands = truePtr
	case "use_browser":
		settings.AutoApprovalSettings.Actions.UseBrowser = truePtr
	case "use_mcp":
		settings.AutoApprovalSettings.Actions.UseMcp = truePtr
	default:
		return fmt.Errorf("unknown auto-approval action: %s", actionKey)
	}

	_, err := m.client.State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
		Settings: settings,
	})
	if err != nil {
		return fmt.Errorf("failed to update task settings: %w", err)
	}

	return nil
}

// Cleanup cleans up resources
func (m *Manager) Cleanup() {
	// Clean up streaming display resources if needed
	if m.streamingDisplay != nil {
		m.streamingDisplay.Cleanup()
	}
}
