package taskboard

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

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

const (
	taskCollaboratorScopeTask    = "task"
	taskCollaboratorScopeThread  = "thread"
	taskCollaboratorScopeChannel = "channel"

	taskCollaboratorRoleContributor = "contributor"

	taskCollaboratorNotificationStatusNone    = "none"
	taskCollaboratorNotificationStatusPending = "pending"
	taskCollaboratorNotificationStatusSent    = "sent"
	taskCollaboratorNotificationStatusFailed  = "failed"

	taskDiscussionAuthorizationOwner        = "owner"
	taskDiscussionAuthorizationAgent        = "agent"
	taskDiscussionAuthorizationCollaborator = "collaborator"
)

var taskReadThreadAction = access.HumanAction{
	Resource: access.ResourceThread,
	Action:   access.ActionRead,
}

type taskCollaboratorTarget struct {
	WorkspaceID            string
	ScopeType              string
	ScopeID                string
	ExecutionOwnerMemberID string
	TaskID                 string
	TaskScopeID            string
	ChannelID              string
	ThreadChannelID        string
}

type taskScopeExecution struct {
	ID            string
	WorkspaceID   string
	OwnerMemberID string
	ScopeType     string
	ScopeID       string
}

type taskCollaboratorTaskAnchor struct {
	WorkspaceID            string
	TaskID                 string
	TaskScopeID            string
	ChannelID              string
	ExecutionOwnerMemberID string
	SourceThreadChannelID  string
}

type agentCollaboratorSourceKey struct {
	ScopeType string
	ScopeID   string
}

