// Package config 定义 user-service 的启动配置和 YAML 加载入口。
package config

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

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

// Config 描述 user-service 启动配置。
type Config struct {
	// ServiceName 是进程服务名，当前主要用于配置可读性。
	ServiceName string `yaml:"service_name"`
	// NodeID 是当前服务节点标识，预留给日志和后续健康响应使用。
	NodeID string `yaml:"node_id"`
	// Environment 是日志和部署策略使用的环境标签。
	Environment string `yaml:"environment"`
	// GRPCAddr 是 user-service 内部 gRPC 监听地址。
	GRPCAddr string `yaml:"grpc_addr"`
	// HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址，不承载业务流量。
	HealthHTTPAddr string `yaml:"health_http_addr"`
	// MySQLDSN 是用户主数据、认证身份、session 和短号事务的必需存储连接串。
	MySQLDSN string `yaml:"mysql_dsn"`
	// MySQLAutoMigrate 保留给显式本地迁移；线上 schema 由发布流程或 initdb 管理。
	MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
	// IDGenerator 控制 user_id 发号节点。
	IDGenerator IDGeneratorConfig `yaml:"id_generator"`
	// DisplayUserID 控制短号分配重试和默认短号修改冷却期。
	DisplayUserID DisplayUserIDConfig `yaml:"display_user_id"`
	// ThirdParty 控制本地三方登录 provider allowlist。
	ThirdParty ThirdPartyConfig `yaml:"third_party"`
	// InviteRechargeWorker 控制邀请有效人数的 wallet recharge outbox 消费。
	InviteRechargeWorker InviteRechargeWorkerConfig `yaml:"invite_recharge_worker"`
	// MicTimeWorker 控制 room-service 麦位事件消费和用户麦上时长聚合。
	MicTimeWorker MicTimeWorkerConfig `yaml:"mic_time_worker"`
	// RoomTimeWorker 控制 room-service 进退房事件消费和用户在房时长聚合。
	RoomTimeWorker RoomTimeWorkerConfig `yaml:"room_time_worker"`
	// CPRelationshipWorker 控制 room-service 送礼事件消费和 CP 关系申请/亲密值维护。
	CPRelationshipWorker CPRelationshipWorkerConfig `yaml:"cp_relationship_worker"`
	// CPIntimacyLeaderboard 控制 CP 亲密值排行榜 Redis 读模型。
	CPIntimacyLeaderboard CPIntimacyLeaderboardConfig `yaml:"cp_intimacy_leaderboard"`
	// OutboxWorker 控制 user_outbox 到 MQ 的补偿投递。
	OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
	// RocketMQ 控制 wallet/room outbox MQ 消费。
	RocketMQ RocketMQConfig `yaml:"rocketmq"`
	// LoginRisk 控制登录后 IP 风控策略、Redis session denylist 和 IP 查询 provider。
	LoginRisk LoginRiskConfig `yaml:"login_risk"`
	// RoomService 是封禁后驱逐当前房间用户的内部 gRPC 依赖。
	RoomService RoomServiceConfig `yaml:"room_service"`
	// ActivityService 是注册成功后发放注册奖励的内部 gRPC 依赖。
	ActivityService ActivityServiceConfig `yaml:"activity_service"`
	// WalletService 是 CP 榜头像框等资源读模型的内部 gRPC 依赖。
	WalletService WalletServiceConfig `yaml:"wallet_service"`
	// TencentIM 是封禁后失效用户 IM 登录态的外部集成配置。
	TencentIM TencentIMConfig `yaml:"tencent_im"`
	// JWT 控制 access token 和 refresh session 的签发参数。
	JWT JWTConfig `yaml:"jwt"`
	// Log 控制 stdout 结构化日志格式、等级和 access body 策略。
	Log logx.Config `yaml:"log"`
}

// LoginRiskConfig 保存登录地区风控的 Redis、TTL 和 provider 顺序；后台扫描由 cron-service 调度。
type LoginRiskConfig struct {
	Enabled              bool     `yaml:"enabled"`
	RedisAddr            string   `yaml:"redis_addr"`
	RedisPassword        string   `yaml:"redis_password"`
	RedisDB              int      `yaml:"redis_db"`
	UnsupportedCountries []string `yaml:"unsupported_countries"`
	BlockedTTLSec        int64    `yaml:"blocked_ttl_sec"`
	AllowedTTLSec        int64    `yaml:"allowed_ttl_sec"`
	UnknownTTLSec        int64    `yaml:"unknown_ttl_sec"`
	ProviderTimeoutMs    int64    `yaml:"provider_timeout_ms"`
	Providers            []string `yaml:"providers"`
}

