package taskboard

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

func (txs TxStore) sendTaskScopeMessageForUser(ctx context.Context, userID string, workspaceID string, taskRef TaskRef, taskScopeID string, entry TaskScopeMessageInput, clientMessageID string) (SendTaskScopeMessageResponse, error) {
	authority, err := txs.authorizeUserTaskScopeDiscussionWrite(
		ctx,
		userID,
		workspaceID,
		taskRef,
		taskScopeID,
		"",
	)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	return txs.sendTaskScopeMessageInTxWithFreshness(
		ctx,
		txs.tx,
		workspaceID,
		authority.ChannelID,
		authority.TaskID,
		authority.ActorMemberID,
		false,
		entry,
		clientMessageID,
		message.SendMessageFreshness{},
		authority.ExecutionOwnerMemberID,
	)
}

func (txs TxStore) sendTaskScopeMessageForAgentNumber(ctx context.Context, agent AgentActor, board BoardRef, taskNumber int64, entry TaskScopeMessageInput, clientMessageID string, freshness message.SendMessageFreshness) (SendTaskScopeMessageResponse, error) {
	tx := txs.tx
	channelID, taskID, err := txs.loadAccessibleAgentTaskID(ctx, tx, agent, board, taskNumber)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	return txs.sendTaskScopeMessageInTxWithFreshness(ctx, tx, agent.WorkspaceID, channelID, taskID, agent.MemberID, true, entry, clientMessageID, freshness, "")
}

func (txs TxStore) sendTaskScopeMessageForAgentTaskTarget(ctx context.Context, agent AgentActor, taskRef TaskRef, taskScopeID string, entry TaskScopeMessageInput, clientMessageID string, freshness message.SendMessageFreshness) (SendTaskScopeMessageResponse, error) {
	authority, err := txs.authorizeAgentTaskScopeDiscussionWrite(
		ctx,
		agent,
		taskRef,
		taskScopeID,
	)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	return txs.sendTaskScopeMessageInTxWithFreshness(
		ctx,
		txs.tx,
		agent.WorkspaceID,
		authority.ChannelID,
		authority.TaskID,
		authority.ActorMemberID,
		true,
		entry,
		clientMessageID,
		freshness,
		authority.ExecutionOwnerMemberID,
	)
}

func (txs TxStore) loadTaskIDForTaskScope(ctx context.Context, queryer coredb.Queryer, workspaceID string, taskScopeID string) (string, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedTaskScopeID := strings.TrimSpace(taskScopeID)
	if trimmedWorkspaceID == "" || trimmedTaskScopeID == "" {
		return "", badRequest("workspace and task scope are required")
	}
	var taskID string
	if err := queryer.QueryRow(ctx, `
SELECT t.id
FROM tasks t
JOIN task_boards b
  ON b.id = t.board_id
JOIN scopes s
  ON s.id = t.task_scope_id
 AND s.workspace_id = $1
 AND s.type = 'task'
 AND s.status = 'active'
 AND s.archived_at IS NULL
WHERE b.workspace_id = $1
  AND t.task_scope_id = $2
LIMIT 1
`, trimmedWorkspaceID, trimmedTaskScopeID).Scan(&taskID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("load task for task scope: %w", err)
	}
	return taskID, nil
}

func (txs TxStore) sendTaskScopeMessageInTx(ctx context.Context, tx coredb.Tx, workspaceID string, channelID string, taskID string, actorMemberID string, allowAgentAuthor bool, input TaskScopeMessageInput) (Task, TaskDiscussionEntry, error) {
	result, err := txs.sendTaskScopeMessageInTxWithFreshness(ctx, tx, workspaceID, channelID, taskID, actorMemberID, allowAgentAuthor, input, "", message.SendMessageFreshness{}, "")
	if err != nil {
		return Task{}, TaskDiscussionEntry{}, err
	}
	return result.Task, result.Message, nil
}

