package im

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	storeim "github.com/tidbcloud/AgentTeam/internal/store/im"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
)

const (
	imEventTypeMessageCreated              = "message.created"
	imEventTypeMessageUpdated              = "message.updated"
	imEventTypeMessageRecalled             = "message.recalled"
	imEventTypeReactionChanged             = "reaction.changed"
	imEventTypeConversationUpdated         = "conversation.updated"
	imEventTypeConversationRead            = "conversation.read"
	imEventTypeConversationUnread          = "conversation.unread"
	imEventTypeConversationArchived        = "conversation.archived"
	imEventTypeConversationBulkRead        = "conversation.bulk.read"
	imEventTypeDMOpened                    = "dm.opened"
	imEventTypeMessageDelivery             = "message.delivery.updated"
	imEventTypeMessagePinChanged           = "message.pin.changed"
	imEventTypeMessageTopNoticeChanged     = "message.top_notice.changed"
	imEventTypeMessageTopNoticeBannerState = "message.top_notice.banner_state_changed"
	imEventTypeMessagePinsInvalidated      = "message.pins.invalidated"
	imEventTypeNotificationUpdated         = "notification.updated"
	imEventTypeOwnerContextDismissed       = "owner_context.dismissed"
)

func publishMessageCreatedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, appMessage message.AppMessage, delivery message.MessageDelivery) error {
	if writer == nil || delivery.IdempotentReplay {
		return nil
	}
	message.RedactServiceAccountPublisherMetadataForSharedProjection(&appMessage)
	dtoMessage := converter.ToAppMessage(appMessage)
	payload := map[string]any{"message": dtoMessage}
	if delivery.SuppressBrowserNotification {
		payload["suppressBrowserNotification"] = true
	}
	if mention, recipients := messageMentionPayload(appMessage, delivery); mention != nil {
		payload["mention"] = mention
		payload["mentionRecipients"] = recipients
	}
	patches := map[string]any{}
	if delivery.ReplyThreadUpdated != nil {
		patches["thread"] = replyThreadUpdatedPayload(*delivery.ReplyThreadUpdated)
	}
	if len(patches) == 0 {
		patches = nil
	}
	entitySeq := appMessage.Seq
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     scopeID,
		Event: storeim.StreamEvent{
			Type:      imEventTypeMessageCreated,
			EntityID:  appMessage.ID,
			EntitySeq: &entitySeq,
			Payload:   payload,
			Patches:   patches,
			CreatedAt: appMessage.CreatedAt,
		},
		Conversation: conversationIndexFromMessage(dtoMessage),
	})
	if err != nil {
		return fmt.Errorf("append IM message stream event: %w", err)
	}
	return nil
}

func publishMessageUpdatedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, appMessage message.AppMessage) error {
	if writer == nil {
		return nil
	}
	messageID := strings.TrimSpace(appMessage.ID)
	if messageID == "" {
		return nil
	}
	message.RedactServiceAccountPublisherMetadataForSharedProjection(&appMessage)
	entitySeq := appMessage.Seq
	payload := map[string]any{
		"scopeId":          strings.TrimSpace(scopeID),
		"messageId":        messageID,
		"seq":              appMessage.Seq,
		"content":          appMessage.Content,
		"metadata":         message.CopyMessageMetadata(appMessage.Metadata),
		"editedAt":         appMessage.EditedAt,
		"editedByMemberId": strings.TrimSpace(appMessage.EditedByMemberID),
		"editRevision":     appMessage.EditRevision,
	}
	createdAt := time.Now().UTC()
	if appMessage.EditedAt != nil && !appMessage.EditedAt.IsZero() {
		createdAt = appMessage.EditedAt.UTC()
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     strings.TrimSpace(scopeID),
		Event: storeim.StreamEvent{
			Type:      imEventTypeMessageUpdated,
			EntityID:  messageID,
			EntitySeq: &entitySeq,
			Payload:   payload,
			CreatedAt: createdAt,
		},
	})
	if err != nil {
		return fmt.Errorf("append IM message update stream event: %w", err)
	}
	return nil
}

func publishMessageRecalledEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, appMessage message.AppMessage, updateConversation bool) error {
	if writer == nil {
		return nil
	}
	messageID := strings.TrimSpace(appMessage.ID)
	if messageID == "" {
		return nil
	}
	message.RedactServiceAccountPublisherMetadataForSharedProjection(&appMessage)
	dtoMessage := converter.ToAppMessage(appMessage)
	entitySeq := appMessage.Seq
	payload := map[string]any{
		"scopeId":   strings.TrimSpace(scopeID),
		"messageId": messageID,
		"seq":       appMessage.Seq,
		"message":   dtoMessage,
	}
	if tombstone := message.CopyMessageMetadata(appMessage.Metadata)[message.MessageRecallMetadataKey]; tombstone != nil {
		payload["tombstone"] = tombstone
	}
	var conversation *storeim.ConversationIndexItem
	if updateConversation {
		conversation = conversationIndexFromMessage(dtoMessage)
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     strings.TrimSpace(scopeID),
		Event: storeim.StreamEvent{
			Type:      imEventTypeMessageRecalled,
			EntityID:  messageID,
			EntitySeq: &entitySeq,
			Payload:   payload,
			CreatedAt: time.Now().UTC(),
		},
		Conversation: conversation,
	})
	if err != nil {
		return fmt.Errorf("append IM message recall stream event: %w", err)
	}
	return nil
}

func publishMessageDeliveryUpdatedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, messageID string) error {
	if writer == nil {
		return nil
	}
	trimmedScopeID := strings.TrimSpace(scopeID)
	trimmedMessageID := strings.TrimSpace(messageID)
	if trimmedScopeID == "" || trimmedMessageID == "" {
		return nil
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     trimmedScopeID,
		Event: storeim.StreamEvent{
			Type:     imEventTypeMessageDelivery,
			EntityID: trimmedMessageID,
			Payload: map[string]any{
				"channelId":  trimmedScopeID,
				"messageIds": []string{trimmedMessageID},
			},
		},
	})
	if err != nil {
		return fmt.Errorf("append IM message delivery stream event: %w", err)
	}
	return nil
}

func publishConversationRewoundEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, plan rewind.Plan) error {
	if writer == nil {
		return nil
	}
	broadcastScopeID := strings.TrimSpace(plan.RealtimeBroadcastScopeID())
	rewoundScopeID := strings.TrimSpace(plan.RewindScopeID())
	if broadcastScopeID == "" || rewoundScopeID == "" {
		return nil
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     broadcastScopeID,
		Event: storeim.StreamEvent{
			Type:     "conversation.rewound",
			EntityID: strings.TrimSpace(plan.TargetMessageID),
			Payload: map[string]any{
				"channelId": rewoundScopeID,
			},
		},
	})
	if err != nil {
		return fmt.Errorf("append IM rewind stream event: %w", err)
	}
	return nil
}

func publishReactionChangedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, messageID string, reactions []dto.MessageReactionAggregate, notification *message.HumanReactionNotification) error {
	if writer == nil {
		return nil
	}
	payload := map[string]any{
		"messageId": messageID,
		"reactions": reactions,
	}
	if notificationPayload := reactionNotificationPayload(notification); notificationPayload != nil {
		payload["reaction"] = notificationPayload
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     scopeID,
		Event: storeim.StreamEvent{
			Type:     imEventTypeReactionChanged,
			EntityID: messageID,
			Payload:  payload,
		},
	})
	if err != nil {
		return fmt.Errorf("append IM reaction stream event: %w", err)
	}
	return nil
}

func publishMessagePinChangedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, result message.MessagePinMutationResult) error {
	if writer == nil {
		return nil
	}
	messageID := strings.TrimSpace(result.MessageID)
	if messageID == "" {
		return nil
	}
	pinned := result.Pin != nil
	payload := map[string]any{
		"scopeId":     strings.TrimSpace(scopeID),
		"messageId":   messageID,
		"pinned":      pinned,
		"pinRevision": result.List.PinRevision,
		"list":        converter.ToMessagePinList(result.List),
	}
	if result.Pin != nil {
		payload["pin"] = converter.ToMessagePin(*result.Pin)
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     scopeID,
		Event: storeim.StreamEvent{
			Type:      imEventTypeMessagePinChanged,
			EntityID:  messageID,
			Payload:   payload,
			CreatedAt: time.Now().UTC(),
		},
	})
	if err != nil {
		return fmt.Errorf("append IM message pin stream event: %w", err)
	}
	return nil
}

func publishMessageTopNoticeChangedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, result message.MessageTopNoticeMutationResult) error {
	if writer == nil {
		return nil
	}
	messageID := strings.TrimSpace(result.MessageID)
	topNoticeID := strings.TrimSpace(result.TopNoticeID)
	if messageID == "" && topNoticeID == "" {
		return nil
	}
	payload := map[string]any{
		"scopeId":           strings.TrimSpace(scopeID),
		"messageId":         messageID,
		"topNoticeId":       topNoticeID,
		"hasTopNotice":      result.TopNotice != nil,
		"topNoticeRevision": result.State.TopNoticeRevision,
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     scopeID,
		Event: storeim.StreamEvent{
			Type:      imEventTypeMessageTopNoticeChanged,
			EntityID:  firstNonEmptyString(topNoticeID, messageID, strings.TrimSpace(scopeID)),
			Payload:   payload,
			CreatedAt: time.Now().UTC(),
		},
	})
	if err != nil {
		return fmt.Errorf("append IM message top notice stream event: %w", err)
	}
	return nil
}

func publishMessageTopNoticeBannerStateChangedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, memberID string, scopeID string, result message.MessageTopNoticeMutationResult) error {
	if writer == nil {
		return nil
	}
	memberID = strings.TrimSpace(memberID)
	if memberID == "" {
		return nil
	}
	messageID := strings.TrimSpace(result.MessageID)
	topNoticeID := strings.TrimSpace(result.TopNoticeID)
	payload := map[string]any{
		"scopeId":                 strings.TrimSpace(scopeID),
		"messageId":               messageID,
		"topNoticeId":             topNoticeID,
		"hiddenFromBannerForMe":   messageTopNoticeHiddenFromBanner(result.State, messageID, topNoticeID),
		"viewerTopNoticeRevision": result.State.ViewerTopNoticeRevision,
		"state":                   converter.ToMessageTopNoticeState(result.State),
	}
	if messageID == "" && topNoticeID == "" {
		payload["reset"] = true
	}
	_, err := writer.AppendMemberEvent(ctx, workspaceID, memberID, scopeID, storeim.StreamEvent{
		Type:      imEventTypeMessageTopNoticeBannerState,
		EntityID:  firstNonEmptyString(topNoticeID, messageID, strings.TrimSpace(scopeID)),
		Payload:   payload,
		CreatedAt: time.Now().UTC(),
	}, nil)
	if err != nil {
		return fmt.Errorf("append IM message top notice banner state stream event: %w", err)
	}
	return nil
}

func publishMessagePinsInvalidatedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, scopeID string, messageIDs []string) error {
	if writer == nil {
		return nil
	}
	scopeID = strings.TrimSpace(scopeID)
	if scopeID == "" {
		return nil
	}
	ids := make([]string, 0, len(messageIDs))
	for _, messageID := range messageIDs {
		if trimmed := strings.TrimSpace(messageID); trimmed != "" {
			ids = append(ids, trimmed)
		}
	}
	_, err := writer.AppendScopeEvent(ctx, storeim.ScopeEventRequest{
		WorkspaceID: workspaceID,
		ScopeID:     scopeID,
		Event: storeim.StreamEvent{
			Type:     imEventTypeMessagePinsInvalidated,
			EntityID: scopeID,
			Payload: map[string]any{
				"scopeId":    scopeID,
				"messageIds": ids,
			},
			CreatedAt: time.Now().UTC(),
		},
	})
	if err != nil {
		return fmt.Errorf("append IM message pins invalidated stream event: %w", err)
	}
	return nil
}

