package config

import (
	"errors"
	"net/http"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/configx"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentim"
)

// Config 描述 activity-service 启动配置。
type Config struct {
	ServiceName string `yaml:"service_name"`
	NodeID      string `yaml:"node_id"`
	Environment string `yaml:"environment"`
	GRPCAddr    string `yaml:"grpc_addr"`
	// HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址，不承载业务流量。
	HealthHTTPAddr string `yaml:"health_http_addr"`
	// MySQLDSN 是活动状态、事件消费幂等和 outbox 的必需存储连接串。
	MySQLDSN string `yaml:"mysql_dsn"`
	// UserServiceAddr 是 fanout worker 读取目标用户游标的内部 gRPC 地址。
	UserServiceAddr string `yaml:"user_service_addr"`
	// WalletServiceAddr 是任务奖励入账依赖；activity 只发命令，不直接写余额。
	WalletServiceAddr string `yaml:"wallet_service_addr"`
	// RoomServiceAddr 是房间流水奖励结算读取房主快照的内部 gRPC 地址。
	RoomServiceAddr string `yaml:"room_service_addr"`
	// FirstRechargeRewardWorker 控制首冲奖励对 wallet_outbox 充值事实的本地消费。
	FirstRechargeRewardWorker FirstRechargeRewardWorkerConfig `yaml:"first_recharge_reward_worker"`
	// CumulativeRechargeRewardWorker 控制累充奖励对 wallet_outbox 充值事实的本地消费。
	CumulativeRechargeRewardWorker CumulativeRechargeRewardWorkerConfig `yaml:"cumulative_recharge_reward_worker"`
	// InviteActivityRewardWorker 控制邀请活动对 wallet/user outbox 事实的本地消费。
	InviteActivityRewardWorker InviteActivityRewardWorkerConfig `yaml:"invite_activity_reward_worker"`
	// RedPacketBroadcastWorker 控制红包创建事实转区域飘屏 outbox 的本地消费。
	RedPacketBroadcastWorker RedPacketBroadcastWorkerConfig `yaml:"red_packet_broadcast_worker"`
	// TaskEventWorker 控制任务系统对 wallet/user outbox 事实的本地消费。
	TaskEventWorker TaskEventWorkerConfig `yaml:"task_event_worker"`
	// UserLeaderboardWorker 控制用户送礼榜单对 wallet_outbox 送礼事实的 Redis 投影。
	UserLeaderboardWorker UserLeaderboardWorkerConfig `yaml:"user_leaderboard_worker"`
	// WheelRewardWorker 补偿抽奖事务已经确定、但同步钱包发奖未完成的 WheelRewardSettlement outbox。
	WheelRewardWorker WheelRewardWorkerConfig `yaml:"wheel_reward_worker"`
	// ActivityTemplateStatsWorker relays committed runtime facts to statistics-service through RocketMQ.
	ActivityTemplateStatsWorker ActivityTemplateStatsWorkerConfig `yaml:"activity_template_stats_worker"`
	// ActivityTemplateRuntime 固化迟到事件水位等运行策略；发布时会写入物化周期，后续改配置不重写历史版本。
	ActivityTemplateRuntime ActivityTemplateRuntimeConfig `yaml:"activity_template_runtime"`
	// TencentIM 是 activity-service 发送全局/区域播报群消息的 REST 配置。
	TencentIM TencentIMConfig `yaml:"tencent_im"`
	// Broadcast 控制 IM 播报群 reconciler、outbox worker 和贵重礼物阈值。
	Broadcast BroadcastConfig `yaml:"broadcast"`
	// RocketMQ 可直接消费 room-service 发布的 room_outbox fanout topic。
	RocketMQ RocketMQConfig `yaml:"rocketmq"`
	// MySQLAutoMigrate 保留给显式本地迁移；线上 schema 由发布流程或 initdb 管理。
	MySQLAutoMigrate bool               `yaml:"mysql_auto_migrate"`
	Consumer         ConsumerConfig     `yaml:"consumer"`
	MessageInbox     MessageInboxConfig `yaml:"message_inbox"`
	// MessageActionConfirmWorker 控制通用确认消息的 user_outbox 消费和 IM outbox 发布。
	MessageActionConfirmWorker MessageActionConfirmWorkerConfig `yaml:"message_action_confirm_worker"`
	Log                        logx.Config                      `yaml:"log"`
}

// ConsumerConfig 保存 room outbox 消费底座配置。
type ConsumerConfig struct {
	RoomOutboxPollIntervalMs int64 `yaml:"room_outbox_poll_interval_ms"`
	BatchSize                int   `yaml:"batch_size"`
}

// MessageInboxConfig 保存 App system/activity inbox 的读取缓存参数；fanout 扫描由 cron-service 调度。
type MessageInboxConfig struct {
	UnreadCacheTTL string `yaml:"unread_cache_ttl"`
}

// MessageActionConfirmWorkerConfig 保存确认消息消费和 MQ 发布策略。
type MessageActionConfirmWorkerConfig struct {
	Enabled bool `yaml:"enabled"`
	// AppCodes 显式限定确认消息发布分区；worker 必须逐 App 使用 (app_code, status, created_at_ms) 前缀索引 claim。
	AppCodes           []string      `yaml:"app_codes"`
	OutboxPollInterval time.Duration `yaml:"outbox_poll_interval"`
	OutboxBatchSize    int           `yaml:"outbox_batch_size"`
	OutboxLockTTL      time.Duration `yaml:"outbox_lock_ttl"`
	OutboxMaxRetry     int           `yaml:"outbox_max_retry"`
	PublishTimeout     time.Duration `yaml:"publish_timeout"`
}

