package tools

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

	"github.com/google/uuid"

	"github.com/nextlevelbuilder/goclaw/internal/store"
	"github.com/nextlevelbuilder/goclaw/internal/tracing"
	"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)

func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]any) *Result {
	team, agentID, err := t.manager.resolveTeam(ctx)
	if err != nil {
		return ErrorResult(err.Error())
	}
	if err := t.manager.requireLead(ctx, team, agentID); err != nil {
		return ErrorResult(err.Error())
	}

	// Gate: must list tasks before creating to prevent duplicates in concurrent group chat.
	if ptd := PendingTeamDispatchFromCtx(ctx); ptd != nil && !ptd.HasListed() {
		return ErrorResult("You must check existing tasks first. Call team_tasks(action=\"search\", query=\"<keywords>\") to check for similar tasks before creating — this saves tokens vs listing all. Alternatively use action=\"list\" to see the full board.")
	}

	subject, _ := args["subject"].(string)
	if subject == "" {
		return ErrorResult("subject is required for create action")
	}

	description, _ := args["description"].(string)
	priority := 0
	if p, ok := args["priority"].(float64); ok {
		priority = int(p)
	}

	var blockedBy []uuid.UUID
	if raw, ok := args["blocked_by"].([]any); ok {
		for _, v := range raw {
			if s, ok := v.(string); ok {
				id, err := uuid.Parse(s)
				if err != nil {
					return ErrorResult(fmt.Sprintf("blocked_by contains invalid task ID %q — must be a real task UUID from a previous create call. Create dependency tasks first, then use their IDs.", s))
				}
				blockedBy = append(blockedBy, id)
			}
		}
	}

	// Validate that all blocked_by tasks belong to the same team and are not terminal.
	for _, depID := range blockedBy {
		depTask, err := t.manager.teamStore.GetTask(ctx, depID)
		if err != nil {
			return ErrorResult(fmt.Sprintf("blocked_by task %s not found: %v", depID, err))
		}
		if depTask.TeamID != team.ID {
			return ErrorResult(fmt.Sprintf("blocked_by task %s belongs to a different team", depID))
		}
		switch depTask.Status {
		case store.TeamTaskStatusCompleted, store.TeamTaskStatusCancelled, store.TeamTaskStatusFailed:
			return ErrorResult(fmt.Sprintf(
				"blocked_by task %s (%s) is already %s. "+
					"Do not block on finished tasks — create this task without blocked_by, "+
					"or pass the completed task's result in the description instead.",
				depID, depTask.Subject, depTask.Status))
		}
	}

	// Resolve assignee (agent key → UUID). Required — every task must be assigned.
	assigneeKey, _ := args["assignee"].(string)
	if assigneeKey == "" {
		return ErrorResult("assignee is required — specify which team member should handle this task")
	}
	assigneeID, err := t.manager.resolveAgentByKey(assigneeKey)
	if err != nil {
		return ErrorResult(fmt.Sprintf("assignee %q not found: %v", assigneeKey, err))
	}
	// Verify assignee is a member of this team.
	members, err := t.manager.cachedListMembers(ctx, team.ID, agentID)
	if err != nil {
		return ErrorResult("failed to verify team membership: " + err.Error())
	}
	isMember := false
	for _, m := range members {
		if m.AgentID == assigneeID {
			isMember = true
			break
		}
	}
	if !isMember {
		return ErrorResult(fmt.Sprintf("agent %q is not a member of this team", assigneeKey))
	}

	requireApproval, _ := args["require_approval"].(bool)
	status := store.TeamTaskStatusPending
	if requireApproval {
		status = store.TeamTaskStatusInReview
	} else if len(blockedBy) > 0 {
		status = store.TeamTaskStatusBlocked
	}
	// Assigned tasks without blockers stay pending — dispatched after the turn
	// ends via post-turn processing (avoids race with blocked_by setup).

	chatID := ToolChatIDFromCtx(ctx)

	// Shared workspace: scope by teamID only. Isolated (default): scope by chatID too.
	wsChat := chatID
	if IsSharedWorkspace(team.Settings) {
		wsChat = ""
	}

	// Compute the team workspace directory so member agents write files to the
	// shared team folder instead of their own personal workspace.
	taskMeta := make(map[string]any)
	if teamWsDir, err := WorkspaceDir(t.manager.dataDir, team.ID, wsChat); err == nil {
		taskMeta["team_workspace"] = teamWsDir
	}
	// Auto-collect media files from current run to team workspace.
	// When leader received files from user and creates a task, copy those
	// files to the team workspace so members can access them via read_file.
	// Also rewrite any media paths in the description to point to the workspace copy,
	// since members can't access the original .media/ paths outside their workspace.
	if mediaPaths := RunMediaPathsFromCtx(ctx); len(mediaPaths) > 0 {
		if wsDir, _ := taskMeta["team_workspace"].(string); wsDir != "" {
			nameMap := RunMediaNamesFromCtx(ctx)
			if copiedPaths := copyMediaToWorkspace(mediaPaths, wsDir, nameMap); len(copiedPaths) > 0 {
				// Store as []any so type assertion works both before and after JSON round-trip.
				files := make([]any, len(copiedPaths))
				for i, p := range copiedPaths {
					files[i] = p
				}
				taskMeta["attached_files"] = files

				// Rewrite media paths in description so members see workspace paths.
				for i, src := range mediaPaths {
					if i < len(copiedPaths) {
						description = strings.ReplaceAll(description, src, copiedPaths[i])
					}
				}
			}
		}
	}

	// Preserve original blocked_by list for blocker-result forwarding when task unblocks.
	if len(blockedBy) > 0 {
		ids := make([]string, len(blockedBy))
		for i, id := range blockedBy {
			ids[i] = id.String()
		}
		taskMeta["original_blocked_by"] = ids
	}
	// Store peer kind so dispatches preserve the correct session scope (group vs direct).
	if pk := ToolPeerKindFromCtx(ctx); pk != "" {
		taskMeta["peer_kind"] = pk
	}
	// Store local key so forum-topic routing works on deferred/unblocked dispatches.
	if lk := ToolLocalKeyFromCtx(ctx); lk != "" {
		taskMeta["local_key"] = lk
	}
	// Store leader's trace context so unblocked dispatch links back to the leader's trace.
	if traceID := tracing.TraceIDFromContext(ctx); traceID != uuid.Nil {
		taskMeta["origin_trace_id"] = traceID.String()
	}
	if rootSpanID := tracing.ParentSpanIDFromContext(ctx); rootSpanID != uuid.Nil {
		taskMeta["origin_root_span_id"] = rootSpanID.String()
	}

	task := &store.TeamTaskData{
		TeamID:           team.ID,
		Subject:          subject,
		Description:      description,
		Status:           status,
		BlockedBy:        blockedBy,
		Priority:         priority,
		UserID:           store.UserIDFromContext(ctx),
		Channel:          ToolChannelFromCtx(ctx),
		TaskType:         "general",
		CreatedByAgentID: &agentID,
		ChatID:           chatID,
		Metadata:         taskMeta,
	}
	task.OwnerAgentID = &assigneeID

	if err := t.manager.teamStore.CreateTask(ctx, task); err != nil {
		return ErrorResult("failed to create task: " + err.Error())
	}

	agentKey := t.manager.agentKeyFromID(ctx, agentID)
	t.manager.broadcastTeamEvent(protocol.EventTeamTaskCreated, protocol.TeamTaskEventPayload{
		TeamID:    team.ID.String(),
		TaskID:    task.ID.String(),
		Subject:   subject,
		Status:    status,
		UserID:    store.UserIDFromContext(ctx),
		Channel:   ToolChannelFromCtx(ctx),
		ChatID:    chatID,
		Timestamp: task.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
		ActorType: "agent",
		ActorID:   agentKey,
	})
	// Track for post-turn dispatch. If no post-turn hook (e.g. HTTP API), dispatch immediately.
	if status == store.TeamTaskStatusPending {
		if ptd := PendingTeamDispatchFromCtx(ctx); ptd != nil {
			ptd.Add(team.ID, task.ID)
		} else {
			// Fallback: assign (pending → in_progress + lock) then dispatch.
			if err := t.manager.teamStore.AssignTask(ctx, task.ID, assigneeID, team.ID); err != nil {
				slog.Warn("executeCreate: fallback assign failed", "task_id", task.ID, "error", err)
			} else {
				t.manager.broadcastTeamEvent(protocol.EventTeamTaskDispatched, protocol.TeamTaskEventPayload{
					TeamID:        team.ID.String(),
					TaskID:        task.ID.String(),
					TaskNumber:    task.TaskNumber,
					Subject:       task.Subject,
					Status:        store.TeamTaskStatusInProgress,
					OwnerAgentKey: t.manager.agentKeyFromID(ctx, assigneeID),
					Channel:       task.Channel,
					ChatID:        task.ChatID,
					Timestamp:     time.Now().UTC().Format("2006-01-02T15:04:05Z"),
					ActorType:     "system",
					ActorID:       "fallback_dispatch",
				})
				t.manager.dispatchTaskToAgent(ctx, task, team.ID, assigneeID)
			}
		}
	}

	assigneeName := t.manager.agentDisplayName(ctx, t.manager.agentKeyFromID(ctx, assigneeID))
	if assigneeName == "" {
		assigneeName = t.manager.agentKeyFromID(ctx, assigneeID)
	}
	return NewResult(fmt.Sprintf("Task created: %s (id=%s, task_number=%d, status=%s, assignee=%s)", subject, task.ID, task.TaskNumber, status, assigneeName))
}

