package tools

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/YspCoder/clawgo/pkg/bus"
	"github.com/YspCoder/clawgo/pkg/ekg"
	"github.com/YspCoder/clawgo/pkg/providers"
)

type SubagentTask struct {
	ID               string                 `json:"id"`
	Task             string                 `json:"task"`
	Label            string                 `json:"label"`
	Role             string                 `json:"role"`
	AgentID          string                 `json:"agent_id"`
	Transport        string                 `json:"transport,omitempty"`
	NodeID           string                 `json:"node_id,omitempty"`
	ParentAgentID    string                 `json:"parent_agent_id,omitempty"`
	NotifyMainPolicy string                 `json:"notify_main_policy,omitempty"`
	SessionKey       string                 `json:"session_key"`
	MemoryNS         string                 `json:"memory_ns"`
	SystemPromptFile string                 `json:"system_prompt_file,omitempty"`
	ToolAllowlist    []string               `json:"tool_allowlist,omitempty"`
	MaxRetries       int                    `json:"max_retries,omitempty"`
	RetryBackoff     int                    `json:"retry_backoff,omitempty"`
	TimeoutSec       int                    `json:"timeout_sec,omitempty"`
	MaxTaskChars     int                    `json:"max_task_chars,omitempty"`
	MaxResultChars   int                    `json:"max_result_chars,omitempty"`
	RetryCount       int                    `json:"retry_count,omitempty"`
	ThreadID         string                 `json:"thread_id,omitempty"`
	CorrelationID    string                 `json:"correlation_id,omitempty"`
	ParentRunID      string                 `json:"parent_run_id,omitempty"`
	LastMessageID    string                 `json:"last_message_id,omitempty"`
	WaitingReply     bool                   `json:"waiting_for_reply,omitempty"`
	SharedState      map[string]interface{} `json:"shared_state,omitempty"`
	OriginChannel    string                 `json:"origin_channel,omitempty"`
	OriginChatID     string                 `json:"origin_chat_id,omitempty"`
	Status           string                 `json:"status"`
	Result           string                 `json:"result,omitempty"`
	Steering         []string               `json:"steering,omitempty"`
	Created          int64                  `json:"created"`
	Updated          int64                  `json:"updated"`
}

type SubagentManager struct {
	tasks              map[string]*SubagentTask
	cancelFuncs        map[string]context.CancelFunc
	waiters            map[string]map[chan struct{}]struct{}
	recoverableTaskIDs []string
	archiveAfterMinute int64
	mu                 sync.RWMutex
	provider           providers.LLMProvider
	bus                *bus.MessageBus
	workspace          string
	nextID             int
	runFunc            SubagentRunFunc
	profileStore       *SubagentProfileStore
	runStore           *SubagentRunStore
	mailboxStore       *AgentMailboxStore
	ekg                *ekg.Engine
}

type SubagentSpawnOptions struct {
	Task             string
	Label            string
	Role             string
	AgentID          string
	NotifyMainPolicy string
	MaxRetries       int
	RetryBackoff     int
	TimeoutSec       int
	MaxTaskChars     int
	MaxResultChars   int
	OriginChannel    string
	OriginChatID     string
	ThreadID         string
	CorrelationID    string
	ParentRunID      string
}

func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *bus.MessageBus) *SubagentManager {
	store := NewSubagentProfileStore(workspace)
	runStore := NewSubagentRunStore(workspace)
	mailboxStore := NewAgentMailboxStore(workspace)
	mgr := &SubagentManager{
		tasks:              make(map[string]*SubagentTask),
		cancelFuncs:        make(map[string]context.CancelFunc),
		waiters:            make(map[string]map[chan struct{}]struct{}),
		archiveAfterMinute: 60,
		provider:           provider,
		bus:                bus,
		workspace:          workspace,
		nextID:             1,
		profileStore:       store,
		runStore:           runStore,
		mailboxStore:       mailboxStore,
		ekg:                ekg.New(workspace),
	}
	if runStore != nil {
		for _, task := range runStore.List() {
			mgr.tasks[task.ID] = task
			if task.Status == RuntimeStatusRunning {
				mgr.recoverableTaskIDs = append(mgr.recoverableTaskIDs, task.ID)
			}
		}
		mgr.nextID = runStore.NextIDSeed()
	}
	go mgr.resumeRecoveredTasks()
	return mgr
}

func (sm *SubagentManager) Spawn(ctx context.Context, opts SubagentSpawnOptions) (string, error) {
	task, err := sm.spawnTask(ctx, opts)
	if err != nil {
		return "", err
	}
	desc := fmt.Sprintf("Spawned subagent for task: %s (agent=%s)", task.Task, task.AgentID)
	if task.Label != "" {
		desc = fmt.Sprintf("Spawned subagent '%s' for task: %s (agent=%s)", task.Label, task.Task, task.AgentID)
	}
	if task.Role != "" {
		desc += fmt.Sprintf(" role=%s", task.Role)
	}
	return desc, nil
}

func (sm *SubagentManager) SpawnTask(ctx context.Context, opts SubagentSpawnOptions) (*SubagentTask, error) {
	return sm.spawnTask(ctx, opts)
}