// FirstRechargeRewardWorkerConfig 保存钱包充值事实消费策略。
type FirstRechargeRewardWorkerConfig struct {
	// Enabled 控制是否启动 wallet_outbox MQ 消费；关闭后仍可通过 gRPC 消费入口补偿。
	Enabled bool `yaml:"enabled"`
}

// CumulativeRechargeRewardWorkerConfig 保存累充奖励钱包充值事实消费策略。
type CumulativeRechargeRewardWorkerConfig struct {
	// Enabled 控制是否启动 wallet_outbox MQ 消费；关闭后仍可通过 gRPC 消费入口补偿。
	Enabled bool `yaml:"enabled"`
}

// InviteActivityRewardWorkerConfig 保存邀请活动钱包和用户事实消费策略。
type InviteActivityRewardWorkerConfig struct {
	// Enabled 控制是否启动 wallet/user outbox MQ 消费。
	Enabled bool `yaml:"enabled"`
}

// RedPacketBroadcastWorkerConfig 保存红包创建事实消费策略。
type RedPacketBroadcastWorkerConfig struct {
	// Enabled 控制是否启动 wallet_outbox MQ 消费。
	Enabled bool `yaml:"enabled"`
}

// TaskEventWorkerConfig 保存任务进度事实消费策略。
type TaskEventWorkerConfig struct {
	// Enabled 控制是否启动 wallet/user outbox 任务消费者；room outbox 任务投影跟随 room_outbox 主消费者。
	Enabled bool `yaml:"enabled"`
}

// UserLeaderboardWorkerConfig 保存用户榜单 Redis 投影策略。
type UserLeaderboardWorkerConfig struct {
	// Enabled 控制是否打开榜单 Redis 并启动 wallet_outbox 消费；游戏等级事件也复用该连接写 game 榜。
	// 关闭时 gateway 只能读到已有 Redis 聚合，但等级和任务主链路仍继续处理。
	Enabled bool `yaml:"enabled"`
	// RedisAddr 是 activity 写入用户榜单聚合的 Redis 地址，必须与 gateway 读取地址一致。
	RedisAddr string `yaml:"redis_addr"`
	// RedisPassword 是 Redis 鉴权密码，本地默认留空。
	RedisPassword string `yaml:"redis_password"`
	// RedisDB 是 Redis logical DB，必须与 gateway 读取配置一致。
	RedisDB int `yaml:"redis_db"`
	// KeyPrefix 隔离用户榜单 key，必须与 gateway 读取配置一致。
	KeyPrefix string `yaml:"key_prefix"`
}

// WheelRewardWorkerConfig 控制转盘发奖 outbox 的租户分区、锁和重试边界。
// AppCodes 必须显式覆盖线上业务 App；worker 每轮逐 App claim，禁止空 context 默认只处理 lalu。
type WheelRewardWorkerConfig struct {
	Enabled      bool          `yaml:"enabled"`
	AppCodes     []string      `yaml:"app_codes"`
	PollInterval time.Duration `yaml:"poll_interval"`
	BatchSize    int           `yaml:"batch_size"`
	LockTTL      time.Duration `yaml:"lock_ttl"`
	MaxRetry     int           `yaml:"max_retry"`
}

type ActivityTemplateStatsWorkerConfig struct {
	Enabled      bool          `yaml:"enabled"`
	PollInterval time.Duration `yaml:"poll_interval"`
	BatchSize    int           `yaml:"batch_size"`
	LockTTL      time.Duration `yaml:"lock_ttl"`
}

type ActivityTemplateRuntimeConfig struct {
	SettlementLateness time.Duration `yaml:"settlement_lateness"`
}

// TencentIMConfig 描述 activity-service 调用腾讯云 IM REST API 的配置。
type TencentIMConfig struct {
	Enabled         bool          `yaml:"enabled"`
	SDKAppID        int64         `yaml:"sdk_app_id"`
	SecretKey       string        `yaml:"secret_key"`
	AdminIdentifier string        `yaml:"admin_identifier"`
	AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"`
	Endpoint        string        `yaml:"endpoint"`
	GroupType       string        `yaml:"group_type"`
	GroupIDPrefix   string        `yaml:"group_id_prefix"`
	RequestTimeout  time.Duration `yaml:"request_timeout"`
}

// RESTConfig converts YAML into the shared Tencent IM REST client config.
func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig {
	timeout := c.RequestTimeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	return tencentim.RESTConfig{
		SDKAppID:        c.SDKAppID,
		SecretKey:       c.SecretKey,
		AdminIdentifier: c.AdminIdentifier,
		AdminUserSigTTL: c.AdminUserSigTTL,
		Endpoint:        c.Endpoint,
		GroupType:       c.GroupType,
		HTTPClient:      &http.Client{Timeout: timeout},
	}
}