func (t *TeamTasksTool) executeComment(ctx context.Context, args map[string]any) *Result {
	team, agentID, err := t.manager.resolveTeam(ctx)
	if err != nil {
		return ErrorResult(err.Error())
	}

	taskID, err := resolveTaskID(ctx, args)
	if err != nil {
		return ErrorResult(err.Error())
	}

	text, _ := args["text"].(string)
	if text == "" {
		return ErrorResult("text is required for comment action")
	}
	if len(text) > 10000 {
		return ErrorResult("comment text too long (max 10000 chars)")
	}

	// Verify task belongs to team.
	task, err := t.manager.teamStore.GetTask(ctx, taskID)
	if err != nil {
		return ErrorResult("task not found: " + err.Error())
	}
	if task.TeamID != team.ID {
		return ErrorResult("task does not belong to your team")
	}

	if err := t.manager.teamStore.AddTaskComment(ctx, &store.TeamTaskCommentData{
		TaskID:  taskID,
		AgentID: &agentID,
		Content: text,
	}); err != nil {
		return ErrorResult("failed to add comment: " + err.Error())
	}

	t.manager.broadcastTeamEvent(protocol.EventTeamTaskCommented, protocol.TeamTaskEventPayload{
		TeamID:    team.ID.String(),
		TaskID:    taskID.String(),
		UserID:    store.UserIDFromContext(ctx),
		Channel:   ToolChannelFromCtx(ctx),
		ChatID:    ToolChatIDFromCtx(ctx),
		Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
	})

	return NewResult(fmt.Sprintf("Comment added to task %s.", taskID))
}