func (txs TxStore) sendTaskScopeMessageInTxWithFreshness(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	channelID string,
	taskID string,
	actorMemberID string,
	allowAgentAuthor bool,
	input TaskScopeMessageInput,
	clientMessageID string,
	freshness message.SendMessageFreshness,
	preauthorizedExecutionOwnerMemberID string,
) (SendTaskScopeMessageResponse, error) {
	normalized, err := normalizeTaskScopeMessageInput(input)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}

	executionOwnerMemberID := strings.TrimSpace(preauthorizedExecutionOwnerMemberID)
	if executionOwnerMemberID == "" {
		executionOwnerMemberID, _, _, err = txs.taskDiscussionAuthorizationAudit(ctx, tx, workspaceID, taskID, actorMemberID, allowAgentAuthor)
		if err != nil {
			return SendTaskScopeMessageResponse{}, err
		}
	}
	taskScopeID, err := txs.loadTaskScopeID(ctx, tx, taskID)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	task, err := txs.taskByID(ctx, tx, workspaceID, taskID)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}

	freshnessEnabled := freshness.Enabled()
	clientMessageID = strings.TrimSpace(clientMessageID)
	if normalized.IdempotencyPayloadDigest != "" && clientMessageID == "" {
		return SendTaskScopeMessageResponse{}, badRequest("clientMessageId is required for an idempotency payload digest")
	}
	if freshnessEnabled {
		if clientMessageID == "" {
			return SendTaskScopeMessageResponse{}, badRequest("clientMessageId is required for freshness send")
		}
		if freshness.SeenUpToSeq < 0 {
			return SendTaskScopeMessageResponse{}, badRequest("freshness.seenUpToSeq must be non-negative")
		}
		if strings.TrimSpace(freshness.TargetKey) != strings.TrimSpace(taskScopeID) {
			return SendTaskScopeMessageResponse{}, badRequest("freshness.targetKey does not match canonical target")
		}
	}
	// Idempotency describes the client-owned command, not the current result of
	// mutable mention resolution. Compute it before routing; the canonical
	// message digest deliberately removes server-derived mention metadata.
	encodedCommandMetadata, _, err := message.EncodeMessageMetadata(normalized.Metadata)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	messageCreateDigest := ""
	if clientMessageID != "" {
		messageCreateDigest = strings.TrimSpace(normalized.IdempotencyPayloadDigest)
		if messageCreateDigest == "" {
			messageCreateDigest, err = message.ComputeMessageCreatePayloadDigest(message.MessageCreateDigestInput{
				MessageType:   message.MessageTypeTaskDiscussion,
				Content:       normalized.Content,
				AttachmentIDs: normalized.AttachmentIDs,
				Metadata:      encodedCommandMetadata,
			})
			if err != nil {
				return SendTaskScopeMessageResponse{}, err
			}
		}
	}
	acceptedSeq := int64(0)
	latestSeq := int64(0)
	if clientMessageID != "" {
		existing, err := message.NewScopeRepositories(tx).Messages.LoadByClientMessageID(ctx, taskScopeID, actorMemberID, actorMemberID, clientMessageID)
		if err == nil {
			if err := requireTaskDiscussionReplayDigest(existing, messageCreateDigest); err != nil {
				return SendTaskScopeMessageResponse{}, err
			}
			entry, entryErr := loadTaskDiscussionEntryByIDForViewer(ctx, tx, workspaceID, taskID, existing.ID, actorMemberID)
			if entryErr != nil {
				return SendTaskScopeMessageResponse{}, entryErr
			}
			return SendTaskScopeMessageResponse{
				Task:            task,
				Message:         entry,
				State:           message.SendStateAlreadySent,
				MessageID:       existing.ID,
				TargetKey:       taskScopeID,
				ClientMessageID: clientMessageID,
				SeenUpToSeq:     freshness.SeenUpToSeq,
				AcceptedSeq:     existing.Seq,
				LatestSeq:       existing.Seq,
			}, nil
		} else if !isStoreMessageNotFound(err) {
			return SendTaskScopeMessageResponse{}, err
		}
	}
	mentionTargets, err := resolveTaskDiscussionMentionTargetsFromContent(ctx, tx, workspaceID, channelID, actorMemberID, executionOwnerMemberID, normalized.Content, normalized.MentionHints)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	metadata := message.CopyMessageMetadata(normalized.Metadata)
	for key, value := range taskDiscussionMentionMetadata(normalized.MentionHints, mentionTargets, normalized.Content) {
		if metadata == nil {
			metadata = make(map[string]any, 1)
		}
		metadata[key] = value
	}
	now := time.Now().UTC()
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return SendTaskScopeMessageResponse{}, fmt.Errorf("generate task discussion message id: %w", err)
	}
	if freshnessEnabled {
		allocation, err := message.AllocateNextScopeMessageSeqWithVisibleFreshness(ctx, tx, message.ScopeMessageSeqVisibleFreshnessRequest{
			ScopeID:             taskScopeID,
			ViewerMemberID:      actorMemberID,
			ViewerType:          taskDiscussionFreshnessViewerType(allowAgentAuthor),
			SeenUpToSeq:         freshness.SeenUpToSeq,
			UseVisibleMissCheck: allowAgentAuthor,
		})
		if err != nil {
			return SendTaskScopeMessageResponse{}, err
		}
		if allocation.State == message.SendStateHeld {
			existing, err := message.NewScopeRepositories(tx).Messages.LoadByClientMessageID(ctx, taskScopeID, actorMemberID, actorMemberID, clientMessageID)
			if err == nil {
				if err := requireTaskDiscussionReplayDigest(existing, messageCreateDigest); err != nil {
					return SendTaskScopeMessageResponse{}, err
				}
				entry, entryErr := loadTaskDiscussionEntryByIDForViewer(ctx, tx, workspaceID, taskID, existing.ID, actorMemberID)
				if entryErr != nil {
					return SendTaskScopeMessageResponse{}, entryErr
				}
				return SendTaskScopeMessageResponse{
					Task:            task,
					Message:         entry,
					State:           message.SendStateAlreadySent,
					MessageID:       existing.ID,
					TargetKey:       taskScopeID,
					ClientMessageID: clientMessageID,
					SeenUpToSeq:     freshness.SeenUpToSeq,
					AcceptedSeq:     existing.Seq,
					LatestSeq:       existing.Seq,
				}, nil
			} else if !isStoreMessageNotFound(err) {
				return SendTaskScopeMessageResponse{}, err
			}
			return SendTaskScopeMessageResponse{
				Task:                 task,
				State:                message.SendStateHeld,
				TargetKey:            taskScopeID,
				ClientMessageID:      clientMessageID,
				SeenUpToSeq:          freshness.SeenUpToSeq,
				LatestSeq:            allocation.LatestSeq,
				NeedsRefreshUntilSeq: allocation.LatestSeq,
			}, nil
		}
		acceptedSeq = allocation.Seq
		latestSeq = allocation.LatestSeq
	}
	if err := txs.ensureTaskDiscussionScopeParticipants(ctx, tx, workspaceID, taskScopeID, actorMemberID, task, mentionTargets, now); err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	persistedSeq, err := txs.appendTaskScopeMessage(ctx, tx, workspaceID, channelID, taskID, taskScopeID, messageID, actorMemberID, taskScopeDiscussionAuthorType(allowAgentAuthor), normalized.Content, metadata, now, taskScopeMessageAppendOptions{Seq: acceptedSeq, ClientMessageID: clientMessageID, IdempotencyPayloadDigest: messageCreateDigest, AllowEmptyBody: len(normalized.AttachmentIDs) > 0, AttachmentIDs: normalized.AttachmentIDs})
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	if acceptedSeq == 0 {
		acceptedSeq = persistedSeq
	}
	if latestSeq == 0 {
		latestSeq = acceptedSeq
	}
	if err := applyTaskDiscussionMessageSideEffects(ctx, tx, taskID, actorMemberID, messageID, now); err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	task, err = txs.taskByID(ctx, tx, workspaceID, taskID)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	createdEntry, err := loadTaskDiscussionEntryByIDForViewer(ctx, tx, workspaceID, taskID, messageID, actorMemberID)
	if err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	entries := []TaskDiscussionEntry{createdEntry}
	if err := attachResolvedMentionsForTaskDiscussionEntries(ctx, tx, workspaceID, actorMemberID, taskScopeID, entries); err != nil {
		return SendTaskScopeMessageResponse{}, err
	}
	createdEntry = entries[0]
	return SendTaskScopeMessageResponse{
		Task:            task,
		Message:         createdEntry,
		State:           message.SendStateSent,
		MessageID:       messageID,
		TargetKey:       taskScopeID,
		ClientMessageID: clientMessageID,
		SeenUpToSeq:     freshness.SeenUpToSeq,
		AcceptedSeq:     acceptedSeq,
		LatestSeq:       latestSeq,
	}, nil
}

