package task

import (
	"context"
	"fmt"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

const (
	taskActiveTargetScopeThreadFocus  = message.TaskActiveTargetScopeThreadFocus
	taskActiveTargetScopeAgentSession = message.TaskActiveTargetScopeAgentSession
	taskActiveTargetScopeOneShot      = "one_shot"

	taskActiveTargetSourceHumanSelected = "human_selected"
)

var allowedTaskAnnouncementIntents = map[string]struct{}{
	"human_decision":     {},
	"blocker":            {},
	"milestone":          {},
	"review_request":     {},
	"artifact_delivery":  {},
	"completion_summary": {},
}

// ExecutionHistory handles GET /api/tasks/:taskId/execution-history.
// It returns task discussion, activity, artifacts, and reviews as one chronological history.
func (handler *ParityHandler) ExecutionHistory(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID, err := taskParityTaskID(r)
	if err != nil {
		return api.Response{}, err
	}

	detail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID)
	if err != nil {
		return api.Response{}, err
	}

	items := make([]dto.TaskExecutionHistoryItem, 0,
		len(detail.Detail.Timeline)+len(detail.Detail.Artifacts)+len(detail.Detail.ReviewHistory))
	for _, entry := range detail.Detail.Timeline {
		switch entry.Kind {
		case taskboard.TaskTimelineEntryKindDiscussion:
			if entry.Discussion == nil {
				continue
			}
			items = append(items, dto.TaskExecutionHistoryItem{
				Kind:      "discussion",
				ID:        entry.Discussion.ID,
				CreatedAt: entry.Discussion.CreatedAt,
				Payload:   converter.ToTaskDiscussionEntry(*entry.Discussion),
			})
		case taskboard.TaskTimelineEntryKindActivity:
			if entry.Activity == nil {
				continue
			}
			items = append(items, dto.TaskExecutionHistoryItem{
				Kind:      "activity",
				ID:        entry.Activity.ID,
				CreatedAt: entry.Activity.CreatedAt,
				Payload:   converter.ToTaskActivityEvent(*entry.Activity),
			})
		}
	}
	for _, artifact := range detail.Detail.Artifacts {
		items = append(items, dto.TaskExecutionHistoryItem{
			Kind:      "artifact",
			ID:        artifact.ID,
			CreatedAt: artifact.CreatedAt,
			Payload:   converter.ToTaskArtifact(artifact),
		})
	}
	sort.SliceStable(items, func(i, j int) bool {
		if !items[i].CreatedAt.Equal(items[j].CreatedAt) {
			return items[i].CreatedAt.After(items[j].CreatedAt)
		}
		if items[i].Kind != items[j].Kind {
			return items[i].Kind < items[j].Kind
		}
		return items[i].ID < items[j].ID
	})

	return api.OK(dto.TaskExecutionHistoryResponse{
		Task:  converter.ToTask(detail.Detail.Task),
		Items: items,
	}), nil
}