func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOptions) (*SubagentTask, error) {
	task := strings.TrimSpace(opts.Task)
	if task == "" {
		return nil, fmt.Errorf("task is required")
	}
	label := strings.TrimSpace(opts.Label)
	role := strings.TrimSpace(opts.Role)
	agentID := normalizeSubagentIdentifier(opts.AgentID)
	originalRole := role
	var profile *SubagentProfile
	if sm.profileStore != nil {
		if agentID != "" {
			if p, ok, err := sm.profileStore.Get(agentID); err != nil {
				return nil, err
			} else if ok {
				profile = p
			}
		} else if role != "" {
			if p, ok, err := sm.profileStore.FindByRole(role); err != nil {
				return nil, err
			} else if ok {
				profile = p
				agentID = normalizeSubagentIdentifier(p.AgentID)
			}
		}
	}
	if agentID == "" {
		agentID = normalizeSubagentIdentifier(role)
	}
	if agentID == "" {
		agentID = "default"
	}
	memoryNS := agentID
	systemPromptFile := ""
	transport := "local"
	nodeID := ""
	parentAgentID := ""
	notifyMainPolicy := "final_only"
	toolAllowlist := []string(nil)
	maxRetries := 0
	retryBackoff := 1000
	timeoutSec := 0
	maxTaskChars := 0
	maxResultChars := 0
	if profile == nil && sm.profileStore != nil {
		if p, ok, err := sm.profileStore.Get(agentID); err != nil {
			return nil, err
		} else if ok {
			profile = p
		}
	}
	if profile != nil {
		if strings.EqualFold(strings.TrimSpace(profile.Status), "disabled") {
			return nil, fmt.Errorf("subagent profile '%s' is disabled", profile.AgentID)
		}
		if label == "" {
			label = strings.TrimSpace(profile.Name)
		}
		if role == "" {
			role = strings.TrimSpace(profile.Role)
		}
		if ns := normalizeSubagentIdentifier(profile.MemoryNamespace); ns != "" {
			memoryNS = ns
		}
		transport = strings.TrimSpace(profile.Transport)
		if transport == "" {
			transport = "local"
		}
		nodeID = strings.TrimSpace(profile.NodeID)
		parentAgentID = strings.TrimSpace(profile.ParentAgentID)
		notifyMainPolicy = normalizeNotifyMainPolicy(profile.NotifyMainPolicy)
		systemPromptFile = strings.TrimSpace(profile.SystemPromptFile)
		toolAllowlist = append([]string(nil), profile.ToolAllowlist...)
		maxRetries = profile.MaxRetries
		retryBackoff = profile.RetryBackoff
		timeoutSec = profile.TimeoutSec
		maxTaskChars = profile.MaxTaskChars
		maxResultChars = profile.MaxResultChars
	}
	if opts.MaxRetries > 0 {
		maxRetries = opts.MaxRetries
	}
	if opts.RetryBackoff > 0 {
		retryBackoff = opts.RetryBackoff
	}
	if opts.TimeoutSec > 0 {
		timeoutSec = opts.TimeoutSec
	}
	if opts.MaxTaskChars > 0 {
		maxTaskChars = opts.MaxTaskChars
	}
	if opts.MaxResultChars > 0 {
		maxResultChars = opts.MaxResultChars
	}
	if maxTaskChars > 0 && len(task) > maxTaskChars {
		return nil, fmt.Errorf("task exceeds max_task_chars quota (%d > %d)", len(task), maxTaskChars)
	}
	maxRetries = normalizePositiveBound(maxRetries, 0, 8)
	retryBackoff = normalizePositiveBound(retryBackoff, 500, 120000)
	timeoutSec = normalizePositiveBound(timeoutSec, 0, 3600)
	maxTaskChars = normalizePositiveBound(maxTaskChars, 0, 400000)
	maxResultChars = normalizePositiveBound(maxResultChars, 0, 400000)
	if role == "" {
		role = originalRole
	}
	originChannel := strings.TrimSpace(opts.OriginChannel)
	originChatID := strings.TrimSpace(opts.OriginChatID)
	if raw := strings.TrimSpace(opts.NotifyMainPolicy); raw != "" {
		notifyMainPolicy = normalizeNotifyMainPolicy(raw)
	}
	threadID := strings.TrimSpace(opts.ThreadID)
	correlationID := strings.TrimSpace(opts.CorrelationID)
	parentRunID := strings.TrimSpace(opts.ParentRunID)

	sm.mu.Lock()
	defer sm.mu.Unlock()

	taskID := fmt.Sprintf("subagent-%d", sm.nextID)
	sm.nextID++
	sessionKey := buildSubagentSessionKey(agentID, taskID)

	now := time.Now().UnixMilli()
	if correlationID == "" {
		correlationID = taskID
	}
	if sm.mailboxStore != nil {
		thread, err := sm.mailboxStore.EnsureThread(AgentThread{
			ThreadID:     threadID,
			Owner:        "main",
			Participants: []string{"main", agentID},
			Status:       "open",
			Topic:        task,
			CreatedAt:    now,
			UpdatedAt:    now,
		})
		if err == nil {
			threadID = thread.ThreadID
		}
	}
	subagentTask := &SubagentTask{
		ID:               taskID,
		Task:             task,
		Label:            label,
		Role:             role,
		AgentID:          agentID,
		Transport:        transport,
		NodeID:           nodeID,
		ParentAgentID:    parentAgentID,
		NotifyMainPolicy: notifyMainPolicy,
		SessionKey:       sessionKey,
		MemoryNS:         memoryNS,
		SystemPromptFile: systemPromptFile,
		ToolAllowlist:    toolAllowlist,
		MaxRetries:       maxRetries,
		RetryBackoff:     retryBackoff,
		TimeoutSec:       timeoutSec,
		MaxTaskChars:     maxTaskChars,
		MaxResultChars:   maxResultChars,
		RetryCount:       0,
		ThreadID:         threadID,
		CorrelationID:    correlationID,
		ParentRunID:      parentRunID,
		OriginChannel:    originChannel,
		OriginChatID:     originChatID,
		Status:           RuntimeStatusRouting,
		Created:          now,
		Updated:          now,
	}
	taskCtx, cancel := context.WithCancel(ctx)
	sm.tasks[taskID] = subagentTask
	sm.cancelFuncs[taskID] = cancel
	sm.recordMailboxMessageLocked(subagentTask, AgentMessage{
		ThreadID:      threadID,
		FromAgent:     "main",
		ToAgent:       agentID,
		CorrelationID: correlationID,
		Type:          "task",
		Content:       task,
		RequiresReply: true,
		Status:        "queued",
		CreatedAt:     now,
	})
	sm.persistTaskLocked(subagentTask, "spawned", "")

	go sm.runTask(taskCtx, subagentTask)
	return cloneSubagentTask(subagentTask), nil
}