// RoomServiceConfig 保存 user-service 到 room-service 的低频治理 RPC 连接参数。
type RoomServiceConfig struct {
	Enabled          bool   `yaml:"enabled"`
	Addr             string `yaml:"addr"`
	RequestTimeoutMs int64  `yaml:"request_timeout_ms"`
}

// ActivityServiceConfig 保存 user-service 到 activity-service 的低频注册奖励 RPC 连接参数。
type ActivityServiceConfig struct {
	Enabled          bool   `yaml:"enabled"`
	Addr             string `yaml:"addr"`
	RequestTimeoutMs int64  `yaml:"request_timeout_ms"`
}

// WalletServiceConfig 保存 user-service 到 wallet-service 的资源读模型 RPC 连接参数。
type WalletServiceConfig struct {
	Enabled          bool   `yaml:"enabled"`
	Addr             string `yaml:"addr"`
	RequestTimeoutMs int64  `yaml:"request_timeout_ms"`
}

// TencentIMConfig 描述 user-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"`
	AdminUserSigTTLSec int64  `yaml:"admin_user_sig_ttl_sec"`
	Endpoint           string `yaml:"endpoint"`
	RequestTimeoutMs   int64  `yaml:"request_timeout_ms"`
}

// RESTConfig 转换成 pkg/tencentim REST client 配置。
func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig {
	timeout := time.Duration(c.RequestTimeoutMs) * time.Millisecond
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	ttl := time.Duration(c.AdminUserSigTTLSec) * time.Second
	if ttl <= 0 {
		ttl = 24 * time.Hour
	}
	return tencentim.RESTConfig{
		SDKAppID:        c.SDKAppID,
		SecretKey:       c.SecretKey,
		AdminIdentifier: c.AdminIdentifier,
		AdminUserSigTTL: ttl,
		Endpoint:        c.Endpoint,
		GroupType:       tencentim.DefaultGroupType,
		HTTPClient:      &http.Client{Timeout: timeout},
	}
}

// IDGeneratorConfig 保存 user_id 发号节点配置。
type IDGeneratorConfig struct {
	// NodeID 参与雪花 ID 发号，多个 user-service 节点必须不同。
	NodeID int64 `yaml:"node_id"`
}

// DisplayUserIDConfig 保存短号分配和修改策略。
type DisplayUserIDConfig struct {
	// AllocateMaxAttempts 是默认短号冲突时的最大重试次数。
	AllocateMaxAttempts int `yaml:"allocate_max_attempts"`
	// ChangeCooldownSec 是默认短号修改冷却期秒数。
	ChangeCooldownSec int64 `yaml:"change_cooldown_sec"`
}

// ThirdPartyConfig 保存三方登录 provider allowlist 和真实 verifier 配置。
type ThirdPartyConfig struct {
	// AllowedProviders 是 provider allowlist；firebase 会被强制路由到 Firebase verifier。
	AllowedProviders []string `yaml:"allowed_providers"`
	// Firebase 保存 Firebase Auth ID token verifier 配置。
	Firebase FirebaseConfig `yaml:"firebase"`
}

// FirebaseConfig 保存 Firebase Auth ID token verifier 配置。
type FirebaseConfig struct {
	// ProjectID 必须匹配 Firebase token 的 aud 和 issuer。
	ProjectID string `yaml:"project_id"`
	// AllowedSignInProviders 限制 firebase.sign_in_provider，首版默认 google.com。
	AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"`
	// CertsURL 只用于测试或私有代理；生产为空时使用 Firebase 官方公钥端点。
	CertsURL string `yaml:"certs_url"`
	// Projects 按 app_code 配置 Firebase 项目；Huwaa/Lalu 必须各自校验自己的 aud/iss。
	Projects map[string]FirebaseProjectConfig `yaml:"projects"`
}

// FirebaseProjectConfig 保存单个 App 的 Firebase 项目配置。
type FirebaseProjectConfig struct {
	// ProjectID 必须匹配该 App Firebase token 的 aud 和 issuer。
	ProjectID string `yaml:"project_id"`
	// AllowedSignInProviders 为空时继承 firebase.allowed_sign_in_providers。
	AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"`
	// CertsURL 为空时继承 firebase.certs_url，生产通常不需要配置。
	CertsURL string `yaml:"certs_url"`
}

