package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

const agentWorkspaceSnapshotDefaultCollectionLimit = 200

// ResolveWritableAgentTargetScopeID maps a daemon-style target (for example
// #general, dm:@peer, or #general:shortid) into the concrete message scope id
// that write-oriented agent APIs should use. Agent resolve-channel responses are
// scope-only; callers must not route through legacy channel/space wrappers.
func (store *Store) ResolveWritableAgentTargetScopeID(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (string, error) {
	resolved, err := store.ResolveWritableAgentMessageTarget(ctx, authenticatedAgent, target)
	if err != nil {
		return "", err
	}
	return resolved.StorageScope.ScopeID, nil
}

type AgentTaskScopeTarget struct {
	TaskID      string
	TaskScopeID string
	ChannelID   string
	TaskNumber  int64
}

func (store *Store) ResolveTaskIDForUserTaskScopeTarget(ctx context.Context, userID string, workspaceID string, taskScopeID string) (string, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedUserID := strings.TrimSpace(userID)
	trimmedTaskScopeID := strings.TrimSpace(taskScopeID)
	if trimmedWorkspaceID == "" || trimmedUserID == "" || trimmedTaskScopeID == "" {
		return "", badRequest("workspace, user, and task scope are required")
	}

	var taskID string
	if err := store.db.QueryRow(ctx, `
SELECT t.id
FROM tasks t
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
JOIN members m
  ON m.workspace_id = $1
 AND m.user_id = $3
 AND m.type = 'human'
 AND m.status = 'active'
WHERE t.task_scope_id = $2
LIMIT 1
`, trimmedWorkspaceID, trimmedTaskScopeID, trimmedUserID).Scan(&taskID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("resolve task scope target: %w", err)
	}
	return taskID, nil
}

// ResolveTaskForAgentTaskScopeTarget resolves an agent-runtime raw task-scope
// target to the canonical Task/board authority. It validates the task belongs to
// the same workspace, its board resolves to one active channel, and the
// same-workspace active Agent+Machine+Member currently holds active membership
// on that channel scope (canonical Task->channel authorizer). The client-supplied
// taskScope is only a location/consistency hint and never the authorization fact.
func (store *Store) ResolveTaskForAgentTaskScopeTarget(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, taskScopeID string) (AgentTaskScopeTarget, error) {
	trimmedWorkspaceID := strings.TrimSpace(authenticatedAgent.WorkspaceID)
	trimmedMemberID := strings.TrimSpace(authenticatedAgent.MemberID)
	trimmedTaskScopeID := strings.TrimSpace(taskScopeID)
	if trimmedWorkspaceID == "" || trimmedMemberID == "" || trimmedTaskScopeID == "" {
		return AgentTaskScopeTarget{}, badRequest("workspace, agent member, and task scope are required")
	}

	// Authorize before returning so every caller of the resolved target inherits
	// the canonical Task->board Channel decision (fail-closed, non-enumerating).
	// The server derives the unique channel from the active Task/board authority
	// (tasks.board_id -> task_boards), not a client-supplied taskScope.
	resolved, err := access.AuthorizeActiveAgentTaskBoardChannel(ctx, store.db, trimmedWorkspaceID, trimmedMemberID, trimmedTaskScopeID)
	if err != nil {
		// Map both missing/unresolvable and existing-unauthorized raw Task scopes
		// to the same app.ErrForbidden so the transport layer returns one
		// identical 403 (non-enumerating: a caller cannot learn whether a Task
		// exists from the transport response alone).
		if errors.Is(err, access.ErrForbidden) || errors.Is(err, access.ErrNotFound) {
			return AgentTaskScopeTarget{}, ErrForbidden
		}
		return AgentTaskScopeTarget{}, err
	}
	return AgentTaskScopeTarget{
		TaskID:      resolved.TaskID,
		TaskScopeID: resolved.TaskScopeID,
		ChannelID:   resolved.ChannelID,
		TaskNumber:  resolved.TaskNumber,
	}, nil
}

// ResolveReadableAgentTargetScopeID maps a daemon history/search target to an
// existing readable scope id. Unlike ResolveWritableAgentTargetScopeID, it must
// not create DMs or reply-thread scopes while serving read-only endpoints.
func (store *Store) ResolveReadableAgentTargetScopeID(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (string, error) {
	resolved, err := store.ResolveReadableAgentMessageTarget(ctx, authenticatedAgent, target)
	if err != nil {
		return "", err
	}
	return resolved.ReadScope.ScopeID, nil
}

// ResolveAgentReviewSubjectScopeID resolves a DM/reply-thread target into the
// scope id used as the review-context subject. Plain channel subjects remain
// unsupported to keep review contexts anchored to one concrete conversation.
func (store *Store) ResolveAgentReviewSubjectScopeID(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (string, error) {
	resolved, err := store.ResolveAgentReviewSubjectTarget(ctx, authenticatedAgent, target)
	if err != nil {
		return "", err
	}
	return resolved.ReadScope.ScopeID, nil
}

// SendAgentThreadMessage persists an agent-authored channel / DM / reply-thread message.
// Agent-facing sends must target real parent surfaces; hidden execution context
// is resolved from the authenticated runtime/session binding.
func (store *Store) SendAgentThreadMessage(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.MessageDelivery, error) {
	result, err := store.sendAgentThreadMessageResult(ctx, authorAgent, params)
	if err != nil {
		return message.MessageDelivery{}, err
	}
	if result.State == message.SendStateHeld {
		return message.MessageDelivery{}, badRequest("message send was held by freshness gate")
	}
	return result.Delivery, nil
}

func (store *Store) SendAgentThreadMessageWithFreshness(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.SendMessageResult, error) {
	return store.sendAgentThreadMessageResult(ctx, authorAgent, params)
}

func agentDirectMentionMetadataRecipients(recipients []message.AgentRecipient) []message.AgentRecipient {
	if len(recipients) == 0 {
		return nil
	}
	filtered := make([]message.AgentRecipient, 0, len(recipients))
	for _, recipient := range recipients {
		if recipient.DeliveryReason != message.AgentDeliveryReasonDirectMention {
			continue
		}
		filtered = append(filtered, recipient)
	}
	return filtered
}

func (store *Store) sendAgentThreadMessageResult(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.SendMessageResult, error) {
	normalizedSource, err := params.Source.NormalizeForWrite()
	if err != nil {
		return message.SendMessageResult{}, mapMessagingError(err)
	}
	params.Source = normalizedSource

	targetRef := strings.TrimSpace(params.TargetRef)
	dmTo := strings.TrimSpace(params.DMTo)
	content := strings.TrimSpace(params.Content)
	normalizedAttachmentIDs, err := normalizeAttachmentIDs(params.AttachmentIDs)
	if err != nil {
		return message.SendMessageResult{}, err
	}
	if content == "" && len(normalizedAttachmentIDs) == 0 {
		return message.SendMessageResult{}, badRequest("content is required")
	}
	targetCount := 0
	for _, target := range []string{targetRef, dmTo} {
		if target != "" {
			targetCount++
		}
	}
	if targetCount != 1 {
		return message.SendMessageResult{}, badRequest("exactly one of target ref or dm recipient is required")
	}

	var result message.SendMessageResult
	var appMessage message.AppMessage
	var target message.ThreadAccess
	var authorTargetID string
	var authorDisplayName string
	var authorMachineID string
	if err := coordinator.New(store.db).Run(ctx, "send agent message workflow", func(tx coredb.Tx) error {
		var err error
		target, err = store.resolveSendTarget(ctx, tx, authorAgent, targetRef, dmTo)
		if err != nil {
			if errors.Is(err, ErrNotFound) {
				return fmt.Errorf("send target not found: %w", err)
			}
			return err
		}

		if err := tx.QueryRow(ctx, `
SELECT a.name, COALESCE(NULLIF(m.display_name, ''), a.name), a.machine_id
FROM agents a
JOIN members m ON m.id = a.member_id
WHERE a.workspace_id = $1 AND a.id = $2 AND a.member_id = $3
`, authorAgent.WorkspaceID, authorAgent.AgentID, authorAgent.MemberID).Scan(&authorTargetID, &authorDisplayName, &authorMachineID); err != nil {
			return fmt.Errorf("load agent author identity: %w", err)
		}

		if target.ReplyThread != nil && !params.Freshness.Enabled() {
			if err := store.syncReplyThreadSubscriptions(
				ctx,
				tx,
				authorAgent.MemberID,
				domain.MemberTypeAgent,
				target.ChannelID,
				target.ReplyThread.ParentMessageID,
				nil,
				nil,
				0,
				0,
				time.Now().UTC(),
			); err != nil {
				return err
			}
		}

		metadata := message.CopyMessageMetadata(params.Metadata)
		directMentionTokens := message.ParseDirectMentionTokensLimit(content, message.MaxScopeDirectMentionTokens)
		routingPlan, err := message.LoadScopeAgentRoutingPlan(ctx, tx, authorAgent.MemberID, target, content, false)
		if err != nil {
			return err
		}
		if target.ThreadType == domain.ThreadTypeChannel {
			ownerPrivateRouting, err := store.resolveOwnerPrivateRecipientsForMessage(
				ctx,
				tx,
				authorAgent.WorkspaceID,
				target,
				authorAgent.MemberID,
				domain.MemberTypeAgent,
				content,
				ownerPrivateRecipientResolutionOptions{
					IncludeDirectMentions:  len(directMentionTokens) > 0,
					IncludeChannelAutoWake: target.ReplyThread == nil,
					IncludeThreadContext:   target.ReplyThread != nil,
					DirectMentionTokens:    directMentionTokens,
				},
			)
			if err != nil {
				return err
			}
			routingPlan.DurableRecipients = message.MergeAgentRecipients(routingPlan.DurableRecipients, ownerPrivateRouting.DurableRecipients)
			routingPlan.WakeRecipients = message.MergeAgentRecipients(routingPlan.WakeRecipients, ownerPrivateRouting.WakeRecipients)
		}
		routingPlan, err = store.attachAgentTeamInvokeContexts(ctx, tx, authorAgent.WorkspaceID, target, authorAgent.MemberID, params.TeamInvokeTeamID, routingPlan)
		if err != nil {
			return err
		}
		if mentions := message.BuildPersistedMentionMetadata(nil, agentDirectMentionMetadataRecipients(routingPlan.DurableRecipients), directMentionTokens); len(mentions) > 0 {
			if metadata == nil {
				metadata = make(map[string]any, 1)
			}
			metadata[message.MentionMetadataKey] = mentions
		}

		result, err = store.appendMessageToThreadResult(ctx, tx, authorAgent.MemberID, domain.MemberTypeAgent, target, content, normalizedAttachmentIDs, appendMessageOptions{
			ExplicitRoutingPlan: &routingPlan,
			Metadata:            metadata,
			Source:              params.Source,
			ClientMessageID:     params.ClientMessageID,
			Freshness:           params.Freshness,
		}, messageAppendCallerOwned)
		if err != nil {
			if errors.Is(err, ErrNotFound) {
				return fmt.Errorf("thread not found: %w", err)
			}
			return err
		}
		if result.State != message.SendStateSent {
			return nil
		}
		delivery := result.Delivery
		if _, err := store.handleAppMentionDeliveriesForMessage(ctx, tx, authorAgent.MemberID, string(domain.MemberTypeAgent), authorAgent.WorkspaceID, target, delivery.MessageID, "", content); err != nil {
			return err
		}
		delivery.AuthorMachineID = strings.TrimSpace(authorMachineID)
		appMessage = appMessageFromDelivery(appChannelIDForTarget(target), message.AuthorProfile{
			ActorType:   message.AuthorActorTypeAgent,
			PrincipalID: strings.TrimSpace(authorAgent.AgentID),
			MemberID:    strings.TrimSpace(authorAgent.MemberID),
			TargetID:    strings.TrimSpace(authorTargetID),
			DisplayName: authorDisplayName,
		}, delivery)
		if !params.Source.IsZero() {
			appMessage.Source = params.Source.Normalize()
		}
		if err := attachResolvedMentionsForSentAppMessage(ctx, tx, authorAgent.WorkspaceID, &appMessage); err != nil {
			return err
		}
		delivery.AppMessage = &appMessage
		result.Delivery = delivery
		result.MessageID = delivery.MessageID
		if result.AcceptedSeq == 0 {
			result.AcceptedSeq = delivery.Seq
		}
		if result.LatestSeq == 0 {
			result.LatestSeq = delivery.Seq
		}
		return store.persistScopeFanoutPayloads(ctx, tx, delivery)
	}); err != nil {
		return message.SendMessageResult{}, err
	}
	return result, nil
}

func (store *Store) GetAgentWorkspaceSnapshot(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent) (agent.WorkspaceSnapshot, error) {
	limit := agentWorkspaceSnapshotDefaultCollectionLimit
	queryLimit := limit + 1
	snapshot := agent.WorkspaceSnapshot{
		Channels: make([]agent.WorkspaceChannel, 0, limit),
		Agents:   make([]agent.WorkspaceActor, 0, limit),
		Humans:   make([]agent.WorkspaceActor, 0, limit),
		Limits: agent.WorkspaceSnapshotLimits{
			Channels: agent.WorkspaceSnapshotCollectionLimit{Limit: limit},
			Agents:   agent.WorkspaceSnapshotCollectionLimit{Limit: limit},
			Humans:   agent.WorkspaceSnapshotCollectionLimit{Limit: limit},
		},
	}

	channelsRows, err := store.db.Query(ctx, `
SELECT p.slug, COALESCE(NULLIF(p.display_name, ''), p.slug), p.description,
       EXISTS (SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = s.id AND sm.member_id = $2 AND sm.removed_at IS NULL) AS joined
FROM scopes s
JOIN scope_channel_profiles p ON p.scope_id = s.id
WHERE s.workspace_id = $1
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND (
      p.subsystem = 'im'
      OR (
          p.subsystem = 'tutorial'
          AND EXISTS (
              SELECT 1
              FROM scope_memberships tutorial_sm
              WHERE tutorial_sm.scope_id = s.id
                AND tutorial_sm.member_id = $2
                AND tutorial_sm.removed_at IS NULL
                AND tutorial_sm.archived_at IS NULL
                AND tutorial_sm.effective_to_seq IS NULL
          )
      )
  )
ORDER BY p.slug ASC
LIMIT $3
`, authenticatedAgent.WorkspaceID, authenticatedAgent.MemberID, queryLimit)
	if err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("query channels snapshot: %w", err)
	}
	defer channelsRows.Close()

	for channelsRows.Next() {
		var channel agent.WorkspaceChannel
		if err := channelsRows.Scan(&channel.Name, &channel.DisplayName, &channel.Description, &channel.Joined); err != nil {
			return agent.WorkspaceSnapshot{}, fmt.Errorf("scan channel snapshot: %w", err)
		}
		snapshot.Channels = append(snapshot.Channels, channel)
	}
	if err := channelsRows.Err(); err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("iterate channels snapshot: %w", err)
	}
	snapshot.Channels, snapshot.Limits.Channels.Truncated = capWorkspaceSnapshotChannels(snapshot.Channels, limit)

	humansRows, err := store.db.Query(ctx, `
SELECT
  m.id,
  COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), NULLIF(u.name, ''), m.id) AS display_name
FROM members m
JOIN users u ON u.id = m.user_id
WHERE m.workspace_id = $1 AND m.type = 'human' AND m.status = 'active'
ORDER BY m.id ASC
LIMIT $2
`, authenticatedAgent.WorkspaceID, queryLimit)
	if err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("query humans snapshot: %w", err)
	}
	defer humansRows.Close()
	for humansRows.Next() {
		var actor agent.WorkspaceActor
		if err := humansRows.Scan(&actor.Name, &actor.DisplayName); err != nil {
			return agent.WorkspaceSnapshot{}, fmt.Errorf("scan human snapshot: %w", err)
		}
		snapshot.Humans = append(snapshot.Humans, actor)
	}
	if err := humansRows.Err(); err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("iterate humans snapshot: %w", err)
	}
	snapshot.Humans, snapshot.Limits.Humans.Truncated = capWorkspaceSnapshotActors(snapshot.Humans, limit)

	agentsRows, err := store.db.Query(ctx, `
SELECT a.id, a.name, COALESCE(NULLIF(m.display_name, ''), a.name) AS display_name,
       'inactive', 'offline', '', mac.name,
       COALESCE(NULLIF(mac.hostname, ''), '') AS hostname, mac.id,
       COALESCE(NULLIF(creator.name, ''), creator.id) AS owner_name,
       FALSE AS machine_online
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
JOIN users creator ON creator.id = mac.created_by_user_id
WHERE a.workspace_id = $1
  AND a.status <> 'archived'
  AND mac.status = 'active'
ORDER BY a.name ASC, m.id ASC
LIMIT $2
`, authenticatedAgent.WorkspaceID, queryLimit)
	if err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("query agents snapshot: %w", err)
	}
	defer agentsRows.Close()
	for agentsRows.Next() {
		var actor agent.WorkspaceActor
		var rawStatus string
		var machineOnline bool
		if err := agentsRows.Scan(&actor.ID, &actor.Name, &actor.DisplayName, &rawStatus, &actor.Activity, &actor.ActivityDetail, &actor.MachineName, &actor.MachineHost, &actor.MachineID, &actor.OwnerName, &machineOnline); err != nil {
			return agent.WorkspaceSnapshot{}, fmt.Errorf("scan agent snapshot: %w", err)
		}
		actor.Status = agentWorkspacePresenceStatus(rawStatus, actor.Activity, machineOnline)
		snapshot.Agents = append(snapshot.Agents, actor)
	}
	if err := agentsRows.Err(); err != nil {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("iterate agents snapshot: %w", err)
	}
	snapshot.Agents, snapshot.Limits.Agents.Truncated = capWorkspaceSnapshotActors(snapshot.Agents, limit)
	return snapshot, nil
}

