package taskboard

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

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

const defaultPrivateAgentContextStateWindowLimit = 100
const maxPrivateAgentContextStateWindowLimit = 200

type privateAgentContextMessage struct {
	ID                 string
	ScopeID            string
	ScopeKind          string
	Seq                int64
	WorkspaceGlobalSeq int64
	AuthorMemberID     string
	AuthorType         string
	MessageType        string
}

type privateAgentContextMessageFacts struct {
	ViewerReadableMessageIDs    map[string]struct{}
	OwnerPrivateAgentMessageIDs map[string]struct{}
	AuthorReadableMessageIDs    map[string]struct{}
}

type sourceScopeReadabilityRequest struct {
	RequestID string
	MessageID string
	MemberID  string
	ScopeID   string
}

type privateAgentContextOwnerPrivateAgentAuthorRequest struct {
	RequestID       string
	AuthorMemberID  string
	SourceScopeID   string
	SourceScopeKind string
}

func (txs TxStore) ResolvePrivateAgentContextState(ctx context.Context, request ResolvePrivateAgentContextStateRequest) (ResolvePrivateAgentContextStateResponse, error) {
	workspaceID, err := request.Context.requireWorkspaceID()
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	userID, _, err := request.Context.requireUser()
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}

	messageIDs := normalizeUniqueStringList(request.MessageIDs)
	if len(messageIDs) > maxPrivateAgentContextStateWindowLimit {
		return ResolvePrivateAgentContextStateResponse{}, badRequest("messageIds exceeds private agent context state limit")
	}
	window := normalizePrivateAgentContextStateWindow(request.Window)
	if len(messageIDs) == 0 && window == nil {
		return ResolvePrivateAgentContextStateResponse{}, badRequest("messageIds or window is required")
	}

	target, viewerMemberID, err := txs.resolveTaskCollaboratorTargetForUser(ctx, txs.db, userID, workspaceID, request.Task, request.Scope, taskReadChannelAction)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	canManage, err := txs.canManagePrivateAgentContextTarget(ctx, txs.db, workspaceID, viewerMemberID, target.ExecutionOwnerMemberID)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}

	sourceTargets, err := txs.privateAgentContextSourceTargets(ctx, txs.db, target)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	sourceScopeIDs := privateAgentContextSourceScopeIDs(sourceTargets)
	sourceScopeSet := make(map[string]struct{}, len(sourceScopeIDs))
	for _, scopeID := range sourceScopeIDs {
		sourceScopeSet[scopeID] = struct{}{}
	}

	messages, err := loadPrivateAgentContextMessages(ctx, txs.db, workspaceID, sourceScopeIDs, messageIDs, window)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}

	now := time.Now().UTC()
	people, err := txs.loadActiveTaskCollaboratorsForTargets(ctx, txs.db, sourceTargets, now, true)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	peopleByMember := privateAgentContextPeopleByMember(people)
	viewerReadableMessageIDs, err := loadSourceScopeReadableRequestIDs(ctx, txs.db, workspaceID, privateAgentContextReadabilityRequestsForMember(messages, viewerMemberID))
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	ownerPrivateAgentMessageIDs, err := txs.loadPrivateAgentContextOwnerPrivateAgentAuthorMessageIDs(ctx, txs.db, workspaceID, target, messages)
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	authorReadableMessageIDs, err := loadSourceScopeReadableRequestIDs(ctx, txs.db, workspaceID, privateAgentContextAuthorReadabilityRequests(messages, peopleByMember))
	if err != nil {
		return ResolvePrivateAgentContextStateResponse{}, err
	}
	facts := privateAgentContextMessageFacts{
		ViewerReadableMessageIDs:    viewerReadableMessageIDs,
		OwnerPrivateAgentMessageIDs: ownerPrivateAgentMessageIDs,
		AuthorReadableMessageIDs:    authorReadableMessageIDs,
	}

	states := make([]PrivateAgentContextState, 0, len(messages))
	seen := make(map[string]struct{}, len(messages))
	for _, message := range messages {
		if _, exists := seen[message.ID]; exists {
			continue
		}
		if _, sourceScoped := sourceScopeSet[strings.TrimSpace(message.ScopeID)]; !sourceScoped {
			continue
		}
		_, visible := facts.ViewerReadableMessageIDs[message.ID]
		if !visible || !storemessage.IsHumanVisibleMessageType(message.MessageType) {
			continue
		}
		state := classifyPrivateAgentContextMessage(target, message, facts, peopleByMember, canManage)
		states = append(states, state)
		seen[message.ID] = struct{}{}
	}

	return ResolvePrivateAgentContextStateResponse{States: states}, nil
}