func (t *TeamTasksTool) executeProgress(ctx context.Context, args map[string]any) *Result {
	team, agentID, err := t.manager.resolveTeam(ctx)
	if err != nil {
		return ErrorResult(err.Error())
	}

	taskID, err := resolveTaskID(ctx, args)
	if err != nil {
		return ErrorResult(err.Error())
	}

	percent := 0
	if p, ok := args["percent"].(float64); ok {
		percent = int(p)
	}
	if percent < 0 || percent > 100 {
		return ErrorResult("percent must be 0-100")
	}
	step, _ := args["text"].(string)

	// Verify ownership.
	task, err := t.manager.teamStore.GetTask(ctx, taskID)
	if err != nil {
		return ErrorResult("task not found: " + err.Error())
	}
	if task.TeamID != team.ID {
		return ErrorResult("task does not belong to your team")
	}
	if task.OwnerAgentID == nil || *task.OwnerAgentID != agentID {
		return ErrorResult("only the assigned task owner can update progress. As team lead, task results arrive automatically when members complete their work.")
	}

	if err := t.manager.teamStore.UpdateTaskProgress(ctx, taskID, team.ID, percent, step); err != nil {
		return ErrorResult("failed to update progress: " + err.Error())
	}

	ownerKey := ""
	if task.OwnerAgentID != nil {
		ownerKey = t.manager.agentKeyFromID(ctx, *task.OwnerAgentID)
	}
	t.manager.broadcastTeamEvent(protocol.EventTeamTaskProgress, protocol.TeamTaskEventPayload{
		TeamID:          team.ID.String(),
		TaskID:          taskID.String(),
		TaskNumber:      task.TaskNumber,
		Subject:         task.Subject,
		Status:          store.TeamTaskStatusInProgress,
		OwnerAgentKey:   ownerKey,
		ProgressPercent: percent,
		ProgressStep:    step,
		UserID:          store.UserIDFromContext(ctx),
		Channel:         ToolChannelFromCtx(ctx),
		ChatID:          ToolChatIDFromCtx(ctx),
		Timestamp:       time.Now().UTC().Format("2006-01-02T15:04:05Z"),
	})

	return SilentResult(fmt.Sprintf("Progress updated: %d%% %s", percent, step))
}