// BroadcastConfig stores activity-service broadcast worker policy.
type BroadcastConfig struct {
	Enabled bool `yaml:"enabled"`
	// AppCodes 是共享腾讯 IM 应用内需要主动维护播报群的业务租户清单。
	// outbox 发现只能覆盖已经产生过消息的 App；新 App 上线前必须显式列入，才能提前建好全局/区域群。
	AppCodes          []string `yaml:"app_codes"`
	SuperGiftMinValue int64    `yaml:"super_gift_min_value"`
	// GameWinMinCoins 是数据库尚未写入 App 级策略时的安全默认值；后台保存后以持久化配置为准。
	GameWinMinCoins       int64         `yaml:"game_win_min_coins"`
	GameWinConfigCacheTTL time.Duration `yaml:"game_win_config_cache_ttl"`
	WorkerPollInterval    time.Duration `yaml:"worker_poll_interval"`
	WorkerBatchSize       int           `yaml:"worker_batch_size"`
	WorkerLockTTL         time.Duration `yaml:"worker_lock_ttl"`
	WorkerMaxRetry        int           `yaml:"worker_max_retry"`
	EnsureGroupsOnStartup bool          `yaml:"ensure_groups_on_startup"`
	// EnsureGroupsInterval 是播报群定时 reconcile 周期；0 表示只在启动时补偿一次。
	// 新区域上线或启动补偿失败时靠它自动补群，不再依赖人工调用 gRPC。
	EnsureGroupsInterval time.Duration `yaml:"ensure_groups_interval"`
}

// RocketMQConfig 描述 activity-service 消费房间事实的 MQ 连接。
type RocketMQConfig struct {
	Enabled                bool                           `yaml:"enabled"`
	NameServers            []string                       `yaml:"name_servers"`
	NameServerDomain       string                         `yaml:"name_server_domain"`
	AccessKey              string                         `yaml:"access_key"`
	SecretKey              string                         `yaml:"secret_key"`
	SecurityToken          string                         `yaml:"security_token"`
	Namespace              string                         `yaml:"namespace"`
	VIPChannel             bool                           `yaml:"vip_channel"`
	RoomOutbox             RoomOutboxMQConfig             `yaml:"room_outbox"`
	LuckyGiftOutbox        LuckyGiftOutboxMQConfig        `yaml:"lucky_gift_outbox"`
	GameOutbox             GameOutboxMQConfig             `yaml:"game_outbox"`
	WalletOutbox           WalletOutboxMQConfig           `yaml:"wallet_outbox"`
	UserOutbox             UserOutboxMQConfig             `yaml:"user_outbox"`
	MessageActionOutbox    MessageActionOutboxMQConfig    `yaml:"message_action_outbox"`
	ActivityTemplateOutbox ActivityTemplateOutboxMQConfig `yaml:"activity_template_outbox"`
}

// RoomOutboxMQConfig 控制 activity 对 room_outbox topic 的消费位点。
type RoomOutboxMQConfig struct {
	Enabled                       bool   `yaml:"enabled"`
	Topic                         string `yaml:"topic"`
	ConsumerGroup                 string `yaml:"consumer_group"`
	ActivityTemplateConsumerGroup string `yaml:"activity_template_consumer_group"`
	ConsumerMaxReconsumeTimes     int32  `yaml:"consumer_max_reconsume_times"`
}

// LuckyGiftOutboxMQConfig 控制 activity 对幸运礼物 owner fact 的独立消费位点。
// 它不能复用 room/wallet consumer group，否则 RocketMQ 的负载均衡会让区域飘屏事实被其他业务确认掉。
type LuckyGiftOutboxMQConfig struct {
	Enabled                   bool   `yaml:"enabled"`
	Topic                     string `yaml:"topic"`
	ConsumerGroup             string `yaml:"consumer_group"`
	ConsumerMaxReconsumeTimes int32  `yaml:"consumer_max_reconsume_times"`
}

// GameOutboxMQConfig 为 game_win 实时副作用保留独立消费位点，不能与统计 consumer group 共用。
type GameOutboxMQConfig struct {
	Enabled                   bool   `yaml:"enabled"`
	Topic                     string `yaml:"topic"`
	ConsumerGroup             string `yaml:"consumer_group"`
	ConsumerMaxReconsumeTimes int32  `yaml:"consumer_max_reconsume_times"`
}

// WalletOutboxMQConfig 控制 activity 对 wallet_outbox topic 的独立消费组。
type WalletOutboxMQConfig struct {
	Enabled                         bool   `yaml:"enabled"`
	Topic                           string `yaml:"topic"`
	RealtimeTopic                   string `yaml:"realtime_topic"`
	FirstRechargeConsumerGroup      string `yaml:"first_recharge_consumer_group"`
	CumulativeRechargeConsumerGroup string `yaml:"cumulative_recharge_consumer_group"`
	InviteActivityConsumerGroup     string `yaml:"invite_activity_consumer_group"`
	RedPacketBroadcastConsumerGroup string `yaml:"red_packet_broadcast_consumer_group"`
	VIPRebateNoticeConsumerGroup    string `yaml:"vip_rebate_notice_consumer_group"`
	TaskConsumerGroup               string `yaml:"task_consumer_group"`
	UserLeaderboardConsumerGroup    string `yaml:"user_leaderboard_consumer_group"`
	ConsumerMaxReconsumeTimes       int32  `yaml:"consumer_max_reconsume_times"`
}

// UserOutboxMQConfig 控制 activity 对 user_outbox topic 的独立消费组。
type UserOutboxMQConfig struct {
	Enabled                              bool   `yaml:"enabled"`
	Topic                                string `yaml:"topic"`
	ConsumerGroup                        string `yaml:"consumer_group"`
	TaskConsumerGroup                    string `yaml:"task_consumer_group"`
	InviteActivityConsumerGroup          string `yaml:"invite_activity_consumer_group"`
	MessageActionConsumerGroup           string `yaml:"message_action_consumer_group"`
	AgencyHostRemovedNoticeConsumerGroup string `yaml:"agency_host_removed_notice_consumer_group"`
	ConsumerMaxReconsumeTimes            int32  `yaml:"consumer_max_reconsume_times"`
}