// CreateAnnouncement handles POST /api/tasks/:taskId/announcements.
// It publishes an intentional task announcement to the task source surface.
func (handler *ParityHandler) CreateAnnouncement(r api.WorkspaceScopedJSON[dto.TaskAnnouncementRequest]) (api.Response, error) {
	request := r.Body
	taskID, err := taskParityTaskID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}

	intent := strings.TrimSpace(request.Intent)
	if _, ok := allowedTaskAnnouncementIntents[intent]; !ok {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "invalid announcement intent")
	}
	body := strings.TrimSpace(request.Body)
	if body == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "body is required")
	}
	if handler.store == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
	}

	detail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID)
	if err != nil {
		return api.Response{}, err
	}
	targetScopeID := strings.TrimSpace(request.TargetScopeID)
	if targetScopeID == "" {
		targetScopeID = defaultTaskAnnouncementScopeID(detail.Detail.Task)
	}
	if targetScopeID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "task has no announcement target")
	}

	metadata := map[string]any{
		"taskAnnouncement": map[string]any{
			"taskId":       detail.Detail.Task.ID,
			"taskNumber":   detail.Detail.Task.TaskNumber,
			"channelId":    detail.Detail.Task.ChannelID,
			"intent":       intent,
			"sourceThread": detail.Detail.Task.SourceThread,
		},
	}
	message, delivery, err := handler.store.SendMessageForUserWithMetadata(
		r.Context(),
		r.User.User,
		r.Workspace.WorkspaceID,
		targetScopeID,
		body,
		request.AttachmentIDs,
		metadata,
	)
	if err != nil {
		return api.Response{}, err
	}
	if handler.fanout != nil {
		handler.fanout.EmitCommittedMessage(r.Context(), r.Workspace.WorkspaceID, message, delivery)
	}

	var entry *dto.TaskDiscussionEntry
	taskScopeID := firstNonEmptyTaskString(detail.Detail.Task.TaskScopeID, detail.Detail.Task.ID)
	auditMetadata := map[string]any{
		"taskAnnouncementAudit": map[string]any{
			"taskId":              detail.Detail.Task.ID,
			"taskNumber":          detail.Detail.Task.TaskNumber,
			"channelId":           detail.Detail.Task.ChannelID,
			"intent":              intent,
			"sourceTargetScopeId": targetScopeID,
			"sourceMessageId":     message.ID,
		},
	}
	auditMessage, auditDelivery, err := handler.store.SendMessageForUserWithMetadata(
		r.Context(),
		r.User.User,
		r.Workspace.WorkspaceID,
		taskScopeID,
		buildTaskAnnouncementDiscussionBody(intent, targetScopeID, body),
		nil,
		auditMetadata,
	)
	if err == nil {
		if handler.fanout != nil {
			handler.fanout.EmitCommittedMessage(r.Context(), r.Workspace.WorkspaceID, auditMessage, auditDelivery)
		}
		convertedEntry := taskAnnouncementEntryFromAppMessage(detail.Detail.Task.ID, auditMessage)
		entry = &convertedEntry
		if refreshed, refreshErr := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID); refreshErr == nil {
			detail = refreshed
		} else if handler.logger != nil {
			handler.logger.Warn("refresh task after announcement audit message", "task_id", taskID, "error", refreshErr)
		}
	} else if handler.logger != nil {
		handler.logger.Warn("record atomic task announcement audit message", "task_id", taskID, "error", err)
	}

	return api.OK(dto.TaskAnnouncementResponse{
		Task:    converter.ToTask(detail.Detail.Task),
		Message: converter.ToAppMessage(message),
		Entry:   entry,
	}), nil
}

// GetNotificationPolicy handles GET /api/tasks/:taskId/notification-policy.
// It returns the task scope notification and agent wake policy.
func (handler *ParityHandler) GetNotificationPolicy(r api.WorkspaceScopedRequest) (api.Response, error) {
	return handler.withNotificationPolicy(r, func(ctx context.Context, userID string, workspaceID string, task taskboard.Task, taskScopeID string) (message.ScopeNotificationPanel, error) {
		return handler.store.GetTaskNotificationPanelForUser(ctx, userID, workspaceID, taskScopeID)
	}, nil)
}

const (
	defaultTaskWatcherPageSize = message.DefaultTaskWatcherPageSize
	maxTaskWatcherPageSize     = message.MaxTaskWatcherPageSize
	maxTaskWatcherCursorBytes  = 8192
)

// ListWatchers handles GET /api/tasks/:taskId/watchers.
// It returns a bounded page of the complete explicit watcher roster shared by
// every authorized Task reader.
func (handler *ParityHandler) ListWatchers(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID, err := taskParityTaskID(r)
	if err != nil {
		return api.Response{}, err
	}
	if handler.store == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
	}
	if handler.taskBoard == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task board is not configured")
	}
	cursor := strings.TrimSpace(r.Gin.Query("cursor"))
	if len(cursor) > maxTaskWatcherCursorBytes {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "cursor is too long")
	}
	limit := defaultTaskWatcherPageSize
	if rawLimit := strings.TrimSpace(r.Gin.Query("limit")); rawLimit != "" {
		parsed, parseErr := strconv.Atoi(rawLimit)
		if parseErr != nil || parsed <= 0 || parsed > maxTaskWatcherPageSize {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "limit must be between 1 and 100")
		}
		limit = parsed
	}
	readable, err := handler.taskBoard.GetReadableTaskScope(r.Context(), taskboard.GetReadableTaskScopeRequest{
		Context: taskboard.NewUserServiceContext(r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name),
		Task:    taskboard.TaskRef{TaskID: taskID},
	})
	if err != nil {
		return api.Response{}, err
	}
	taskScopeID := strings.TrimSpace(readable.TaskScopeID)
	if taskScopeID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "task has no notification surface")
	}
	page, err := handler.store.GetTaskWatchersForUser(
		r.Context(),
		r.User.User.ID,
		r.Workspace.WorkspaceID,
		taskScopeID,
		cursor,
		limit,
	)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.TaskWatcherPageResponse{
		Watchers:     converter.ToAuthorProfiles(page.Watchers),
		WatcherCount: page.WatcherCount,
		HasMore:      page.HasMore,
		NextCursor:   page.NextCursor,
	}), nil
}