func (t *TeamTasksTool) executeAttach(ctx context.Context, args map[string]any) *Result {
	team, agentID, err := t.manager.resolveTeam(ctx)
	if err != nil {
		return ErrorResult(err.Error())
	}

	taskID, err := resolveTaskID(ctx, args)
	if err != nil {
		return ErrorResult(err.Error())
	}

	fileIDStr, _ := args["file_id"].(string)
	if fileIDStr == "" {
		return ErrorResult("file_id is required for attach action")
	}
	fileID, err := uuid.Parse(fileIDStr)
	if err != nil {
		return ErrorResult("invalid file_id")
	}

	// Verify task belongs to team.
	task, err := t.manager.teamStore.GetTask(ctx, taskID)
	if err != nil {
		return ErrorResult("task not found: " + err.Error())
	}
	if task.TeamID != team.ID {
		return ErrorResult("task does not belong to your team")
	}

	if err := t.manager.teamStore.AttachFileToTask(ctx, &store.TeamTaskAttachmentData{
		TaskID:  taskID,
		FileID:  fileID,
		AddedBy: &agentID,
	}); err != nil {
		return ErrorResult("failed to attach file: " + err.Error())
	}

	return NewResult(fmt.Sprintf("File attached to task %s.", taskID))
}

func (t *TeamTasksTool) executeUpdate(ctx context.Context, args map[string]any) *Result {
	team, agentID, err := t.manager.resolveTeam(ctx)
	if err != nil {
		return ErrorResult(err.Error())
	}
	if err := t.manager.requireLead(ctx, team, agentID); err != nil {
		return ErrorResult(err.Error())
	}

	taskID, err := resolveTaskID(ctx, args)
	if err != nil {
		return ErrorResult(err.Error())
	}

	// Verify task belongs to this team (prevent cross-team update).
	task, err := t.manager.teamStore.GetTask(ctx, taskID)
	if err != nil {
		return ErrorResult("task not found: " + err.Error())
	}
	if task.TeamID != team.ID {
		return ErrorResult("task does not belong to your team")
	}

	updates := map[string]any{}
	if desc, ok := args["description"].(string); ok {
		updates["description"] = desc
	}
	if subj, ok := args["subject"].(string); ok && subj != "" {
		updates["subject"] = subj
	}
	if raw, ok := args["blocked_by"].([]any); ok {
		var blockedBy []uuid.UUID
		for _, v := range raw {
			if s, ok := v.(string); ok {
				id, err := uuid.Parse(s)
				if err != nil {
					return ErrorResult(fmt.Sprintf("blocked_by contains invalid task ID %q — must be a real task UUID.", s))
				}
				blockedBy = append(blockedBy, id)
			}
		}
		// Batch-validate all blocker tasks in one query.
		if len(blockedBy) > 0 {
			depTasks, err := t.manager.teamStore.GetTasksByIDs(ctx, blockedBy)
			if err != nil {
				return ErrorResult("failed to validate blocked_by: " + err.Error())
			}
			depMap := make(map[uuid.UUID]*store.TeamTaskData, len(depTasks))
			for i := range depTasks {
				depMap[depTasks[i].ID] = &depTasks[i]
			}
			for _, id := range blockedBy {
				dt, ok := depMap[id]
				if !ok {
					return ErrorResult(fmt.Sprintf("blocked_by task %s not found", id))
				}
				if dt.TeamID != team.ID {
					return ErrorResult(fmt.Sprintf("blocked_by task %s belongs to a different team", id))
				}
				switch dt.Status {
				case store.TeamTaskStatusCompleted, store.TeamTaskStatusCancelled, store.TeamTaskStatusFailed:
					return ErrorResult(fmt.Sprintf(
						"blocked_by task %s (%s) is already %s. "+
							"Remove it from blocked_by — finished tasks cannot block new work.",
						id, dt.Subject, dt.Status))
				}
			}
		}
		updates["blocked_by"] = blockedBy
	}
	if len(updates) == 0 {
		return ErrorResult("no updates provided (set description, subject, or blocked_by)")
	}

	if err := t.manager.teamStore.UpdateTask(ctx, taskID, updates); err != nil {
		return ErrorResult("failed to update task: " + err.Error())
	}

	t.manager.broadcastTeamEvent(protocol.EventTeamTaskUpdated, protocol.TeamTaskEventPayload{
		TeamID:    team.ID.String(),
		TaskID:    taskID.String(),
		Subject:   task.Subject,
		Status:    task.Status,
		UserID:    store.UserIDFromContext(ctx),
		Channel:   ToolChannelFromCtx(ctx),
		ChatID:    ToolChatIDFromCtx(ctx),
		Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
		ActorType: "agent",
		ActorID:   t.manager.agentKeyFromID(ctx, agentID),
	})

	return NewResult(fmt.Sprintf("Task %s updated.", taskID))
}