// InviteRechargeWorkerConfig 保存钱包充值事实消费策略。
type InviteRechargeWorkerConfig struct {
	// Enabled 控制是否启动钱包充值 MQ 消费。
	Enabled bool `yaml:"enabled"`
}

// MicTimeWorkerConfig 保存房间麦位事件消费策略。
type MicTimeWorkerConfig struct {
	// Enabled 控制是否启动 room outbox MQ 消费；关闭后查询仍可读取已聚合数据。
	Enabled bool `yaml:"enabled"`
}

// RoomTimeWorkerConfig 保存房间进退房事件消费策略。
type RoomTimeWorkerConfig struct {
	// Enabled 控制是否启动 room outbox MQ 消费；关闭后查询仍可读取已聚合数据。
	Enabled bool `yaml:"enabled"`
	// ConsumerGroup 必须独立于麦位/CP 消费，避免读模型共享 MQ 位点。
	ConsumerGroup string `yaml:"consumer_group"`
}

// CPRelationshipWorkerConfig 保存 CP 礼物事件消费策略。
type CPRelationshipWorkerConfig struct {
	// Enabled 控制是否启动 room gift MQ 消费；关闭后只能处理历史已消费关系。
	Enabled bool `yaml:"enabled"`
	// ConsumerGroup 必须独立于麦位消费，避免两个读模型共享 MQ 位点。
	ConsumerGroup string `yaml:"consumer_group"`
}

// CPIntimacyLeaderboardConfig 保存 CP 亲密榜 Redis zset 的连接和 key 前缀。
type CPIntimacyLeaderboardConfig struct {
	Enabled       bool   `yaml:"enabled"`
	RedisAddr     string `yaml:"redis_addr"`
	RedisPassword string `yaml:"redis_password"`
	RedisDB       int    `yaml:"redis_db"`
	KeyPrefix     string `yaml:"key_prefix"`
}

// OutboxWorkerConfig 保存 user_outbox MQ 发布策略。
type OutboxWorkerConfig struct {
	Enabled bool `yaml:"enabled"`
	// AppCodes 是可投递租户 allowlist；空值使用低频数据库发现，非空支持按 App 灰度回放。
	AppCodes       []string      `yaml:"app_codes"`
	PollInterval   time.Duration `yaml:"poll_interval"`
	BatchSize      int           `yaml:"batch_size"`
	PublishTimeout time.Duration `yaml:"publish_timeout"`
}

// RocketMQConfig 保存 user-service 消费 owner service outbox 的 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"`
	WalletOutbox     WalletOutboxMQConfig `yaml:"wallet_outbox"`
	RoomOutbox       RoomOutboxMQConfig   `yaml:"room_outbox"`
	UserOutbox       UserOutboxMQConfig   `yaml:"user_outbox"`
}

type WalletOutboxMQConfig struct {
	Enabled                   bool   `yaml:"enabled"`
	Topic                     string `yaml:"topic"`
	ConsumerGroup             string `yaml:"consumer_group"`
	ConsumerMaxReconsumeTimes int32  `yaml:"consumer_max_reconsume_times"`
}

type RoomOutboxMQConfig struct {
	Enabled                   bool   `yaml:"enabled"`
	Topic                     string `yaml:"topic"`
	ConsumerGroup             string `yaml:"consumer_group"`
	ConsumerMaxReconsumeTimes int32  `yaml:"consumer_max_reconsume_times"`
}

type UserOutboxMQConfig struct {
	Enabled       bool   `yaml:"enabled"`
	Topic         string `yaml:"topic"`
	ProducerGroup string `yaml:"producer_group"`
}

// JWTConfig 保存 access token 和 refresh token 的签发策略。
type JWTConfig struct {
	// Issuer 写入 access token iss claim。
	Issuer string `yaml:"issuer"`
	// AccessTokenTTLSec 是 App 登录 access token 有效期；默认 168 小时，减少正常用户被动重登。
	AccessTokenTTLSec int64 `yaml:"access_token_ttl_sec"`
	// RefreshTokenTTLSec 是服务端 session/refresh token 有效期。
	RefreshTokenTTLSec int64 `yaml:"refresh_token_ttl_sec"`
	// RefreshRotationGraceSec 是直接上一代 refresh token 幂等恢复窗口，默认 30 秒。
	RefreshRotationGraceSec int64 `yaml:"refresh_rotation_grace_sec"`
	// SigningAlg 当前只支持 HS256。
	SigningAlg string `yaml:"signing_alg"`
	// SigningSecret 是 HS256 签名密钥，生产不能使用默认开发值。
	SigningSecret string `yaml:"signing_secret"`
}