func requireTaskDiscussionReplayDigest(existing message.ScopeMessageRecord, requestedDigest string) error {
	if strings.EqualFold(strings.TrimSpace(existing.IdempotencyPayloadDigest), strings.TrimSpace(requestedDigest)) {
		return nil
	}
	return conflict("clientMessageId was already used with a different message payload")
}

func applyTaskDiscussionMessageSideEffects(ctx context.Context, tx coredb.Tx, taskID string, actorMemberID string, messageID string, now time.Time) error {
	if _, err := tx.Exec(ctx, `
UPDATE tasks
SET discussion_count = discussion_count + 1,
    last_activity_at = $2,
    updated_at = $2
WHERE id = $1
`, taskID, now); err != nil {
		return fmt.Errorf("touch task after discussion: %w", err)
	}
	if err := applyTaskTypedEventPlan(ctx, tx, taskTypedEventIntent(taskID, actorMemberID, taskEventDiscussionAdded, messageID, now)); err != nil {
		return err
	}
	return nil
}

func taskDiscussionFreshnessViewerType(allowAgentAuthor bool) string {
	if allowAgentAuthor {
		return message.ScopeActorPrivateAgent
	}
	return message.ScopeActorHuman
}

func isStoreMessageNotFound(err error) bool {
	return errors.Is(err, message.ErrNotFound)
}