func messageTopNoticeHiddenFromBanner(state message.MessageTopNoticeState, messageID string, topNoticeID string) bool {
	messageID = strings.TrimSpace(messageID)
	topNoticeID = strings.TrimSpace(topNoticeID)
	if state.TopNotice == nil {
		return false
	}
	if topNoticeID != "" && strings.TrimSpace(state.TopNotice.ID) == topNoticeID {
		return state.TopNotice.ViewerState.HiddenFromBannerForMe
	}
	if messageID != "" && strings.TrimSpace(state.TopNotice.MessageID) == messageID {
		return state.TopNotice.ViewerState.HiddenFromBannerForMe
	}
	return false
}

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

func publishConversationActionEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, memberID string, action string, item message.ConversationItem) error {
	if writer == nil {
		return nil
	}
	dtoItem := converter.ToConversationItem(item)
	eventType := conversationActionEventType(action)
	payload := map[string]any{
		"action":       strings.TrimSpace(action),
		"conversation": dtoItem,
	}
	if item.Task != nil && strings.TrimSpace(item.Task.ID) != "" {
		payload["taskId"] = strings.TrimSpace(item.Task.ID)
	}
	_, err := writer.AppendMemberEvent(ctx, workspaceID, memberID, item.ScopeID, storeim.StreamEvent{
		Type:      eventType,
		Payload:   payload,
		CreatedAt: time.Now().UTC(),
	}, conversationIndexFromConversation(dtoItem))
	if err != nil {
		return fmt.Errorf("append IM conversation stream event: %w", err)
	}
	return nil
}

func publishConversationBulkActionEvent(
	ctx context.Context,
	writer StreamEventWriter,
	workspaceID string,
	memberID string,
	action string,
	kinds []string,
	result dto.ConversationsBulkActionResponse,
	readScopes []message.ConversationReadScope,
	workspaceUnreadConversationCount int64,
) error {
	if writer == nil {
		return nil
	}
	_, err := writer.AppendConversationBulkReadEvent(ctx, workspaceID, memberID, storeim.StreamEvent{
		Type: imEventTypeConversationBulkRead,
		Payload: map[string]any{
			"action":                           strings.TrimSpace(action),
			"kinds":                            append([]string{}, kinds...),
			"result":                           result,
			"readScopes":                       conversationBulkReadScopePayloads(readScopes),
			"workspaceUnreadConversationCount": workspaceUnreadConversationCount,
		},
		CreatedAt: time.Now().UTC(),
	}, kinds)
	if err != nil {
		return fmt.Errorf("append IM conversation bulk stream event: %w", err)
	}
	return nil
}

func conversationBulkReadScopePayloads(readScopes []message.ConversationReadScope) []map[string]any {
	payloads := make([]map[string]any, 0, len(readScopes))
	for _, scope := range readScopes {
		scopeID := strings.TrimSpace(scope.ScopeID)
		if scopeID == "" {
			continue
		}
		payloads = append(payloads, map[string]any{
			"scopeId":                     scopeID,
			"kind":                        strings.TrimSpace(scope.Kind),
			"lastMessageReadSeq":          scope.LastReadSeq,
			"lastActivityReadSeq":         scope.LastActivityReadSeq,
			"manualUnreadGeneration":      scope.ManualUnreadGeneration,
			"manualUnreadGenerationKnown": scope.ManualUnreadGenerationKnown,
		})
	}
	return payloads
}

func publishDMOpenedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, memberID string, channel message.DMChannel) error {
	if writer == nil {
		return nil
	}
	dtoChannel := converter.ToDMChannel(channel)
	_, err := writer.AppendMemberEvent(ctx, workspaceID, memberID, dtoChannel.ID, storeim.StreamEvent{
		Type:     imEventTypeDMOpened,
		EntityID: dtoChannel.ID,
		Payload: map[string]any{
			"channelId": dtoChannel.ID,
			"channel":   dtoChannel,
		},
		CreatedAt: time.Now().UTC(),
	}, conversationIndexFromDMChannel(dtoChannel))
	if err != nil {
		return fmt.Errorf("append IM DM stream event: %w", err)
	}
	return nil
}

func publishOwnerContextDismissedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, memberID string, scopeID string, dismissedThroughSeq int64) error {
	if writer == nil {
		return nil
	}
	entitySeq := dismissedThroughSeq
	_, err := writer.AppendMemberEvent(ctx, workspaceID, memberID, scopeID, storeim.StreamEvent{
		Type:      imEventTypeOwnerContextDismissed,
		EntityID:  strings.TrimSpace(scopeID),
		EntitySeq: &entitySeq,
		Payload: map[string]any{
			"scopeId":                   strings.TrimSpace(scopeID),
			"dismissedThroughSeq":       dismissedThroughSeq,
			"dismissedThroughGlobalSeq": dismissedThroughSeq,
		},
		CreatedAt: time.Now().UTC(),
	}, nil)
	if err != nil {
		return fmt.Errorf("append IM owner context stream event: %w", err)
	}
	return nil
}

func publishNotificationUpdatedEvent(ctx context.Context, writer StreamEventWriter, workspaceID string, memberID string, scopeID string, subject string, panel dto.ScopeNotificationPanel) error {
	if writer == nil {
		return nil
	}
	_, err := writer.AppendMemberEvent(ctx, workspaceID, memberID, scopeID, storeim.StreamEvent{
		Type:     imEventTypeNotificationUpdated,
		EntityID: strings.TrimSpace(scopeID),
		Payload: map[string]any{
			"scopeId": strings.TrimSpace(scopeID),
			"subject": strings.TrimSpace(subject),
			"panel":   panel,
		},
		CreatedAt: time.Now().UTC(),
	}, nil)
	if err != nil {
		return fmt.Errorf("append IM notification stream event: %w", err)
	}
	return nil
}

func reactionNotificationPayload(notification *message.HumanReactionNotification) map[string]any {
	if notification == nil {
		return nil
	}
	if strings.TrimSpace(notification.ReactionID) == "" || strings.TrimSpace(notification.MessageID) == "" || strings.TrimSpace(notification.ChannelID) == "" || !notification.AuthorProfile.Normalize().Valid() {
		return nil
	}
	payload := map[string]any{
		"reactionId":        strings.TrimSpace(notification.ReactionID),
		"messageId":         strings.TrimSpace(notification.MessageID),
		"channelId":         strings.TrimSpace(notification.ChannelID),
		"channelType":       strings.TrimSpace(notification.ChannelType),
		"parentChannelId":   strings.TrimSpace(notification.ParentChannelID),
		"parentChannelType": strings.TrimSpace(notification.ParentChannelType),
		"parentMessageId":   strings.TrimSpace(notification.ParentMessageID),
		"authorProfile":     converter.ToAuthorProfile(notification.AuthorProfile),
		"emoji":             strings.TrimSpace(notification.Emoji),
		"contentPreview":    strings.TrimSpace(notification.ContentPreview),
	}
	if taskContext := taskNotificationContextPayload(notification.TaskContext); taskContext != nil {
		payload["taskContext"] = taskContext
	}
	return payload
}

func taskNotificationContextPayload(context *message.AppMessageTaskContext) map[string]any {
	if context == nil {
		return nil
	}
	channelID := strings.TrimSpace(context.ChannelID)
	if channelID == "" || context.TaskNumber <= 0 {
		return nil
	}
	return map[string]any{
		"channelId":  channelID,
		"taskNumber": context.TaskNumber,
	}
}