func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) {
	defer func() {
		sm.mu.Lock()
		delete(sm.cancelFuncs, task.ID)
		sm.mu.Unlock()
	}()

	sm.mu.Lock()
	task.Status = RuntimeStatusRunning
	task.Created = time.Now().UnixMilli()
	task.Updated = task.Created
	sm.persistTaskLocked(task, "started", "")
	sm.mu.Unlock()

	result, runErr := sm.runWithRetry(ctx, task)
	sm.mu.Lock()
	if runErr != nil {
		task.Status = RuntimeStatusFailed
		task.Result = fmt.Sprintf("Error: %v", runErr)
		task.Result = applySubagentResultQuota(task.Result, task.MaxResultChars)
		task.Updated = time.Now().UnixMilli()
		task.WaitingReply = false
		sm.recordMailboxMessageLocked(task, AgentMessage{
			ThreadID:      task.ThreadID,
			FromAgent:     task.AgentID,
			ToAgent:       "main",
			ReplyTo:       task.LastMessageID,
			CorrelationID: task.CorrelationID,
			Type:          "result",
			Content:       task.Result,
			Status:        "delivered",
			CreatedAt:     task.Updated,
		})
		sm.persistTaskLocked(task, "failed", task.Result)
	} else {
		task.Status = RuntimeStatusCompleted
		task.Result = applySubagentResultQuota(result, task.MaxResultChars)
		task.Updated = time.Now().UnixMilli()
		task.WaitingReply = false
		sm.recordMailboxMessageLocked(task, AgentMessage{
			ThreadID:      task.ThreadID,
			FromAgent:     task.AgentID,
			ToAgent:       "main",
			ReplyTo:       task.LastMessageID,
			CorrelationID: task.CorrelationID,
			Type:          "result",
			Content:       task.Result,
			Status:        "delivered",
			CreatedAt:     task.Updated,
		})
		sm.persistTaskLocked(task, "completed", task.Result)
	}
	sm.mu.Unlock()

	sm.recordEKG(task, runErr)

	// 2. Result broadcast
	if sm.bus != nil && shouldNotifyMainOnFinal(task.NotifyMainPolicy, runErr, task) {
		announceContent, notifyReason := buildSubagentMainNotification(task, runErr)
		sm.bus.PublishInbound(bus.InboundMessage{
			Channel:    "system",
			SenderID:   fmt.Sprintf("subagent:%s", task.ID),
			ChatID:     fmt.Sprintf("%s:%s", task.OriginChannel, task.OriginChatID),
			SessionKey: task.SessionKey,
			Content:    announceContent,
			Metadata: map[string]string{
				"trigger":       "subagent",
				"subagent_id":   task.ID,
				"agent_id":      task.AgentID,
				"role":          task.Role,
				"session_key":   task.SessionKey,
				"memory_ns":     task.MemoryNS,
				"retry_count":   fmt.Sprintf("%d", task.RetryCount),
				"timeout_sec":   fmt.Sprintf("%d", task.TimeoutSec),
				"status":        task.Status,
				"notify_reason": notifyReason,
			},
		})
	}
	sm.mu.Lock()
	sm.notifyTaskWaitersLocked(task.ID)
	sm.mu.Unlock()
}

func (sm *SubagentManager) recordEKG(task *SubagentTask, runErr error) {
	if sm == nil || sm.ekg == nil || task == nil {
		return
	}
	status := "success"
	logText := strings.TrimSpace(task.Result)
	if runErr != nil {
		status = "error"
		if isBlockedSubagentError(runErr) {
			logText = "blocked: " + strings.TrimSpace(task.Result)
		}
	}
	sm.ekg.Record(ekg.Event{
		TaskID:  task.ID,
		Session: task.SessionKey,
		Channel: task.OriginChannel,
		Source:  "subagent",
		Status:  status,
		Log:     logText,
	})
}