// SetNotificationSelf handles PUT /api/tasks/:taskId/notification-policy/self.
// It updates the current user's task scope notification state.
func (handler *ParityHandler) SetNotificationSelf(r api.WorkspaceScopedJSON[dto.TaskNotificationPolicyRequest]) (api.Response, error) {
	request := r.Body
	return handler.withNotificationPolicy(r.WorkspaceScopedRequest, func(ctx context.Context, userID string, workspaceID string, task taskboard.Task, taskScopeID string) (message.ScopeNotificationPanel, error) {
		return handler.store.SetTaskNotificationSelfStateForUser(ctx, userID, workspaceID, taskScopeID, request.State)
	}, func(workspaceID string, memberID string, task taskboard.Task, panel message.ScopeNotificationPanel) {
		if handler.conversationPolicyProjector == nil {
			return
		}
		if err := handler.conversationPolicyProjector.ProjectTaskConversationPolicy(r.Context(), TaskConversationPolicyProjection{
			WorkspaceID: workspaceID,
			UserID:      r.UserID(),
			MemberID:    memberID,
			TaskID:      task.ID,
			ScopeID:     task.TaskScopeID,
			State:       panel.Self.State,
		}); err != nil && handler.logger != nil {
			// The policy mutation has already committed. Keep the API response
			// successful and let stream recovery repair transient publication.
			handler.logger.WarnContext(r.Context(), "project Task Watch state", "task_id", task.ID, "error", err)
		}
	})
}

// SetNotificationAgent handles PUT /api/tasks/:taskId/notification-policy/agents/:agentId.
// It updates an agent wake policy for the task scope.
func (handler *ParityHandler) SetNotificationAgent(r api.WorkspaceScopedJSON[dto.TaskNotificationPolicyRequest]) (api.Response, error) {
	request := r.Body
	agentID := strings.TrimSpace(r.Gin.Param("agentId"))
	if agentID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "agentId is required")
	}
	return handler.withNotificationPolicy(r.WorkspaceScopedRequest, func(ctx context.Context, userID string, workspaceID string, task taskboard.Task, taskScopeID string) (message.ScopeNotificationPanel, error) {
		return handler.store.SetTaskNotificationAgentWakeStateForUser(ctx, userID, workspaceID, taskScopeID, agentID, request.WakeState)
	}, nil)
}

// SetNotificationAgents handles PUT /api/tasks/:taskId/notification-policy/agents/all.
// It bulk-applies a wake state to every owned agent on the task scope, backing
// the mute-all / unmute-all quick button on the task members page.
func (handler *ParityHandler) SetNotificationAgents(r api.WorkspaceScopedJSON[dto.TaskNotificationPolicyRequest]) (api.Response, error) {
	request := r.Body
	return handler.withNotificationPolicy(r.WorkspaceScopedRequest, func(ctx context.Context, userID string, workspaceID string, task taskboard.Task, taskScopeID string) (message.ScopeNotificationPanel, error) {
		return handler.store.SetTaskNotificationAgentsWakeStateForUser(ctx, userID, workspaceID, taskScopeID, request.WakeState)
	}, nil)
}

// ListActiveTargets handles GET /api/tasks/:taskId/active-targets.
// It returns persisted active-target state for the task and, optionally, its subtasks.
func (handler *ParityHandler) ListActiveTargets(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID, err := taskParityTaskID(r)
	if err != nil {
		return api.Response{}, err
	}
	if handler.store == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
	}
	detail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID)
	if err != nil {
		return api.Response{}, err
	}
	taskIDs := []string{detail.Detail.Task.ID}
	if strings.EqualFold(strings.TrimSpace(r.Gin.Query("includeSubtasks")), "true") {
		for _, subtask := range detail.Detail.Subtasks {
			taskIDs = append(taskIDs, subtask.ID)
		}
	}
	records, err := handler.store.ListTaskActiveTargetsForUser(r.Context(), r.User.User.ID, r.Workspace.WorkspaceID, taskIDs)
	if err != nil {
		return api.Response{}, err
	}
	targets := make([]dto.TaskActiveTarget, 0, len(records))
	for _, record := range records {
		targets = append(targets, taskActiveTargetFromRecord(record))
	}
	return api.OK(dto.TaskActiveTargetsResponse{
		Task:              converter.ToTask(detail.Detail.Task),
		ActiveTaskTargets: targets,
	}), nil
}