// MessageActionOutboxMQConfig 控制确认消息 outbox 投递到 notice-service 的 MQ producer。
type MessageActionOutboxMQConfig struct {
	Enabled       bool          `yaml:"enabled"`
	Topic         string        `yaml:"topic"`
	ProducerGroup string        `yaml:"producer_group"`
	SendTimeout   time.Duration `yaml:"send_timeout"`
	Retry         int           `yaml:"retry"`
}

type ActivityTemplateOutboxMQConfig struct {
	Enabled       bool          `yaml:"enabled"`
	Topic         string        `yaml:"topic"`
	ProducerGroup string        `yaml:"producer_group"`
	SendTimeout   time.Duration `yaml:"send_timeout"`
	Retry         int           `yaml:"retry"`
}

// Default 返回本地默认配置。
func Default() Config {
	return Config{
		ServiceName:       "activity-service",
		NodeID:            "activity-local",
		Environment:       "local",
		GRPCAddr:          ":13006",
		HealthHTTPAddr:    ":13106",
		MySQLDSN:          "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC",
		UserServiceAddr:   "127.0.0.1:13005",
		WalletServiceAddr: "127.0.0.1:13004",
		RoomServiceAddr:   "127.0.0.1:13001",
		FirstRechargeRewardWorker: FirstRechargeRewardWorkerConfig{
			Enabled: false,
		},
		CumulativeRechargeRewardWorker: CumulativeRechargeRewardWorkerConfig{
			Enabled: false,
		},
		InviteActivityRewardWorker: InviteActivityRewardWorkerConfig{
			Enabled: true,
		},
		RedPacketBroadcastWorker: RedPacketBroadcastWorkerConfig{
			Enabled: false,
		},
		TaskEventWorker: TaskEventWorkerConfig{
			Enabled: true,
		},
		UserLeaderboardWorker: UserLeaderboardWorkerConfig{
			Enabled: false,
		},
		WheelRewardWorker: WheelRewardWorkerConfig{
			Enabled:      true,
			AppCodes:     []string{"lalu", "fami", "huwaa"},
			PollInterval: time.Second,
			BatchSize:    50,
			LockTTL:      30 * time.Second,
			MaxRetry:     16,
		},
		ActivityTemplateStatsWorker: ActivityTemplateStatsWorkerConfig{
			Enabled: true, PollInterval: time.Second, BatchSize: 100, LockTTL: 30 * time.Second,
		},
		ActivityTemplateRuntime: ActivityTemplateRuntimeConfig{SettlementLateness: 10 * time.Minute},
		MessageActionConfirmWorker: MessageActionConfirmWorkerConfig{
			Enabled:            true,
			AppCodes:           []string{"lalu", "fami", "huwaa"},
			OutboxPollInterval: time.Second,
			OutboxBatchSize:    100,
			OutboxLockTTL:      30 * time.Second,
			OutboxMaxRetry:     8,
			PublishTimeout:     5 * time.Second,
		},
		TencentIM: TencentIMConfig{
			Enabled:         false,
			AdminIdentifier: "administrator",
			AdminUserSigTTL: 24 * time.Hour,
			Endpoint:        tencentim.DefaultEndpoint,
			GroupType:       tencentim.DefaultGroupType,
			RequestTimeout:  5 * time.Second,
		},
		Broadcast: BroadcastConfig{
			Enabled:               false,
			AppCodes:              []string{"lalu", "fami", "huwaa"},
			SuperGiftMinValue:     100000,
			GameWinMinCoins:       100000,
			GameWinConfigCacheTTL: 5 * time.Second,
			WorkerPollInterval:    time.Second,
			WorkerBatchSize:       100,
			WorkerLockTTL:         30 * time.Second,
			WorkerMaxRetry:        8,
			EnsureGroupsOnStartup: true,
			EnsureGroupsInterval:  30 * time.Minute,
		},
		RocketMQ:         defaultRocketMQConfig(),
		MySQLAutoMigrate: false,
		Consumer: ConsumerConfig{
			RoomOutboxPollIntervalMs: 1000,
			BatchSize:                100,
		},
		MessageInbox: MessageInboxConfig{
			UnreadCacheTTL: "60s",
		},
		Log: logx.Config{
			Level:           "info",
			Format:          "json",
			MaxPayloadBytes: 2048,
		},
	}
}

