package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
)

// WalletOutboxRecord is a committed wallet fact ready for external fanout.
type WalletOutboxRecord struct {
	AppCode        string
	EventID        string
	EventType      string
	TransactionID  string
	CommandID      string
	UserID         int64
	AssetType      string
	AvailableDelta int64
	FrozenDelta    int64
	PayloadJSON    string
	CreatedAtMS    int64
	RetryCount     int
	LastError      string
	WorkerID       string
	LockUntilMS    int64
}

// WalletOutboxClaimFilter narrows one MQ fanout lane by event type.
type WalletOutboxClaimFilter struct {
	// IncludeEventTypes lets a realtime worker own only the configured UI event facts.
	IncludeEventTypes []string
	// ExcludeEventTypes lets the generic wallet worker skip facts owned by the realtime lane.
	ExcludeEventTypes []string
}

type walletOutboxEvent struct {
	AppCode        string
	EventID        string
	EventType      string
	TransactionID  string
	CommandID      string
	UserID         int64
	AssetType      string
	AvailableDelta int64
	FrozenDelta    int64
	Payload        any
	CreatedAtMS    int64
}

func (r *Repository) insertWalletOutbox(ctx context.Context, tx *sql.Tx, events []walletOutboxEvent) error {
	for _, event := range events {
		payloadJSON, err := json.Marshal(event.Payload)
		if err != nil {
			return err
		}
		eventAppCode := normalizedEntryAppCode(ctx, event.AppCode)
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO wallet_outbox (
			   app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
			   available_delta, frozen_delta, payload, status, retry_count, created_at_ms, updated_at_ms
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`,
			eventAppCode,
			event.EventID,
			event.EventType,
			event.TransactionID,
			event.CommandID,
			event.UserID,
			event.AssetType,
			event.AvailableDelta,
			event.FrozenDelta,
			string(payloadJSON),
			outboxStatusPending,
			event.CreatedAtMS,
			event.CreatedAtMS,
		); err != nil {
			if !isMySQLDuplicateError(err) {
				return err
			}
			// event_id 是 outbox 事实的幂等键。读取时懒同步、事务重试或并发首发可能再次
			// 生成同一事实；保留首次落库的 payload/发生时间/投递状态，不把重放改成新事件。
			// 锁定点查同时防止哈希键被不同核心语义复用，避免 INSERT IGNORE 吞掉真实冲突。
			var stored walletOutboxEvent
			if scanErr := tx.QueryRowContext(ctx, `
				SELECT event_type, transaction_id, command_id, user_id, asset_type,
				       available_delta, frozen_delta
				FROM wallet_outbox
				WHERE app_code = ? AND event_id = ?
				FOR UPDATE`,
				eventAppCode, event.EventID,
			).Scan(
				&stored.EventType,
				&stored.TransactionID,
				&stored.CommandID,
				&stored.UserID,
				&stored.AssetType,
				&stored.AvailableDelta,
				&stored.FrozenDelta,
			); scanErr != nil {
				return scanErr
			}
			if stored.EventType != event.EventType ||
				stored.TransactionID != event.TransactionID ||
				stored.CommandID != event.CommandID ||
				stored.UserID != event.UserID ||
				stored.AssetType != event.AssetType ||
				stored.AvailableDelta != event.AvailableDelta ||
				stored.FrozenDelta != event.FrozenDelta {
				return xerr.New(xerr.LedgerConflict, "wallet outbox event id conflicts with a different fact")
			}
		}
	}
	return nil
}

// ListWalletOutboxAppCodes enumerates the tenant key stored in this owner table.
// PRIMARY starts with app_code, so MySQL can use a loose ordered index scan;
// callers cache the result and never run this discovery query on every poll.
func (r *Repository) ListWalletOutboxAppCodes(ctx context.Context) ([]string, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, `SELECT DISTINCT app_code FROM wallet_outbox FORCE INDEX (PRIMARY) ORDER BY app_code`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	appCodes := make([]string, 0, 4)
	for rows.Next() {
		var appCode string
		if err := rows.Scan(&appCode); err != nil {
			return nil, err
		}
		appCodes = append(appCodes, appCode)
	}
	return appCodes, rows.Err()
}

// ClaimPendingWalletOutbox atomically claims committed wallet facts for MQ fanout.
func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID string, limit int, lockUntilMS int64) ([]WalletOutboxRecord, error) {
	return r.ClaimPendingWalletOutboxFiltered(ctx, workerID, limit, lockUntilMS, WalletOutboxClaimFilter{})
}

// ClaimPendingWalletOutboxFiltered atomically claims committed wallet facts for one MQ fanout lane.
func (r *Repository) ClaimPendingWalletOutboxFiltered(ctx context.Context, workerID string, limit int, lockUntilMS int64, filter WalletOutboxClaimFilter) ([]WalletOutboxRecord, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		return nil, xerr.New(xerr.InvalidArgument, "worker_id is required")
	}
	if limit <= 0 {
		limit = 100
	}
	if limit > 500 {
		limit = 500
	}
	nowMS := time.Now().UTC().UnixMilli()
	if lockUntilMS <= nowMS {
		lockUntilMS = time.Now().UTC().Add(30 * time.Second).UnixMilli()
	}

	appCode := appcode.FromContext(ctx)
	eventFilterSQL, eventFilterArgs := walletOutboxEventFilterSQL(filter)
	pendingIndex, claimIndex := walletOutboxClaimIndexes(filter)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()

	records := make([]WalletOutboxRecord, 0, limit)
	// Recovery work must claim before fresh pending facts. A continuously growing
	// pending backlog can otherwise fill every batch and permanently strand both
	// due retries and deliveries whose worker lease expired. Keep the states in
	// separate SKIP LOCKED queries so each branch retains its status-leading index.
	claimBranches := []struct {
		query string
		args  []any
	}{
		{
			query: walletOutboxClaimQuery(pendingIndex, `
		  AND next_retry_at_ms <= ?
		  AND lock_until_ms IS NULL`, eventFilterSQL),
			args: append([]any{appCode, outboxStatusRetryable, nowMS}, eventFilterArgs...),
		},
		{
			query: walletOutboxClaimQuery(claimIndex, `
			  AND lock_until_ms <= ?`, eventFilterSQL),
			args: append([]any{appCode, outboxStatusDelivering, nowMS}, eventFilterArgs...),
		},
		{
			query: walletOutboxClaimQuery(pendingIndex, `
		  AND next_retry_at_ms IS NULL
		  AND lock_until_ms IS NULL`, eventFilterSQL),
			args: append([]any{appCode, outboxStatusPending}, eventFilterArgs...),
		},
	}
	for _, branch := range claimBranches {
		remaining := limit - len(records)
		if remaining <= 0 {
			break
		}
		args := append(append([]any{}, branch.args...), remaining)
		branchRecords, err := queryWalletOutboxRecords(ctx, tx, branch.query, args...)
		if err != nil {
			return nil, err
		}
		records = append(records, branchRecords...)
	}

	for _, record := range records {
		if _, err := tx.ExecContext(ctx, `
			UPDATE wallet_outbox
			SET status = ?, worker_id = ?, lock_until_ms = ?, updated_at_ms = ?
			WHERE app_code = ? AND event_id = ?`,
			outboxStatusDelivering,
			workerID,
			lockUntilMS,
			nowMS,
			record.AppCode,
			record.EventID,
		); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	for index := range records {
		records[index].WorkerID = workerID
		records[index].LockUntilMS = lockUntilMS
	}
	return records, nil
}

func walletOutboxClaimQuery(indexName string, statusCondition string, eventFilterSQL string) string {
	return `
		SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
		       available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
		       retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
		FROM wallet_outbox FORCE INDEX (` + indexName + `)
		WHERE app_code = ? AND status = ?` + statusCondition + eventFilterSQL + `
		ORDER BY created_at_ms ASC, event_id ASC
		LIMIT ?
		FOR UPDATE SKIP LOCKED`
}

func walletOutboxEventFilterSQL(filter WalletOutboxClaimFilter) (string, []any) {
	include := normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)
	if len(include) > 0 {
		return " AND event_type IN (" + walletOutboxPlaceholders(len(include)) + ")", walletOutboxEventArgs(include)
	}
	exclude := normalizeWalletOutboxEventTypes(filter.ExcludeEventTypes)
	if len(exclude) > 0 {
		return " AND event_type NOT IN (" + walletOutboxPlaceholders(len(exclude)) + ")", walletOutboxEventArgs(exclude)
	}
	return "", nil
}

func walletOutboxClaimIndexes(filter WalletOutboxClaimFilter) (string, string) {
	if len(normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)) > 0 {
		return "idx_wallet_outbox_event_pending", "idx_wallet_outbox_event_claim"
	}
	return "idx_wallet_outbox_pending", "idx_wallet_outbox_claim"
}

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

func walletOutboxPlaceholders(count int) string {
	if count <= 0 {
		return ""
	}
	placeholders := make([]string, count)
	for index := range placeholders {
		placeholders[index] = "?"
	}
	return strings.Join(placeholders, ",")
}

func walletOutboxEventArgs(values []string) []any {
	args := make([]any, 0, len(values))
	for _, value := range values {
		args = append(args, value)
	}
	return args
}

func queryWalletOutboxRecords(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]WalletOutboxRecord, error) {
	rows, err := tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	records := make([]WalletOutboxRecord, 0)
	for rows.Next() {
		var record WalletOutboxRecord
		if err := rows.Scan(
			&record.AppCode,
			&record.EventID,
			&record.EventType,
			&record.TransactionID,
			&record.CommandID,
			&record.UserID,
			&record.AssetType,
			&record.AvailableDelta,
			&record.FrozenDelta,
			&record.PayloadJSON,
			&record.CreatedAtMS,
			&record.RetryCount,
			&record.LastError,
			&record.WorkerID,
			&record.LockUntilMS,
		); err != nil {
			return nil, err
		}
		records = append(records, record)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return records, nil
}

// MarkWalletOutboxDelivered marks one wallet fact as accepted by the MQ broker.
func (r *Repository) MarkWalletOutboxDelivered(ctx context.Context, eventID string) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE wallet_outbox
		SET status = ?, worker_id = '', lock_until_ms = NULL, next_retry_at_ms = NULL, last_error = NULL, updated_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		outboxStatusDelivered,
		time.Now().UTC().UnixMilli(),
		appcode.FromContext(ctx),
		eventID,
	)
	return err
}

// MarkWalletOutboxRetryable releases one failed MQ publish with a bounded retry schedule.
func (r *Repository) MarkWalletOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE wallet_outbox
		SET status = ?, worker_id = '', lock_until_ms = NULL, retry_count = retry_count + 1, next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		outboxStatusRetryable,
		nextRetryAtMS,
		trimWalletOutboxError(lastErr),
		time.Now().UTC().UnixMilli(),
		appcode.FromContext(ctx),
		eventID,
	)
	return err
}

// MarkWalletOutboxDead stops retrying a poison wallet fact after the configured retry ceiling.
func (r *Repository) MarkWalletOutboxDead(ctx context.Context, eventID string, lastErr string) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE wallet_outbox
		SET status = ?, worker_id = '', lock_until_ms = NULL, last_error = ?, updated_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		outboxStatusFailed,
		trimWalletOutboxError(lastErr),
		time.Now().UTC().UnixMilli(),
		appcode.FromContext(ctx),
		eventID,
	)
	return err
}

func trimWalletOutboxError(value string) string {
	value = strings.TrimSpace(value)
	if len(value) > 1024 {
		return value[:1024]
	}
	return value
}

func balanceChangedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, availableAfter int64, frozenAfter int64, balanceVersion int64, payload any, nowMs int64, bizTypes ...string) walletOutboxEvent {
	bizType := ""
	if len(bizTypes) > 0 {
		bizType = strings.TrimSpace(bizTypes[0])
	}
	eventPayload := map[string]any{
		"transaction_id":  transactionID,
		"command_id":      commandID,
		"user_id":         userID,
		"asset_type":      assetType,
		"biz_type":        bizType,
		"available_delta": availableDelta,
		"frozen_delta":    frozenDelta,
		"available_after": availableAfter,
		"frozen_after":    frozenAfter,
		"balance_version": balanceVersion,
		"metadata":        payload,
		"created_at_ms":   nowMs,
	}
	if suppressWalletPrivateNotice(assetType, payload) {
		// 抑制决策必须和账务事实一起固化，notice-service 只能消费这份不可变快照，
		// 不能跨库读取用户 source 或根据当前资料反推交易发生时的账号语义。
		eventPayload["suppress_private_notice"] = true
	}
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletBalanceChanged", userID, assetType),
		EventType:      "WalletBalanceChanged",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         userID,
		AssetType:      assetType,
		AvailableDelta: availableDelta,
		FrozenDelta:    frozenDelta,
		Payload:        eventPayload,
		CreatedAtMS:    nowMs,
	}
}

func suppressWalletPrivateNotice(assetType string, payload any) bool {
	if strings.EqualFold(strings.TrimSpace(assetType), ledger.AssetRobotCoin) {
		return true
	}
	// giftMetadata.RobotGift 是 wallet 扣费时的 owner 快照；只认可强类型业务元数据，
	// 不在通用 JSON/map 里猎猜 source、command_id 或 biz_type。
	switch metadata := payload.(type) {
	case giftMetadata:
		return metadata.RobotGift
	case *giftMetadata:
		return metadata != nil && metadata.RobotGift
	default:
		return false
	}
}

func coinSellerTransferredEvent(transactionID string, commandID string, sellerUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletCoinSellerTransferred", sellerUserID, ledger.AssetCoinSellerCoin),
		EventType:      "WalletCoinSellerTransferred",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         sellerUserID,
		AssetType:      ledger.AssetCoinSellerCoin,
		AvailableDelta: availableDelta,
		FrozenDelta:    0,
		Payload:        payload,
		CreatedAtMS:    nowMs,
	}
}

func coinSellerSubTransferredEvent(transactionID string, commandID string, metadata coinSellerSubTransferMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletCoinSellerSubTransferred", metadata.ParentUserID, ledger.AssetCoinSellerCoin),
		EventType:      "WalletCoinSellerSubTransferred",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.ParentUserID,
		AssetType:      ledger.AssetCoinSellerCoin,
		AvailableDelta: -metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":       transactionID,
			"command_id":           commandID,
			"parent_user_id":       metadata.ParentUserID,
			"child_user_id":        metadata.ChildUserID,
			"region_id":            metadata.RegionID,
			"amount":               metadata.Amount,
			"asset_type":           metadata.AssetType,
			"parent_balance_after": metadata.ParentBalanceAfter,
			"child_balance_after":  metadata.ChildBalanceAfter,
			"created_at_ms":        nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func rechargeRecordedEvent(transactionID string, commandID string, targetUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent {
	assetType := ledger.AssetCoin
	if metadata, ok := payload.(coinSellerTransferMetadata); ok {
		if strings.TrimSpace(metadata.TargetAssetType) != "" {
			assetType = metadata.TargetAssetType
		}
		rechargeType := strings.TrimSpace(metadata.RechargeType)
		if rechargeType == "" {
			rechargeType = "coin_seller_transfer"
		}

		payload = map[string]any{
			"app_code":                         metadata.AppCode,
			"payment_order_id":                 metadata.PaymentOrderID,
			"provider":                         metadata.ProviderCode,
			"payment_method_id":                metadata.PaymentMethodID,
			"seller_user_id":                   metadata.SellerUserID,
			"target_user_id":                   metadata.TargetUserID,
			"target_country_id":                metadata.TargetCountryID,
			"country_id":                       metadata.TargetCountryID,
			"google_product_id":                metadata.GoogleProductID,
			"product_code":                     metadata.ProductCode,
			"product_name":                     metadata.ProductName,
			"seller_region_id":                 metadata.SellerRegionID,
			"target_region_id":                 metadata.TargetRegionID,
			"region_id":                        metadata.TargetRegionID,
			"amount":                           metadata.Amount,
			"reason":                           metadata.Reason,
			"seller_asset_type":                metadata.SellerAssetType,
			"target_asset_type":                metadata.TargetAssetType,
			"seller_balance_after":             metadata.SellerBalanceAfter,
			"target_balance_after":             metadata.TargetBalanceAfter,
			"recharge_sequence":                metadata.RechargeSequence,
			"recharge_usd_minor":               metadata.RechargeUSDMinor,
			"recharge_currency_code":           metadata.RechargeCurrencyCode,
			"recharge_policy_id":               metadata.RechargePolicyID,
			"recharge_policy_version":          metadata.RechargePolicyVersion,
			"recharge_policy_coin_amount":      metadata.RechargePolicyCoinAmount,
			"recharge_policy_usd_minor_amount": metadata.RechargePolicyUSDMinorAmount,
			"recharge_type":                    rechargeType,
			"created_at_ms":                    nowMs,
		}
	}
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletRechargeRecorded", targetUserID, assetType),
		EventType:      "WalletRechargeRecorded",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         targetUserID,
		AssetType:      assetType,
		AvailableDelta: availableDelta,
		FrozenDelta:    0,
		Payload:        payload,
		CreatedAtMS:    nowMs,
	}
}

func taskRewardCreditedEvent(transactionID string, commandID string, metadata taskRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletTaskRewardCredited", metadata.TargetUserID, metadata.AssetType),
		EventType:      "WalletTaskRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      metadata.AssetType,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id": transactionID,
			"command_id":     commandID,
			"user_id":        metadata.TargetUserID,
			"task_type":      metadata.TaskType,
			"task_id":        metadata.TaskID,
			"cycle_key":      metadata.CycleKey,
			"asset_type":     metadata.AssetType,
			"amount":         metadata.Amount,
			"reason":         metadata.Reason,
			"created_at_ms":  nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func luckyGiftRewardCreditedEvent(transactionID string, commandID string, metadata luckyGiftRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletLuckyGiftRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletLuckyGiftRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":    transactionID,
			"command_id":        commandID,
			"user_id":           metadata.TargetUserID,
			"draw_id":           metadata.DrawID,
			"room_id":           metadata.RoomID,
			"visible_region_id": metadata.VisibleRegionID,
			"region_id":         metadata.VisibleRegionID,
			"country_id":        metadata.CountryID,
			"gift_id":           metadata.GiftID,
			"pool_id":           metadata.PoolID,
			"amount":            metadata.Amount,
			"reason":            metadata.Reason,
			"created_at_ms":     nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func wheelRewardCreditedEvent(transactionID string, commandID string, metadata wheelRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletWheelRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletWheelRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":    transactionID,
			"command_id":        commandID,
			"user_id":           metadata.TargetUserID,
			"draw_id":           metadata.DrawID,
			"wheel_id":          metadata.WheelID,
			"selected_tier_id":  metadata.SelectedTierID,
			"visible_region_id": metadata.VisibleRegionID,
			"region_id":         metadata.VisibleRegionID,
			"amount":            metadata.Amount,
			"reason":            metadata.Reason,
			"created_at_ms":     nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func roomTurnoverRewardCreditedEvent(transactionID string, commandID string, metadata roomTurnoverRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletRoomTurnoverRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletRoomTurnoverRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":  transactionID,
			"command_id":      commandID,
			"user_id":         metadata.TargetUserID,
			"settlement_id":   metadata.SettlementID,
			"room_id":         metadata.RoomID,
			"period_start_ms": metadata.PeriodStartMS,
			"period_end_ms":   metadata.PeriodEndMS,
			"coin_spent":      metadata.CoinSpent,
			"tier_id":         metadata.TierID,
			"tier_code":       metadata.TierCode,
			"amount":          metadata.Amount,
			"reason":          metadata.Reason,
			"created_at_ms":   nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func inviteActivityRewardCreditedEvent(transactionID string, commandID string, metadata inviteActivityRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletInviteActivityRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletInviteActivityRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id": transactionID,
			"command_id":     commandID,
			"user_id":        metadata.TargetUserID,
			"claim_id":       metadata.ClaimID,
			"reward_type":    metadata.RewardType,
			"tier_id":        metadata.TierID,
			"tier_code":      metadata.TierCode,
			"cycle_key":      metadata.CycleKey,
			"reached_value":  metadata.ReachedValue,
			"amount":         metadata.Amount,
			"reason":         metadata.Reason,
			"created_at_ms":  nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func agencyOpeningRewardCreditedEvent(transactionID string, commandID string, metadata agencyOpeningRewardMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletAgencyOpeningRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletAgencyOpeningRewardCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.Amount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id": transactionID,
			"command_id":     commandID,
			"user_id":        metadata.TargetUserID,
			"application_id": metadata.ApplicationID,
			"cycle_id":       metadata.CycleID,
			"agency_id":      metadata.AgencyID,
			"rank_no":        metadata.RankNo,
			"score_coins":    metadata.ScoreCoins,
			"amount":         metadata.Amount,
			"reason":         metadata.Reason,
			"created_at_ms":  nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func coinSellerStockCreditedEvent(transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) walletOutboxEvent {
	eventType := "WalletCoinSellerCoinCompensated"
	if metadata.StockType == ledger.StockTypeUSDTPurchase || metadata.StockType == ledger.StockTypeUSDTDeduction {
		eventType = "WalletCoinSellerStockPurchased"
	}
	return walletOutboxEvent{
		EventID:        eventID(transactionID, eventType, metadata.SellerUserID, ledger.AssetCoinSellerCoin),
		EventType:      eventType,
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.SellerUserID,
		AssetType:      ledger.AssetCoinSellerCoin,
		AvailableDelta: metadata.CoinAmount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":            transactionID,
			"command_id":                commandID,
			"seller_user_id":            metadata.SellerUserID,
			"seller_country_id":         metadata.SellerCountryID,
			"seller_region_id":          metadata.SellerRegionID,
			"country_id":                metadata.SellerCountryID,
			"region_id":                 metadata.SellerRegionID,
			"stock_type":                metadata.StockType,
			"coin_amount":               metadata.CoinAmount,
			"paid_currency_code":        metadata.PaidCurrencyCode,
			"paid_amount_micro":         metadata.PaidAmountMicro,
			"counts_as_seller_recharge": metadata.CountsAsSellerRecharge,
			"operator_user_id":          metadata.OperatorUserID,
			"created_at_ms":             nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func giftDebitedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, payload any, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletGiftDebited", userID, assetType),
		EventType:      "WalletGiftDebited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         userID,
		AssetType:      assetType,
		AvailableDelta: availableDelta,
		FrozenDelta:    frozenDelta,
		Payload:        payload,
		CreatedAtMS:    nowMs,
	}
}

func giftIncomeCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "WalletGiftIncomeCredited", metadata.TargetUserID, ledger.AssetCoin),
		EventType:      "WalletGiftIncomeCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: metadata.GiftIncomeCoinAmount,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":            transactionID,
			"command_id":                commandID,
			"user_id":                   metadata.TargetUserID,
			"sender_user_id":            metadata.SenderUserID,
			"room_id":                   metadata.RoomID,
			"direct_gift":               metadata.DirectGift,
			"gift_id":                   metadata.GiftID,
			"gift_count":                metadata.GiftCount,
			"gift_type_code":            metadata.GiftTypeCode,
			"gift_charge_amount":        metadata.ChargeAmount,
			"charge_source":             metadata.ChargeSource,
			"amount":                    metadata.GiftIncomeCoinAmount,
			"ratio_percent":             metadata.GiftIncomeRatioPercent,
			"ratio_region_id":           metadata.GiftIncomeRatioRegionID,
			"gift_income_balance_after": metadata.GiftIncomeBalanceAfter,
			"created_at_ms":             nowMs,
		},
		CreatedAtMS: nowMs,
	}
}

func hostPeriodDiamondCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "HostPeriodDiamondCredited", metadata.TargetUserID, ledger.AssetHostPeriodDiamond),
		EventType:      "HostPeriodDiamondCredited",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         metadata.TargetUserID,
		AssetType:      ledger.AssetHostPeriodDiamond,
		AvailableDelta: metadata.HostPeriodDiamondAdded,
		FrozenDelta:    0,
		Payload: map[string]any{
			"transaction_id":                         transactionID,
			"command_id":                             commandID,
			"host_user_id":                           metadata.TargetUserID,
			"cycle_key":                              metadata.HostPeriodCycleKey,
			"region_id":                              metadata.TargetHostRegionID,
			"agency_owner_user_id":                   metadata.TargetAgencyOwnerUserID,
			"diamond_delta":                          metadata.HostPeriodDiamondAdded,
			"diamond_after":                          metadata.HostPeriodDiamondAfter,
			"gift_charge_asset_type":                 metadata.ChargeAssetType,
			"gift_charge_amount":                     metadata.ChargeAmount,
			"gift_id":                                metadata.GiftID,
			"gift_count":                             metadata.GiftCount,
			"gift_type_code":                         metadata.GiftTypeCode,
			"sender_user_id":                         metadata.SenderUserID,
			"sender_region_id":                       metadata.SenderRegionID,
			"room_id":                                metadata.RoomID,
			"gift_diamond_ratio_percent":             metadata.GiftDiamondRatioPercent,
			"gift_diamond_ratio_ppm":                 metadata.GiftDiamondRatioPPM,
			"gift_diamond_ratio_region_id":           metadata.GiftDiamondRatioRegionID,
			"host_salary_policy_id":                  metadata.HostSalaryPolicyID,
			"host_salary_policy_version":             metadata.HostSalaryPolicyVersion,
			"host_salary_policy_diamond_coefficient": metadata.HostSalaryPolicyDiamondCoefficient,
			"host_salary_policy_diamond_coefficient_ppm": metadata.HostSalaryPolicyDiamondCoefficientPPM,
			"host_income_policy_type":                    metadata.HostIncomePolicyType,
			"income_asset_type":                          metadata.IncomeAssetType,
			"host_base_amount":                           metadata.HostBaseAmount,
			"agency_amount":                              metadata.AgencyAmount,
			"agency_income_balance_after":                metadata.AgencyIncomeBalanceAfter,
			"agency_point_share_bps":                     metadata.AgencyPointShareBPS,
			"created_at_ms":                              nowMs,
		},
		CreatedAtMS: nowMs,
	}
}