func (store *Store) GetAgentWorkspaceSnapshotPage(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, request agent.WorkspaceSnapshotRequest) (agent.WorkspaceSnapshot, error) {
	kind := strings.TrimSpace(request.Kind)
	if kind == "" {
		kind = "summary"
	}
	limit := request.Limit
	switch {
	case limit <= 0:
		limit = 20
	case limit > 100:
		limit = 100
	}
	offset, err := strconv.Atoi(firstNonEmptyApp(strings.TrimSpace(request.Cursor), "0"))
	if err != nil || offset < 0 {
		return agent.WorkspaceSnapshot{}, fmt.Errorf("invalid workspace cursor")
	}
	snapshot := agent.WorkspaceSnapshot{
		Channels: []agent.WorkspaceChannel{},
		Agents:   []agent.WorkspaceActor{},
		Humans:   []agent.WorkspaceActor{},
		Kind:     kind,
		Limit:    limit,
	}
	query := strings.ToLower(strings.TrimSpace(request.Query))
	switch kind {
	case "summary":
		return snapshot, nil
	case "channel", "channels":
		channels, hasMore, err := store.listAgentWorkspaceChannelsPage(ctx, authenticatedAgent, query, limit, offset)
		if err != nil {
			return agent.WorkspaceSnapshot{}, err
		}
		snapshot.Channels = channels
		snapshot.HasMore = hasMore
	case "agent", "agents":
		actors, hasMore, err := store.listAgentWorkspaceAgentsPage(ctx, authenticatedAgent, query, limit, offset)
		if err != nil {
			return agent.WorkspaceSnapshot{}, err
		}
		snapshot.Agents = actors
		snapshot.HasMore = hasMore
	case "human", "humans":
		actors, hasMore, err := store.listAgentWorkspaceHumansPage(ctx, authenticatedAgent, query, limit, offset)
		if err != nil {
			return agent.WorkspaceSnapshot{}, err
		}
		snapshot.Humans = actors
		snapshot.HasMore = hasMore
	default:
		return agent.WorkspaceSnapshot{}, fmt.Errorf("invalid workspace kind")
	}
	if snapshot.HasMore {
		snapshot.NextCursor = strconv.Itoa(offset + limit)
	}
	return snapshot, nil
}