func normalizePrivateAgentContextStateWindow(input *PrivateAgentContextStateWindowInput) *PrivateAgentContextStateWindowInput {
	if input == nil {
		return nil
	}
	limit := input.Limit
	if limit <= 0 {
		limit = defaultPrivateAgentContextStateWindowLimit
	}
	if limit > maxPrivateAgentContextStateWindowLimit {
		limit = maxPrivateAgentContextStateWindowLimit
	}
	return &PrivateAgentContextStateWindowInput{
		AfterSeq:  input.AfterSeq,
		BeforeSeq: input.BeforeSeq,
		Limit:     limit,
	}
}

func (txs TxStore) canManagePrivateAgentContextTarget(ctx context.Context, queryer rowQueryer, workspaceID string, viewerMemberID string, executionOwnerMemberID string) (bool, error) {
	if err := txs.authorizeTaskCollaboratorManager(ctx, queryer, workspaceID, viewerMemberID, executionOwnerMemberID); err != nil {
		if errors.Is(err, ErrForbidden) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (txs TxStore) privateAgentContextSourceTargets(ctx context.Context, queryer rowQueryer, target taskCollaboratorTarget) ([]taskCollaboratorTarget, error) {
	switch target.ScopeType {
	case taskCollaboratorScopeTask:
		anchor := taskCollaboratorTaskAnchor{
			WorkspaceID:            target.WorkspaceID,
			TaskID:                 target.TaskID,
			TaskScopeID:            target.TaskScopeID,
			ChannelID:              target.ChannelID,
			ExecutionOwnerMemberID: target.ExecutionOwnerMemberID,
			SourceThreadChannelID:  target.ThreadChannelID,
		}
		if strings.TrimSpace(anchor.TaskID) == "" || strings.TrimSpace(anchor.TaskScopeID) == "" || strings.TrimSpace(anchor.ChannelID) == "" {
			loaded, err := txs.loadTaskCollaboratorTaskAnchor(ctx, queryer, target.WorkspaceID, target.ScopeID)
			if err != nil {
				return nil, err
			}
			anchor = loaded
		}
		return taskEffectiveCollaboratorTargets(anchor), nil
	case taskCollaboratorScopeThread:
		targets := []taskCollaboratorTarget{target}
		parentChannelID, err := loadTaskCollaboratorThreadParentChannelID(ctx, queryer, target.WorkspaceID, target.ScopeID)
		if err != nil {
			return nil, err
		}
		if parentChannelID != "" {
			targets = append(targets, taskCollaboratorTarget{
				WorkspaceID:            target.WorkspaceID,
				ScopeType:              taskCollaboratorScopeChannel,
				ScopeID:                parentChannelID,
				ExecutionOwnerMemberID: target.ExecutionOwnerMemberID,
				ChannelID:              parentChannelID,
			})
		}
		return targets, nil
	case taskCollaboratorScopeChannel:
		return []taskCollaboratorTarget{target}, nil
	default:
		return nil, badRequest("invalid collaborator scopeType")
	}
}

func loadTaskCollaboratorThreadParentChannelID(ctx context.Context, queryer rowQueryer, workspaceID string, threadChannelID string) (string, error) {
	row := queryer.QueryRow(ctx, `
SELECT parent.type, COALESCE(s.parent_scope_id, '')
FROM scopes s
JOIN scopes parent
  ON parent.id = s.parent_scope_id
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'thread'
`, workspaceID, threadChannelID)
	var parentType string
	var parentChannelID string
	if err := row.Scan(&parentType, &parentChannelID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("load task collaborator thread parent channel: %w", err)
	}
	if parentType == string(domain.ThreadTypeChannel) {
		return strings.TrimSpace(parentChannelID), nil
	}
	return "", nil
}

func privateAgentContextSourceScopeIDs(targets []taskCollaboratorTarget) []string {
	seen := make(map[string]struct{}, len(targets))
	scopeIDs := make([]string, 0, len(targets))
	for _, target := range targets {
		candidates := []string{target.ChannelID, target.ThreadChannelID}
		if target.ScopeType == taskCollaboratorScopeTask {
			candidates = append(candidates, target.TaskScopeID)
		}
		if target.ScopeType == taskCollaboratorScopeChannel || target.ScopeType == taskCollaboratorScopeThread {
			candidates = append(candidates, target.ScopeID)
		}
		for _, candidate := range candidates {
			trimmed := strings.TrimSpace(candidate)
			if trimmed == "" {
				continue
			}
			if _, ok := seen[trimmed]; ok {
				continue
			}
			seen[trimmed] = struct{}{}
			scopeIDs = append(scopeIDs, trimmed)
		}
	}
	return scopeIDs
}

func loadPrivateAgentContextMessages(ctx context.Context, queryer coredb.Queryer, workspaceID string, sourceScopeIDs []string, messageIDs []string, window *PrivateAgentContextStateWindowInput) ([]privateAgentContextMessage, error) {
	messages := make([]privateAgentContextMessage, 0, len(messageIDs))
	seen := make(map[string]struct{}, len(messageIDs))
	if len(messageIDs) > 0 {
		requestedMessagesCTE, requestedMessageArgs, _, err := coredb.BuildIndexedStringValuesCTE("requested", "message_id", "ordinality", messageIDs, 2)
		if err != nil {
			return nil, err
		}
		query := fmt.Sprintf(`
WITH %s
SELECT
    sm.id,
    sm.scope_id,
    COALESCE(NULLIF(source_scope.type, ''), ''),
    sm.seq,
    sm.workspace_global_seq,
    sm.author_member_id,
    sm.author_type,
    sm.message_type
FROM requested
JOIN scope_messages sm
  ON sm.id = requested.message_id
LEFT JOIN scopes source_scope
  ON source_scope.id = sm.scope_id
WHERE sm.workspace_id = $1
ORDER BY requested.ordinality ASC
`, requestedMessagesCTE)
		args := append([]any{strings.TrimSpace(workspaceID)}, requestedMessageArgs...)
		loaded, err := scanPrivateAgentContextMessages(ctx, queryer, query, args...)
		if err != nil {
			return nil, err
		}
		for _, message := range loaded {
			if _, ok := seen[message.ID]; ok {
				continue
			}
			seen[message.ID] = struct{}{}
			messages = append(messages, message)
		}
	}
	if window != nil {
		windowMessages, err := loadPrivateAgentContextWindowMessages(ctx, queryer, workspaceID, sourceScopeIDs, window)
		if err != nil {
			return nil, err
		}
		for _, message := range windowMessages {
			if _, ok := seen[message.ID]; ok {
				continue
			}
			seen[message.ID] = struct{}{}
			messages = append(messages, message)
		}
	}
	return messages, nil
}

func loadPrivateAgentContextWindowMessages(ctx context.Context, queryer coredb.Queryer, workspaceID string, sourceScopeIDs []string, window *PrivateAgentContextStateWindowInput) ([]privateAgentContextMessage, error) {
	scopeIDs := normalizeUniqueStringList(sourceScopeIDs)
	if len(scopeIDs) == 0 {
		return []privateAgentContextMessage{}, nil
	}
	args := []any{strings.TrimSpace(workspaceID)}
	scopeClauses := make([]string, 0, len(scopeIDs))
	for _, scopeID := range scopeIDs {
		scopeClauses = append(scopeClauses, fmt.Sprintf("sm.scope_id = $%d", len(args)+1))
		args = append(args, scopeID)
	}
	query := `
SELECT
    sm.id,
    sm.scope_id,
    COALESCE(NULLIF(source_scope.type, ''), ''),
    sm.seq,
    sm.workspace_global_seq,
    sm.author_member_id,
    sm.author_type,
    sm.message_type
FROM scope_messages sm
LEFT JOIN scopes source_scope
  ON source_scope.id = sm.scope_id
WHERE sm.workspace_id = $1
  AND (` + strings.Join(scopeClauses, " OR ") + `)
`
	if window.AfterSeq != nil {
		query += fmt.Sprintf("  AND sm.workspace_global_seq > $%d\n", len(args)+1)
		args = append(args, *window.AfterSeq)
	}
	if window.BeforeSeq != nil {
		query += fmt.Sprintf("  AND sm.workspace_global_seq < $%d\n", len(args)+1)
		args = append(args, *window.BeforeSeq)
	}
	query += fmt.Sprintf("ORDER BY sm.workspace_global_seq ASC, sm.seq ASC\nLIMIT $%d\n", len(args)+1)
	args = append(args, window.Limit)
	return scanPrivateAgentContextMessages(ctx, queryer, query, args...)
}

func scanPrivateAgentContextMessages(ctx context.Context, queryer coredb.Queryer, query string, args ...any) ([]privateAgentContextMessage, error) {
	rows, err := queryer.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("query private agent context messages: %w", err)
	}
	defer rows.Close()
	messages := make([]privateAgentContextMessage, 0)
	for rows.Next() {
		var message privateAgentContextMessage
		if err := rows.Scan(&message.ID, &message.ScopeID, &message.ScopeKind, &message.Seq, &message.WorkspaceGlobalSeq, &message.AuthorMemberID, &message.AuthorType, &message.MessageType); err != nil {
			return nil, fmt.Errorf("scan private agent context message: %w", err)
		}
		messages = append(messages, message)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate private agent context messages: %w", err)
	}
	return messages, nil
}

func privateAgentContextReadabilityRequestsForMember(messages []privateAgentContextMessage, memberID string) []sourceScopeReadabilityRequest {
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedMemberID == "" || len(messages) == 0 {
		return nil
	}
	requests := make([]sourceScopeReadabilityRequest, 0, len(messages))
	for _, message := range messages {
		requests = append(requests, sourceScopeReadabilityRequest{
			RequestID: message.ID,
			MessageID: message.ID,
			MemberID:  trimmedMemberID,
			ScopeID:   message.ScopeID,
		})
	}
	return requests
}

func privateAgentContextAuthorReadabilityRequests(messages []privateAgentContextMessage, peopleByMember map[string]TaskCollaborator) []sourceScopeReadabilityRequest {
	if len(messages) == 0 || len(peopleByMember) == 0 {
		return nil
	}
	requests := make([]sourceScopeReadabilityRequest, 0, len(messages))
	for _, message := range messages {
		authorMemberID := strings.TrimSpace(message.AuthorMemberID)
		if strings.TrimSpace(message.AuthorType) != memberTypeHuman || authorMemberID == "" {
			continue
		}
		if _, ok := peopleByMember[authorMemberID]; !ok {
			continue
		}
		requests = append(requests, sourceScopeReadabilityRequest{
			RequestID: message.ID,
			MessageID: message.ID,
			MemberID:  authorMemberID,
			ScopeID:   message.ScopeID,
		})
	}
	return requests
}

func loadSourceScopeReadableRequestIDs(ctx context.Context, queryer coredb.Queryer, workspaceID string, requests []sourceScopeReadabilityRequest) (map[string]struct{}, error) {
	normalizedRequests := normalizeSourceScopeReadabilityRequests(requests)
	if len(normalizedRequests) == 0 {
		return map[string]struct{}{}, nil
	}
	items := make([]storemessage.SidecarReadBatchItem, 0, len(normalizedRequests))
	for _, request := range normalizedRequests {
		items = append(items, storemessage.SidecarReadBatchItem{
			RequestID:      strings.TrimSpace(request.RequestID),
			MessageID:      strings.TrimSpace(request.MessageID),
			ViewerMemberID: strings.TrimSpace(request.MemberID),
		})
	}
	decisions, err := storemessage.NewScopeAPIServiceForQueryer(queryer).AuthorizeSidecarReadBatch(ctx, storemessage.SidecarReadBatchRequest{
		WorkspaceID: strings.TrimSpace(workspaceID),
		Kind:        storemessage.SidecarKindSourceRef,
		Items:       items,
	})
	if err != nil {
		return nil, fmt.Errorf("authorize source-ref sidecar reads: %w", err)
	}
	readable := make(map[string]struct{}, len(normalizedRequests))
	for requestID, decision := range decisions {
		if decision.Allowed && strings.TrimSpace(requestID) != "" {
			readable[strings.TrimSpace(requestID)] = struct{}{}
		}
	}
	return readable, nil
}

func normalizeSourceScopeReadabilityRequests(requests []sourceScopeReadabilityRequest) []sourceScopeReadabilityRequest {
	if len(requests) == 0 {
		return nil
	}
	normalized := make([]sourceScopeReadabilityRequest, 0, len(requests))
	seen := make(map[string]struct{}, len(requests))
	for _, request := range requests {
		candidate := sourceScopeReadabilityRequest{
			RequestID: strings.TrimSpace(request.RequestID),
			MessageID: strings.TrimSpace(request.MessageID),
			MemberID:  strings.TrimSpace(request.MemberID),
			ScopeID:   strings.TrimSpace(request.ScopeID),
		}
		if candidate.RequestID == "" || candidate.MessageID == "" || candidate.MemberID == "" {
			continue
		}
		key := strings.Join([]string{candidate.RequestID, candidate.MessageID, candidate.MemberID}, "\x00")
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, candidate)
	}
	return normalized
}