// SetActiveTarget handles POST /api/tasks/:taskId/active-target.
// It returns a structured task routing target for thread or agent focus.
func (handler *ParityHandler) SetActiveTarget(r api.WorkspaceScopedJSON[dto.TaskActiveTargetRequest]) (api.Response, error) {
	request := r.Body
	taskID, err := taskParityTaskID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}

	scope, err := normalizeActiveTargetScope(request.Scope, request.AgentID)
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	if scope == taskActiveTargetScopeAgentSession && strings.TrimSpace(request.AgentID) == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "agentId is required for agent_session scope")
	}

	detail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID)
	if err != nil {
		return api.Response{}, err
	}
	var rootTask *taskboard.Task
	if parentTaskRef := strings.TrimSpace(detail.Detail.Task.ParentTaskRef); parentTaskRef != "" {
		parentDetail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, parentTaskRef)
		if err != nil {
			return api.Response{}, err
		}
		rootTask = &parentDetail.Detail.Task
	}
	target := taskActiveTargetFromRequest(detail.Detail.Task, rootTask, request, scope, r.User.User.ID)
	if scope != taskActiveTargetScopeOneShot {
		if handler.store == nil {
			return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
		}
		if scope == taskActiveTargetScopeAgentSession {
			if err := handler.store.RequireTaskActiveTargetAgentForUser(
				r.Context(),
				r.User.User.ID,
				r.Workspace.WorkspaceID,
				detail.Detail.Task.TaskScopeID,
				target.AgentID,
			); err != nil {
				return api.Response{}, err
			}
		}
		record, err := handler.store.SetTaskActiveTargetForUser(
			r.Context(),
			r.User.User.ID,
			r.Workspace.WorkspaceID,
			taskActiveTargetRecord(r.Workspace.WorkspaceID, detail.Detail.Task, target, taskActiveTargetPersistenceKey(detail.Detail.Task, scope, target.AgentID)),
		)
		if err != nil {
			return api.Response{}, err
		}
		target = taskActiveTargetFromRecord(record)
	}
	return api.OK(dto.TaskActiveTargetResponse{
		Task:             converter.ToTask(detail.Detail.Task),
		ActiveTaskTarget: &target,
		Scope:            scope,
		AgentID:          strings.TrimSpace(request.AgentID),
	}), nil
}

// ClearActiveTarget handles DELETE /api/tasks/:taskId/active-target.
// It returns a cleared active-target acknowledgement for the requested scope.
func (handler *ParityHandler) ClearActiveTarget(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID, err := taskParityTaskID(r)
	if err != nil {
		return api.Response{}, err
	}
	scope, err := normalizeActiveTargetScope(r.Gin.Query("scope"), r.Gin.Query("agentId"))
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	agentID := strings.TrimSpace(r.Gin.Query("agentId"))
	if scope == taskActiveTargetScopeAgentSession && agentID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "agentId is required for agent_session scope")
	}
	detail, err := handler.loadTaskDetail(r.Context(), r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name, taskID)
	if err != nil {
		return api.Response{}, err
	}
	if scope != taskActiveTargetScopeOneShot {
		if handler.store == nil {
			return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
		}
		if scope == taskActiveTargetScopeAgentSession {
			if err := handler.store.RequireTaskActiveTargetAgentForUser(
				r.Context(),
				r.User.User.ID,
				r.Workspace.WorkspaceID,
				detail.Detail.Task.TaskScopeID,
				agentID,
			); err != nil {
				return api.Response{}, err
			}
		}
		if err := handler.store.ClearTaskActiveTargetForUser(
			r.Context(),
			r.User.User.ID,
			r.Workspace.WorkspaceID,
			scope,
			taskActiveTargetPersistenceKey(detail.Detail.Task, scope, agentID),
		); err != nil {
			return api.Response{}, err
		}
	}
	return api.OK(dto.TaskActiveTargetResponse{
		Task:    converter.ToTask(detail.Detail.Task),
		Cleared: true,
		Scope:   scope,
		AgentID: agentID,
	}), nil
}