func normalizeTaskScopeMessageInput(input TaskScopeMessageInput) (TaskScopeMessageInput, error) {
	body := strings.TrimSpace(input.Content)
	attachmentIDs := normalizeUniqueStringList(input.AttachmentIDs)
	if body == "" && len(attachmentIDs) == 0 {
		return TaskScopeMessageInput{}, badRequest("discussion body or attachmentIds is required")
	}

	mentionHints := normalizeTaskScopeMessageMentionHints(input.MentionHints)
	mentionHints = taskScopeMessageMentionHintsWithResolvedLinks(mentionHints, body)
	idempotencyPayloadDigest := strings.TrimSpace(input.IdempotencyPayloadDigest)
	if idempotencyPayloadDigest != "" {
		decoded, err := hex.DecodeString(idempotencyPayloadDigest)
		if err != nil || len(decoded) != 32 {
			return TaskScopeMessageInput{}, badRequest("idempotency payload digest is invalid")
		}
		idempotencyPayloadDigest = strings.ToLower(idempotencyPayloadDigest)
	}

	return TaskScopeMessageInput{
		Content:                  body,
		AttachmentIDs:            attachmentIDs,
		MentionHints:             mentionHints,
		Metadata:                 message.CopyMessageMetadata(input.Metadata),
		IdempotencyPayloadDigest: idempotencyPayloadDigest,
	}, nil
}

func normalizeTaskScopeMessageMentionHints(hints []TaskScopeMessageMentionHint) []TaskScopeMessageMentionHint {
	if len(hints) == 0 {
		return nil
	}
	normalized := make([]TaskScopeMessageMentionHint, 0, min(len(hints), message.MaxPersistedMentionHints))
	seen := make(map[string]struct{}, len(hints))
	for _, hint := range hints {
		if len(normalized) >= message.MaxPersistedMentionHints {
			break
		}
		mentionType := strings.ToLower(strings.TrimSpace(hint.Type))
		id := strings.TrimSpace(hint.ID)
		token := strings.TrimSpace(hint.Token)
		if mentionType == "" || id == "" || token == "" {
			continue
		}
		key := mentionType + "\x00" + id + "\x00" + strings.ToLower(token)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, TaskScopeMessageMentionHint{
			Type:     mentionType,
			ID:       id,
			Token:    token,
			Metadata: hint.Metadata,
		})
	}
	return normalized
}

func taskScopeMessageMentionHintsWithResolvedLinks(hints []TaskScopeMessageMentionHint, body string) []TaskScopeMessageMentionHint {
	links := message.ParseResolvedMentionLinks(body)
	if len(links) == 0 {
		return hints
	}
	messageHints := make([]message.MessageMentionHint, 0, len(hints))
	for _, hint := range hints {
		messageHints = append(messageHints, message.MessageMentionHint{
			Type:     hint.Type,
			ID:       hint.ID,
			Token:    hint.Token,
			Metadata: hint.Metadata,
		})
	}
	enriched := message.MentionHintsWithResolvedLinks(messageHints, links)
	if len(enriched) == 0 {
		return nil
	}
	out := make([]TaskScopeMessageMentionHint, 0, len(enriched))
	for _, hint := range enriched {
		out = append(out, TaskScopeMessageMentionHint{
			Type:     hint.Type,
			ID:       hint.ID,
			Token:    hint.Token,
			Metadata: hint.Metadata,
		})
	}
	return out
}

