package reaction

import (
	"context"
	"errors"
	"fmt"
	"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/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

var (
	ErrForbidden = storeerr.ErrForbidden
	ErrNotFound  = storeerr.ErrNotFound
)

const maxReactionEmojiKindsPerMessage = message.MaxReactionEmojiKindsPerMessage
const maxReactionReactorsPerEmoji = 10

func badRequest(message string) error { return storeerr.BadRequest(message) }
func forbidden(message string) error  { return storeerr.Forbidden(message) }
func conflict(message string) error   { return storeerr.Conflict(message) }

func withTxValue[T any](ctx context.Context, db coredb.TxBeginner, fn func(coredb.Tx) (T, error)) (T, error) {
	var value T
	err := coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
		var err error
		value, err = fn(tx)
		return err
	})
	if err != nil {
		var zero T
		return zero, err
	}
	return value, nil
}

func normalizeReactionToolMessageShortID(messageID string) (string, bool) {
	return message.NormalizeReactionToolMessageShortID(messageID)
}

func validateReactionEmoji(emoji string) (string, error) {
	return message.ValidateReactionEmoji(emoji)
}

func truncateReactionTargetPreview(content string) string {
	return message.TruncateReactionTargetPreview(content)
}

func truncateReactionNotificationPreview(content string) string {
	return message.TruncateReactionNotificationPreview(content)
}

func ReactionSummaryContent(emoji string) string { return message.ReactionSummaryContent(emoji) }
func reactionSummaryContent(emoji string) string { return ReactionSummaryContent(emoji) }

func reactionNotificationContext(target message.ThreadAccess) message.MentionContext {
	return message.MentionContextForTarget(target)
}

func reactionNotificationContextForScope(target message.ThreadAccess, targetScope reactionTargetScope) message.MentionContext {
	context := reactionNotificationContext(target)
	if target.ReplyThread == nil && strings.TrimSpace(targetScope.ScopeType) == string(access.ScopeTargetKindServiceAccount) {
		context.ChannelType = string(access.ScopeTargetKindServiceAccount)
	}
	if strings.TrimSpace(targetScope.ScopeType) == string(access.ScopeTargetKindTask) {
		context.ChannelType = string(access.ScopeTargetKindTask)
	}
	return context
}

func reactionNotificationTaskContextForScope(targetScope reactionTargetScope) *message.AppMessageTaskContext {
	boardChannelID := strings.TrimSpace(targetScope.TaskBoardChannelID)
	if strings.TrimSpace(targetScope.ScopeType) != string(access.ScopeTargetKindTask) || boardChannelID == "" || targetScope.TaskNumber <= 0 {
		return nil
	}
	return &message.AppMessageTaskContext{
		ChannelID:  boardChannelID,
		TaskNumber: targetScope.TaskNumber,
	}
}

func SyntheticReactionDeliverySeq(createdAt time.Time) int64 {
	return message.SyntheticReactionDeliverySeq(createdAt)
}
func syntheticReactionDeliverySeq(createdAt time.Time) int64 {
	return SyntheticReactionDeliverySeq(createdAt)
}

func currentDBTimestamp(ctx context.Context, queryer reactionQueryRower) (time.Time, error) {
	return message.CurrentDBTimestamp(ctx, queryer)
}

func ReactionMetadata(deliveryID string, reactionID string, targetMessageID string, targetMessagePreview string, emoji string, actor message.ReactionActorIdentity) map[string]any {
	return message.ReactionMetadata(deliveryID, reactionID, targetMessageID, targetMessagePreview, emoji, actor)
}
func reactionMetadata(deliveryID string, reactionID string, targetMessageID string, targetMessagePreview string, emoji string, actor message.ReactionActorIdentity) map[string]any {
	return ReactionMetadata(deliveryID, reactionID, targetMessageID, targetMessagePreview, emoji, actor)
}

func buildReactionReceiveMessage(target message.ThreadAccess, actor message.ReactionActorIdentity, deliveryID string, reactionID string, targetMessageID string, targetMessagePreview string, emoji string, createdAt time.Time) message.ReceiveMessage {
	builtMessage := message.BuildReactionReceiveMessage(target, actor, deliveryID, reactionID, targetMessageID, targetMessagePreview, emoji, createdAt)
	return message.ReceiveMessage{
		MessageID:         builtMessage.MessageID,
		ChannelType:       builtMessage.ChannelType,
		ChannelName:       builtMessage.ChannelName,
		ParentChannelType: builtMessage.ParentChannelType,
		ParentChannelName: builtMessage.ParentChannelName,
		AuthorProfile:     builtMessage.AuthorProfile,
		MessageType:       builtMessage.MessageType,
		Content:           builtMessage.Content,
		Metadata:          builtMessage.Metadata,
		Timestamp:         builtMessage.Timestamp,
	}
}