// Default 返回本地默认配置。
func Default() Config {
	// 默认端口遵守项目 13xxx 约束；本地进程默认连接 docker-compose 暴露的 MySQL。
	return Config{
		ServiceName:      "user-service",
		NodeID:           "user-local",
		Environment:      "local",
		GRPCAddr:         ":13005",
		HealthHTTPAddr:   ":13105",
		MySQLDSN:         "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC",
		MySQLAutoMigrate: false,
		IDGenerator: IDGeneratorConfig{
			NodeID: 1,
		},
		DisplayUserID: DisplayUserIDConfig{
			AllocateMaxAttempts: 8,
			ChangeCooldownSec:   2592000,
		},
		ThirdParty: ThirdPartyConfig{
			AllowedProviders: []string{"firebase"},
			Firebase: FirebaseConfig{
				AllowedSignInProviders: []string{"google.com"},
			},
		},
		InviteRechargeWorker: InviteRechargeWorkerConfig{
			Enabled: false,
		},
		MicTimeWorker: MicTimeWorkerConfig{
			Enabled: false,
		},
		RoomTimeWorker: RoomTimeWorkerConfig{
			Enabled:       false,
			ConsumerGroup: "hyapp-user-roomtime-room-outbox",
		},
		CPRelationshipWorker: CPRelationshipWorkerConfig{
			Enabled:       false,
			ConsumerGroup: "hyapp-user-cp-room-outbox",
		},
		CPIntimacyLeaderboard: CPIntimacyLeaderboardConfig{
			Enabled:   true,
			RedisAddr: "127.0.0.1:13379",
			KeyPrefix: "user:cp_intimacy_leaderboard",
		},
		OutboxWorker: OutboxWorkerConfig{
			Enabled:        false,
			PollInterval:   time.Second,
			BatchSize:      100,
			PublishTimeout: 3 * time.Second,
		},
		RocketMQ: defaultRocketMQConfig(),
		LoginRisk: LoginRiskConfig{
			Enabled:              true,
			RedisAddr:            "127.0.0.1:13379",
			UnsupportedCountries: []string{"CN"},
			BlockedTTLSec:        2592000,
			AllowedTTLSec:        604800,
			UnknownTTLSec:        1800,
			ProviderTimeoutMs:    800,
			Providers:            []string{"edge_country"},
		},
		RoomService: RoomServiceConfig{
			Enabled:          true,
			Addr:             "127.0.0.1:13001",
			RequestTimeoutMs: 3000,
		},
		ActivityService: ActivityServiceConfig{
			Enabled:          true,
			Addr:             "127.0.0.1:13006",
			RequestTimeoutMs: 3000,
		},
		WalletService: WalletServiceConfig{
			Enabled:          true,
			Addr:             "127.0.0.1:13004",
			RequestTimeoutMs: 3000,
		},
		TencentIM: TencentIMConfig{
			Enabled:            false,
			AdminIdentifier:    "administrator",
			AdminUserSigTTLSec: 86400,
			Endpoint:           tencentim.DefaultEndpoint,
			RequestTimeoutMs:   5000,
		},
		JWT: JWTConfig{
			Issuer:                  "hyapp",
			AccessTokenTTLSec:       604800,
			RefreshTokenTTLSec:      2592000,
			RefreshRotationGraceSec: 30,
			SigningAlg:              "HS256",
			SigningSecret:           "dev-shared-secret",
		},
		Log: logx.Config{
			Level:           "info",
			Format:          "json",
			MaxPayloadBytes: 2048,
		},
	}
}