func conversationActionEventType(action string) string {
	switch strings.TrimSpace(action) {
	case message.ConversationActionMarkRead:
		return imEventTypeConversationRead
	case message.ConversationActionMarkUnread:
		return imEventTypeConversationUnread
	case message.ConversationActionHide:
		return imEventTypeConversationArchived
	default:
		return imEventTypeConversationUpdated
	}
}

func conversationIndexFromDMChannel(channel dto.DMChannel) *storeim.ConversationIndexItem {
	scopeID := strings.TrimSpace(channel.ID)
	if scopeID == "" {
		return nil
	}
	sortAt := time.Now().UTC()
	if channel.LastMessageAt != nil && !channel.LastMessageAt.IsZero() {
		sortAt = channel.LastMessageAt.UTC()
	}
	conversationID := "dm:" + scopeID
	projection := jsonMap(channel)
	projection["id"] = conversationID
	projection["kind"] = message.ConversationKindDM
	projection["scopeId"] = scopeID
	return &storeim.ConversationIndexItem{
		ConversationID: conversationID,
		ScopeID:        scopeID,
		Kind:           message.ConversationKindDM,
		SortAt:         sortAt,
		ArchivedAt:     archivedAtFromDMChannel(channel),
		Projection:     projection,
	}
}

func conversationIndexFromMessage(message dto.AppMessage) *storeim.ConversationIndexItem {
	scopeID := strings.TrimSpace(message.ChannelID)
	if scopeID == "" {
		return nil
	}
	if strings.TrimSpace(message.ChannelType) == "task" {
		return nil
	}
	projection := jsonMap(message)
	projection["lastMessage"] = message
	if strings.TrimSpace(message.ChannelType) == "service_account" {
		profile := message.AuthorProfile
		title := "Service account"
		peerID := scopeID
		subtitle := ""
		avatarURL := ""
		if profile != nil {
			title = firstNonEmpty(strings.TrimSpace(profile.DisplayName), strings.TrimSpace(profile.ServiceSlug), title)
			peerID = firstNonEmpty(strings.TrimSpace(profile.PrincipalID), peerID)
			subtitle = strings.TrimSpace(profile.ServiceSlug)
			avatarURL = strings.TrimSpace(profile.AvatarURL)
		}
		projection["title"] = title
		projection["subtitle"] = subtitle
		projection["avatarUrl"] = avatarURL
		projection["peerType"] = "service"
		projection["peerId"] = peerID
		projection["preview"] = mentionNotificationPreview(message.Content)
		projection["lastAuthorName"] = title
		projection["lastMessageAt"] = message.CreatedAt
		projection["lastActivityAt"] = message.CreatedAt
		projection["subscriptionState"] = "active"
		projection["route"] = "/inbox/service-accounts/" + url.PathEscape(peerID)
		return &storeim.ConversationIndexItem{
			ConversationID: "serviceAccount:" + scopeID,
			ScopeID:        scopeID,
			Kind:           "service_account",
			SortAt:         message.CreatedAt,
			Projection:     projection,
		}
	}
	return &storeim.ConversationIndexItem{
		ScopeID:    scopeID,
		SortAt:     message.CreatedAt,
		Projection: projection,
	}
}

func conversationIndexFromConversation(item dto.ConversationItem) *storeim.ConversationIndexItem {
	scopeID := strings.TrimSpace(item.ScopeID)
	conversationID := strings.TrimSpace(item.ID)
	if scopeID == "" || conversationID == "" {
		return nil
	}
	kind := storeConversationKindFromDTO(item.Kind)
	return &storeim.ConversationIndexItem{
		ConversationID: conversationID,
		ScopeID:        scopeID,
		Kind:           kind,
		SortAt:         itemSortAt(item),
		PinnedAt:       item.PinnedAt,
		PinnedAtSet:    true,
		ArchivedAt:     archivedAtFromConversation(item),
		Projection:     jsonMap(item),
	}
}

func storeConversationKindFromDTO(kind string) string {
	switch strings.TrimSpace(kind) {
	case "replyThread":
		return message.ConversationKindThread
	case "humanDm", "agentDm", "serviceDm":
		return message.ConversationKindDM
	case "serviceAccount":
		return message.ConversationKindServiceAccount
	default:
		return strings.TrimSpace(kind)
	}
}