func taskParityTaskID(r api.WorkspaceScopedRequest) (string, error) {
	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return "", api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}
	return taskID, nil
}

func (handler *ParityHandler) loadTaskDetail(ctx context.Context, workspaceID string, userID string, userName string, taskID string) (taskboard.GetTaskDetailResponse, error) {
	if handler.taskBoard == nil {
		return taskboard.GetTaskDetailResponse{}, api.NewHTTPError(http.StatusInternalServerError, "Task board is not configured")
	}
	response, err := handler.taskBoard.GetTaskDetail(ctx, taskboard.GetTaskDetailRequest{
		Context: taskboard.NewUserServiceContext(workspaceID, userID, userName),
		Task:    taskboard.TaskRef{TaskID: taskID},
	})
	if err != nil {
		return taskboard.GetTaskDetailResponse{}, err
	}
	return response, nil
}

func (handler *ParityHandler) withNotificationPolicy(
	r api.WorkspaceScopedRequest,
	load func(ctx context.Context, userID string, workspaceID string, task taskboard.Task, taskScopeID string) (message.ScopeNotificationPanel, error),
	afterSuccess func(workspaceID string, memberID string, task taskboard.Task, panel message.ScopeNotificationPanel),
) (api.Response, error) {
	taskID, err := taskParityTaskID(r)
	if err != nil {
		return api.Response{}, err
	}
	if handler.store == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task parity store is not configured")
	}
	if handler.taskBoard == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Task board is not configured")
	}
	coreDetail, err := handler.taskBoard.GetTaskDetail(r.Context(), taskboard.GetTaskDetailRequest{
		Context:  taskboard.NewUserServiceContext(r.Workspace.WorkspaceID, r.User.User.ID, r.User.User.Name),
		Task:     taskboard.TaskRef{TaskID: taskID},
		TaskOnly: true,
	})
	if err != nil {
		return api.Response{}, err
	}
	task := coreDetail.Detail.Task
	taskScopeID := strings.TrimSpace(task.TaskScopeID)
	if taskScopeID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "task has no notification surface")
	}
	panel, err := load(r.Context(), r.User.User.ID, r.Workspace.WorkspaceID, task, taskScopeID)
	if err != nil {
		return api.Response{}, err
	}
	if afterSuccess != nil {
		afterSuccess(r.Workspace.WorkspaceID, r.Workspace.MemberID, task, panel)
	}
	watcherPreview := converter.ToAuthorProfiles(panel.TaskWatcherPreview)
	return api.OK(dto.TaskNotificationPolicyResponse{
		Task:           converter.ToTask(task),
		ScopeType:      "task",
		ScopeID:        taskScopeID,
		Panel:          converter.ToScopeNotificationPanel(panel),
		WatcherPreview: watcherPreview,
		WatcherCount:   panel.TaskWatcherCount,
		MemberCount:    panel.TaskMemberCount,
	}), nil
}

func taskPolicyScope(task taskboard.Task) (string, string) {
	if task.SourceThread != nil && strings.TrimSpace(task.SourceThread.ThreadChannelID) != "" {
		return "thread", strings.TrimSpace(task.SourceThread.ThreadChannelID)
	}
	return "channel", strings.TrimSpace(task.ChannelID)
}

func defaultTaskAnnouncementScopeID(task taskboard.Task) string {
	_, scopeID := taskPolicyScope(task)
	return scopeID
}

func buildTaskAnnouncementDiscussionBody(intent string, targetScopeID string, body string) string {
	return fmt.Sprintf("Announcement sent to scope %s (%s):\n\n%s", strings.TrimSpace(targetScopeID), strings.TrimSpace(intent), strings.TrimSpace(body))
}

func normalizeActiveTargetScope(rawScope string, rawAgentID string) (string, error) {
	scope := strings.TrimSpace(rawScope)
	if scope == "" {
		if strings.TrimSpace(rawAgentID) != "" {
			return taskActiveTargetScopeAgentSession, nil
		}
		return taskActiveTargetScopeThreadFocus, nil
	}
	switch scope {
	case taskActiveTargetScopeThreadFocus, taskActiveTargetScopeAgentSession, taskActiveTargetScopeOneShot:
		return scope, nil
	default:
		return "", fmt.Errorf("invalid active target scope")
	}
}