// Load 从独立 config.yaml 读取 user-service 配置。
func Load(path string) (Config, error) {
	cfg := Default()
	if path == "" {
		// 空路径显式使用默认配置，便于单元测试和最小启动。
		return cfg, nil
	}

	if err := configx.LoadYAML(path, &cfg); err != nil {
		// 配置解析失败返回空 Config，避免调用方误用部分默认值继续启动。
		return Config{}, err
	}

	cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
	if cfg.ServiceName == "" {
		cfg.ServiceName = "user-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 = ":13105"
	}
	cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.LoginRisk.RedisAddr)
	cfg.CPIntimacyLeaderboard.RedisAddr = strings.TrimSpace(cfg.CPIntimacyLeaderboard.RedisAddr)
	cfg.CPIntimacyLeaderboard.KeyPrefix = strings.Trim(strings.TrimSpace(cfg.CPIntimacyLeaderboard.KeyPrefix), ":")
	if cfg.CPIntimacyLeaderboard.RedisAddr == "" {
		cfg.CPIntimacyLeaderboard.RedisAddr = cfg.LoginRisk.RedisAddr
	}
	if cfg.CPIntimacyLeaderboard.KeyPrefix == "" {
		cfg.CPIntimacyLeaderboard.KeyPrefix = "user:cp_intimacy_leaderboard"
	}
	cfg.ThirdParty.AllowedProviders = normalizeStringSlice(cfg.ThirdParty.AllowedProviders)
	cfg.ThirdParty.Firebase = normalizeFirebaseConfig(cfg.ThirdParty.Firebase)
	cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
	if cfg.RoomService.Addr == "" {
		cfg.RoomService.Addr = "127.0.0.1:13001"
	}
	if cfg.RoomService.RequestTimeoutMs <= 0 {
		cfg.RoomService.RequestTimeoutMs = 3000
	}
	cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
	if cfg.ActivityService.Addr == "" {
		cfg.ActivityService.Addr = "127.0.0.1:13006"
	}
	if cfg.ActivityService.RequestTimeoutMs <= 0 {
		cfg.ActivityService.RequestTimeoutMs = 3000
	}
	cfg.WalletService.Addr = strings.TrimSpace(cfg.WalletService.Addr)
	if cfg.WalletService.Addr == "" {
		cfg.WalletService.Addr = "127.0.0.1:13004"
	}
	if cfg.WalletService.RequestTimeoutMs <= 0 {
		cfg.WalletService.RequestTimeoutMs = 3000
	}
	cfg.TencentIM.SecretKey = strings.TrimSpace(cfg.TencentIM.SecretKey)
	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
	}
	if cfg.TencentIM.AdminUserSigTTLSec <= 0 {
		cfg.TencentIM.AdminUserSigTTLSec = 86400
	}
	if cfg.TencentIM.RequestTimeoutMs <= 0 {
		cfg.TencentIM.RequestTimeoutMs = 5000
	}
	if cfg.LoginRisk.BlockedTTLSec <= 0 {
		cfg.LoginRisk.BlockedTTLSec = 2592000
	}
	if cfg.LoginRisk.AllowedTTLSec <= 0 {
		cfg.LoginRisk.AllowedTTLSec = 604800
	}
	if cfg.LoginRisk.UnknownTTLSec <= 0 {
		cfg.LoginRisk.UnknownTTLSec = 1800
	}
	if cfg.LoginRisk.ProviderTimeoutMs <= 0 {
		cfg.LoginRisk.ProviderTimeoutMs = 800
	}
	if cfg.JWT.RefreshRotationGraceSec <= 0 {
		cfg.JWT.RefreshRotationGraceSec = 30
	}
	for index, provider := range cfg.LoginRisk.Providers {
		cfg.LoginRisk.Providers[index] = strings.TrimSpace(provider)
	}
	rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
	if err != nil {
		return Config{}, err
	}
	cfg.RocketMQ = rocketMQ
	if cfg.InviteRechargeWorker.Enabled && !cfg.RocketMQ.WalletOutbox.Enabled {
		return Config{}, errors.New("invite_recharge_worker requires rocketmq.wallet_outbox.enabled")
	}
	if cfg.MicTimeWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled {
		return Config{}, errors.New("mic_time_worker requires rocketmq.room_outbox.enabled")
	}
	cfg.RoomTimeWorker.ConsumerGroup = strings.TrimSpace(cfg.RoomTimeWorker.ConsumerGroup)
	if cfg.RoomTimeWorker.ConsumerGroup == "" {
		cfg.RoomTimeWorker.ConsumerGroup = "hyapp-user-roomtime-room-outbox"
	}
	if cfg.RoomTimeWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled {
		return Config{}, errors.New("room_time_worker requires rocketmq.room_outbox.enabled")
	}
	cfg.CPRelationshipWorker.ConsumerGroup = strings.TrimSpace(cfg.CPRelationshipWorker.ConsumerGroup)
	if cfg.CPRelationshipWorker.ConsumerGroup == "" {
		cfg.CPRelationshipWorker.ConsumerGroup = "hyapp-user-cp-room-outbox"
	}
	if cfg.CPRelationshipWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled {
		return Config{}, errors.New("cp_relationship_worker requires rocketmq.room_outbox.enabled")
	}
	if cfg.OutboxWorker.PollInterval <= 0 {
		cfg.OutboxWorker.PollInterval = time.Second
	}
	if cfg.OutboxWorker.BatchSize <= 0 {
		cfg.OutboxWorker.BatchSize = 100
	}
	if cfg.OutboxWorker.PublishTimeout <= 0 {
		cfg.OutboxWorker.PublishTimeout = 3 * time.Second
	}
	cfg.OutboxWorker.AppCodes = outboxpartition.Normalize(cfg.OutboxWorker.AppCodes, false)
	if cfg.OutboxWorker.Enabled && !cfg.RocketMQ.UserOutbox.Enabled {
		return Config{}, errors.New("outbox_worker requires rocketmq.user_outbox.enabled")
	}
	return cfg, nil
}

