package app

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/gamemq"
	"hyapp/pkg/luckygiftmq"
	"hyapp/pkg/rocketmqx"
	"hyapp/pkg/roommq"
	servicemq "hyapp/pkg/servicekit/mq"
	"hyapp/pkg/usermq"
	"hyapp/pkg/walletmq"
	"hyapp/services/activity-service/internal/config"
	broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
)

func buildMQConsumers(cfg config.Config, services *serviceBundle) ([]*rocketmqx.Consumer, error) {
	mqConsumers := make([]*rocketmqx.Consumer, 0, 5)
	if cfg.RocketMQ.RoomOutbox.Enabled {
		consumer, err := newRoomOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
		activityTemplateConsumer, err := newActivityTemplateRoomOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, activityTemplateConsumer)
	}
	if cfg.RocketMQ.LuckyGiftOutbox.Enabled {
		consumer, err := newLuckyGiftOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.RocketMQ.GameOutbox.Enabled {
		consumer, err := newGameOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.RocketMQ.UserOutbox.Enabled && !services.broadcastPublisherAvailable {
		if !cfg.TaskEventWorker.Enabled {
			shutdownConsumers(mqConsumers)
			return nil, errors.New("rocketmq user_outbox consumer requires tencent_im.enabled")
		}
	}
	if cfg.RocketMQ.UserOutbox.Enabled && services.broadcastPublisherAvailable {
		consumer, err := newUserOutboxConsumer(cfg, services.broadcast)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.TaskEventWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
		consumer, err := newTaskUserOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.FirstRechargeRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newFirstRechargeWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.CumulativeRechargeRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newCumulativeRechargeWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.InviteActivityRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newInviteActivityWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.InviteActivityRewardWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
		consumer, err := newInviteActivityUserOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.MessageActionConfirmWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
		consumer, err := newMessageActionUserOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.RocketMQ.UserOutbox.Enabled {
		consumer, err := newAgencyHostRemovedNoticeUserOutboxConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.RedPacketBroadcastWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newRedPacketWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newVIPRebateNoticeWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.UserLeaderboardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newUserLeaderboardWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	if cfg.TaskEventWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
		consumer, err := newTaskWalletConsumer(cfg, services)
		if err != nil {
			shutdownConsumers(mqConsumers)
			return nil, err
		}
		mqConsumers = append(mqConsumers, consumer)
	}
	return mqConsumers, nil
}

func newRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(roomOutboxConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	roomTagExpression, err := roommq.LegacyCompatibleTagExpression(activityRoomEventTypes()...)
	if err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.RoomOutbox.Topic, roomTagExpression, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		envelope, _, err := roommq.DecodeRoomOutboxMessage(message.Body)
		if err != nil {
			return err
		}
		// 一个 room outbox envelope 代表房间状态已经提交；下游模块各自写幂等消费表。
		// 任何模块失败都返回错误给 MQ，让同一个 envelope 重试，避免“播报成功但活动漏计分”的半完成状态。
		eventCtx := appcode.WithContext(ctx, envelope.GetAppCode())
		if _, err := services.broadcast.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox broadcast projection: %w", err)
		}
		if _, err := services.growth.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox growth projection: %w", err)
		}
		if _, err := services.weeklyStar.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox weekly star projection: %w", err)
		}
		if _, err := services.agencyOpening.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox agency opening projection: %w", err)
		}
		if _, err := services.task.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox task projection: %w", err)
		}
		if _, err := services.roomTurnoverReward.HandleRoomEvent(eventCtx, envelope); err != nil {
			return fmt.Errorf("room outbox turnover reward projection: %w", err)
		}
		return nil
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newActivityTemplateRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(activityTemplateRoomOutboxConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	tagExpression, err := roommq.LegacyCompatibleTagExpression(roommq.EventTypeRoomGiftSent)
	if err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.RoomOutbox.Topic, tagExpression, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		envelope, _, err := roommq.DecodeRoomOutboxMessage(message.Body)
		if err != nil {
			return err
		}
		if _, err := services.activityTemplate.HandleRoomEvent(appcode.WithContext(ctx, envelope.GetAppCode()), envelope); err != nil {
			// Returning the error preserves RocketMQ retry/DLQ semantics on this dedicated consumer group without
			// delaying unrelated broadcast/growth/task projections.
			return fmt.Errorf("room outbox activity template projection: %w", err)
		}
		return nil
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func activityRoomEventTypes() []string {
	// 六个投影共用同一 consumer group，因此 selector 必须取其事件集合并集。
	// RoomGiftSent 驱动增长/任务/周星/流水，另外三个事件只驱动播报；其他房间
	// 事实由 Broker 过滤，避免一个送礼高峰放大成所有活动 handler 的无效调用。
	return []string{
		roommq.EventTypeRoomGiftSent,
		roommq.EventTypeRoomPasswordChanged,
		roommq.EventTypeRoomRocketIgnited,
		roommq.EventTypeRoomRocketRewardGranted,
	}
}

func newLuckyGiftOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(luckyGiftOutboxConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	// 新 owner topic 从首版就使用 exact typed Tag；不订阅宽泛 legacy Tag，避免后续审计事实进入区域 IM 投影。
	if err := consumer.Subscribe(cfg.RocketMQ.LuckyGiftOutbox.Topic, luckygiftmq.TagLuckyGiftDrawn, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		envelope, _, err := luckygiftmq.DecodeLuckyGiftOutboxMessage(message.Body)
		if err != nil {
			return err
		}
		eventCtx := appcode.WithContext(ctx, envelope.GetAppCode())
		if _, err := services.broadcast.HandleLuckyGiftEvent(eventCtx, envelope); err != nil {
			// 返回错误给 MQ 保留至少一次语义；im_broadcast_outbox 的 event_id 主键负责重投幂等。
			return fmt.Errorf("lucky gift outbox broadcast projection: %w", err)
		}
		return nil
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newGameOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(gameOutboxConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.GameOutbox.Topic, gamemq.TagGameOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		outboxMessage, err := gamemq.DecodeGameOutboxMessage(message.Body)
		if err != nil {
			return err
		}
		eventCtx := appcode.WithContext(ctx, outboxMessage.AppCode)
		if _, err := services.broadcast.HandleGameEvent(eventCtx, outboxMessage); err != nil {
			// game_win 与其他投影使用独立 group；返回错误保留至少一次重试，event_id 主键负责 IM outbox 幂等。
			return fmt.Errorf("game outbox broadcast projection: %w", err)
		}
		return nil
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newUserOutboxConsumer(cfg config.Config, broadcastSvc *broadcastservice.Service) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(userOutboxConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		outboxMessage, err := usermq.DecodeUserOutboxMessage(message.Body)
		if err != nil {
			return err
		}
		return consumeUserRegionChangedForBroadcast(ctx, broadcastSvc, outboxMessage)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newFirstRechargeWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.FirstRechargeConsumerGroup))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeWalletRechargeRecorded}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := firstRechargeEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return services.firstRechargeReward.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newCumulativeRechargeWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	// 累充奖励使用独立 consumer group，避免和首充奖励共享消费位点后互相影响重放和排查。
	consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.CumulativeRechargeConsumerGroup))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeWalletRechargeRecorded}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := cumulativeRechargeEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return services.cumulativeRecharge.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newInviteActivityWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	// 邀请活动单独使用 wallet_outbox consumer group；重放累计充值时不影响首充、累充和任务的消费进度。
	consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.InviteActivityConsumerGroup))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeWalletRechargeRecorded}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := inviteActivityRechargeEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return services.inviteActivityReward.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newInviteActivityUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	// 邀请绑定和有效邀请都来自 user-service 已提交事实；消费失败必须让同一 outbox 重试，仓储和钱包命令共同保证幂等。
	consumer, err := rocketmqx.NewConsumer(userOutboxInviteActivityConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		inviteEvent, inviteOK, err := inviteActivityInviteEventFromUserMessage(message.Body)
		if err != nil {
			return err
		}
		if inviteOK {
			return services.inviteActivityReward.ConsumeInviteEvent(appcode.WithContext(ctx, inviteEvent.AppCode), inviteEvent)
		}
		event, ok, err := inviteActivityValidInviteEventFromUserMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return services.inviteActivityReward.ConsumeValidInviteEvent(appcode.WithContext(ctx, event.AppCode), event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newRedPacketWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.RedPacketBroadcastConsumerGroup))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, redPacketWalletOutboxTopic(cfg.RocketMQ.WalletOutbox), []string{
		walletmq.EventTypeWalletRedPacketCreated,
		walletmq.EventTypeWalletRedPacketClaimed,
		walletmq.EventTypeWalletRedPacketRefunded,
	}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := redPacketEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return services.broadcast.ConsumeRedPacketWalletEvent(appcode.WithContext(ctx, event.AppCode), event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newVIPRebateNoticeWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	// VIP 返现资格是 wallet 的逐用户事实，必须使用独立 consumer group 投影为系统通知；
	// 不能复用首充/红包位点，也不能用全服 fanout，因为每个用户的 rebate_id、金额和到期时间都不同。
	consumer, err := rocketmqx.NewConsumer(walletOutboxVIPRebateNoticeConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeVIPDailyCoinRebateAvailable}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := vipRebateAvailableEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		return createVIPRebateSystemNotice(appcode.WithContext(ctx, event.AppCode), services.message, event)
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newUserLeaderboardWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	if services == nil || services.userLeaderboard == nil {
		return nil, errors.New("user leaderboard worker requires redis store")
	}
	// 用户送礼榜使用独立 consumer group；重放榜单读模型不能推进首充、任务、红包等业务位点。
	consumer, err := rocketmqx.NewConsumer(walletOutboxUserLeaderboardConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeWalletGiftDebited}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		event, ok, err := userLeaderboardGiftEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		// Redis 聚合自身用 wallet event_id 做幂等；MQ 重投只会重新确认同一事实，不会重复加分。
		_, err = services.userLeaderboard.ApplyGiftEvent(appcode.WithContext(ctx, event.AppCode), event)
		return err
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func newTaskWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.TaskConsumerGroup))
	if err != nil {
		return nil, err
	}
	if err := subscribeWalletOutboxEvents(consumer, cfg.RocketMQ.WalletOutbox.Topic, []string{walletmq.EventTypeWalletRechargeRecorded}, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		// 钱包任务消费者使用独立 consumer group，不能复用红包或其他钱包消费者进度；否则充值任务会被其他业务确认掉。
		event, appCode, ok, err := taskEventFromWalletMessage(message.Body)
		if err != nil || !ok {
			return err
		}
		// ConsumeTaskEvent 内部按 event_id 幂等；这里返回错误给 MQ 才会重试，正常 skipped/duplicate 都视为成功确认。
		_, err = services.task.ConsumeTaskEvent(appcode.WithContext(ctx, appCode), event)
		return err
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

// subscribeWalletOutboxEvents is the only activity-service entry point for wallet
// subscriptions during the typed-tag compatibility phase. Keeping selector assembly
// centralized guarantees every replica in a consumer group registers the same sorted
// expression and that the legacy Tag remains present until the old backlog is drained.
func subscribeWalletOutboxEvents(consumer *rocketmqx.Consumer, topic string, eventTypes []string, handler rocketmqx.Handler) error {
	tagExpression, err := walletmq.LegacyCompatibleTagExpression(eventTypes...)
	if err != nil {
		return err
	}
	return consumer.Subscribe(topic, tagExpression, handler)
}

func newTaskUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
	consumer, err := rocketmqx.NewConsumer(userOutboxTaskConsumerConfig(cfg.RocketMQ))
	if err != nil {
		return nil, err
	}
	if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
		// user outbox 一条消息可能投影成多条任务事件，例如 CP 双方各一条；只有全部写入成功才确认 MQ。
		events, appCode, err := taskEventsFromUserMessage(message.Body)
		if err != nil || len(events) == 0 {
			return err
		}
		eventCtx := appcode.WithContext(ctx, appCode)
		for _, event := range events {
			// 单条失败时让整条 outbox 重试，已写入成功的事件靠 task_event_consumption 幂等表跳过。
			if _, err := services.task.ConsumeTaskEvent(eventCtx, event); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		_ = consumer.Shutdown()
		return nil, err
	}
	return consumer, nil
}

func roomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	consumerConfig := rocketMQConsumerConfig(cfg, cfg.RoomOutbox.ConsumerGroup, cfg.RoomOutbox.ConsumerMaxReconsumeTimes)
	// 一条房间事实会串行投影到多组 MySQL 幂等表和聚合表。SDK 默认开 20 个消费协程，
	// 积压恢复时同一用户的多条送礼事实会并发争抢任务进度和活动聚合锁，把可恢复积压放大成死锁重试风暴。
	// 单实例固定单协程；生产的同 consumer group 仍由多实例分担 queue，不改变消费位点和重试语义。
	consumerConfig.ConsumeGoroutines = 1
	return consumerConfig
}

func activityTemplateRoomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	consumerConfig := rocketMQConsumerConfig(cfg, cfg.RoomOutbox.ActivityTemplateConsumerGroup, cfg.RoomOutbox.ConsumerMaxReconsumeTimes)
	// Gift scoring serializes per queue to reduce rank/task row lock inversions; horizontal instances still
	// distribute RocketMQ queues inside this dedicated group.
	consumerConfig.ConsumeGoroutines = 1
	return consumerConfig
}

func luckyGiftOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	// 顶部飘屏是实时副作用，新建或误改 group 时不能从 topic 最早位点重放历史大奖；
	// 首次上线必须先部署 consumer，再开启 lucky-gift producer，确保 latest offset 不丢上线窗口事实。
	return rocketMQConsumerConfig(cfg, cfg.LuckyGiftOutbox.ConsumerGroup, cfg.LuckyGiftOutbox.ConsumerMaxReconsumeTimes)
}

func gameOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	// 中奖飘屏是不可批量历史重放的实时副作用，新消费组沿用 latest-offset 默认值。
	return rocketMQConsumerConfig(cfg, cfg.GameOutbox.ConsumerGroup, cfg.GameOutbox.ConsumerMaxReconsumeTimes)
}

func userOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, cfg.UserOutbox.ConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}

func userOutboxTaskConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, cfg.UserOutbox.TaskConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}

func userOutboxInviteActivityConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, cfg.UserOutbox.InviteActivityConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}

func userOutboxMessageActionConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, cfg.UserOutbox.MessageActionConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}

func userOutboxAgencyHostRemovedNoticeConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, cfg.UserOutbox.AgencyHostRemovedNoticeConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}

func walletOutboxConsumerConfig(cfg config.RocketMQConfig, group string) rocketmqx.ConsumerConfig {
	return rocketMQConsumerConfig(cfg, group, cfg.WalletOutbox.ConsumerMaxReconsumeTimes)
}

func walletOutboxUserLeaderboardConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	consumerConfig := walletOutboxConsumerConfig(cfg, cfg.WalletOutbox.UserLeaderboardConsumerGroup)
	// 用户榜是 Redis 可重建读模型；新 consumer group 首次上线需要尽量从 broker 保留的最早消息补齐当前周期。
	consumerConfig.ConsumeFromFirst = true
	return consumerConfig
}

func walletOutboxVIPRebateNoticeConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
	// VIP 返币通知会写用户 inbox，属于不可批量重放的业务副作用。新消费组必须沿用 SDK 的
	// latest-offset 默认值；上线窗口内需要补偿的少量资格由明确的位点重置处理，不能扫描全部历史钱包事实。
	return walletOutboxConsumerConfig(cfg, cfg.WalletOutbox.VIPRebateNoticeConsumerGroup)
}

func messageActionProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
	return rocketmqx.ProducerConfig{
		EndpointConfig: rocketmqx.EndpointConfig{
			NameServers:      cfg.NameServers,
			NameServerDomain: cfg.NameServerDomain,
			AccessKey:        cfg.AccessKey,
			SecretKey:        cfg.SecretKey,
			SecurityToken:    cfg.SecurityToken,
			Namespace:        cfg.Namespace,
			VIPChannel:       cfg.VIPChannel,
		},
		GroupName:   cfg.MessageActionOutbox.ProducerGroup,
		SendTimeout: cfg.MessageActionOutbox.SendTimeout,
		Retry:       cfg.MessageActionOutbox.Retry,
	}
}

func activityTemplateProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
	return rocketmqx.ProducerConfig{
		EndpointConfig: rocketmqx.EndpointConfig{
			NameServers: cfg.NameServers, NameServerDomain: cfg.NameServerDomain, AccessKey: cfg.AccessKey,
			SecretKey: cfg.SecretKey, SecurityToken: cfg.SecurityToken, Namespace: cfg.Namespace, VIPChannel: cfg.VIPChannel,
		},
		GroupName: cfg.ActivityTemplateOutbox.ProducerGroup, SendTimeout: cfg.ActivityTemplateOutbox.SendTimeout,
		Retry: cfg.ActivityTemplateOutbox.Retry,
	}
}