func privateAgentContextPeopleByMember(rows []TaskCollaborator) map[string]TaskCollaborator {
	privateAgentContextSortPeople(rows)
	byMember := make(map[string]TaskCollaborator, len(rows))
	for _, row := range rows {
		if strings.TrimSpace(row.MemberID) == "" {
			continue
		}
		if _, exists := byMember[row.MemberID]; exists {
			continue
		}
		byMember[row.MemberID] = row
	}
	return byMember
}

func privateAgentContextSortPeople(rows []TaskCollaborator) {
	sort.SliceStable(rows, func(i, j int) bool {
		leftRank := collaboratorScopeRank(rows[i].ScopeType)
		rightRank := collaboratorScopeRank(rows[j].ScopeType)
		if leftRank != rightRank {
			return leftRank < rightRank
		}
		if rows[i].AddedAt.Equal(rows[j].AddedAt) {
			return rows[i].ID < rows[j].ID
		}
		return rows[i].AddedAt.Before(rows[j].AddedAt)
	})
}

func classifyPrivateAgentContextMessage(target taskCollaboratorTarget, message privateAgentContextMessage, facts privateAgentContextMessageFacts, peopleByMember map[string]TaskCollaborator, canManage bool) PrivateAgentContextState {
	state := basePrivateAgentContextState(target, message.ID)
	if strings.TrimSpace(message.AuthorMemberID) == strings.TrimSpace(target.ExecutionOwnerMemberID) {
		state.State = privateAgentContextStateEligible
		state.Relation = &PrivateAgentContextRelationSource{Kind: "owner", ScopeType: target.ScopeType, ScopeID: target.ScopeID}
		state.CanWake = canManage
		return state
	}
	if strings.TrimSpace(message.AuthorType) == memberTypeAgent {
		if _, isOwnerPrivateAgent := facts.OwnerPrivateAgentMessageIDs[message.ID]; isOwnerPrivateAgent {
			state.State = privateAgentContextStateEligible
			state.Relation = &PrivateAgentContextRelationSource{Kind: "owner", ScopeType: target.ScopeType, ScopeID: target.ScopeID, RelationID: message.AuthorMemberID}
			state.CanWake = canManage
			return state
		}
	}
	if strings.TrimSpace(message.AuthorType) == memberTypeHuman {
		if row, ok := peopleByMember[strings.TrimSpace(message.AuthorMemberID)]; ok {
			if _, canRead := facts.AuthorReadableMessageIDs[message.ID]; canRead {
				state.State = privateAgentContextStateEligible
				state.Relation = &PrivateAgentContextRelationSource{Kind: "people", ScopeType: row.ScopeType, ScopeID: row.ScopeID, RelationID: row.ID}
				state.CanWake = canManage
				return state
			}
		}
	}
	state.State = privateAgentContextStateShareNeeded
	state.CanShare = canManage
	state.CanWake = false
	return state
}