type agentCollaboratorSourceDisplay struct {
	ScopeDisplayName   string
	ChannelID          string
	ChannelDisplayName string
}

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

	target, _, err := txs.resolveTaskCollaboratorTargetForUser(ctx, txs.db, userID, workspaceID, request.Task, request.Scope, taskReadChannelAction)
	if err != nil {
		return ListTaskCollaboratorsResponse{}, err
	}
	// Carry the authoritative server-derived execution owner with the list so a
	// non-owner viewer can see (and request access from) the real owner without
	// supplying an owner query parameter. Empty collaborator lists still resolve
	// the owner, so the request entry point never fails closed on an empty list.
	ownerProfile, err := loadTaskPrincipalProfileByMemberID(ctx, txs.db, workspaceID, target.ExecutionOwnerMemberID)
	if err != nil {
		return ListTaskCollaboratorsResponse{}, err
	}
	owner := taskActorCompatFromPrincipalProfile(ownerProfile, nil)
	if owner == nil {
		return ListTaskCollaboratorsResponse{}, ErrNotFound
	}
	collaborators, err := txs.loadTaskCollaboratorsForScope(ctx, txs.db, target, request.IncludeInactive, time.Now().UTC())
	if err != nil {
		return ListTaskCollaboratorsResponse{}, err
	}
	return ListTaskCollaboratorsResponse{
		ExecutionOwnerMemberID: target.ExecutionOwnerMemberID,
		OwnerProfile:           ownerProfile,
		Owner:                  *owner,
		Collaborators:          collaborators,
	}, nil
}

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

	ownerMemberID, err := loadTaskboardWorkspaceMemberIDForUser(ctx, txs.db, workspaceID, userID)
	if err != nil {
		return ListAgentCollaboratorsResponse{}, err
	}
	ownerProfile, err := loadTaskPrincipalProfileByMemberID(ctx, txs.db, workspaceID, ownerMemberID)
	if err != nil {
		return ListAgentCollaboratorsResponse{}, err
	}
	owner := taskActorCompatFromPrincipalProfile(ownerProfile, nil)
	if owner == nil {
		return ListAgentCollaboratorsResponse{}, ErrNotFound
	}

	scopeType := strings.TrimSpace(request.SourceScopeType)
	scopeID := strings.TrimSpace(request.SourceScopeID)
	if scopeType != "" {
		normalizedScopeType, err := normalizeTaskCollaboratorScope(scopeType)
		if err != nil {
			return ListAgentCollaboratorsResponse{}, err
		}
		scopeType = normalizedScopeType
		if scopeID == "" {
			return ListAgentCollaboratorsResponse{}, badRequest("sourceScopeId is required")
		}
		if err := txs.requireAgentCollaboratorSourceFilterAccess(ctx, txs.db, userID, workspaceID, ownerMemberID, scopeType, scopeID); err != nil {
			return ListAgentCollaboratorsResponse{}, err
		}
	} else if scopeID != "" {
		return ListAgentCollaboratorsResponse{}, badRequest("sourceScopeType is required")
	}

	now := time.Now().UTC()
	rows, err := txs.listAgentCollaboratorRows(ctx, txs.db, workspaceID, ownerMemberID, scopeType, scopeID, request.IncludeInactive, now)
	if err != nil {
		return ListAgentCollaboratorsResponse{}, err
	}
	sourceDisplays, err := loadAgentCollaboratorSourceDisplays(ctx, txs.db, workspaceID, rows, scopeType, scopeID)
	if err != nil {
		return ListAgentCollaboratorsResponse{}, err
	}

	byMember := make(map[string]*AgentCollaboratorSummary)
	for _, row := range rows {
		sourceDisplay := sourceDisplays[newAgentCollaboratorSourceKey(row.ScopeType, row.ScopeID)]
		summary := byMember[row.MemberID]
		if summary == nil {
			summary = &AgentCollaboratorSummary{
				MemberID:      row.MemberID,
				MemberProfile: row.MemberProfile,
				Member:        row.Member,
				Sources:       []AgentCollaboratorSource{},
			}
			byMember[row.MemberID] = summary
		}
		summary.ScopeCount++
		if row.ExpiresAt != nil && row.ExpiresAt.After(now) && row.ExpiresAt.Before(now.Add(7*24*time.Hour)) {
			summary.SoonExpiringCount++
		}
		summary.Sources = append(summary.Sources, AgentCollaboratorSource{
			ScopeType:          row.ScopeType,
			ScopeID:            row.ScopeID,
			ScopeDisplayName:   sourceDisplay.ScopeDisplayName,
			ChannelID:          sourceDisplay.ChannelID,
			ChannelDisplayName: sourceDisplay.ChannelDisplayName,
			RelationID:         row.ID,
			Role:               row.Role,
			ExpiresAt:          row.ExpiresAt,
			Active:             row.Active,
		})
	}

	collaborators := make([]AgentCollaboratorSummary, 0, len(byMember))
	for _, summary := range byMember {
		sort.Slice(summary.Sources, func(i, j int) bool {
			if summary.Sources[i].ScopeType == summary.Sources[j].ScopeType {
				return summary.Sources[i].RelationID < summary.Sources[j].RelationID
			}
			return collaboratorScopeRank(summary.Sources[i].ScopeType) < collaboratorScopeRank(summary.Sources[j].ScopeType)
		})
		collaborators = append(collaborators, *summary)
	}
	sort.Slice(collaborators, func(i, j int) bool {
		leftName := taskCollaboratorProfileDisplayName(collaborators[i].MemberProfile, collaborators[i].Member.Name)
		rightName := taskCollaboratorProfileDisplayName(collaborators[j].MemberProfile, collaborators[j].Member.Name)
		if leftName == rightName {
			return collaborators[i].MemberID < collaborators[j].MemberID
		}
		return leftName < rightName
	})
	if request.Limit > 0 && len(collaborators) > request.Limit {
		collaborators = collaborators[:request.Limit]
	}

	filterDisplay := sourceDisplays[newAgentCollaboratorSourceKey(scopeType, scopeID)]
	return ListAgentCollaboratorsResponse{
		ExecutionOwnerMemberID:   ownerMemberID,
		OwnerProfile:             ownerProfile,
		Owner:                    *owner,
		SourceScopeDisplayName:   filterDisplay.ScopeDisplayName,
		SourceChannelDisplayName: filterDisplay.ChannelDisplayName,
		People:                   collaborators,
		Collaborators:            collaborators,
	}, nil
}

func (txs TxStore) requireAgentCollaboratorSourceFilterAccess(ctx context.Context, queryer coredb.QueryExecer, userID string, workspaceID string, ownerMemberID string, scopeType string, scopeID string) error {
	taskRef := TaskRef{}
	scopeRef := TaskCollaboratorScopeRef{
		ScopeType:              scopeType,
		ScopeID:                scopeID,
		ExecutionOwnerMemberID: ownerMemberID,
	}
	if scopeType == taskCollaboratorScopeTask {
		taskRef = TaskRef{TaskID: scopeID}
	}
	target, err := txs.resolveTaskCollaboratorTarget(ctx, queryer, workspaceID, taskRef, scopeRef)
	if err != nil {
		return err
	}
	return txs.authorizeTaskCollaboratorScopeAccess(ctx, queryer, userID, workspaceID, target, taskReadChannelAction)
}