func redPacketWalletOutboxTopic(cfg config.WalletOutboxMQConfig) string {
	// 钱包实时 topic 显式配置后，红包 worker 只消费实时通道，避免继续被普通账务 topic 的高频消息拖慢。
	if cfg.RealtimeTopic != "" {
		return cfg.RealtimeTopic
	}
	return cfg.Topic
}

func rocketMQConsumerConfig(cfg config.RocketMQConfig, group string, maxReconsume int32) rocketmqx.ConsumerConfig {
	return rocketmqx.ConsumerConfig{
		EndpointConfig: rocketmqx.EndpointConfig{
			NameServers:      cfg.NameServers,
			NameServerDomain: cfg.NameServerDomain,
			AccessKey:        cfg.AccessKey,
			SecretKey:        cfg.SecretKey,
			SecurityToken:    cfg.SecurityToken,
			Namespace:        cfg.Namespace,
			VIPChannel:       cfg.VIPChannel,
		},
		GroupName:          group,
		MaxReconsumeTimes:  maxReconsume,
		ConsumeRetryDelay:  time.Second,
		ConsumePullBatch:   32,
		ConsumePullTimeout: 15 * time.Minute,
	}
}

func shutdownConsumers(consumers []*rocketmqx.Consumer) {
	servicemq.ShutdownConsumers(consumers)
}

type userRegionChangedPayload struct {
	UserID      int64 `json:"user_id"`
	OldRegionID int64 `json:"old_region_id"`
	NewRegionID int64 `json:"new_region_id"`
}

func consumeUserRegionChangedForBroadcast(ctx context.Context, service *broadcastservice.Service, message usermq.UserOutboxMessage) error {
	if message.EventType != "UserRegionChanged" {
		return nil
	}
	var payload userRegionChangedPayload
	if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil {
		return err
	}
	if payload.UserID <= 0 || payload.OldRegionID <= 0 || payload.OldRegionID == payload.NewRegionID {
		return nil
	}
	// 区域广播群成员关系是当前读模型；用户切区后只移除旧群，下一次 UserSig 会按新 users.region_id 返回新群。
	_, _, err := service.RemoveRegionBroadcastMember(appcode.WithContext(ctx, message.AppCode), payload.UserID, payload.OldRegionID)
	return err
}