func normalizeNotifyMainPolicy(v string) string {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "", "final_only":
		return "final_only"
	case "milestone", "on_blocked", "always", "internal_only":
		return strings.ToLower(strings.TrimSpace(v))
	default:
		return "final_only"
	}
}

func shouldNotifyMainOnFinal(policy string, runErr error, task *SubagentTask) bool {
	switch normalizeNotifyMainPolicy(policy) {
	case "internal_only":
		return false
	case "always", "final_only":
		return true
	case "on_blocked":
		return isBlockedSubagentError(runErr)
	case "milestone":
		return false
	default:
		return true
	}
}

func buildSubagentMainNotification(task *SubagentTask, runErr error) (string, string) {
	status := "completed"
	reason := "final"
	if runErr != nil {
		status = "failed"
		if isBlockedSubagentError(runErr) {
			status = "blocked"
			reason = "blocked"
		}
	}
	return fmt.Sprintf(
		"Subagent update\nagent: %s\nrun: %s\nstatus: %s\nreason: %s\ntask: %s\nsummary: %s",
		strings.TrimSpace(task.AgentID),
		strings.TrimSpace(task.ID),
		status,
		reason,
		summarizeSubagentText(firstNonEmpty(task.Label, task.Task), 120),
		summarizeSubagentText(task.Result, 280),
	), reason
}

func isBlockedSubagentError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return true
	}
	msg := strings.ToLower(strings.TrimSpace(err.Error()))
	if msg == "" {
		return false
	}
	blockedHints := []string{
		"timeout",
		"deadline exceeded",
		"quota",
		"rate limit",
		"too many requests",
		"permission denied",
		"requires input",
		"waiting for reply",
		"blocked",
	}
	for _, hint := range blockedHints {
		if strings.Contains(msg, hint) {
			return true
		}
	}
	return false
}

func summarizeSubagentText(s string, max int) string {
	s = strings.TrimSpace(strings.ReplaceAll(s, "\r\n", "\n"))
	s = strings.ReplaceAll(s, "\n", " ")
	s = strings.Join(strings.Fields(s), " ")
	if s == "" {
		return "(empty)"
	}
	if max > 0 && len(s) > max {
		return strings.TrimSpace(s[:max-3]) + "..."
	}
	return s
}

func firstNonEmpty(values ...string) string {
	for _, v := range values {
		if strings.TrimSpace(v) != "" {
			return strings.TrimSpace(v)
		}
	}
	return ""
}

func (sm *SubagentManager) runWithRetry(ctx context.Context, task *SubagentTask) (string, error) {
	maxRetries := normalizePositiveBound(task.MaxRetries, 0, 8)
	backoffMs := normalizePositiveBound(task.RetryBackoff, 500, 120000)
	timeoutSec := normalizePositiveBound(task.TimeoutSec, 0, 3600)

	var lastErr error
	for attempt := 0; attempt <= maxRetries; attempt++ {
		result, err := runStringTaskWithTaskWatchdog(
			ctx,
			timeoutSec,
			2*time.Second,
			stringTaskWatchdogOptions{
				ProgressFn: func() int {
					return sm.taskWatchdogProgress(task)
				},
				CanExtend: func() bool {
					return sm.taskCanAutoExtend(task)
				},
			},
			func(runCtx context.Context) (string, error) {
				return sm.executeTaskOnce(runCtx, task)
			},
		)
		if err == nil {
			sm.mu.Lock()
			task.RetryCount = attempt
			task.Updated = time.Now().UnixMilli()
			sm.persistTaskLocked(task, "attempt_succeeded", "")
			sm.mu.Unlock()
			return result, nil
		}
		lastErr = err
		sm.mu.Lock()
		task.RetryCount = attempt
		task.Updated = time.Now().UnixMilli()
		sm.persistTaskLocked(task, "attempt_failed", err.Error())
		sm.mu.Unlock()
		if attempt >= maxRetries {
			break
		}
		select {
		case <-ctx.Done():
			return "", ctx.Err()
		case <-time.After(time.Duration(backoffMs) * time.Millisecond):
		}
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("subagent task failed with unknown error")
	}
	return "", lastErr
}

func (sm *SubagentManager) taskWatchdogProgress(task *SubagentTask) int {
	if sm == nil || task == nil {
		return 0
	}
	sm.mu.RLock()
	defer sm.mu.RUnlock()
	current, ok := sm.tasks[task.ID]
	if !ok || current == nil {
		current = task
	}
	if current.Updated <= 0 {
		return 0
	}
	return int(current.Updated)
}

func (sm *SubagentManager) taskCanAutoExtend(task *SubagentTask) bool {
	if sm == nil || task == nil {
		return false
	}
	sm.mu.RLock()
	defer sm.mu.RUnlock()
	current, ok := sm.tasks[task.ID]
	if !ok || current == nil {
		current = task
	}
	return strings.EqualFold(strings.TrimSpace(current.Status), "running")
}