func taskActiveTargetFromRequest(task taskboard.Task, rootTask *taskboard.Task, request dto.TaskActiveTargetRequest, scope string, selectedByUserID string) dto.TaskActiveTarget {
	source := strings.TrimSpace(request.Source)
	if source == "" {
		source = taskActiveTargetSourceHumanSelected
	}
	threadTarget := strings.TrimSpace(request.ThreadTarget)
	if threadTarget == "" && task.SourceThread != nil {
		threadTarget = strings.TrimSpace(task.SourceThread.Ref)
		if threadTarget == "" {
			threadTarget = strings.TrimSpace(task.SourceThread.ThreadChannelID)
		}
	}
	if threadTarget == "" && rootTask != nil && rootTask.SourceThread != nil {
		threadTarget = strings.TrimSpace(rootTask.SourceThread.Ref)
		if threadTarget == "" {
			threadTarget = strings.TrimSpace(rootTask.SourceThread.ThreadChannelID)
		}
	}
	rootTaskNumber := task.TaskNumber
	if rootTask != nil {
		rootTaskNumber = rootTask.TaskNumber
	} else if strings.TrimSpace(task.ParentTaskRef) != "" {
		rootTaskNumber = 0
	}
	return dto.TaskActiveTarget{
		Channel:          task.ChannelID,
		TaskID:           task.ID,
		TaskNumber:       task.TaskNumber,
		TaskTitle:        task.Title,
		Source:           source,
		ThreadTarget:     threadTarget,
		Scope:            scope,
		AgentID:          strings.TrimSpace(request.AgentID),
		Role:             strings.TrimSpace(request.Role),
		RootTaskNumber:   rootTaskNumber,
		ParentTaskRef:    strings.TrimSpace(task.ParentTaskRef),
		SelectedByUserID: strings.TrimSpace(selectedByUserID),
	}
}

func taskActiveTargetPersistenceKey(task taskboard.Task, scope string, agentID string) string {
	switch scope {
	case taskActiveTargetScopeAgentSession:
		return strings.TrimSpace(agentID)
	case taskActiveTargetScopeThreadFocus:
		if task.SourceThread != nil && strings.TrimSpace(task.SourceThread.ThreadChannelID) != "" {
			return strings.TrimSpace(task.SourceThread.ThreadChannelID)
		}
		return strings.TrimSpace(task.ChannelID)
	default:
		return ""
	}
}

func taskActiveTargetRecord(workspaceID string, task taskboard.Task, target dto.TaskActiveTarget, targetKey string) message.TaskActiveTargetRecord {
	return message.TaskActiveTargetRecord{
		WorkspaceID:      strings.TrimSpace(workspaceID),
		Scope:            strings.TrimSpace(target.Scope),
		TargetKey:        strings.TrimSpace(targetKey),
		TaskID:           strings.TrimSpace(task.ID),
		TaskChannelID:    strings.TrimSpace(target.Channel),
		TaskNumber:       target.TaskNumber,
		TaskTitle:        strings.TrimSpace(target.TaskTitle),
		Source:           strings.TrimSpace(target.Source),
		ThreadTarget:     strings.TrimSpace(target.ThreadTarget),
		AgentID:          strings.TrimSpace(target.AgentID),
		Role:             strings.TrimSpace(target.Role),
		RootTaskNumber:   target.RootTaskNumber,
		ParentTaskRef:    strings.TrimSpace(target.ParentTaskRef),
		SelectedByUserID: strings.TrimSpace(target.SelectedByUserID),
	}
}

func taskActiveTargetFromRecord(record message.TaskActiveTargetRecord) dto.TaskActiveTarget {
	return dto.TaskActiveTarget{
		Channel:          strings.TrimSpace(record.TaskChannelID),
		TaskID:           strings.TrimSpace(record.TaskID),
		TaskNumber:       record.TaskNumber,
		TaskTitle:        strings.TrimSpace(record.TaskTitle),
		Source:           strings.TrimSpace(record.Source),
		ThreadTarget:     strings.TrimSpace(record.ThreadTarget),
		Scope:            strings.TrimSpace(record.Scope),
		AgentID:          strings.TrimSpace(record.AgentID),
		Role:             strings.TrimSpace(record.Role),
		RootTaskNumber:   record.RootTaskNumber,
		ParentTaskRef:    strings.TrimSpace(record.ParentTaskRef),
		SelectedByUserID: strings.TrimSpace(record.SelectedByUserID),
	}
}