func claimAttachmentsForTaskDiscussionMessage(ctx context.Context, tx coredb.Tx, workspaceID string, channelID string, taskID string, taskScopeID string, uploadedByMemberID string, messageID string, attachmentIDs []string) error {
	normalizedAttachmentIDs := normalizeUniqueStringList(attachmentIDs)
	if len(normalizedAttachmentIDs) == 0 {
		return nil
	}

	cte, attachmentArgs, _, err := coredb.BuildStringValuesCTE("attachment_ids", "attachment_id", normalizedAttachmentIDs, 6)
	if err != nil {
		return fmt.Errorf("claim task discussion attachments: %w", err)
	}

	query := fmt.Sprintf(`
WITH %s
SELECT a.id
FROM attachments AS a
JOIN tasks AS t
  ON t.id = $3
JOIN task_boards AS tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $1
 AND tb.channel_id = $4
JOIN scopes AS task_channel
  ON task_channel.id = tb.channel_id
 AND task_channel.workspace_id = $1
 AND task_channel.type = 'channel'
 AND task_channel.status = 'active'
 AND task_channel.archived_at IS NULL
 AND t.channel_id = tb.channel_id
WHERE a.workspace_id = $1
  AND a.uploaded_by_member_id = $2
  AND a.message_id IS NULL
  AND (
      a.scope_id = tb.channel_id
      OR (NULLIF(t.source_thread_channel_id, '') IS NOT NULL AND a.scope_id = t.source_thread_channel_id)
      OR a.scope_id = $5
  )
  AND EXISTS (
      SELECT 1
      FROM attachment_ids
      WHERE attachment_ids.attachment_id = a.id
  )
ORDER BY a.created_at ASC, a.id ASC
FOR UPDATE
`, cte)
	args := append([]any{workspaceID, uploadedByMemberID, taskID, channelID, taskScopeID}, attachmentArgs...)
	rows, err := tx.Query(ctx, query, args...)
	if err != nil {
		return fmt.Errorf("load task discussion attachments: %w", err)
	}
	defer rows.Close()

	claimedCount := 0
	for rows.Next() {
		var attachmentID string
		if err := rows.Scan(&attachmentID); err != nil {
			return fmt.Errorf("scan task discussion attachment: %w", err)
		}
		claimedCount++
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate task discussion attachments: %w", err)
	}
	if claimedCount != len(normalizedAttachmentIDs) {
		return ErrNotFound
	}

	updateCTE, updateAttachmentArgs, _, err := coredb.BuildStringValuesCTE("attachment_ids", "attachment_id", normalizedAttachmentIDs, 7)
	if err != nil {
		return fmt.Errorf("claim task discussion attachments: %w", err)
	}
	updateQuery := fmt.Sprintf(`
WITH %s
UPDATE attachments AS a
JOIN tasks AS t
  ON t.id = $4
JOIN task_boards AS tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $1
 AND tb.channel_id = $5
JOIN scopes AS task_channel
  ON task_channel.id = tb.channel_id
 AND task_channel.workspace_id = $1
 AND task_channel.type = 'channel'
 AND task_channel.status = 'active'
 AND task_channel.archived_at IS NULL
 AND t.channel_id = tb.channel_id
SET a.scope_id = $3,
    a.message_id = $6
WHERE a.workspace_id = $1
  AND a.uploaded_by_member_id = $2
  AND a.message_id IS NULL
  AND (
      a.scope_id = tb.channel_id
      OR (NULLIF(t.source_thread_channel_id, '') IS NOT NULL AND a.scope_id = t.source_thread_channel_id)
      OR a.scope_id = $3
  )
  AND EXISTS (
      SELECT 1
      FROM attachment_ids
      WHERE attachment_ids.attachment_id = a.id
  )
`, updateCTE)
	updateArgs := append([]any{workspaceID, uploadedByMemberID, taskScopeID, taskID, channelID, messageID}, updateAttachmentArgs...)
	commandTag, err := tx.Exec(ctx, updateQuery, updateArgs...)
	if err != nil {
		return fmt.Errorf("claim task discussion attachments: %w", err)
	}
	if commandTag.RowsAffected() != int64(len(normalizedAttachmentIDs)) {
		return ErrNotFound
	}

	return nil
}

func taskDiscussionMentionMetadata(hints []TaskScopeMessageMentionHint, targets []taskDiscussionMentionTarget, body string) map[string]any {
	if len(hints) == 0 || len(targets) == 0 {
		return nil
	}
	recipients := make([]message.AgentRecipient, 0, len(targets))
	for _, target := range targets {
		agentID := strings.TrimSpace(target.AgentID)
		memberID := strings.TrimSpace(target.MemberID)
		if agentID == "" || memberID == "" {
			continue
		}
		recipients = append(recipients, message.AgentRecipient{
			AgentID:       agentID,
			AgentMemberID: memberID,
			MentionToken:  target.MentionToken,
		})
	}
	if len(recipients) == 0 {
		return nil
	}
	messageHints := make([]message.MessageMentionHint, 0, len(hints))
	for _, hint := range hints {
		messageHints = append(messageHints, message.MessageMentionHint{
			Type:  hint.Type,
			ID:    hint.ID,
			Token: hint.Token,
		})
	}
	mentions := message.BuildPersistedMentionMetadata(messageHints, recipients, message.ParseDirectMentionTokens(body))
	if len(mentions) == 0 {
		return nil
	}
	return map[string]any{message.MentionMetadataKey: mentions}
}