func (sm *SubagentManager) executeTaskOnce(ctx context.Context, task *SubagentTask) (string, error) {
	if task == nil {
		return "", fmt.Errorf("subagent task is nil")
	}
	pending, consumedIDs := sm.consumeThreadInbox(task)
	if sm.runFunc != nil {
		result, err := sm.runFunc(ctx, task)
		if err != nil {
			sm.restoreMessageStatuses(consumedIDs)
		} else {
			sm.ackMessageStatuses(consumedIDs)
		}
		return result, err
	}
	if sm.provider == nil {
		sm.restoreMessageStatuses(consumedIDs)
		return "", fmt.Errorf("no llm provider configured for subagent execution")
	}

	systemPrompt := sm.resolveSystemPrompt(task)
	messages := []providers.Message{
		{
			Role:    "system",
			Content: systemPrompt,
		},
		{
			Role:    "user",
			Content: task.Task,
		},
	}
	if strings.TrimSpace(pending) != "" {
		messages = append(messages, providers.Message{
			Role:    "user",
			Content: "Mailbox updates on this thread:\n" + pending,
		})
	}

	response, err := sm.provider.Chat(ctx, messages, nil, sm.provider.GetDefaultModel(), map[string]interface{}{
		"max_tokens": 4096,
	})
	if err != nil {
		sm.restoreMessageStatuses(consumedIDs)
		return "", err
	}
	sm.ackMessageStatuses(consumedIDs)
	return response.Content, nil
}

func (sm *SubagentManager) resolveSystemPrompt(task *SubagentTask) string {
	systemPrompt := "You are a subagent. Follow workspace AGENTS.md and complete the task independently."
	workspacePrompt := sm.readWorkspacePromptFile("AGENTS.md")
	if workspacePrompt != "" {
		systemPrompt = "Workspace policy (AGENTS.md):\n" + workspacePrompt + "\n\nComplete the given task independently and report the result."
	}
	if task == nil {
		return systemPrompt
	}
	if promptFile := strings.TrimSpace(task.SystemPromptFile); promptFile != "" {
		if promptText := sm.readWorkspacePromptFile(promptFile); promptText != "" {
			return systemPrompt + "\n\nSubagent policy (" + promptFile + "):\n" + promptText
		}
	}
	return systemPrompt
}