func defaultRocketMQConfig() RocketMQConfig {
	return RocketMQConfig{
		Enabled: false,
		RoomOutbox: RoomOutboxMQConfig{
			Enabled:                       false,
			Topic:                         "hyapp_room_outbox",
			ConsumerGroup:                 "hyapp-activity-room-outbox",
			ActivityTemplateConsumerGroup: "hyapp-activity-template-room-outbox",
			ConsumerMaxReconsumeTimes:     16,
		},
		LuckyGiftOutbox: LuckyGiftOutboxMQConfig{
			Enabled:                   false,
			Topic:                     "hyapp_lucky_gift_outbox",
			ConsumerGroup:             "hyapp-activity-lucky-gift-outbox",
			ConsumerMaxReconsumeTimes: 16,
		},
		GameOutbox: GameOutboxMQConfig{
			Enabled:                   false,
			Topic:                     "hyapp_game_outbox",
			ConsumerGroup:             "hyapp-activity-game-win-outbox",
			ConsumerMaxReconsumeTimes: 16,
		},
		WalletOutbox: WalletOutboxMQConfig{
			Enabled:                         false,
			Topic:                           "hyapp_wallet_outbox",
			RealtimeTopic:                   "",
			FirstRechargeConsumerGroup:      "hyapp-activity-first-recharge-wallet-outbox",
			CumulativeRechargeConsumerGroup: "hyapp-activity-cumulative-recharge-wallet-outbox",
			InviteActivityConsumerGroup:     "hyapp-activity-invite-wallet-outbox",
			RedPacketBroadcastConsumerGroup: "hyapp-activity-red-packet-wallet-outbox",
			VIPRebateNoticeConsumerGroup:    "hyapp-activity-vip-rebate-notice-wallet-outbox",
			TaskConsumerGroup:               "hyapp-activity-task-wallet-outbox",
			UserLeaderboardConsumerGroup:    "hyapp-activity-user-leaderboard-wallet-outbox",
			ConsumerMaxReconsumeTimes:       16,
		},
		UserOutbox: UserOutboxMQConfig{
			Enabled:                              false,
			Topic:                                "hyapp_user_outbox",
			ConsumerGroup:                        "hyapp-activity-user-region-broadcast",
			TaskConsumerGroup:                    "hyapp-activity-task-user-outbox",
			InviteActivityConsumerGroup:          "hyapp-activity-invite-user-outbox",
			MessageActionConsumerGroup:           "hyapp-activity-message-action-user-outbox",
			AgencyHostRemovedNoticeConsumerGroup: "hyapp-activity-agency-host-removed-notice",
			ConsumerMaxReconsumeTimes:            16,
		},
		MessageActionOutbox: MessageActionOutboxMQConfig{
			Enabled:       false,
			Topic:         "hyapp_message_action_outbox",
			ProducerGroup: "hyapp-activity-message-action-outbox",
			SendTimeout:   5 * time.Second,
			Retry:         2,
		},
		ActivityTemplateOutbox: ActivityTemplateOutboxMQConfig{
			Enabled: false, Topic: "hyapp_activity_outbox", ProducerGroup: "hyapp-activity-template-outbox",
			SendTimeout: 5 * time.Second, Retry: 2,
		},
	}
}