func defaultRocketMQConfig() RocketMQConfig {
	return RocketMQConfig{
		Enabled: false,
		WalletOutbox: WalletOutboxMQConfig{
			Enabled:                   false,
			Topic:                     "hyapp_wallet_outbox",
			ConsumerGroup:             "hyapp-user-invite-wallet-outbox",
			ConsumerMaxReconsumeTimes: 16,
		},
		RoomOutbox: RoomOutboxMQConfig{
			Enabled:                   false,
			Topic:                     "hyapp_room_outbox",
			ConsumerGroup:             "hyapp-user-mictime-room-outbox",
			ConsumerMaxReconsumeTimes: 16,
		},
		UserOutbox: UserOutboxMQConfig{
			Enabled:       false,
			Topic:         "hyapp_user_outbox",
			ProducerGroup: "hyapp-user-outbox-producer",
		},
	}
}

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.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" {
		cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic
	}
	if cfg.WalletOutbox.ConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.ConsumerGroup); cfg.WalletOutbox.ConsumerGroup == "" {
		cfg.WalletOutbox.ConsumerGroup = defaults.WalletOutbox.ConsumerGroup
	}
	if cfg.WalletOutbox.ConsumerMaxReconsumeTimes <= 0 {
		cfg.WalletOutbox.ConsumerMaxReconsumeTimes = defaults.WalletOutbox.ConsumerMaxReconsumeTimes
	}
	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.ConsumerMaxReconsumeTimes <= 0 {
		cfg.RoomOutbox.ConsumerMaxReconsumeTimes = defaults.RoomOutbox.ConsumerMaxReconsumeTimes
	}
	if cfg.UserOutbox.Topic = strings.TrimSpace(cfg.UserOutbox.Topic); cfg.UserOutbox.Topic == "" {
		cfg.UserOutbox.Topic = defaults.UserOutbox.Topic
	}
	if cfg.UserOutbox.ProducerGroup = strings.TrimSpace(cfg.UserOutbox.ProducerGroup); cfg.UserOutbox.ProducerGroup == "" {
		cfg.UserOutbox.ProducerGroup = defaults.UserOutbox.ProducerGroup
	}
	if cfg.WalletOutbox.Enabled || cfg.RoomOutbox.Enabled || cfg.UserOutbox.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 normalizeFirebaseConfig(cfg FirebaseConfig) FirebaseConfig {
	cfg.ProjectID = strings.TrimSpace(cfg.ProjectID)
	cfg.AllowedSignInProviders = normalizeStringSlice(cfg.AllowedSignInProviders)
	cfg.CertsURL = strings.TrimSpace(cfg.CertsURL)
	if len(cfg.Projects) == 0 {
		return cfg
	}

	projects := make(map[string]FirebaseProjectConfig, len(cfg.Projects))
	for rawAppCode, project := range cfg.Projects {
		app := appcode.Normalize(rawAppCode)
		project.ProjectID = strings.TrimSpace(project.ProjectID)
		project.CertsURL = strings.TrimSpace(project.CertsURL)
		project.AllowedSignInProviders = normalizeStringSlice(project.AllowedSignInProviders)
		if len(project.AllowedSignInProviders) == 0 {
			// 单 App 没写登录方式时继承全局配置；全局为空时 auth verifier 仍会默认 google.com。
			project.AllowedSignInProviders = cfg.AllowedSignInProviders
		}
		if project.CertsURL == "" {
			// Firebase 公钥端点对所有项目相同；测试代理或私有代理可以只写一次。
			project.CertsURL = cfg.CertsURL
		}
		projects[app] = project
	}
	cfg.Projects = projects
	return cfg
}

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