func (sm *SubagentManager) readWorkspacePromptFile(relPath string) string {
	ws := strings.TrimSpace(sm.workspace)
	relPath = strings.TrimSpace(relPath)
	if ws == "" || relPath == "" || filepath.IsAbs(relPath) {
		return ""
	}
	fullPath := filepath.Clean(filepath.Join(ws, relPath))
	relToWorkspace, err := filepath.Rel(ws, fullPath)
	if err != nil || strings.HasPrefix(relToWorkspace, "..") {
		return ""
	}
	data, err := os.ReadFile(fullPath)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

type SubagentRunFunc func(ctx context.Context, task *SubagentTask) (string, error)

func (sm *SubagentManager) SetRunFunc(f SubagentRunFunc) {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	sm.runFunc = f
	go sm.resumeRecoveredTasks()
}

func (sm *SubagentManager) ProfileStore() *SubagentProfileStore {
	sm.mu.RLock()
	defer sm.mu.RUnlock()
	return sm.profileStore
}

func (sm *SubagentManager) resumeRecoveredTasks() {
	if sm == nil {
		return
	}
	sm.mu.Lock()
	if sm.runFunc == nil && sm.provider == nil {
		sm.mu.Unlock()
		return
	}
	taskIDs := append([]string(nil), sm.recoverableTaskIDs...)
	sm.recoverableTaskIDs = nil
	toResume := make([]*SubagentTask, 0, len(taskIDs))
	for _, taskID := range taskIDs {
		task, ok := sm.tasks[taskID]
		if !ok || task == nil || task.Status != "running" {
			continue
		}
		task.Updated = time.Now().UnixMilli()
		sm.persistTaskLocked(task, "recovered", "auto-resumed after restart")
		toResume = append(toResume, task)
	}
	sm.mu.Unlock()

	for _, task := range toResume {
		taskCtx, cancel := context.WithCancel(context.Background())
		sm.mu.Lock()
		sm.cancelFuncs[task.ID] = cancel
		sm.mu.Unlock()
		go sm.runTask(taskCtx, task)
	}
}

func (sm *SubagentManager) NextTaskSequence() int {
	sm.mu.RLock()
	defer sm.mu.RUnlock()
	return sm.nextID
}

func (sm *SubagentManager) GetTask(taskID string) (*SubagentTask, bool) {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	sm.pruneArchivedLocked()
	task, ok := sm.tasks[taskID]
	if !ok && sm.runStore != nil {
		return sm.runStore.Get(taskID)
	}
	return task, ok
}

func (sm *SubagentManager) ListTasks() []*SubagentTask {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	sm.pruneArchivedLocked()

	tasks := make([]*SubagentTask, 0, len(sm.tasks))
	seen := make(map[string]struct{}, len(sm.tasks))
	for _, task := range sm.tasks {
		tasks = append(tasks, task)
		seen[task.ID] = struct{}{}
	}
	if sm.runStore != nil {
		for _, task := range sm.runStore.List() {
			if _, ok := seen[task.ID]; ok {
				continue
			}
			tasks = append(tasks, task)
		}
	}
	return tasks
}

func (sm *SubagentManager) KillTask(taskID string) bool {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	t, ok := sm.tasks[taskID]
	if !ok {
		return false
	}
	if cancel, ok := sm.cancelFuncs[taskID]; ok {
		cancel()
		delete(sm.cancelFuncs, taskID)
	}
	if !IsTerminalRuntimeStatus(t.Status) {
		t.Status = RuntimeStatusCancelled
		t.WaitingReply = false
		t.Updated = time.Now().UnixMilli()
		sm.persistTaskLocked(t, "killed", "")
		sm.notifyTaskWaitersLocked(taskID)
	}
	return true
}

func (sm *SubagentManager) SteerTask(taskID, message string) bool {
	return sm.sendTaskMessage(taskID, "main", "control", message, false, "")
}

func (sm *SubagentManager) SendTaskMessage(taskID, message string) bool {
	return sm.sendTaskMessage(taskID, "main", "message", message, false, "")
}

func (sm *SubagentManager) ReplyToTask(taskID, replyToMessageID, message string) bool {
	return sm.sendTaskMessage(taskID, "main", "reply", message, false, replyToMessageID)
}

func (sm *SubagentManager) AckTaskMessage(taskID, messageID string) bool {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	t, ok := sm.tasks[taskID]
	if !ok {
		return false
	}
	if sm.mailboxStore == nil {
		return false
	}
	if strings.TrimSpace(messageID) == "" {
		return false
	}
	t.Updated = time.Now().UnixMilli()
	msg, err := sm.mailboxStore.UpdateMessageStatus(messageID, "acked", t.Updated)
	if err != nil {
		return false
	}
	t.LastMessageID = msg.MessageID
	t.WaitingReply = false
	sm.persistTaskLocked(t, "acked", messageID)
	return true
}

func (sm *SubagentManager) ResumeTask(ctx context.Context, taskID string) (string, bool) {
	sm.mu.RLock()
	t, ok := sm.tasks[taskID]
	sm.mu.RUnlock()
	if !ok {
		return "", false
	}
	if strings.TrimSpace(t.Task) == "" {
		return "", false
	}
	label := strings.TrimSpace(t.Label)
	if label == "" {
		label = "resumed"
	} else {
		label = label + "-resumed"
	}
	_, err := sm.Spawn(ctx, SubagentSpawnOptions{
		Task:           t.Task,
		Label:          label,
		Role:           t.Role,
		AgentID:        t.AgentID,
		MaxRetries:     t.MaxRetries,
		RetryBackoff:   t.RetryBackoff,
		TimeoutSec:     t.TimeoutSec,
		MaxTaskChars:   t.MaxTaskChars,
		MaxResultChars: t.MaxResultChars,
		OriginChannel:  t.OriginChannel,
		OriginChatID:   t.OriginChatID,
		ThreadID:       t.ThreadID,
		CorrelationID:  t.CorrelationID,
		ParentRunID:    t.ID,
	})
	if err != nil {
		return "", false
	}
	sm.mu.Lock()
	if original, ok := sm.tasks[taskID]; ok {
		sm.persistTaskLocked(original, "resumed", label)
	}
	sm.mu.Unlock()
	return label, true
}

func (sm *SubagentManager) Events(taskID string, limit int) ([]SubagentRunEvent, error) {
	if sm.runStore == nil {
		return nil, nil
	}
	return sm.runStore.Events(taskID, limit)
}

func (sm *SubagentManager) RuntimeSnapshot(limit int) RuntimeSnapshot {
	if sm == nil {
		return RuntimeSnapshot{}
	}
	tasks := sm.ListTasks()
	snapshot := RuntimeSnapshot{
		Tasks: make([]TaskRecord, 0, len(tasks)),
		Runs:  make([]RunRecord, 0, len(tasks)),
	}
	seenThreads := map[string]struct{}{}
	for _, task := range tasks {
		snapshot.Tasks = append(snapshot.Tasks, taskToTaskRecord(task))
		snapshot.Runs = append(snapshot.Runs, taskToRunRecord(task))
		if evts, err := sm.Events(task.ID, limit); err == nil {
			for _, evt := range evts {
				snapshot.Events = append(snapshot.Events, EventRecord{
					ID:         EventRecordID(evt.RunID, evt.Type, evt.At),
					RunID:      evt.RunID,
					TaskID:     evt.RunID,
					AgentID:    evt.AgentID,
					Type:       evt.Type,
					Status:     evt.Status,
					Message:    evt.Message,
					RetryCount: evt.RetryCount,
					At:         evt.At,
				})
			}
		}
		threadID := strings.TrimSpace(task.ThreadID)
		if threadID == "" {
			continue
		}
		if _, ok := seenThreads[threadID]; !ok {
			if thread, found := sm.Thread(threadID); found {
				snapshot.Threads = append(snapshot.Threads, threadToThreadRecord(thread))
			}
			seenThreads[threadID] = struct{}{}
		}
		if msgs, err := sm.ThreadMessages(threadID, limit); err == nil {
			for _, msg := range msgs {
				snapshot.Artifacts = append(snapshot.Artifacts, messageToArtifactRecord(msg))
			}
		}
	}
	return snapshot
}

func (sm *SubagentManager) Thread(threadID string) (*AgentThread, bool) {
	if sm.mailboxStore == nil {
		return nil, false
	}
	return sm.mailboxStore.Thread(threadID)
}

func (sm *SubagentManager) ThreadMessages(threadID string, limit int) ([]AgentMessage, error) {
	if sm.mailboxStore == nil {
		return nil, nil
	}
	return sm.mailboxStore.MessagesByThread(threadID, limit)
}

func (sm *SubagentManager) Inbox(agentID string, limit int) ([]AgentMessage, error) {
	if sm.mailboxStore == nil {
		return nil, nil
	}
	return sm.mailboxStore.Inbox(agentID, limit)
}

func (sm *SubagentManager) TaskInbox(taskID string, limit int) ([]AgentMessage, error) {
	sm.mu.RLock()
	task, ok := sm.tasks[taskID]
	sm.mu.RUnlock()
	if !ok || sm.mailboxStore == nil {
		return nil, nil
	}
	return sm.mailboxStore.ThreadInbox(task.ThreadID, task.AgentID, limit)
}

func (sm *SubagentManager) Message(messageID string) (*AgentMessage, bool) {
	if sm.mailboxStore == nil {
		return nil, false
	}
	return sm.mailboxStore.Message(messageID)
}

func (sm *SubagentManager) pruneArchivedLocked() {
	if sm.archiveAfterMinute <= 0 {
		return
	}
	cutoff := time.Now().Add(-time.Duration(sm.archiveAfterMinute) * time.Minute).UnixMilli()
	for id, t := range sm.tasks {
		if !IsTerminalRuntimeStatus(t.Status) {
			continue
		}
		if t.Updated > 0 && t.Updated < cutoff {
			delete(sm.tasks, id)
			delete(sm.cancelFuncs, id)
		}
	}
}

func normalizePositiveBound(v, min, max int) int {
	if v < min {
		return min
	}
	if max > 0 && v > max {
		return max
	}
	return v
}

func applySubagentResultQuota(result string, maxChars int) string {
	if maxChars <= 0 {
		return result
	}
	if len(result) <= maxChars {
		return result
	}
	suffix := "\n\n[TRUNCATED: result exceeds max_result_chars quota]"
	trimmed := result[:maxChars]
	if len(trimmed)+len(suffix) > maxChars && maxChars > len(suffix) {
		trimmed = trimmed[:maxChars-len(suffix)]
	}
	return strings.TrimSpace(trimmed) + suffix
}

func normalizeSubagentIdentifier(in string) string {
	in = strings.TrimSpace(strings.ToLower(in))
	if in == "" {
		return ""
	}
	var sb strings.Builder
	for _, r := range in {
		switch {
		case r >= 'a' && r <= 'z':
			sb.WriteRune(r)
		case r >= '0' && r <= '9':
			sb.WriteRune(r)
		case r == '-' || r == '_' || r == '.':
			sb.WriteRune(r)
		case r == ' ':
			sb.WriteRune('-')
		}
	}
	out := strings.Trim(sb.String(), "-_.")
	if out == "" {
		return ""
	}
	return out
}

func buildSubagentSessionKey(agentID, taskID string) string {
	a := normalizeSubagentIdentifier(agentID)
	if a == "" {
		a = "default"
	}
	t := normalizeSubagentIdentifier(taskID)
	if t == "" {
		t = "task"
	}
	return fmt.Sprintf("subagent:%s:%s", a, t)
}

func (sm *SubagentManager) persistTaskLocked(task *SubagentTask, eventType, message string) {
	if task == nil || sm.runStore == nil {
		return
	}
	cp := cloneSubagentTask(task)
	_ = sm.runStore.AppendRun(cp)
	_ = sm.runStore.AppendEvent(SubagentRunEvent{
		RunID:      cp.ID,
		AgentID:    cp.AgentID,
		Type:       strings.TrimSpace(eventType),
		Status:     cp.Status,
		Message:    strings.TrimSpace(message),
		RetryCount: cp.RetryCount,
		At:         cp.Updated,
	})
}

func (sm *SubagentManager) WaitTask(ctx context.Context, taskID string) (*SubagentTask, bool, error) {
	if sm == nil {
		return nil, false, fmt.Errorf("subagent manager not available")
	}
	taskID = strings.TrimSpace(taskID)
	if taskID == "" {
		return nil, false, fmt.Errorf("task id is required")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	ch := make(chan struct{}, 1)
	sm.mu.Lock()
	sm.pruneArchivedLocked()
	task, ok := sm.tasks[taskID]
	if !ok && sm.runStore != nil {
		if persisted, found := sm.runStore.Get(taskID); found && persisted != nil {
			if IsTerminalRuntimeStatus(persisted.Status) {
				sm.mu.Unlock()
				return persisted, true, nil
			}
		}
	}
	if ok && task != nil && IsTerminalRuntimeStatus(task.Status) {
		cp := cloneSubagentTask(task)
		sm.mu.Unlock()
		return cp, true, nil
	}
	waiters := sm.waiters[taskID]
	if waiters == nil {
		waiters = map[chan struct{}]struct{}{}
		sm.waiters[taskID] = waiters
	}
	waiters[ch] = struct{}{}
	sm.mu.Unlock()

	defer sm.removeTaskWaiter(taskID, ch)
	for {
		select {
		case <-ctx.Done():
			return nil, false, ctx.Err()
		case <-ch:
			sm.mu.Lock()
			sm.pruneArchivedLocked()
			task, ok := sm.tasks[taskID]
			if ok && task != nil && IsTerminalRuntimeStatus(task.Status) {
				cp := cloneSubagentTask(task)
				sm.mu.Unlock()
				return cp, true, nil
			}
			if !ok && sm.runStore != nil {
				if persisted, found := sm.runStore.Get(taskID); found && persisted != nil && IsTerminalRuntimeStatus(persisted.Status) {
					sm.mu.Unlock()
					return persisted, true, nil
				}
			}
			sm.mu.Unlock()
		}
	}
}

func (sm *SubagentManager) removeTaskWaiter(taskID string, ch chan struct{}) {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	waiters := sm.waiters[taskID]
	if len(waiters) == 0 {
		delete(sm.waiters, taskID)
		return
	}
	delete(waiters, ch)
	if len(waiters) == 0 {
		delete(sm.waiters, taskID)
	}
}

func (sm *SubagentManager) notifyTaskWaitersLocked(taskID string) {
	waiters := sm.waiters[taskID]
	if len(waiters) == 0 {
		delete(sm.waiters, taskID)
		return
	}
	for ch := range waiters {
		select {
		case ch <- struct{}{}:
		default:
		}
	}
	delete(sm.waiters, taskID)
}

func (sm *SubagentManager) recordMailboxMessageLocked(task *SubagentTask, msg AgentMessage) {
	if sm.mailboxStore == nil || task == nil {
		return
	}
	if strings.TrimSpace(msg.ThreadID) == "" {
		msg.ThreadID = task.ThreadID
	}
	stored, err := sm.mailboxStore.AppendMessage(msg)
	if err != nil {
		return
	}
	task.LastMessageID = stored.MessageID
	if stored.RequiresReply {
		task.WaitingReply = true
	}
}

func (sm *SubagentManager) sendTaskMessage(taskID, fromAgent, msgType, message string, requiresReply bool, replyTo string) bool {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	t, ok := sm.tasks[taskID]
	if !ok {
		return false
	}
	message = strings.TrimSpace(message)
	if message == "" {
		return false
	}
	fromAgent = strings.TrimSpace(fromAgent)
	if fromAgent == "" {
		fromAgent = "main"
	}
	t.Updated = time.Now().UnixMilli()
	if fromAgent == "main" {
		t.Steering = append(t.Steering, message)
	}
	if strings.TrimSpace(replyTo) == "" {
		replyTo = t.LastMessageID
	}
	toAgent := t.AgentID
	if fromAgent != "main" {
		toAgent = "main"
	}
	sm.recordMailboxMessageLocked(t, AgentMessage{
		ThreadID:      t.ThreadID,
		FromAgent:     fromAgent,
		ToAgent:       toAgent,
		ReplyTo:       replyTo,
		CorrelationID: t.CorrelationID,
		Type:          msgType,
		Content:       message,
		RequiresReply: requiresReply,
		Status:        "queued",
		CreatedAt:     t.Updated,
	})
	switch msgType {
	case "control":
		sm.persistTaskLocked(t, "steered", message)
	case "reply":
		sm.persistTaskLocked(t, "reply_sent", message)
	default:
		sm.persistTaskLocked(t, "message_sent", message)
	}
	return true
}

func (sm *SubagentManager) consumeThreadInbox(task *SubagentTask) (string, []string) {
	if task == nil || sm.mailboxStore == nil {
		return "", nil
	}
	msgs, err := sm.mailboxStore.ThreadInbox(task.ThreadID, task.AgentID, 0)
	if err != nil || len(msgs) == 0 {
		return "", nil
	}
	var sb strings.Builder
	consumed := make([]string, 0, len(msgs))
	now := time.Now().UnixMilli()
	for _, msg := range msgs {
		if _, err := sm.mailboxStore.UpdateMessageStatus(msg.MessageID, "processing", now); err != nil {
			continue
		}
		consumed = append(consumed, msg.MessageID)
		sb.WriteString(fmt.Sprintf("- [%s] from=%s type=%s", msg.MessageID, msg.FromAgent, msg.Type))
		if strings.TrimSpace(msg.ReplyTo) != "" {
			sb.WriteString(fmt.Sprintf(" reply_to=%s", msg.ReplyTo))
		}
		sb.WriteString("\n")
		sb.WriteString(strings.TrimSpace(msg.Content))
		sb.WriteString("\n")
	}
	return strings.TrimSpace(sb.String()), consumed
}

func (sm *SubagentManager) restoreMessageStatuses(messageIDs []string) {
	if sm.mailboxStore == nil || len(messageIDs) == 0 {
		return
	}
	now := time.Now().UnixMilli()
	for _, messageID := range messageIDs {
		_, _ = sm.mailboxStore.UpdateMessageStatus(messageID, "queued", now)
	}
}

func (sm *SubagentManager) ackMessageStatuses(messageIDs []string) {
	if sm.mailboxStore == nil || len(messageIDs) == 0 {
		return
	}
	now := time.Now().UnixMilli()
	for _, messageID := range messageIDs {
		_, _ = sm.mailboxStore.UpdateMessageStatus(messageID, "acked", now)
	}
}
