// Package tools provides the subagent system for spawning child agent instances.
//
// Subagents run in background goroutines with restricted tool access.
// Key constraints from OpenClaw spec:
//   - Depth limit: configurable maxSpawnDepth (default 3)
//   - Max children per parent: configurable (default 8)
//   - Auto-archive after configurable TTL (default 30 min)
//   - Tool deny lists: ALWAYS_DENY + LEAF_DENY at max depth
//   - Results announced back to parent via message bus
package tools

import (
	"context"
	"fmt"
	"log/slog"
	"sync"
	"time"

	"github.com/google/uuid"

	"github.com/nextlevelbuilder/goclaw/internal/bus"
	"github.com/nextlevelbuilder/goclaw/internal/providers"
	"github.com/nextlevelbuilder/goclaw/internal/store"
	"github.com/nextlevelbuilder/goclaw/internal/tracing"
)

// SubagentConfig configures the subagent system.
type SubagentConfig struct {
	MaxConcurrent       int    // max concurrent subagents (default 4)
	MaxSpawnDepth       int    // max nesting depth (default 3)
	MaxChildrenPerAgent int    // max children per parent (default 8)
	ArchiveAfterMinutes int    // auto-archive completed tasks (default 30)
	Model               string // model override for subagents (empty = inherit)
}

// Subagent task status constants.
const (
	TaskStatusRunning   = "running"
	TaskStatusCompleted = "completed"
	TaskStatusFailed    = "failed"
	TaskStatusCancelled = "cancelled"
)

// SubagentTask tracks a running or completed subagent.
type SubagentTask struct {
	ID              string `json:"id"`
	ParentID        string `json:"parentId"`
	Task            string `json:"task"`
	Label           string `json:"label"`
	Status          string `json:"status"` // "running", "completed", "failed", "cancelled"
	Result          string `json:"result,omitempty"`
	Depth           int    `json:"depth"`
	Model           string `json:"model,omitempty"`           // model override for this subagent
	OriginChannel    string `json:"originChannel,omitempty"`
	OriginChatID     string `json:"originChatId,omitempty"`
	OriginPeerKind   string `json:"originPeerKind,omitempty"`  // "direct" or "group" (for session key building)
	OriginLocalKey   string `json:"originLocalKey,omitempty"`  // composite key with topic/thread suffix for routing
	OriginUserID     string `json:"originUserId,omitempty"`    // parent's userID for per-user scoping propagation
	OriginSessionKey string `json:"originSessionKey,omitempty"` // exact parent session key for announce routing (WS uses non-standard format)
	CreatedAt        int64  `json:"createdAt"`
	CompletedAt      int64  `json:"completedAt,omitempty"`
	Media            []bus.MediaFile `json:"-"` // media files from tool results
	OriginTraceID    uuid.UUID `json:"-"` // parent trace for announce linking
	OriginRootSpanID uuid.UUID `json:"-"` // parent agent's root span ID
	cancelFunc       context.CancelFunc `json:"-"` // per-task context cancel
	spawnConfig      SubagentConfig `json:"-"` // resolved config at spawn time (per-agent override merged)
}

// SubagentManager manages the lifecycle of spawned subagents.
type SubagentManager struct {
	mu          sync.RWMutex
	tasks       map[string]*SubagentTask
	config      SubagentConfig
	provider    providers.Provider   // default provider (fallback)
	providerReg *providers.Registry  // registry for resolving parent's provider
	model       string
	msgBus      *bus.MessageBus

	// createTools builds a tool registry for subagents (without spawn/subagent tools).
	createTools   func() *Registry
	announceQueue *AnnounceQueue // optional: batches announces with debounce
}

// NewSubagentManager creates a new subagent manager.
func NewSubagentManager(
	provider providers.Provider,
	providerReg *providers.Registry,
	model string,
	msgBus *bus.MessageBus,
	createTools func() *Registry,
	cfg SubagentConfig,
) *SubagentManager {
	return &SubagentManager{
		tasks:       make(map[string]*SubagentTask),
		config:      cfg,
		provider:    provider,
		providerReg: providerReg,
		model:       model,
		msgBus:      msgBus,
		createTools: createTools,
	}
}

// SetAnnounceQueue sets the announce queue for batched announce delivery.
// If set, runTask() enqueues announces instead of publishing directly.
func (sm *SubagentManager) SetAnnounceQueue(q *AnnounceQueue) {
	sm.announceQueue = q
}

// effectiveConfig returns the per-agent context override merged with defaults,
// or falls back to sm.config when no override is present.
func (sm *SubagentManager) effectiveConfig(ctx context.Context) SubagentConfig {
	override := SubagentConfigFromCtx(ctx)
	if override == nil {
		return sm.config
	}
	cfg := sm.config
	if override.MaxConcurrent > 0 {
		cfg.MaxConcurrent = override.MaxConcurrent
	}
	if override.MaxSpawnDepth > 0 {
		cfg.MaxSpawnDepth = override.MaxSpawnDepth
	}
	if override.MaxChildrenPerAgent > 0 {
		cfg.MaxChildrenPerAgent = override.MaxChildrenPerAgent
	}
	if override.ArchiveAfterMinutes > 0 {
		cfg.ArchiveAfterMinutes = override.ArchiveAfterMinutes
	}
	if override.Model != "" {
		cfg.Model = override.Model
	}
	return cfg
}

// CountRunningForParent returns the number of running tasks for a parent.
func (sm *SubagentManager) CountRunningForParent(parentID string) int {
	sm.mu.RLock()
	defer sm.mu.RUnlock()
	count := 0
	for _, t := range sm.tasks {
		if t.ParentID == parentID && t.Status == TaskStatusRunning {
			count++
		}
	}
	return count
}