func (store *Store) listAgentWorkspaceChannelsPage(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, query string, limit int, offset int) ([]agent.WorkspaceChannel, bool, error) {
	args := []any{authenticatedAgent.WorkspaceID, authenticatedAgent.MemberID}
	filter := ""
	if query != "" {
		args = append(args, "%"+query+"%")
		filter = fmt.Sprintf("AND LOWER(CONCAT_WS(' ', p.slug, p.display_name, p.description)) LIKE $%d", len(args))
	}
	args = append(args, limit+1, offset)
	rows, err := store.db.Query(ctx, `
SELECT p.slug, COALESCE(NULLIF(p.display_name, ''), p.slug), p.description,
       EXISTS (SELECT 1 FROM scope_memberships sm WHERE sm.scope_id = s.id AND sm.member_id = $2 AND sm.removed_at IS NULL) AS joined
FROM scopes s
JOIN scope_channel_profiles p ON p.scope_id = s.id
WHERE s.workspace_id = $1
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND (
      p.subsystem = 'im'
      OR (
          p.subsystem = 'tutorial'
          AND EXISTS (
              SELECT 1
              FROM scope_memberships tutorial_sm
              WHERE tutorial_sm.scope_id = s.id
                AND tutorial_sm.member_id = $2
                AND tutorial_sm.removed_at IS NULL
                AND tutorial_sm.archived_at IS NULL
                AND tutorial_sm.effective_to_seq IS NULL
          )
      )
  )
  `+filter+`
ORDER BY p.slug ASC
LIMIT $`+strconv.Itoa(len(args)-1)+` OFFSET $`+strconv.Itoa(len(args))+`
`, args...)
	if err != nil {
		return nil, false, fmt.Errorf("query channels snapshot page: %w", err)
	}
	defer rows.Close()
	channels := make([]agent.WorkspaceChannel, 0, limit+1)
	for rows.Next() {
		var channel agent.WorkspaceChannel
		if err := rows.Scan(&channel.Name, &channel.DisplayName, &channel.Description, &channel.Joined); err != nil {
			return nil, false, fmt.Errorf("scan channel snapshot page: %w", err)
		}
		channels = append(channels, channel)
	}
	if err := rows.Err(); err != nil {
		return nil, false, fmt.Errorf("iterate channel snapshot page: %w", err)
	}
	hasMore := len(channels) > limit
	if hasMore {
		channels = channels[:limit]
	}
	return channels, hasMore, nil
}