func (txs TxStore) loadPrivateAgentContextOwnerPrivateAgentAuthorMessageIDs(ctx context.Context, queryer coredb.Queryer, workspaceID string, target taskCollaboratorTarget, messages []privateAgentContextMessage) (map[string]struct{}, error) {
	requests := privateAgentContextOwnerPrivateAgentAuthorRequests(messages)
	if len(requests) == 0 {
		return map[string]struct{}{}, nil
	}
	requestsCTE, requestArgs, err := buildPrivateAgentContextOwnerPrivateAgentAuthorRequestsCTE("requested_owner_agent_authors", requests, 3)
	if err != nil {
		return nil, err
	}
	query := fmt.Sprintf(`
WITH %s
SELECT DISTINCT requested_owner_agent_authors.request_id
FROM requested_owner_agent_authors
JOIN scopes source_scope
  ON source_scope.workspace_id = $1
 AND source_scope.id = requested_owner_agent_authors.source_scope_id
 AND source_scope.status = 'active'
 AND source_scope.archived_at IS NULL
LEFT JOIN tasks task_source
  ON source_scope.type = 'task'
 AND task_source.task_scope_id = source_scope.id
LEFT JOIN task_boards task_board
  ON task_board.id = task_source.board_id
 AND task_board.workspace_id = $1
 AND task_source.channel_id = task_board.channel_id
LEFT JOIN scopes task_channel
  ON task_channel.id = task_board.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
JOIN scope_memberships sm
  ON sm.member_id = requested_owner_agent_authors.author_member_id
 AND sm.owner_member_id = $2
 AND sm.visibility = 'private'
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
 AND (
      sm.scope_id = source_scope.id
      OR (source_scope.type = 'thread' AND sm.scope_id = source_scope.parent_scope_id)
      OR (source_scope.type = 'task' AND sm.scope_id = task_board.channel_id)
 )
`, requestsCTE)
	args := append([]any{strings.TrimSpace(workspaceID), strings.TrimSpace(target.ExecutionOwnerMemberID)}, requestArgs...)
	rows, err := queryer.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("load private agent context owner agent authors: %w", err)
	}
	defer rows.Close()

	messageIDs := make(map[string]struct{}, len(requests))
	for rows.Next() {
		var messageID string
		if err := rows.Scan(&messageID); err != nil {
			return nil, fmt.Errorf("scan private agent context owner agent author: %w", err)
		}
		if trimmed := strings.TrimSpace(messageID); trimmed != "" {
			messageIDs[trimmed] = struct{}{}
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate private agent context owner agent authors: %w", err)
	}
	return messageIDs, nil
}