func reactionActorAuthorProfile(actor message.ReactionActorIdentity, principalID string) message.AuthorProfile {
	profile := actor.AuthorProfile.Normalize()
	if profile.Valid() {
		return profile
	}
	return message.AuthorProfile{
		ActorType:   strings.TrimSpace(actor.ActorType),
		PrincipalID: strings.TrimSpace(principalID),
		MemberID:    strings.TrimSpace(actor.MemberID),
		TargetID:    strings.TrimSpace(actor.ActorName),
		DisplayName: strings.TrimSpace(actor.ActorName),
	}.Normalize()
}

func loadExistingActorReaction(ctx context.Context, queryer reactionQueryRower, messageID string, emoji string, reactorMemberID string) (model.StoredReaction, bool, error) {
	row := queryer.QueryRow(ctx, `
SELECT id, emoji, created_at
FROM scope_message_reactions
WHERE message_id = $1
  AND emoji = $2
  AND reactor_member_id = $3
`, strings.TrimSpace(messageID), strings.TrimSpace(emoji), strings.TrimSpace(reactorMemberID))

	var reaction model.StoredReaction
	if err := row.Scan(&reaction.ID, &reaction.Emoji, &reaction.CreatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.StoredReaction{}, false, nil
		}
		return model.StoredReaction{}, false, fmt.Errorf("load existing actor reaction: %w", err)
	}
	return reaction, true, nil
}

func enforceReactionEmojiKindsLimit(ctx context.Context, queryer reactionQueryRower, messageID string, emoji string) error {
	row := queryer.QueryRow(ctx, `
SELECT
    EXISTS (
        SELECT 1
        FROM scope_message_reactions
        WHERE message_id = $1
          AND emoji = $2
    ),
    COALESCE(COUNT(DISTINCT emoji), 0)
FROM scope_message_reactions
WHERE message_id = $1
`, strings.TrimSpace(messageID), strings.TrimSpace(emoji))

	var emojiExists bool
	var emojiKinds int64
	if err := row.Scan(&emojiExists, &emojiKinds); err != nil {
		return fmt.Errorf("load reaction emoji kinds: %w", err)
	}
	if !emojiExists && emojiKinds >= maxReactionEmojiKindsPerMessage {
		return badRequest(fmt.Sprintf("a message can have at most %d reaction types", maxReactionEmojiKindsPerMessage))
	}
	return nil
}

func attachHistoryMessageReactions(messages []message.HistoryMessage, reactionsByMessageID map[string][]message.MessageReactionAggregate) {
	if len(messages) == 0 || len(reactionsByMessageID) == 0 {
		return
	}
	for i := range messages {
		reactions := reactionsByMessageID[strings.TrimSpace(messages[i].ID)]
		if len(reactions) == 0 {
			continue
		}
		metadata := message.CopyMessageMetadata(messages[i].Metadata)
		if metadata == nil {
			metadata = make(map[string]any, 1)
		}
		metadata["reactions"] = reactions
		messages[i].Metadata = metadata
	}
}

func attachAppMessageReactions(messages []message.AppMessage, reactionsByMessageID map[string][]message.MessageReactionAggregate) {
	if len(messages) == 0 || len(reactionsByMessageID) == 0 {
		return
	}
	for i := range messages {
		reactions := reactionsByMessageID[strings.TrimSpace(messages[i].ID)]
		if len(reactions) == 0 {
			continue
		}
		messages[i].Reactions = reactions
	}
}

func appChannelIDForTarget(target message.ThreadAccess) string {
	trimmedChannelID := strings.TrimSpace(target.ChannelID)
	if trimmedChannelID != "" {
		return trimmedChannelID
	}
	return strings.TrimSpace(target.DMThreadID)
}

func mapMessagingError(err error) error {
	if err == nil {
		return nil
	}
	switch {
	case errors.Is(err, domain.ErrInvalidArgument):
		return badRequest(err.Error())
	case errors.Is(err, message.ErrConflict):
		return conflict(err.Error())
	case errors.Is(err, message.ErrForbidden):
		return ErrForbidden
	case errors.Is(err, message.ErrNotFound):
		return ErrNotFound
	default:
		return err
	}
}

func isUniqueViolation(err error) bool { return coredb.IsUniqueViolation(err) }

func nullableReactionThreadID(threadID string) any {
	trimmed := strings.TrimSpace(threadID)
	if trimmed == "" {
		return nil
	}
	return trimmed
}

func normalizeNonEmptyStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	normalized := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, exists := seen[trimmed]; exists {
			continue
		}
		seen[trimmed] = struct{}{}
		normalized = append(normalized, trimmed)
	}
	return normalized
}

func publicMemberNameForMemberID(memberID string, publicNames map[string]string) string {
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedMemberID == "" {
		return ""
	}
	if publicName := strings.TrimSpace(publicNames[trimmedMemberID]); publicName != "" {
		return publicName
	}
	return trimmedMemberID
}

func collectAppMessageIDs(messages []message.AppMessage) []string {
	if len(messages) == 0 {
		return nil
	}
	ids := make([]string, 0, len(messages))
	for _, message := range messages {
		trimmed := strings.TrimSpace(message.ID)
		if trimmed == "" {
			continue
		}
		ids = append(ids, trimmed)
	}
	return ids
}

func rollback(tx coredb.Tx, ctx context.Context) {
	_ = tx.Rollback(ctx)
}