func (txs TxStore) listAgentCollaboratorRows(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, ownerMemberID string, scopeType string, scopeID string, includeInactive bool, now time.Time) ([]TaskCollaborator, error) {
	query := taskCollaboratorSelectQuery() + `
WHERE tse.workspace_id = $1
  AND tse.owner_member_id = $2
`
	args := []any{workspaceID, ownerMemberID}
	if scopeType != "" {
		query += fmt.Sprintf("  AND tse.scope_type = $%d\n  AND tse.scope_id = $%d\n", len(args)+1, len(args)+2)
		args = append(args, scopeType, scopeID)
	}
	if !includeInactive {
		query += "  AND tc.removed_at IS NULL\n  AND tc.left_at IS NULL\n  AND (tc.expires_at IS NULL OR tc.expires_at > $" + fmt.Sprint(len(args)+1) + ")\n" + activeTaskCollaboratorMemberAvailabilitySQL()
		args = append(args, now)
	}
	query += "ORDER BY collaborator_member.display_name ASC, collaborator_member.id ASC, tc.added_at ASC, tc.id ASC\n"
	return scanTaskCollaboratorRows(ctx, queryer, query, now, args...)
}

func newAgentCollaboratorSourceKey(scopeType string, scopeID string) agentCollaboratorSourceKey {
	return agentCollaboratorSourceKey{ScopeType: strings.TrimSpace(scopeType), ScopeID: strings.TrimSpace(scopeID)}
}

func loadAgentCollaboratorSourceDisplays(ctx context.Context, queryer coredb.Queryer, workspaceID string, collaborators []TaskCollaborator, sourceScopeType string, sourceScopeID string) (map[agentCollaboratorSourceKey]agentCollaboratorSourceDisplay, error) {
	result := make(map[agentCollaboratorSourceKey]agentCollaboratorSourceDisplay)
	scopeTypes := make([]string, 0, len(collaborators)+1)
	scopeIDs := make([]string, 0, len(collaborators)+1)
	seen := make(map[agentCollaboratorSourceKey]struct{}, len(collaborators)+1)
	appendSource := func(scopeType string, scopeID string) {
		key := newAgentCollaboratorSourceKey(scopeType, scopeID)
		if key.ScopeType == "" || key.ScopeID == "" {
			return
		}
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		scopeTypes = append(scopeTypes, key.ScopeType)
		scopeIDs = append(scopeIDs, key.ScopeID)
	}
	for _, collaborator := range collaborators {
		appendSource(collaborator.ScopeType, collaborator.ScopeID)
	}
	appendSource(sourceScopeType, sourceScopeID)
	if len(scopeTypes) == 0 {
		return result, nil
	}

	requestedSourcesCTE, requestedSourcesArgs, _, err := coredb.BuildStringPairsCTE("requested_sources", "scope_type", "scope_id", scopeTypes, scopeIDs, 2)
	if err != nil {
		return nil, err
	}
	query := fmt.Sprintf(`
WITH %s
SELECT
    requested.scope_type,
    requested.scope_id,
    COALESCE(
      CASE
        WHEN requested.scope_type = 'task' AND task_source.id IS NOT NULL AND task_board.id IS NOT NULL THEN CONCAT(
          '#t',
          task_source.task_number,
          CASE
            WHEN NULLIF(TRIM(task_source.title), '') IS NULL THEN ''
            ELSE CONCAT(' ', task_source.title)
          END
        )
        WHEN requested.scope_type IN ('channel', 'thread') THEN COALESCE(
          NULLIF(source_profile.display_name, ''),
          NULLIF(source_profile.slug, ''),
          NULLIF(root_message.content, ''),
          CASE
            WHEN source_scope.type = 'thread' THEN CONCAT('thread-', COALESCE(source_scope.root_message_short_id, source_scope.id))
            ELSE ''
          END
        )
        ELSE ''
      END,
      ''
    ) AS scope_display_name,
    COALESCE(
      CASE
        WHEN requested.scope_type = 'channel' THEN source_scope.id
        WHEN requested.scope_type = 'thread' THEN source_scope.parent_scope_id
        WHEN requested.scope_type = 'task' THEN task_board.channel_id
        ELSE ''
      END,
      ''
    ) AS channel_id,
    COALESCE(NULLIF(channel_profile.display_name, ''), NULLIF(channel_profile.slug, ''), '') AS channel_display_name
FROM requested_sources requested
LEFT JOIN scopes source_scope
  ON source_scope.workspace_id = $1
 AND source_scope.id = requested.scope_id
 AND source_scope.type IN ('channel', 'thread')
LEFT JOIN scope_channel_profiles source_profile
  ON source_profile.workspace_id = $1
 AND source_profile.scope_id = source_scope.id
LEFT JOIN scope_messages root_message
  ON root_message.workspace_id = $1
 AND root_message.scope_id = source_scope.parent_scope_id
 AND root_message.id = source_scope.root_message_id
LEFT JOIN tasks task_source
  ON requested.scope_type = 'task'
 AND task_source.id = requested.scope_id
LEFT JOIN task_boards task_board
  ON task_board.id = task_source.board_id
 AND task_board.workspace_id = $1
LEFT JOIN scopes task_scope
  ON requested.scope_type = 'task'
 AND task_scope.id = task_source.task_scope_id
 AND task_scope.workspace_id = $1
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
LEFT JOIN scopes task_channel_scope
  ON requested.scope_type = 'task'
 AND task_channel_scope.id = task_board.channel_id
 AND task_channel_scope.workspace_id = $1
 AND task_channel_scope.type = 'channel'
 AND task_channel_scope.status = 'active'
 AND task_channel_scope.archived_at IS NULL
 AND task_source.channel_id = task_board.channel_id
LEFT JOIN scope_channel_profiles channel_profile
  ON channel_profile.workspace_id = $1
 AND (
      (requested.scope_type = 'channel' AND channel_profile.scope_id = source_scope.id)
   OR (requested.scope_type = 'thread' AND channel_profile.scope_id = source_scope.parent_scope_id)
   OR (requested.scope_type = 'task' AND channel_profile.scope_id = task_board.channel_id)
 )
`, requestedSourcesCTE)
	args := append([]any{workspaceID}, requestedSourcesArgs...)
	rows, err := queryer.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("query agent collaborator source displays: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var scopeType string
		var scopeID string
		var display agentCollaboratorSourceDisplay
		if err := rows.Scan(&scopeType, &scopeID, &display.ScopeDisplayName, &display.ChannelID, &display.ChannelDisplayName); err != nil {
			return nil, fmt.Errorf("scan agent collaborator source display: %w", err)
		}
		result[newAgentCollaboratorSourceKey(scopeType, scopeID)] = display
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate agent collaborator source displays: %w", err)
	}
	return result, nil
}