func privateAgentContextOwnerPrivateAgentAuthorRequests(messages []privateAgentContextMessage) []privateAgentContextOwnerPrivateAgentAuthorRequest {
	if len(messages) == 0 {
		return nil
	}
	requests := make([]privateAgentContextOwnerPrivateAgentAuthorRequest, 0, len(messages))
	seen := make(map[string]struct{}, len(messages))
	for _, message := range messages {
		request := privateAgentContextOwnerPrivateAgentAuthorRequest{
			RequestID:       strings.TrimSpace(message.ID),
			AuthorMemberID:  strings.TrimSpace(message.AuthorMemberID),
			SourceScopeID:   strings.TrimSpace(message.ScopeID),
			SourceScopeKind: strings.TrimSpace(message.ScopeKind),
		}
		if strings.TrimSpace(message.AuthorType) != memberTypeAgent || request.RequestID == "" || request.AuthorMemberID == "" || request.SourceScopeID == "" {
			continue
		}
		key := strings.Join([]string{request.RequestID, request.AuthorMemberID, request.SourceScopeID, request.SourceScopeKind}, "\x00")
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		requests = append(requests, request)
	}
	return requests
}

func buildPrivateAgentContextOwnerPrivateAgentAuthorRequestsCTE(name string, requests []privateAgentContextOwnerPrivateAgentAuthorRequest, startParam int) (string, []any, error) {
	trimmedName := strings.TrimSpace(name)
	if trimmedName == "" {
		return "", nil, fmt.Errorf("cte name is required")
	}
	if len(requests) == 0 {
		return "", nil, fmt.Errorf("owner private-agent author requests are empty")
	}
	var builder strings.Builder
	builder.WriteString(trimmedName)
	builder.WriteString(" AS (\n")
	args := make([]any, 0, len(requests)*4)
	param := startParam
	for index, request := range requests {
		if index > 0 {
			builder.WriteString("\n    UNION ALL\n")
		}
		builder.WriteString(fmt.Sprintf("    SELECT $%d AS request_id, $%d AS author_member_id, $%d AS source_scope_id, $%d AS source_scope_kind", param, param+1, param+2, param+3))
		args = append(args, request.RequestID, request.AuthorMemberID, request.SourceScopeID, request.SourceScopeKind)
		param += 4
	}
	builder.WriteString("\n)")
	return builder.String(), args, nil
}

func basePrivateAgentContextState(target taskCollaboratorTarget, messageID string) PrivateAgentContextState {
	return PrivateAgentContextState{
		MessageID:       strings.TrimSpace(messageID),
		TargetScopeType: target.ScopeType,
		TargetScopeID:   target.ScopeID,
		Relation:        nil,
		CanShare:        false,
		CanWake:         false,
	}
}