func itemSortAt(item dto.ConversationItem) time.Time {
	if item.LastActivityAt != nil && !item.LastActivityAt.IsZero() {
		return item.LastActivityAt.UTC()
	}
	if item.LastMessageAt != nil && !item.LastMessageAt.IsZero() {
		return item.LastMessageAt.UTC()
	}
	return time.Now().UTC()
}

func archivedAtFromConversation(item dto.ConversationItem) *time.Time {
	if !item.Archived {
		return nil
	}
	now := time.Now().UTC()
	return &now
}

func archivedAtFromDMChannel(channel dto.DMChannel) *time.Time {
	if !channel.SidebarHidden {
		return nil
	}
	now := time.Now().UTC()
	return &now
}

func replyThreadUpdatedPayload(updated message.ReplyThreadUpdated) map[string]any {
	return map[string]any{
		"parentMessageId": updated.ParentMessageID,
		"threadChannelId": updated.ThreadChannelID,
		"replyCount":      updated.ReplyCount,
		"lastReplyAt":     updated.LastReplyAt,
		"participantIds":  append([]string{}, updated.ParticipantIDs...),
		"participants":    converter.ToReplyThreadParticipants(updated.Participants),
		"replyAuthorIds":  append([]string{}, updated.ReplyAuthorIDs...),
		"replyAuthors":    converter.ToReplyThreadParticipants(updated.ReplyAuthors),
	}
}

func messageMentionPayload(appMessage message.AppMessage, delivery message.MessageDelivery) (map[string]any, []map[string]string) {
	if strings.TrimSpace(appMessage.ID) == "" || len(delivery.MentionedHumans) == 0 {
		return nil, nil
	}
	recipients := make([]map[string]string, 0, len(delivery.MentionedHumans))
	seen := make(map[string]struct{}, len(delivery.MentionedHumans))
	for _, recipient := range delivery.MentionedHumans {
		userID := strings.TrimSpace(recipient.UserID)
		memberID := strings.TrimSpace(recipient.MemberID)
		if userID == "" || memberID == "" {
			continue
		}
		key := userID + "\x00" + memberID
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		recipients = append(recipients, map[string]string{"userId": userID, "memberId": memberID})
	}
	if len(recipients) == 0 {
		return nil, nil
	}
	payload := map[string]any{
		"messageId":         strings.TrimSpace(appMessage.ID),
		"channelId":         strings.TrimSpace(appMessage.ChannelID),
		"channelType":       strings.TrimSpace(delivery.MentionContext.ChannelType),
		"parentChannelId":   strings.TrimSpace(delivery.MentionContext.ParentChannelID),
		"parentChannelType": strings.TrimSpace(delivery.MentionContext.ParentChannelType),
		"parentMessageId":   strings.TrimSpace(delivery.MentionContext.ParentMessageID),
		"authorProfile":     converter.ToAuthorProfile(appMessage.EffectiveAuthorProfile()),
		"contentPreview":    mentionNotificationPreview(appMessage.Content),
	}
	if taskContext := taskNotificationContextPayload(appMessage.TaskContext); taskContext != nil {
		payload["taskContext"] = taskContext
	}
	return payload, recipients
}

func mentionNotificationPreview(content string) string {
	trimmed := strings.TrimSpace(content)
	if trimmed == "" {
		return ""
	}
	const maxRunes = 140
	runes := []rune(trimmed)
	if len(runes) <= maxRunes {
		return trimmed
	}
	return string(runes[:maxRunes]) + "..."
}

func jsonMap(value any) map[string]any {
	if value == nil {
		return map[string]any{}
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return map[string]any{}
	}
	var decoded map[string]any
	if err := json.Unmarshal(encoded, &decoded); err != nil || decoded == nil {
		return map[string]any{}
	}
	return decoded
}