func resolveTaskDiscussionMentionTargetsFromContent(ctx context.Context, queryer coredb.Queryer, workspaceID string, channelID string, actorMemberID string, executionOwnerMemberID string, body string, hints []TaskScopeMessageMentionHint) ([]taskDiscussionMentionTarget, error) {
	tokens := normalizeUniqueStringList(message.ParseDirectMentionTokens(body))
	if len(tokens) == 0 {
		return nil, nil
	}

	resolvedMentionLinks := message.ParseResolvedMentionLinks(body)
	hintRouting, err := resolveTaskScopeMessageMentionHintTargets(ctx, queryer, workspaceID, channelID, actorMemberID, executionOwnerMemberID, hints, tokens, resolvedMentionLinks)
	if err != nil {
		return nil, err
	}
	canonicalMentionTokens := taskDiscussionResolvedMentionTokensForDirectTokens(resolvedMentionLinks, tokens)
	excludedTokens := append(append([]string(nil), hintRouting.tokens...), canonicalMentionTokens...)
	liveTokens := taskDiscussionMentionTokensExcluding(tokens, excludedTokens)

	targets := make([]taskDiscussionMentionTarget, 0, len(tokens))
	seenMemberIDs := make(map[string]struct{}, len(tokens))
	for _, target := range hintRouting.targets {
		memberID := strings.TrimSpace(target.MemberID)
		if memberID == "" {
			continue
		}
		if _, ok := seenMemberIDs[memberID]; ok {
			continue
		}
		seenMemberIDs[memberID] = struct{}{}
		targets = append(targets, target)
	}
	if len(liveTokens) == 0 {
		return targets, nil
	}

	cte, tokenArgs, _, err := coredb.BuildStringValuesCTE("mentioned_tokens", "token", liveTokens, 5)
	if err != nil {
		return nil, fmt.Errorf("resolve task discussion mentions: %w", err)
	}
	query := fmt.Sprintf(`
WITH %s
SELECT
    mentioned_tokens.token,
    a.id,
    a.member_id,
    a.name,
    COALESCE(NULLIF(m.display_name, ''), '') AS display_name
FROM agents a
JOIN members m
  ON m.id = a.member_id
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND mac.status = 'active'
JOIN scope_memberships sm
  ON sm.scope_id = $2
 AND sm.member_id = a.member_id
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
JOIN mentioned_tokens
  ON (
      LOWER(a.name) = mentioned_tokens.token
      OR LOWER(a.member_id) = mentioned_tokens.token
      OR LOWER(COALESCE(NULLIF(m.display_name, ''), '')) = mentioned_tokens.token
      OR mentioned_tokens.token LIKE CONCAT(LOWER(NULLIF(a.name, '')), '%%')
      OR mentioned_tokens.token LIKE CONCAT(LOWER(NULLIF(a.member_id, '')), '%%')
      OR mentioned_tokens.token LIKE CONCAT(LOWER(NULLIF(m.display_name, '')), '%%')
  )
WHERE a.workspace_id = $1
  AND a.status <> 'archived'
  AND a.member_id <> $3
  AND (
      sm.visibility = 'public'
      OR (sm.visibility = 'private' AND (sm.owner_member_id = $3 OR sm.member_id = $3 OR sm.owner_member_id = $4))
  )
ORDER BY mentioned_tokens.token ASC, a.name ASC, a.member_id ASC
`, cte)
	args := append([]any{strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), strings.TrimSpace(actorMemberID), strings.TrimSpace(executionOwnerMemberID)}, tokenArgs...)
	rows, err := queryer.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("load task discussion mention targets: %w", err)
	}
	defer rows.Close()

	type taskDiscussionMentionCandidate struct {
		target      taskDiscussionMentionTarget
		rawToken    string
		matchLength int
	}
	candidates := make([]taskDiscussionMentionCandidate, 0, len(liveTokens))
	longestMatchByRawToken := make(map[string]int, len(liveTokens))
	for rows.Next() {
		var rawToken string
		var agentID string
		var memberID string
		var agentName string
		var displayName string
		if err := rows.Scan(&rawToken, &agentID, &memberID, &agentName, &displayName); err != nil {
			return nil, fmt.Errorf("scan task discussion mention target: %w", err)
		}
		mentionToken, ok := message.MatchedMentionTargetToken(rawToken, []string{
			agentName,
			memberID,
			displayName,
		})
		if !ok {
			continue
		}
		normalizedRawToken := strings.ToLower(strings.TrimSpace(rawToken))
		matchLength := len(mentionToken)
		if current := longestMatchByRawToken[normalizedRawToken]; matchLength > current {
			longestMatchByRawToken[normalizedRawToken] = matchLength
		}
		trimmedMemberID := strings.TrimSpace(memberID)
		if trimmedMemberID == "" {
			continue
		}
		candidates = append(candidates, taskDiscussionMentionCandidate{
			target: taskDiscussionMentionTarget{
				AgentID:      strings.TrimSpace(agentID),
				MemberID:     trimmedMemberID,
				MentionToken: mentionToken,
			},
			rawToken:    normalizedRawToken,
			matchLength: matchLength,
		})
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate task discussion mention targets: %w", err)
	}
	for _, candidate := range candidates {
		if candidate.matchLength != longestMatchByRawToken[candidate.rawToken] {
			continue
		}
		if _, ok := seenMemberIDs[candidate.target.MemberID]; ok {
			continue
		}
		seenMemberIDs[candidate.target.MemberID] = struct{}{}
		targets = append(targets, candidate.target)
	}
	return targets, nil
}