func (txs TxStore) AddTaskCollaborator(ctx context.Context, request AddTaskCollaboratorRequest) (AddTaskCollaboratorResponse, error) {
	workspaceID, err := request.Context.requireWorkspaceID()
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	userID, _, err := request.Context.requireUser()
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	memberID := strings.TrimSpace(request.Collaborator.MemberID)
	if memberID == "" {
		return AddTaskCollaboratorResponse{}, badRequest("memberId is required")
	}
	role, err := normalizeTaskCollaboratorRole(request.Collaborator.Role)
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	now := time.Now().UTC()
	if request.Collaborator.ExpiresAt != nil && !request.Collaborator.ExpiresAt.After(now) {
		return AddTaskCollaboratorResponse{}, badRequest("expiresAt must be in the future")
	}

	tx := txs.tx
	target, actorMemberID, err := txs.resolveTaskCollaboratorTargetForUser(ctx, tx, userID, workspaceID, request.Task, request.Scope, taskWriteChannelAction)
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	if err := txs.lockTaskScopeCollaboratorAuthority(ctx, tx, workspaceID, target, humanTaskFencerFor(userID, workspaceID, taskWriteChannelAction)); err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	if err := txs.authorizeTaskCollaboratorManager(ctx, tx, workspaceID, actorMemberID, target.ExecutionOwnerMemberID); err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	if memberID == target.ExecutionOwnerMemberID {
		return AddTaskCollaboratorResponse{}, badRequest("execution owner is an implicit collaborator")
	}
	collaboratorProfile, err := loadActiveTaskCollaboratorProfile(ctx, tx, workspaceID, memberID)
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	if collaboratorProfile.ActorType == message.AuthorActorTypeService {
		if err := txs.authorizeAppServiceTaskCollaboratorForUser(
			ctx, tx, userID, workspaceID, actorMemberID, memberID,
		); err != nil {
			return AddTaskCollaboratorResponse{}, err
		}
	} else {
		if err := txs.ensureMemberCanReadTaskCollaboratorScope(ctx, tx, workspaceID, target, memberID); err != nil {
			return AddTaskCollaboratorResponse{}, err
		}
	}
	collaborator, _, err := txs.grantTaskCollaboratorInTx(
		ctx, tx, workspaceID, target, memberID, role, request.Collaborator.ExpiresAt, actorMemberID, now,
	)
	if err != nil {
		return AddTaskCollaboratorResponse{}, err
	}
	return AddTaskCollaboratorResponse{Collaborator: collaborator}, nil
}