func (store *Store) listAgentWorkspaceHumansPage(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, query string, limit int, offset int) ([]agent.WorkspaceActor, bool, error) {
	args := []any{authenticatedAgent.WorkspaceID}
	filter := ""
	if query != "" {
		args = append(args, "%"+query+"%")
		filter = fmt.Sprintf("AND LOWER(CONCAT_WS(' ', m.id, u.name, u.display_name, m.display_name)) LIKE $%d", len(args))
	}
	args = append(args, limit+1, offset)
	rows, err := store.db.Query(ctx, `
SELECT
  m.id,
  COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), NULLIF(u.name, ''), m.id) AS display_name
FROM members m
JOIN users u ON u.id = m.user_id
WHERE m.workspace_id = $1 AND m.type = 'human' AND m.status = 'active'
  `+filter+`
ORDER BY m.id ASC
LIMIT $`+strconv.Itoa(len(args)-1)+` OFFSET $`+strconv.Itoa(len(args))+`
`, args...)
	if err != nil {
		return nil, false, fmt.Errorf("query humans snapshot page: %w", err)
	}
	defer rows.Close()
	actors := make([]agent.WorkspaceActor, 0, limit+1)
	for rows.Next() {
		var actor agent.WorkspaceActor
		if err := rows.Scan(&actor.Name, &actor.DisplayName); err != nil {
			return nil, false, fmt.Errorf("scan human snapshot page: %w", err)
		}
		actors = append(actors, actor)
	}
	if err := rows.Err(); err != nil {
		return nil, false, fmt.Errorf("iterate human snapshot page: %w", err)
	}
	hasMore := len(actors) > limit
	if hasMore {
		actors = actors[:limit]
	}
	return actors, hasMore, nil
}