// Load 从独立 config.yaml 读取 activity-service 配置。
func Load(path string) (Config, error) {
	cfg := Default()
	if path == "" {
		return cfg, nil
	}

	if err := configx.LoadYAML(path, &cfg); err != nil {
		return Config{}, err
	}
	if strings.TrimSpace(cfg.UserServiceAddr) == "" {
		cfg.UserServiceAddr = Default().UserServiceAddr
	}
	if strings.TrimSpace(cfg.WalletServiceAddr) == "" {
		cfg.WalletServiceAddr = Default().WalletServiceAddr
	}
	if strings.TrimSpace(cfg.RoomServiceAddr) == "" {
		cfg.RoomServiceAddr = Default().RoomServiceAddr
	}
	cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
	if cfg.ServiceName == "" {
		cfg.ServiceName = "activity-service"
	}
	cfg.NodeID = strings.TrimSpace(cfg.NodeID)
	if cfg.NodeID == "" {
		cfg.NodeID = cfg.ServiceName
	}
	cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
	if cfg.Environment == "" {
		cfg.Environment = "local"
	}
	cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr)
	if cfg.HealthHTTPAddr == "" {
		cfg.HealthHTTPAddr = ":13106"
	}
	cfg.TencentIM.AdminIdentifier = strings.TrimSpace(cfg.TencentIM.AdminIdentifier)
	if cfg.TencentIM.AdminIdentifier == "" {
		cfg.TencentIM.AdminIdentifier = "administrator"
	}
	cfg.TencentIM.Endpoint = strings.TrimSpace(cfg.TencentIM.Endpoint)
	if cfg.TencentIM.Endpoint == "" {
		cfg.TencentIM.Endpoint = tencentim.DefaultEndpoint
	}
	cfg.TencentIM.GroupType = strings.TrimSpace(cfg.TencentIM.GroupType)
	if cfg.TencentIM.GroupType == "" {
		cfg.TencentIM.GroupType = tencentim.DefaultGroupType
	}
	cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix)
	if cfg.TencentIM.AdminUserSigTTL <= 0 {
		cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour
	}
	if cfg.TencentIM.RequestTimeout <= 0 {
		cfg.TencentIM.RequestTimeout = 5 * time.Second
	}
	if cfg.Broadcast.SuperGiftMinValue <= 0 {
		cfg.Broadcast.SuperGiftMinValue = 100000
	}
	if cfg.Broadcast.GameWinMinCoins <= 0 {
		cfg.Broadcast.GameWinMinCoins = 100000
	}
	if cfg.Broadcast.GameWinConfigCacheTTL <= 0 {
		cfg.Broadcast.GameWinConfigCacheTTL = 5 * time.Second
	}
	cfg.Broadcast.AppCodes = normalizeAppCodes(cfg.Broadcast.AppCodes)
	if len(cfg.Broadcast.AppCodes) == 0 {
		cfg.Broadcast.AppCodes = append([]string(nil), Default().Broadcast.AppCodes...)
	}
	if cfg.Broadcast.WorkerPollInterval <= 0 {
		cfg.Broadcast.WorkerPollInterval = time.Second
	}
	if cfg.Broadcast.WorkerBatchSize <= 0 {
		cfg.Broadcast.WorkerBatchSize = 100
	}
	if cfg.Broadcast.WorkerLockTTL <= 0 {
		cfg.Broadcast.WorkerLockTTL = 30 * time.Second
	}
	if cfg.Broadcast.WorkerMaxRetry <= 0 {
		cfg.Broadcast.WorkerMaxRetry = 8
	}
	if cfg.MessageActionConfirmWorker.OutboxPollInterval <= 0 {
		cfg.MessageActionConfirmWorker.OutboxPollInterval = time.Second
	}
	if cfg.MessageActionConfirmWorker.OutboxBatchSize <= 0 {
		cfg.MessageActionConfirmWorker.OutboxBatchSize = 100
	}
	if cfg.MessageActionConfirmWorker.OutboxLockTTL <= 0 {
		cfg.MessageActionConfirmWorker.OutboxLockTTL = 30 * time.Second
	}
	if cfg.MessageActionConfirmWorker.OutboxMaxRetry <= 0 {
		cfg.MessageActionConfirmWorker.OutboxMaxRetry = 8
	}
	if cfg.MessageActionConfirmWorker.PublishTimeout <= 0 {
		cfg.MessageActionConfirmWorker.PublishTimeout = 5 * time.Second
	}
	cfg.UserLeaderboardWorker.RedisAddr = strings.TrimSpace(cfg.UserLeaderboardWorker.RedisAddr)
	cfg.UserLeaderboardWorker.KeyPrefix = strings.TrimSpace(cfg.UserLeaderboardWorker.KeyPrefix)
	cfg.WheelRewardWorker.AppCodes = normalizeAppCodes(cfg.WheelRewardWorker.AppCodes)
	if len(cfg.WheelRewardWorker.AppCodes) == 0 {
		cfg.WheelRewardWorker.AppCodes = append([]string(nil), Default().WheelRewardWorker.AppCodes...)
	}
	if cfg.WheelRewardWorker.PollInterval <= 0 {
		cfg.WheelRewardWorker.PollInterval = time.Second
	}
	if cfg.WheelRewardWorker.BatchSize <= 0 {
		cfg.WheelRewardWorker.BatchSize = 50
	}
	if cfg.WheelRewardWorker.LockTTL <= 0 {
		cfg.WheelRewardWorker.LockTTL = 30 * time.Second
	}
	if cfg.WheelRewardWorker.MaxRetry <= 0 {
		cfg.WheelRewardWorker.MaxRetry = 16
	}
	if cfg.ActivityTemplateStatsWorker.PollInterval <= 0 {
		cfg.ActivityTemplateStatsWorker.PollInterval = time.Second
	}
	if cfg.ActivityTemplateStatsWorker.BatchSize <= 0 {
		cfg.ActivityTemplateStatsWorker.BatchSize = 100
	}
	if cfg.ActivityTemplateStatsWorker.LockTTL <= 0 {
		cfg.ActivityTemplateStatsWorker.LockTTL = 30 * time.Second
	}
	if cfg.ActivityTemplateRuntime.SettlementLateness <= 0 {
		cfg.ActivityTemplateRuntime.SettlementLateness = 10 * time.Minute
	}
	if cfg.ActivityTemplateRuntime.SettlementLateness > 30*time.Minute {
		return Config{}, errors.New("activity_template_runtime.settlement_lateness must not exceed 30m")
	}
	rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
	if err != nil {
		return Config{}, err
	}
	cfg.RocketMQ = rocketMQ
	if cfg.RocketMQ.LuckyGiftOutbox.Enabled && (!cfg.Broadcast.Enabled || !cfg.TencentIM.Enabled) {
		// 消费成功只代表区域 outbox 已持久化；没有 worker 或腾讯 IM publisher 时记录会永久堆积，
		// 因此不能让 lucky consumer 以“已开启”配置启动成只进不出的半链路。
		return Config{}, errors.New("rocketmq.lucky_gift_outbox.enabled requires broadcast.enabled and tencent_im.enabled")
	}
	if cfg.RocketMQ.GameOutbox.Enabled && (!cfg.Broadcast.Enabled || !cfg.TencentIM.Enabled) {
		return Config{}, errors.New("rocketmq.game_outbox.enabled requires broadcast.enabled and tencent_im.enabled")
	}
	if (cfg.FirstRechargeRewardWorker.Enabled || cfg.CumulativeRechargeRewardWorker.Enabled || cfg.InviteActivityRewardWorker.Enabled || cfg.RedPacketBroadcastWorker.Enabled || cfg.UserLeaderboardWorker.Enabled) && !cfg.RocketMQ.WalletOutbox.Enabled {
		return Config{}, errors.New("wallet outbox workers require rocketmq.wallet_outbox.enabled")
	}
	if cfg.UserLeaderboardWorker.Enabled && cfg.UserLeaderboardWorker.RedisAddr == "" {
		return Config{}, errors.New("user leaderboard worker requires redis_addr")
	}
	if cfg.InviteActivityRewardWorker.Enabled && !cfg.RocketMQ.UserOutbox.Enabled {
		return Config{}, errors.New("invite activity reward worker requires rocketmq.user_outbox.enabled")
	}
	if cfg.MessageActionConfirmWorker.Enabled && (!cfg.RocketMQ.UserOutbox.Enabled || !cfg.RocketMQ.MessageActionOutbox.Enabled) {
		return Config{}, errors.New("message action confirm worker requires rocketmq.user_outbox.enabled and rocketmq.message_action_outbox.enabled")
	}
	cfg.MessageActionConfirmWorker.AppCodes = normalizeAppCodes(cfg.MessageActionConfirmWorker.AppCodes)
	if cfg.MessageActionConfirmWorker.Enabled && len(cfg.MessageActionConfirmWorker.AppCodes) == 0 {
		return Config{}, errors.New("message action confirm worker requires app_codes")
	}
	if cfg.ActivityTemplateStatsWorker.Enabled && !cfg.RocketMQ.ActivityTemplateOutbox.Enabled {
		return Config{}, errors.New("activity template stats worker requires rocketmq.activity_template_outbox.enabled")
	}
	return cfg, nil
}

