// Package walletmq defines RocketMQ payloads owned by wallet-service.
package walletmq

import (
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
)

const (
	MessageTypeWalletOutboxEvent = "wallet_outbox_event"

	// TagWalletOutboxEvent 是 typed tag 上线前所有钱包事实共用的 legacy Tag。
	// 兼容窗口内消费者必须同时订阅它和自己需要的 EventType Tag，确保 producer
	// 切换前已经进入 Broker 的旧消息仍会被同一 consumer group 完整消费。
	TagWalletOutboxEvent = "wallet_outbox_event"

	// 以下常量只描述当前 wallet topic 消费者实际处理的事件类型。producer 使用
	// outbox.event_type 作为 Tag，因此消费者和消息体分派必须复用同一拼写，避免
	// Tag 过滤成功但 handler 因事件名漂移而静默跳过。
	EventTypeWalletRechargeRecorded             = "WalletRechargeRecorded"
	EventTypeWalletCoinSellerStockPurchased     = "WalletCoinSellerStockPurchased"
	EventTypeWalletLuckyGiftRewardCredited      = "WalletLuckyGiftRewardCredited"
	EventTypeWalletTaskRewardCredited           = "WalletTaskRewardCredited"
	EventTypeWalletWheelRewardCredited          = "WalletWheelRewardCredited"
	EventTypeWalletRoomTurnoverRewardCredited   = "WalletRoomTurnoverRewardCredited"
	EventTypeWalletInviteActivityRewardCredited = "WalletInviteActivityRewardCredited"
	EventTypeWalletAgencyOpeningRewardCredited  = "WalletAgencyOpeningRewardCredited"
	EventTypeWalletGiftIncomeCredited           = "WalletGiftIncomeCredited"
	EventTypeWalletBalanceChanged               = "WalletBalanceChanged"
	EventTypeWalletGiftDebited                  = "WalletGiftDebited"
	EventTypeWalletRedPacketCreated             = "WalletRedPacketCreated"
	EventTypeWalletRedPacketClaimed             = "WalletRedPacketClaimed"
	EventTypeWalletRedPacketRefunded            = "WalletRedPacketRefunded"
	EventTypeVIPActivated                       = "VipActivated"
	EventTypeVIPProgramChanged                  = "VipProgramChanged"
	EventTypeVIPDailyCoinRebateAvailable        = "VipDailyCoinRebateAvailable"
	EventTypeResourceGranted                    = "ResourceGranted"
	EventTypeResourceGroupGranted               = "ResourceGroupGranted"
)

// EventTypeTag converts one committed wallet event type into its RocketMQ Tag.
// Event types are controlled identifiers rather than user input; rejecting expression
// operators and punctuation here prevents a malformed outbox row from widening a
// subscription or being published under a Tag no consumer can express safely.
func EventTypeTag(eventType string) (string, error) {
	tag := strings.TrimSpace(eventType)
	if tag == "" {
		return "", errors.New("wallet event_type tag is required")
	}
	if tag == TagWalletOutboxEvent {
		return "", errors.New("wallet event_type cannot reuse the legacy wallet tag")
	}
	for _, char := range tag {
		if (char >= 'a' && char <= 'z') ||
			(char >= 'A' && char <= 'Z') ||
			(char >= '0' && char <= '9') ||
			char == '_' || char == '-' {
			continue
		}
		return "", fmt.Errorf("wallet event_type %q cannot be used as a RocketMQ tag", eventType)
	}
	return tag, nil
}

// LegacyCompatibleTagExpression builds the compatibility selector used during
// the typed-tag migration: legacy messages remain consumable, while newly published
// messages are filtered by the exact event types handled by this consumer.
// Sorting and deduplication make the expression deterministic so every process in
// the same consumer group registers byte-for-byte identical subscription metadata.
func LegacyCompatibleTagExpression(eventTypes ...string) (string, error) {
	typedTags := make([]string, 0, len(eventTypes))
	seen := make(map[string]struct{}, len(eventTypes))
	for _, eventType := range eventTypes {
		tag, err := EventTypeTag(eventType)
		if err != nil {
			return "", err
		}
		if _, exists := seen[tag]; exists {
			continue
		}
		seen[tag] = struct{}{}
		typedTags = append(typedTags, tag)
	}
	if len(typedTags) == 0 {
		return "", errors.New("at least one typed wallet event tag is required")
	}
	sort.Strings(typedTags)
	return TagWalletOutboxEvent + " || " + strings.Join(typedTags, " || "), nil
}

// WalletOutboxMessage is the MQ representation of one committed wallet_outbox fact.
type WalletOutboxMessage struct {
	MessageType    string `json:"message_type"`
	AppCode        string `json:"app_code"`
	EventID        string `json:"event_id"`
	EventType      string `json:"event_type"`
	TransactionID  string `json:"transaction_id"`
	CommandID      string `json:"command_id"`
	UserID         int64  `json:"user_id"`
	AssetType      string `json:"asset_type"`
	AvailableDelta int64  `json:"available_delta"`
	FrozenDelta    int64  `json:"frozen_delta"`
	PayloadJSON    string `json:"payload_json"`
	OccurredAtMS   int64  `json:"occurred_at_ms"`
}

// EncodeWalletOutboxMessage serializes a wallet fact for fanout through RocketMQ.
func EncodeWalletOutboxMessage(message WalletOutboxMessage) ([]byte, error) {
	message.MessageType = MessageTypeWalletOutboxEvent
	if strings.TrimSpace(message.AppCode) == "" ||
		strings.TrimSpace(message.EventID) == "" ||
		strings.TrimSpace(message.EventType) == "" ||
		strings.TrimSpace(message.TransactionID) == "" ||
		strings.TrimSpace(message.CommandID) == "" ||
		message.OccurredAtMS <= 0 {
		return nil, errors.New("wallet outbox message is incomplete")
	}
	if strings.TrimSpace(message.PayloadJSON) == "" {
		message.PayloadJSON = "{}"
	}
	return json.Marshal(message)
}

// DecodeWalletOutboxMessage validates the MQ body and restores the wallet fact.
func DecodeWalletOutboxMessage(body []byte) (WalletOutboxMessage, error) {
	var message WalletOutboxMessage
	if err := json.Unmarshal(body, &message); err != nil {
		return WalletOutboxMessage{}, err
	}
	if message.MessageType != MessageTypeWalletOutboxEvent {
		return WalletOutboxMessage{}, errors.New("unexpected wallet outbox message_type")
	}
	if strings.TrimSpace(message.AppCode) == "" ||
		strings.TrimSpace(message.EventID) == "" ||
		strings.TrimSpace(message.EventType) == "" ||
		strings.TrimSpace(message.TransactionID) == "" ||
		strings.TrimSpace(message.CommandID) == "" ||
		message.OccurredAtMS <= 0 {
		return WalletOutboxMessage{}, errors.New("wallet outbox message is incomplete")
	}
	if strings.TrimSpace(message.PayloadJSON) == "" {
		message.PayloadJSON = "{}"
	}
	return message, nil
}