type taskDiscussionMentionHintRouting struct {
	targets []taskDiscussionMentionTarget
	tokens  []string
}

func resolveTaskScopeMessageMentionHintTargets(ctx context.Context, queryer coredb.Queryer, workspaceID string, channelID string, actorMemberID string, executionOwnerMemberID string, hints []TaskScopeMessageMentionHint, directMentionTokens []string, resolvedMentionLinks []message.ResolvedMentionLink) (taskDiscussionMentionHintRouting, error) {
	if len(hints) == 0 || len(directMentionTokens) == 0 {
		return taskDiscussionMentionHintRouting{}, nil
	}
	allowedTokens := make([]string, 0, len(directMentionTokens))
	seenAllowedTokens := make(map[string]struct{}, len(directMentionTokens))
	for _, token := range directMentionTokens {
		normalized := strings.ToLower(strings.TrimSpace(token))
		if normalized == "" {
			continue
		}
		if _, ok := seenAllowedTokens[normalized]; ok {
			continue
		}
		seenAllowedTokens[normalized] = struct{}{}
		allowedTokens = append(allowedTokens, normalized)
	}
	if len(allowedTokens) == 0 {
		return taskDiscussionMentionHintRouting{}, nil
	}

	agentIDs := make([]string, 0, len(hints))
	hintTokenByAgentID := make(map[string]string, len(hints))
	seenAgentIDs := make(map[string]struct{}, len(hints))
	seenHintTokens := make(map[string]struct{}, len(hints))
	canonicalAgentIDsByToken := taskDiscussionResolvedAgentMentionIDsByToken(resolvedMentionLinks, allowedTokens)
	for _, hint := range hints {
		if strings.ToLower(strings.TrimSpace(hint.Type)) != "agent" {
			continue
		}
		agentID := strings.TrimSpace(hint.ID)
		if agentID == "" {
			continue
		}
		if _, ok := seenAgentIDs[agentID]; ok {
			continue
		}
		token := strings.ToLower(strings.TrimSpace(hint.Token))
		if !taskDiscussionMentionTokenMatchesAny(token, allowedTokens) {
			continue
		}
		if canonicalAgentIDs := canonicalAgentIDsByToken[token]; len(canonicalAgentIDs) > 0 {
			if _, ok := canonicalAgentIDs[agentID]; !ok {
				continue
			}
		} else {
			if _, ok := seenHintTokens[token]; ok {
				continue
			}
			seenHintTokens[token] = struct{}{}
		}
		hintTokenByAgentID[agentID] = token
		seenAgentIDs[agentID] = struct{}{}
		agentIDs = append(agentIDs, agentID)
		if len(agentIDs) >= message.MaxPersistedMentionHints {
			break
		}
	}
	if len(agentIDs) == 0 {
		return taskDiscussionMentionHintRouting{}, nil
	}

	cte, agentArgs, _, err := coredb.BuildStringValuesCTE("mentioned_agents", "agent_id", agentIDs, 5)
	if err != nil {
		return taskDiscussionMentionHintRouting{}, fmt.Errorf("resolve task discussion mention hint targets: %w", err)
	}
	rows, err := queryer.Query(ctx, fmt.Sprintf(`
WITH %s
SELECT
    a.id,
    a.member_id
FROM mentioned_agents
JOIN agents a
  ON a.workspace_id = $1
 AND a.id = mentioned_agents.agent_id
 AND a.status <> 'archived'
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND mac.status = 'active'
JOIN scope_memberships sm
  ON sm.scope_id = $2
 AND sm.member_id = a.member_id
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
WHERE a.member_id <> $3
  AND (
      sm.visibility = 'public'
      OR (sm.visibility = 'private' AND (sm.owner_member_id = $3 OR sm.member_id = $3 OR sm.owner_member_id = $4))
  )
ORDER BY a.name ASC, a.member_id ASC
`, cte), append([]any{strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), strings.TrimSpace(actorMemberID), strings.TrimSpace(executionOwnerMemberID)}, agentArgs...)...)
	if err != nil {
		return taskDiscussionMentionHintRouting{}, fmt.Errorf("load task discussion mention hint targets: %w", err)
	}
	defer rows.Close()

	targets := make([]taskDiscussionMentionTarget, 0, len(agentIDs))
	resolvedAgentIDs := make(map[string]struct{}, len(agentIDs))
	for rows.Next() {
		var target taskDiscussionMentionTarget
		if err := rows.Scan(&target.AgentID, &target.MemberID); err != nil {
			return taskDiscussionMentionHintRouting{}, fmt.Errorf("scan task discussion mention hint target: %w", err)
		}
		target.AgentID = strings.TrimSpace(target.AgentID)
		target.MemberID = strings.TrimSpace(target.MemberID)
		if target.AgentID != "" && target.MemberID != "" {
			target.MentionToken = hintTokenByAgentID[target.AgentID]
			resolvedAgentIDs[target.AgentID] = struct{}{}
			targets = append(targets, target)
		}
	}
	if err := rows.Err(); err != nil {
		return taskDiscussionMentionHintRouting{}, fmt.Errorf("iterate task discussion mention hint targets: %w", err)
	}

	acceptedTokens := make([]string, 0, len(hints))
	seenTokens := make(map[string]struct{}, len(hints))
	for _, hint := range hints {
		if strings.ToLower(strings.TrimSpace(hint.Type)) != "agent" {
			continue
		}
		if _, ok := resolvedAgentIDs[strings.TrimSpace(hint.ID)]; !ok {
			continue
		}
		token := strings.ToLower(strings.TrimSpace(hint.Token))
		if !taskDiscussionMentionTokenMatchesAny(token, allowedTokens) {
			continue
		}
		if _, ok := seenTokens[token]; ok {
			continue
		}
		seenTokens[token] = struct{}{}
		acceptedTokens = append(acceptedTokens, token)
	}
	return taskDiscussionMentionHintRouting{targets: targets, tokens: acceptedTokens}, nil
}