func (store *Store) listAgentWorkspaceAgentsPage(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, query string, limit int, offset int) ([]agent.WorkspaceActor, bool, error) {
	args := []any{authenticatedAgent.WorkspaceID}
	filter := ""
	if query != "" {
		args = append(args, "%"+query+"%")
		filter = fmt.Sprintf("AND LOWER(CONCAT_WS(' ', a.name, m.display_name, mac.name, mac.hostname, creator.name)) LIKE $%d", len(args))
	}
	args = append(args, limit+1, offset)
	rows, err := store.db.Query(ctx, `
SELECT a.id, a.name, COALESCE(NULLIF(m.display_name, ''), a.name) AS display_name,
       'inactive', 'offline', '', mac.name,
       COALESCE(NULLIF(mac.hostname, ''), '') AS hostname, mac.id,
       COALESCE(NULLIF(creator.name, ''), creator.id) AS owner_name,
       FALSE AS machine_online
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
JOIN users creator ON creator.id = mac.created_by_user_id
WHERE a.workspace_id = $1
  AND a.status <> 'archived'
  AND mac.status = 'active'
  `+filter+`
ORDER BY a.name ASC, m.id ASC
LIMIT $`+strconv.Itoa(len(args)-1)+` OFFSET $`+strconv.Itoa(len(args))+`
`, args...)
	if err != nil {
		return nil, false, fmt.Errorf("query agents snapshot page: %w", err)
	}
	defer rows.Close()
	actors := make([]agent.WorkspaceActor, 0, limit+1)
	for rows.Next() {
		var actor agent.WorkspaceActor
		var rawStatus string
		var machineOnline bool
		if err := rows.Scan(&actor.ID, &actor.Name, &actor.DisplayName, &rawStatus, &actor.Activity, &actor.ActivityDetail, &actor.MachineName, &actor.MachineHost, &actor.MachineID, &actor.OwnerName, &machineOnline); err != nil {
			return nil, false, fmt.Errorf("scan agent snapshot page: %w", err)
		}
		actor.Status = agentWorkspacePresenceStatus(rawStatus, actor.Activity, machineOnline)
		actors = append(actors, actor)
	}
	if err := rows.Err(); err != nil {
		return nil, false, fmt.Errorf("iterate agent snapshot page: %w", err)
	}
	hasMore := len(actors) > limit
	if hasMore {
		actors = actors[:limit]
	}
	return actors, hasMore, nil
}

func firstNonEmptyApp(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func capWorkspaceSnapshotChannels(channels []agent.WorkspaceChannel, limit int) ([]agent.WorkspaceChannel, bool) {
	if limit <= 0 || len(channels) <= limit {
		return channels, false
	}
	return channels[:limit], true
}

func capWorkspaceSnapshotActors(actors []agent.WorkspaceActor, limit int) ([]agent.WorkspaceActor, bool) {
	if limit <= 0 || len(actors) <= limit {
		return actors, false
	}
	return actors[:limit], true
}

func agentWorkspacePresenceStatus(agentStatus string, activity string, machineOnline bool) string {
	if !machineOnline {
		return "offline"
	}
	switch strings.TrimSpace(agentStatus) {
	case "inactive":
		return "idle"
	case "sleeping":
		return "sleeping"
	}
	switch strings.TrimSpace(activity) {
	case "working", "thinking":
		return "busy"
	case "sleeping":
		return "sleeping"
	default:
		return "online"
	}
}