func normalizeAppCodes(input []string) []string {
	seen := make(map[string]struct{}, len(input))
	out := make([]string, 0, len(input))
	for _, value := range input {
		if strings.TrimSpace(value) == "" {
			continue
		}
		value = appcode.Normalize(value)
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
	defaults := defaultRocketMQConfig()
	cfg.NameServers = normalizeStringSlice(cfg.NameServers)
	cfg.NameServerDomain = strings.TrimSpace(cfg.NameServerDomain)
	cfg.AccessKey = strings.TrimSpace(cfg.AccessKey)
	cfg.SecretKey = strings.TrimSpace(cfg.SecretKey)
	cfg.SecurityToken = strings.TrimSpace(cfg.SecurityToken)
	cfg.Namespace = strings.TrimSpace(cfg.Namespace)
	if cfg.RoomOutbox.Topic = strings.TrimSpace(cfg.RoomOutbox.Topic); cfg.RoomOutbox.Topic == "" {
		cfg.RoomOutbox.Topic = defaults.RoomOutbox.Topic
	}
	if cfg.RoomOutbox.ConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.ConsumerGroup); cfg.RoomOutbox.ConsumerGroup == "" {
		cfg.RoomOutbox.ConsumerGroup = defaults.RoomOutbox.ConsumerGroup
	}
	if cfg.RoomOutbox.ActivityTemplateConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.ActivityTemplateConsumerGroup); cfg.RoomOutbox.ActivityTemplateConsumerGroup == "" {
		cfg.RoomOutbox.ActivityTemplateConsumerGroup = defaults.RoomOutbox.ActivityTemplateConsumerGroup
	}
	if cfg.RoomOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.RoomOutbox.ConsumerMaxReconsumeTimes = defaults.RoomOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.LuckyGiftOutbox.Topic = strings.TrimSpace(cfg.LuckyGiftOutbox.Topic); cfg.LuckyGiftOutbox.Topic == "" {
		cfg.LuckyGiftOutbox.Topic = defaults.LuckyGiftOutbox.Topic
	}
	if cfg.LuckyGiftOutbox.ConsumerGroup = strings.TrimSpace(cfg.LuckyGiftOutbox.ConsumerGroup); cfg.LuckyGiftOutbox.ConsumerGroup == "" {
		cfg.LuckyGiftOutbox.ConsumerGroup = defaults.LuckyGiftOutbox.ConsumerGroup
	}
	if cfg.LuckyGiftOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.LuckyGiftOutbox.ConsumerMaxReconsumeTimes = defaults.LuckyGiftOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.GameOutbox.Topic = strings.TrimSpace(cfg.GameOutbox.Topic); cfg.GameOutbox.Topic == "" {
		cfg.GameOutbox.Topic = defaults.GameOutbox.Topic
	}
	if cfg.GameOutbox.ConsumerGroup = strings.TrimSpace(cfg.GameOutbox.ConsumerGroup); cfg.GameOutbox.ConsumerGroup == "" {
		cfg.GameOutbox.ConsumerGroup = defaults.GameOutbox.ConsumerGroup
	}
	if cfg.GameOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.GameOutbox.ConsumerMaxReconsumeTimes = defaults.GameOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" {
		cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic
	}
	cfg.WalletOutbox.RealtimeTopic = strings.TrimSpace(cfg.WalletOutbox.RealtimeTopic)
	if cfg.WalletOutbox.FirstRechargeConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.FirstRechargeConsumerGroup); cfg.WalletOutbox.FirstRechargeConsumerGroup == "" {
		cfg.WalletOutbox.FirstRechargeConsumerGroup = defaults.WalletOutbox.FirstRechargeConsumerGroup
	}
	if cfg.WalletOutbox.CumulativeRechargeConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.CumulativeRechargeConsumerGroup); cfg.WalletOutbox.CumulativeRechargeConsumerGroup == "" {
		cfg.WalletOutbox.CumulativeRechargeConsumerGroup = defaults.WalletOutbox.CumulativeRechargeConsumerGroup
	}
	if cfg.WalletOutbox.InviteActivityConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.InviteActivityConsumerGroup); cfg.WalletOutbox.InviteActivityConsumerGroup == "" {
		cfg.WalletOutbox.InviteActivityConsumerGroup = defaults.WalletOutbox.InviteActivityConsumerGroup
	}
	if cfg.WalletOutbox.RedPacketBroadcastConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.RedPacketBroadcastConsumerGroup); cfg.WalletOutbox.RedPacketBroadcastConsumerGroup == "" {
		cfg.WalletOutbox.RedPacketBroadcastConsumerGroup = defaults.WalletOutbox.RedPacketBroadcastConsumerGroup
	}
	if cfg.WalletOutbox.VIPRebateNoticeConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.VIPRebateNoticeConsumerGroup); cfg.WalletOutbox.VIPRebateNoticeConsumerGroup == "" {
		cfg.WalletOutbox.VIPRebateNoticeConsumerGroup = defaults.WalletOutbox.VIPRebateNoticeConsumerGroup
	}
	if cfg.WalletOutbox.TaskConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.TaskConsumerGroup); cfg.WalletOutbox.TaskConsumerGroup == "" {
		cfg.WalletOutbox.TaskConsumerGroup = defaults.WalletOutbox.TaskConsumerGroup
	}
	if cfg.WalletOutbox.UserLeaderboardConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.UserLeaderboardConsumerGroup); cfg.WalletOutbox.UserLeaderboardConsumerGroup == "" {
		cfg.WalletOutbox.UserLeaderboardConsumerGroup = defaults.WalletOutbox.UserLeaderboardConsumerGroup
	}
	if cfg.WalletOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.WalletOutbox.ConsumerMaxReconsumeTimes = defaults.WalletOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.UserOutbox.Topic = strings.TrimSpace(cfg.UserOutbox.Topic); cfg.UserOutbox.Topic == "" {
		cfg.UserOutbox.Topic = defaults.UserOutbox.Topic
	}
	if cfg.UserOutbox.ConsumerGroup = strings.TrimSpace(cfg.UserOutbox.ConsumerGroup); cfg.UserOutbox.ConsumerGroup == "" {
		cfg.UserOutbox.ConsumerGroup = defaults.UserOutbox.ConsumerGroup
	}
	if cfg.UserOutbox.TaskConsumerGroup = strings.TrimSpace(cfg.UserOutbox.TaskConsumerGroup); cfg.UserOutbox.TaskConsumerGroup == "" {
		cfg.UserOutbox.TaskConsumerGroup = defaults.UserOutbox.TaskConsumerGroup
	}
	if cfg.UserOutbox.InviteActivityConsumerGroup = strings.TrimSpace(cfg.UserOutbox.InviteActivityConsumerGroup); cfg.UserOutbox.InviteActivityConsumerGroup == "" {
		cfg.UserOutbox.InviteActivityConsumerGroup = defaults.UserOutbox.InviteActivityConsumerGroup
	}
	if cfg.UserOutbox.MessageActionConsumerGroup = strings.TrimSpace(cfg.UserOutbox.MessageActionConsumerGroup); cfg.UserOutbox.MessageActionConsumerGroup == "" {
		cfg.UserOutbox.MessageActionConsumerGroup = defaults.UserOutbox.MessageActionConsumerGroup
	}
	if cfg.UserOutbox.AgencyHostRemovedNoticeConsumerGroup = strings.TrimSpace(cfg.UserOutbox.AgencyHostRemovedNoticeConsumerGroup); cfg.UserOutbox.AgencyHostRemovedNoticeConsumerGroup == "" {
		cfg.UserOutbox.AgencyHostRemovedNoticeConsumerGroup = defaults.UserOutbox.AgencyHostRemovedNoticeConsumerGroup
	}
	if cfg.UserOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.UserOutbox.ConsumerMaxReconsumeTimes = defaults.UserOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.MessageActionOutbox.Topic = strings.TrimSpace(cfg.MessageActionOutbox.Topic); cfg.MessageActionOutbox.Topic == "" {
		cfg.MessageActionOutbox.Topic = defaults.MessageActionOutbox.Topic
	}
	if cfg.MessageActionOutbox.ProducerGroup = strings.TrimSpace(cfg.MessageActionOutbox.ProducerGroup); cfg.MessageActionOutbox.ProducerGroup == "" {
		cfg.MessageActionOutbox.ProducerGroup = defaults.MessageActionOutbox.ProducerGroup
	}
	if cfg.MessageActionOutbox.SendTimeout <= 0 {
		cfg.MessageActionOutbox.SendTimeout = defaults.MessageActionOutbox.SendTimeout
	}
	if cfg.MessageActionOutbox.Retry < 0 {
		cfg.MessageActionOutbox.Retry = defaults.MessageActionOutbox.Retry
	}
	if cfg.ActivityTemplateOutbox.Topic = strings.TrimSpace(cfg.ActivityTemplateOutbox.Topic); cfg.ActivityTemplateOutbox.Topic == "" {
		cfg.ActivityTemplateOutbox.Topic = defaults.ActivityTemplateOutbox.Topic
	}
	if cfg.ActivityTemplateOutbox.ProducerGroup = strings.TrimSpace(cfg.ActivityTemplateOutbox.ProducerGroup); cfg.ActivityTemplateOutbox.ProducerGroup == "" {
		cfg.ActivityTemplateOutbox.ProducerGroup = defaults.ActivityTemplateOutbox.ProducerGroup
	}
	if cfg.ActivityTemplateOutbox.SendTimeout <= 0 {
		cfg.ActivityTemplateOutbox.SendTimeout = defaults.ActivityTemplateOutbox.SendTimeout
	}
	if cfg.ActivityTemplateOutbox.Retry < 0 {
		cfg.ActivityTemplateOutbox.Retry = defaults.ActivityTemplateOutbox.Retry
	}
	if cfg.RoomOutbox.Enabled || cfg.LuckyGiftOutbox.Enabled || cfg.GameOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled || cfg.MessageActionOutbox.Enabled || cfg.ActivityTemplateOutbox.Enabled {
		cfg.Enabled = true
	}
	if cfg.Enabled {
		if len(cfg.NameServers) == 0 && cfg.NameServerDomain == "" {
			return RocketMQConfig{}, errors.New("rocketmq name_servers or name_server_domain is required")
		}
		if (cfg.AccessKey == "") != (cfg.SecretKey == "") {
			return RocketMQConfig{}, errors.New("rocketmq access_key and secret_key must be configured together")
		}
	}
	return cfg, nil
}

func normalizeStringSlice(input []string) []string {
	out := make([]string, 0, len(input))
	for _, value := range input {
		if value = strings.TrimSpace(value); value != "" {
			out = append(out, value)
		}
	}
	return out
}