func taskDiscussionMentionTokensExcluding(tokens []string, excluded []string) []string {
	if len(tokens) == 0 {
		return nil
	}
	if len(excluded) == 0 {
		return append([]string(nil), tokens...)
	}
	excludedSet := make(map[string]struct{}, len(excluded))
	for _, token := range excluded {
		normalized := strings.ToLower(strings.TrimSpace(token))
		if normalized != "" {
			excludedSet[normalized] = struct{}{}
		}
	}
	filtered := make([]string, 0, len(tokens))
	for _, token := range tokens {
		normalized := strings.ToLower(strings.TrimSpace(token))
		if normalized == "" {
			continue
		}
		if _, ok := excludedSet[normalized]; ok {
			continue
		}
		excludedByTarget := false
		for excludedToken := range excludedSet {
			if message.MentionTokenMatchesTarget(normalized, excludedToken) {
				excludedByTarget = true
				break
			}
		}
		if excludedByTarget {
			continue
		}
		filtered = append(filtered, normalized)
	}
	return filtered
}

func taskDiscussionResolvedAgentMentionIDsByToken(links []message.ResolvedMentionLink, allowedTokens []string) map[string]map[string]struct{} {
	if len(links) == 0 || len(allowedTokens) == 0 {
		return nil
	}
	byToken := make(map[string]map[string]struct{}, len(links))
	for _, link := range links {
		if strings.ToLower(strings.TrimSpace(link.Type)) != "agent" {
			continue
		}
		agentID := strings.TrimSpace(link.ID)
		token := strings.ToLower(strings.TrimSpace(link.Token))
		if agentID == "" || token == "" || !taskDiscussionMentionTokenMatchesAny(token, allowedTokens) {
			continue
		}
		if byToken[token] == nil {
			byToken[token] = make(map[string]struct{})
		}
		byToken[token][agentID] = struct{}{}
	}
	return byToken
}

func taskDiscussionResolvedMentionTokensForDirectTokens(links []message.ResolvedMentionLink, directTokens []string) []string {
	if len(links) == 0 || len(directTokens) == 0 {
		return nil
	}
	tokens := make([]string, 0, len(links))
	seen := make(map[string]struct{}, len(links))
	for _, link := range links {
		token := strings.ToLower(strings.TrimSpace(link.Token))
		if token == "" || !taskDiscussionMentionTokenMatchesAny(token, directTokens) {
			continue
		}
		if _, ok := seen[token]; ok {
			continue
		}
		seen[token] = struct{}{}
		tokens = append(tokens, token)
	}
	return tokens
}

func taskDiscussionMentionTokenMatchesAny(target string, directTokens []string) bool {
	for _, token := range directTokens {
		if message.MentionTokenMatchesTarget(token, target) {
			return true
		}
	}
	return false
}