// SubagentDenyAlways is the list of tools always denied to subagents.
var SubagentDenyAlways = []string{
	"gateway",
	"agents_list",
	"whatsapp_login",
	"session_status",
	"cron",
	"memory_search",
	"memory_get",
	"sessions_send",
}

// SubagentDenyLeaf is the additional deny list for subagents at max depth.
var SubagentDenyLeaf = []string{
	"sessions_list",
	"sessions_history",
	"sessions_spawn",
	"spawn",
}

// Spawn creates a new subagent task that runs asynchronously.
// Returns immediately with a status message. The subagent runs in a goroutine.
// modelOverride optionally overrides the LLM model for this subagent (matching TS sessions-spawn-tool.ts).
func (sm *SubagentManager) Spawn(
	ctx context.Context,
	parentID string,
	depth int,
	task, label, modelOverride string,
	channel, chatID, peerKind string,
	callback AsyncCallback,
) (string, error) {
	cfg := sm.effectiveConfig(ctx)
	sm.mu.Lock()

	// Check depth limit
	if depth >= cfg.MaxSpawnDepth {
		sm.mu.Unlock()
		return "", fmt.Errorf("spawn depth limit reached (%d/%d)", depth, cfg.MaxSpawnDepth)
	}

	// Check concurrent limit
	running := 0
	for _, t := range sm.tasks {
		if t.Status == TaskStatusRunning {
			running++
		}
	}
	if running >= cfg.MaxConcurrent {
		sm.mu.Unlock()
		return "", fmt.Errorf("max concurrent subagents reached (%d/%d)", running, cfg.MaxConcurrent)
	}

	// Check per-parent children limit
	childCount := 0
	for _, t := range sm.tasks {
		if t.ParentID == parentID {
			childCount++
		}
	}
	if childCount >= cfg.MaxChildrenPerAgent {
		sm.mu.Unlock()
		return "", fmt.Errorf("max children per agent reached (%d/%d)", childCount, cfg.MaxChildrenPerAgent)
	}

	id := generateSubagentID()
	if label == "" {
		label = truncate(task, 50)
	}

	subTask := &SubagentTask{
		ID:               id,
		ParentID:         parentID,
		Task:             task,
		Label:            label,
		Status:           "running",
		Depth:            depth + 1,
		Model:            modelOverride,
		OriginChannel:    channel,
		OriginChatID:     chatID,
		OriginPeerKind:   peerKind,
		OriginLocalKey:    ToolLocalKeyFromCtx(ctx),
		OriginUserID:      store.UserIDFromContext(ctx),
		OriginSessionKey:  ToolSessionKeyFromCtx(ctx),
		OriginTraceID:     tracing.TraceIDFromContext(ctx),
		OriginRootSpanID:  tracing.ParentSpanIDFromContext(ctx),
		CreatedAt:         time.Now().UnixMilli(),
		spawnConfig:       cfg,
	}
	// Detach from parent's cancellation chain so subagent survives after parent run completes.
	// WithoutCancel preserves all context values (agent ID, workspace, trace info, etc.)
	// but parent Done() no longer propagates. Manual cancel via taskCancel() still works.
	detached := context.WithoutCancel(ctx)
	taskCtx, taskCancel := context.WithCancel(detached)
	subTask.cancelFunc = taskCancel

	sm.tasks[id] = subTask
	sm.mu.Unlock()

	slog.Info("subagent spawned", "id", id, "parent", parentID, "depth", subTask.Depth, "label", label)

	go sm.runTask(taskCtx, subTask, callback)

	return fmt.Sprintf("Spawned subagent '%s' (id=%s, depth=%d) for task: %s",
		label, id, subTask.Depth, truncate(task, 100)), nil
}

// RunSync executes a subagent task synchronously, blocking until completion.
func (sm *SubagentManager) RunSync(
	ctx context.Context,
	parentID string,
	depth int,
	task, label string,
	channel, chatID string,
) (string, int, error) {
	cfg := sm.effectiveConfig(ctx)

	sm.mu.Lock()

	if depth >= cfg.MaxSpawnDepth {
		sm.mu.Unlock()
		return "", 0, fmt.Errorf("spawn depth limit reached (%d/%d)", depth, cfg.MaxSpawnDepth)
	}

	id := generateSubagentID()
	if label == "" {
		label = truncate(task, 50)
	}

	subTask := &SubagentTask{
		ID:               id,
		ParentID:         parentID,
		Task:             task,
		Label:            label,
		Status:           "running",
		Depth:            depth + 1,
		OriginChannel:    channel,
		OriginChatID:     chatID,
		OriginLocalKey:   ToolLocalKeyFromCtx(ctx),
		OriginUserID:     store.UserIDFromContext(ctx),
		OriginTraceID:    tracing.TraceIDFromContext(ctx),
		OriginRootSpanID: tracing.ParentSpanIDFromContext(ctx),
		CreatedAt:        time.Now().UnixMilli(),
		spawnConfig:      cfg,
	}
	sm.tasks[id] = subTask
	sm.mu.Unlock()

	slog.Info("subagent sync started", "id", id, "parent", parentID, "depth", subTask.Depth, "label", label)

	iterations := sm.executeTask(ctx, subTask)

	if subTask.Status == TaskStatusFailed {
		return subTask.Result, iterations, fmt.Errorf("subagent failed: %s", subTask.Result)
	}

	return subTask.Result, iterations, nil
}