func (txs TxStore) RemoveTaskCollaborator(ctx context.Context, request RemoveTaskCollaboratorRequest) (RemoveTaskCollaboratorResponse, error) {
	workspaceID, err := request.Context.requireWorkspaceID()
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	userID, _, err := request.Context.requireUser()
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	memberID := strings.TrimSpace(request.MemberID)
	if memberID == "" {
		return RemoveTaskCollaboratorResponse{}, badRequest("memberId is required")
	}

	tx := txs.tx
	target, actorMemberID, err := txs.resolveTaskCollaboratorTargetForUser(ctx, tx, userID, workspaceID, request.Task, request.Scope, taskWriteChannelAction)
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	if err := txs.lockTaskScopeCollaboratorAuthority(ctx, tx, workspaceID, target, humanTaskFencerFor(userID, workspaceID, taskWriteChannelAction)); err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	if err := txs.authorizeTaskCollaboratorManager(ctx, tx, workspaceID, actorMemberID, target.ExecutionOwnerMemberID); err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	if memberID == target.ExecutionOwnerMemberID {
		return RemoveTaskCollaboratorResponse{}, badRequest("execution owner is an implicit collaborator")
	}

	now := time.Now().UTC()
	execution, err := txs.loadTaskScopeExecution(ctx, tx, target)
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	if err := cleanupExpiredTaskScopeParticipants(ctx, tx, []taskScopeExecution{execution}, now); err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	collaboratorID, err := lockActiveTaskCollaboratorID(ctx, tx, execution.ID, memberID, now)
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	if collaboratorID == "" {
		return RemoveTaskCollaboratorResponse{}, ErrNotFound
	}
	if _, err := tx.Exec(ctx, `
UPDATE task_scope_participant_metadata
SET removed_at = $2,
    removed_by_member_id = $3,
    active_key = id,
    updated_at = $2
WHERE id = $1
`, collaboratorID, now, actorMemberID); err != nil {
		return RemoveTaskCollaboratorResponse{}, fmt.Errorf("remove task collaborator: %w", err)
	}
	collaborator, err := loadTaskCollaboratorByID(ctx, tx, collaboratorID, now)
	if err != nil {
		return RemoveTaskCollaboratorResponse{}, err
	}
	return RemoveTaskCollaboratorResponse{Collaborator: collaborator}, nil
}

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

	tx := txs.tx
	target, actorMemberID, err := txs.resolveTaskCollaboratorTargetForUser(ctx, tx, userID, workspaceID, request.Task, request.Scope, taskReadChannelAction)
	if err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	if err := txs.lockTaskScopeCollaboratorAuthority(ctx, tx, workspaceID, target, humanTaskFencerFor(userID, workspaceID, taskWriteChannelAction)); err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	if actorMemberID == target.ExecutionOwnerMemberID {
		return LeaveTaskCollaboratorResponse{}, badRequest("execution owner is an implicit collaborator")
	}
	now := time.Now().UTC()
	execution, err := txs.loadTaskScopeExecution(ctx, tx, target)
	if err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	if err := cleanupExpiredTaskScopeParticipants(ctx, tx, []taskScopeExecution{execution}, now); err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	collaboratorID, err := lockActiveTaskCollaboratorID(ctx, tx, execution.ID, actorMemberID, now)
	if err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	if collaboratorID == "" {
		return LeaveTaskCollaboratorResponse{}, ErrNotFound
	}
	if _, err := tx.Exec(ctx, `
UPDATE task_scope_participant_metadata
SET left_at = $2,
    active_key = id,
    updated_at = $2
WHERE id = $1
`, collaboratorID, now); err != nil {
		return LeaveTaskCollaboratorResponse{}, fmt.Errorf("leave task collaborator: %w", err)
	}
	collaborator, err := loadTaskCollaboratorByID(ctx, tx, collaboratorID, now)
	if err != nil {
		return LeaveTaskCollaboratorResponse{}, err
	}
	return LeaveTaskCollaboratorResponse{Collaborator: collaborator}, nil
}

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

	target, _, err := txs.resolveTaskCollaboratorTargetForUser(ctx, txs.db, userID, workspaceID, request.Task, request.Scope, taskReadChannelAction)
	if err != nil {
		return ListEffectiveTaskCollaboratorsResponse{}, err
	}
	ownerProfile, err := loadTaskPrincipalProfileByMemberID(ctx, txs.db, workspaceID, target.ExecutionOwnerMemberID)
	if err != nil {
		return ListEffectiveTaskCollaboratorsResponse{}, err
	}
	owner := taskActorCompatFromPrincipalProfile(ownerProfile, nil)
	if owner == nil {
		return ListEffectiveTaskCollaboratorsResponse{}, ErrNotFound
	}

	sources, err := txs.privateAgentContextSourceTargets(ctx, txs.db, target)
	if err != nil {
		return ListEffectiveTaskCollaboratorsResponse{}, err
	}
	now := time.Now().UTC()
	rows, err := txs.loadActiveTaskCollaboratorsForTargets(ctx, txs.db, sources, now, true)
	if err != nil {
		return ListEffectiveTaskCollaboratorsResponse{}, err
	}
	targetsByScope := taskCollaboratorTargetsByScope(sources)
	readableRelationIDs, err := loadReadableTaskCollaboratorScopeRows(ctx, txs.db, workspaceID, taskCollaboratorScopeReadabilityRequests(rows, targetsByScope))
	if err != nil {
		return ListEffectiveTaskCollaboratorsResponse{}, err
	}
	byMember := make(map[string]*EffectiveTaskCollaborator)
	for _, row := range rows {
		if _, ok := readableRelationIDs[strings.TrimSpace(row.ID)]; !ok {
			continue
		}
		effective := byMember[row.MemberID]
		if effective == nil {
			effective = &EffectiveTaskCollaborator{
				MemberID:      row.MemberID,
				MemberProfile: row.MemberProfile,
				Member:        row.Member,
				Role:          row.Role,
				Sources:       []TaskCollaboratorSource{},
			}
			byMember[row.MemberID] = effective
		}
		effective.Sources = append(effective.Sources, TaskCollaboratorSource{
			ScopeType:  row.ScopeType,
			ScopeID:    row.ScopeID,
			RelationID: row.ID,
			Role:       row.Role,
		})
	}

	collaborators := make([]EffectiveTaskCollaborator, 0, len(byMember))
	for _, row := range byMember {
		sort.Slice(row.Sources, func(i, j int) bool {
			if row.Sources[i].ScopeType == row.Sources[j].ScopeType {
				return row.Sources[i].RelationID < row.Sources[j].RelationID
			}
			return collaboratorScopeRank(row.Sources[i].ScopeType) < collaboratorScopeRank(row.Sources[j].ScopeType)
		})
		collaborators = append(collaborators, *row)
	}
	sort.Slice(collaborators, func(i, j int) bool {
		leftName := taskCollaboratorProfileDisplayName(collaborators[i].MemberProfile, collaborators[i].Member.Name)
		rightName := taskCollaboratorProfileDisplayName(collaborators[j].MemberProfile, collaborators[j].Member.Name)
		if leftName == rightName {
			return collaborators[i].MemberID < collaborators[j].MemberID
		}
		return leftName < rightName
	})

	return ListEffectiveTaskCollaboratorsResponse{
		ExecutionOwnerMemberID: target.ExecutionOwnerMemberID,
		OwnerProfile:           ownerProfile,
		Owner:                  *owner,
		People:                 collaborators,
		Collaborators:          collaborators,
	}, nil
}

func taskCollaboratorProfileDisplayName(profile *TaskPrincipalProfile, fallback string) string {
	if profile != nil {
		if displayName := strings.TrimSpace(profile.Normalize().DisplayName); displayName != "" {
			return displayName
		}
	}
	return strings.TrimSpace(fallback)
}
