package config

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"hyapp/pkg/configx"
	"hyapp/pkg/giftlimits"
	"hyapp/pkg/grpcclient"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/tencentrtc"
)

// Config 描述 gateway-service 启动所需的最小配置。
type Config struct {
	ServiceName          string                     `yaml:"service_name"`
	NodeID               string                     `yaml:"node_id"`
	Environment          string                     `yaml:"environment"`
	HTTPAddr             string                     `yaml:"http_addr"`
	CORS                 CORSConfig                 `yaml:"cors"`
	JWTSecret            string                     `yaml:"jwt_secret"`
	RoomServiceAddr      string                     `yaml:"room_service_addr"`
	UserServiceAddr      string                     `yaml:"user_service_addr"`
	WalletServiceAddr    string                     `yaml:"wallet_service_addr"`
	ActivityServiceAddr  string                     `yaml:"activity_service_addr"`
	LuckyGiftServiceAddr string                     `yaml:"lucky_gift_service_addr"`
	GameServiceAddr      string                     `yaml:"game_service_addr"`
	Statistics           StatisticsConfig           `yaml:"statistics"`
	GRPCClient           GRPCClientConfig           `yaml:"grpc_client"`
	AuthRateLimit        AuthRateLimitConfig        `yaml:"auth_rate_limit"`
	GiftCapacityLimit    GiftCapacityLimitConfig    `yaml:"gift_capacity_limit"`
	LoginRisk            LoginRiskConfig            `yaml:"login_risk"`
	AppConfig            AppConfigConfig            `yaml:"app_config"`
	Leaderboard          LeaderboardConfig          `yaml:"leaderboard"`
	TencentIM            TencentIMConfig            `yaml:"tencent_im"`
	TencentRTC           TencentRTCConfig           `yaml:"tencent_rtc"`
	TencentCOS           TencentCOSConfig           `yaml:"tencent-cos"`
	FinanceNotifications FinanceNotificationsConfig `yaml:"finance_notifications"`
	Log                  logx.Config                `yaml:"log"`
}

// CORSConfig 控制浏览器/H5 访问 gateway API 的跨域入口策略。
type CORSConfig struct {
	// Enabled 为 true 时，gateway 在最外层处理浏览器 preflight，避免 OPTIONS 进入业务 handler。
	Enabled bool `yaml:"enabled"`
	// AllowedOrigins 是允许携带凭据访问 API 的 Origin 白名单；生产不要配置裸星号。
	AllowedOrigins []string `yaml:"allowed_origins"`
	// AllowedMethods 是 preflight 允许的方法集合。
	AllowedMethods []string `yaml:"allowed_methods"`
	// AllowedHeaders 是 preflight 允许的请求头集合。
	AllowedHeaders []string `yaml:"allowed_headers"`
	// ExposeHeaders 是实际响应暴露给浏览器读取的响应头集合。
	ExposeHeaders []string `yaml:"expose_headers"`
	// AllowCredentials 控制浏览器是否允许带 cookie/Authorization 等凭据。
	AllowCredentials bool `yaml:"allow_credentials"`
	// MaxAgeSec 是浏览器缓存 preflight 结果的秒数。
	MaxAgeSec int64 `yaml:"max_age_sec"`
}

// GRPCClientConfig 控制 gateway 访问内部服务的 gRPC transport 策略。
type GRPCClientConfig struct {
	// DefaultTimeout 是没有显式 request deadline 时的 RPC 总预算，包含重试耗时。
	DefaultTimeout time.Duration `yaml:"default_timeout"`
	// ConnectTimeout 是单次建连最小预算，避免内网目标不可达时长期卡住。
	ConnectTimeout time.Duration `yaml:"connect_timeout"`
	// KeepaliveTime 是空闲连接探活周期，帮助 CLB/NAT 后的半开连接尽早发现。
	KeepaliveTime time.Duration `yaml:"keepalive_time"`
	// KeepaliveTimeout 是 keepalive ack 等待时间。
	KeepaliveTimeout time.Duration `yaml:"keepalive_timeout"`
	// RetryMaxAttempts 是包含首次调用在内的最大尝试次数；小于 2 时关闭显式重试。
	RetryMaxAttempts int `yaml:"retry_max_attempts"`
	// RetryInitialBackoff 是第一次重试前等待时间。
	RetryInitialBackoff time.Duration `yaml:"retry_initial_backoff"`
	// RetryMaxBackoff 是重试退避上限。
	RetryMaxBackoff time.Duration `yaml:"retry_max_backoff"`
	// RetryBackoffMultiplier 控制退避增长倍率。
	RetryBackoffMultiplier float64 `yaml:"retry_backoff_multiplier"`
	// RetryableStatusCodes 限定可重试状态，生产默认只允许 UNAVAILABLE。
	RetryableStatusCodes []string `yaml:"retryable_status_codes"`
}

// Runtime 转换为共享 gRPC client 配置，避免 app 层重复维护默认值。
func (c GRPCClientConfig) Runtime() grpcclient.Config {
	return grpcclient.Normalize(grpcclient.Config{
		DefaultTimeout:         c.DefaultTimeout,
		ConnectTimeout:         c.ConnectTimeout,
		KeepaliveTime:          c.KeepaliveTime,
		KeepaliveTimeout:       c.KeepaliveTimeout,
		RetryMaxAttempts:       c.RetryMaxAttempts,
		RetryInitialBackoff:    c.RetryInitialBackoff,
		RetryMaxBackoff:        c.RetryMaxBackoff,
		RetryBackoffMultiplier: c.RetryBackoffMultiplier,
		RetryableStatusCodes:   c.RetryableStatusCodes,
	})
}

// LoginRiskConfig 控制 gateway 登录入口快拦和 Redis 风控缓存读取。
type LoginRiskConfig 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"`
	FastGuard     LoginRiskFastGuardConfig `yaml:"fast_guard"`
}

// LoginRiskFastGuardConfig 保存语言和时区弱信号阻断配置。
type LoginRiskFastGuardConfig struct {
	BlockedLanguages           []string `yaml:"blocked_languages"`
	BlockedLanguagePrimaryTags []string `yaml:"blocked_language_primary_tags"`
	BlockedTimezones           []string `yaml:"blocked_timezones"`
	BlockedTimezonePrefixes    []string `yaml:"blocked_timezone_prefixes"`
}

// AppConfigConfig 描述 gateway 访问后台 hyapp_admin 的低频数据源。
type AppConfigConfig struct {
	// MySQLDSN 指向后台管理库 hyapp_admin；gateway 读取 App 配置，并写入 H5 用户提现申请。
	MySQLDSN string `yaml:"mysql_dsn"`
	// RedisAddr 用于缓存低频后台配置判定，默认复用 auth_rate_limit Redis。
	RedisAddr string `yaml:"redis_addr"`
	// RedisPassword 是 Redis 鉴权密码，默认复用 auth_rate_limit Redis。
	RedisPassword string `yaml:"redis_password"`
	// RedisDB 是 Redis logical DB，默认复用 auth_rate_limit Redis。
	RedisDB int `yaml:"redis_db"`
	// KeyPrefix 隔离 App 配置缓存 key，避免和登录风控、频控共用命名空间。
	KeyPrefix string `yaml:"key_prefix"`
	// CacheTTL 控制后台配置读缓存时间；房间白名单默认五分钟。
	CacheTTL time.Duration `yaml:"cache_ttl"`
	// RoomDiscoveryCacheTTL 控制房间发现范围的本进程缓存；后台更新最多经过该时长对单个 gateway 实例可见。
	RoomDiscoveryCacheTTL time.Duration `yaml:"room_discovery_cache_ttl"`
}

// LeaderboardConfig 描述 App 活动榜单读取 Redis 聚合读模型的只读数据源。
type LeaderboardConfig struct {
	// WalletMySQLDSN 是旧 SQL 榜单路径的兼容字段；gateway 当前不再读取钱包库。
	WalletMySQLDSN string `yaml:"wallet_mysql_dsn"`
	// RedisAddr 是 activity-service 写入用户送礼榜单聚合的 Redis 地址。
	RedisAddr string `yaml:"redis_addr"`
	// RedisPassword 是 Redis 鉴权密码，本地默认留空。
	RedisPassword string `yaml:"redis_password"`
	// RedisDB 是 Redis logical DB，必须与 activity-service 写入配置一致。
	RedisDB int `yaml:"redis_db"`
	// KeyPrefix 隔离用户榜单 key，必须与 activity-service 写入配置一致。
	KeyPrefix string `yaml:"key_prefix"`
}

// StatisticsConfig 描述 gateway 转发客户端 H5 埋点到 statistics-service 的内部 HTTP 入口。
type StatisticsConfig struct {
	BaseURL string        `yaml:"base_url"`
	Timeout time.Duration `yaml:"timeout"`
}

type FinanceNotificationsConfig struct {
	DingTalk DingTalkRobotConfig `yaml:"dingtalk"`
}

type DingTalkRobotConfig struct {
	Enabled        bool          `yaml:"enabled"`
	WebhookURL     string        `yaml:"webhook_url"`
	Secret         string        `yaml:"secret"`
	AtMobiles      []string      `yaml:"at_mobiles"`
	AtAll          bool          `yaml:"at_all"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

// AuthRateLimitConfig 控制 gateway public auth 入口的 Redis 频控。
type AuthRateLimitConfig struct {
	// Enabled 为 false 时关闭 gateway auth 入口频控。
	Enabled bool `yaml:"enabled"`
	// RedisAddr 是 public auth 频控 Redis 地址。
	RedisAddr string `yaml:"redis_addr"`
	// RedisPassword 是 Redis 鉴权密码，本地默认留空。
	RedisPassword string `yaml:"redis_password"`
	// RedisDB 是 Redis logical DB。
	RedisDB int `yaml:"redis_db"`
	// KeyPrefix 是频控 key 前缀，用于区分环境和服务。
	KeyPrefix string `yaml:"key_prefix"`
	// WindowSec 是固定窗口秒数。
	WindowSec int64 `yaml:"window_sec"`
	// IPLimit 限制同一入口 IP 对所有 public auth API 的请求数。
	IPLimit int `yaml:"ip_limit"`
	// DeviceIDLimit 限制同一 device_id 对登录、注册和 refresh 的请求数。
	DeviceIDLimit int `yaml:"device_id_limit"`
	// ProviderIPLimit 限制同一 IP 对同一三方 provider 的注册/登录请求数。
	ProviderIPLimit int `yaml:"provider_ip_limit"`
	// DisplayUserIDIPLimit 限制同一 IP 对同一 display_user_id 的密码尝试。
	DisplayUserIDIPLimit int `yaml:"display_user_id_ip_limit"`
	// DisplayUserIDLimit 限制同一 display_user_id 被跨 IP 喷射尝试。
	DisplayUserIDLimit int `yaml:"display_user_id_limit"`
	// SessionIDIPLimit 限制同一 IP 对同一 session_id 的 logout 请求数。
	SessionIDIPLimit int `yaml:"session_id_ip_limit"`
}

// GiftCapacityLimitConfig 控制送礼入口按真实目标成本执行的 Redis 容量保护。
type GiftCapacityLimitConfig struct {
	// Enabled 为 false 时显式关闭保护；本地、Docker 和生产模板均默认开启。
	Enabled bool `yaml:"enabled"`
	// RedisAddr/Password/DB 指向所有 gateway 节点共享的 Redis；地址为空时复用 auth_rate_limit。
	RedisAddr     string `yaml:"redis_addr"`
	RedisPassword string `yaml:"redis_password"`
	RedisDB       int    `yaml:"redis_db"`
	// KeyPrefix 隔离送礼令牌桶和在途租约，避免与登录频控共享计数空间。
	KeyPrefix string `yaml:"key_prefix"`
	// User/Room/App 三层都按 gift_count*去重 target_count 计费；用户层保护单账号，
	// 房间层收敛多人叠加，App 层兜底总抽奖/账务工作量。
	UserBucketCapacity        int     `yaml:"user_bucket_capacity"`
	UserRefillTokensPerSecond float64 `yaml:"user_refill_tokens_per_second"`
	RoomBucketCapacity        int     `yaml:"room_bucket_capacity"`
	RoomRefillTokensPerSecond float64 `yaml:"room_refill_tokens_per_second"`
	AppBucketCapacity         int     `yaml:"app_bucket_capacity"`
	AppRefillTokensPerSecond  float64 `yaml:"app_refill_tokens_per_second"`
	// MaxTargetsPerRequest 是协议入口硬上限，超过后直接 4xx，不进入永远无法补足的大权重令牌重试。
	MaxTargetsPerRequest int `yaml:"max_targets_per_request"`
	// MaxGiftCountPerRequest 限制单目标礼物数量；MaxGiftUnitsPerRequest 进一步限制
	// gift_count*去重目标数，避免 999*全麦把幸运礼物随机/落库成本放大到不可控。
	MaxGiftCountPerRequest int `yaml:"max_gift_count_per_request"`
	MaxGiftUnitsPerRequest int `yaml:"max_gift_units_per_request"`
	// MaxInFlight 是单个 app:user 在所有房间合计允许的并发送礼请求数。
	MaxInFlight int `yaml:"max_in_flight"`
	// InFlightLease 是 gateway 异常退出后并发槽自动释放的上限。
	InFlightLease time.Duration `yaml:"in_flight_lease"`
	// RedisOperationTimeout 是三层 Lua 的共同总预算，不是每层各自预算。
	RedisOperationTimeout time.Duration `yaml:"redis_operation_timeout"`
	// RequestTimeout 覆盖 profile、host 和 room gRPC 的完整编排；lease 必须严格大于它并留安全余量。
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

const giftCapacityLeaseSafetyMargin = 2 * time.Second

// TencentIMConfig 描述 gateway 给客户端签发腾讯云 IM UserSig 的配置。
type TencentIMConfig struct {
	// SDKAppID 是腾讯云 IM 控制台分配的应用 ID。
	SDKAppID int64 `yaml:"sdk_app_id"`
	// SecretKey 只允许出现在服务端配置中，不能下发给客户端。
	SecretKey string `yaml:"secret_key"`
	// UserSigTTL 是客户端 IM 登录票据有效期。
	UserSigTTL time.Duration `yaml:"user_sig_ttl"`
	// AdminIdentifier 是服务端 REST 发播报和系统消息的管理员账号，回调用它识别服务端发送者。
	AdminIdentifier string `yaml:"admin_identifier"`
	// AdminUserSigTTL 控制 gateway 补偿导入 IM 账号时的 admin UserSig 有效期。
	AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"`
	// Endpoint 是腾讯 IM REST 地域入口，例如 adminapisgp.im.qcloud.com。
	Endpoint string `yaml:"endpoint"`
	// RequestTimeout 是 gateway 调腾讯 IM REST 的请求超时。
	RequestTimeout time.Duration `yaml:"request_timeout"`
	// GroupIDPrefix 给测试服等隔离环境使用，正式服为空以保持既有腾讯 IM 群不变。
	GroupIDPrefix string `yaml:"group_id_prefix"`
	// CallbackURL 是在腾讯云 IM 控制台配置的服务端回调地址。
	CallbackURL string `yaml:"callback_url"`
	// CallbackAuthToken 是腾讯云 IM 回调鉴权 token，只用于校验回调来源。
	CallbackAuthToken string `yaml:"callback_auth_token"`
}

// RESTConfig 转换成腾讯 IM REST client 配置，用于 /im/usersig 入口补偿导入账号。
func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig {
	timeout := c.RequestTimeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	ttl := c.AdminUserSigTTL
	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},
	}
}

// TencentRTCConfig 描述 gateway 给客户端签发腾讯 RTC 进房 UserSig 的配置。
type TencentRTCConfig struct {
	// Enabled 控制 RTC token endpoint 是否可以对外签发，配置缺失时必须 fail-closed。
	Enabled bool `yaml:"enabled"`
	// SDKAppID 是腾讯 RTC 控制台分配的应用 ID。
	SDKAppID int64 `yaml:"sdk_app_id"`
	// SecretKey 只允许出现在服务端配置中，不能下发给客户端。
	SecretKey string `yaml:"secret_key"`
	// UserSigTTL 是客户端 RTC 进房票据有效期。
	UserSigTTL time.Duration `yaml:"user_sig_ttl"`
	// RoomIDType 首版固定 string，确保客户端只使用 TRTC strRoomId。
	RoomIDType string `yaml:"room_id_type"`
	// AppScene 首版固定 voice_chat_room，所有端必须一致。
	AppScene string `yaml:"app_scene"`
	// CallbackURL 是在腾讯 RTC 控制台配置的事件回调地址。
	CallbackURL string `yaml:"callback_url"`
	// CallbackSignKey 是腾讯 RTC 事件回调签名 key，只用于校验回调来源。
	CallbackSignKey string `yaml:"callback_sign_key"`
}

// TencentCOSConfig 描述 gateway 代 App 上传文件到腾讯云 COS 所需的服务端配置。
type TencentCOSConfig struct {
	// Enabled 控制上传接口是否真正写 COS；关闭时接口会 fail-closed。
	Enabled bool `yaml:"enabled"`
	// SecretID/SecretKey 只允许保存在服务端配置中，不能下发给客户端。
	SecretID  string `yaml:"secret-id"`
	SecretKey string `yaml:"secret-key"`
	// BucketName 是完整 bucket 名，例如 yumi-assets-1420526837。
	BucketName string `yaml:"bucket-name"`
	// Region 是 COS 地域，例如 me-saudi-arabia。
	Region string `yaml:"region"`
	// AccessURL 是对象对外访问域名，通常是 CDN 或绑定的自定义域名。
	AccessURL string `yaml:"access-url"`
}

// Default 返回本地开发默认配置。
func Default() Config {
	return Config{
		ServiceName: "gateway-service",
		NodeID:      "gateway-local",
		Environment: "local",
		HTTPAddr:    ":13000",
		CORS: CORSConfig{
			Enabled: true,
			AllowedOrigins: []string{
				"http://localhost:3000",
				"http://127.0.0.1:3000",
				"http://localhost:5173",
				"http://127.0.0.1:5173",
				"https://global-interaction.com",
				"https://www.global-interaction.com",
				"https://h5.global-interaction.com",
				"https://api.global-interaction.com",
			},
			AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
			AllowedHeaders: []string{
				"Authorization",
				"Content-Type",
				"X-Request-ID",
				"X-App-Code",
				"X-HY-App-Code",
				"X-App-Package",
				"X-Package-Name",
				"X-App-Bundle-ID",
				"X-Bundle-ID",
				"X-App-Platform",
				"X-Platform",
				"X-App-Version",
				"X-App-Build-Number",
				"X-Device-Model",
				"X-Device-Manufacturer",
				"X-OS-Version",
				"X-Device-IMEI",
			},
			ExposeHeaders:    []string{"X-Request-ID"},
			AllowCredentials: true,
			MaxAgeSec:        600,
		},
		// `make run` deliberately uses in-code defaults when MQ is disabled, so
		// this must remain identical to user-service's local JWT signing secret.
		// A divergent default makes every freshly refreshed token fail gateway
		// verification even though login itself appears successful.
		JWTSecret:            "dev-shared-secret",
		RoomServiceAddr:      "127.0.0.1:13001",
		UserServiceAddr:      "127.0.0.1:13005",
		WalletServiceAddr:    "127.0.0.1:13004",
		ActivityServiceAddr:  "127.0.0.1:13006",
		LuckyGiftServiceAddr: "127.0.0.1:13013",
		GameServiceAddr:      "127.0.0.1:13008",
		Statistics: StatisticsConfig{
			BaseURL: "http://127.0.0.1:13010",
			Timeout: 2 * time.Second,
		},
		GRPCClient: GRPCClientConfig{
			DefaultTimeout:         5 * time.Second,
			ConnectTimeout:         2 * time.Second,
			KeepaliveTime:          30 * time.Second,
			KeepaliveTimeout:       10 * time.Second,
			RetryMaxAttempts:       2,
			RetryInitialBackoff:    100 * time.Millisecond,
			RetryMaxBackoff:        500 * time.Millisecond,
			RetryBackoffMultiplier: 2,
			RetryableStatusCodes:   []string{"UNAVAILABLE"},
		},
		AppConfig: AppConfigConfig{
			MySQLDSN:              "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC",
			RedisAddr:             "127.0.0.1:13379",
			KeyPrefix:             "gateway:app_config:",
			CacheTTL:              5 * time.Minute,
			RoomDiscoveryCacheTTL: 30 * time.Second,
		},
		Leaderboard: LeaderboardConfig{
			RedisAddr: "127.0.0.1:13379",
			KeyPrefix: "activity:user_leaderboard",
		},
		AuthRateLimit: AuthRateLimitConfig{
			Enabled:              true,
			RedisAddr:            "127.0.0.1:13379",
			KeyPrefix:            "gateway:auth:rate:",
			WindowSec:            60,
			IPLimit:              120,
			DeviceIDLimit:        30,
			ProviderIPLimit:      20,
			DisplayUserIDIPLimit: 10,
			DisplayUserIDLimit:   50,
			SessionIDIPLimit:     30,
		},
		GiftCapacityLimit: GiftCapacityLimitConfig{
			Enabled: true,
			// RedisAddr 故意留空，由 Normalize 复用 auth_rate_limit Redis；旧 YAML 没有本 section 也能安全启动。
			KeyPrefix:                 "gateway:gift:capacity:",
			UserBucketCapacity:        5_000,
			UserRefillTokensPerSecond: 500,
			RoomBucketCapacity:        20_000,
			RoomRefillTokensPerSecond: 5_000,
			AppBucketCapacity:         100_000,
			AppRefillTokensPerSecond:  25_000,
			MaxTargetsPerRequest:      30,
			MaxGiftCountPerRequest:    int(giftlimits.MaxGiftCount),
			MaxGiftUnitsPerRequest:    int(giftlimits.MaxTotalGiftUnits),
			MaxInFlight:               2,
			InFlightLease:             10 * time.Second,
			RedisOperationTimeout:     500 * time.Millisecond,
			RequestTimeout:            5 * time.Second,
		},
		LoginRisk: LoginRiskConfig{
			Enabled:   true,
			RedisAddr: "127.0.0.1:13379",
			KeyPrefix: "login_risk:ip:",
		},
		TencentIM: TencentIMConfig{
			UserSigTTL:      24 * time.Hour,
			AdminUserSigTTL: 24 * time.Hour,
			AdminIdentifier: "administrator",
			RequestTimeout:  5 * time.Second,
			Endpoint:        tencentim.DefaultEndpoint,
		},
		TencentRTC: TencentRTCConfig{
			UserSigTTL: 2 * time.Hour,
			RoomIDType: "string",
			AppScene:   "voice_chat_room",
		},
		FinanceNotifications: FinanceNotificationsConfig{
			DingTalk: DingTalkRobotConfig{
				Enabled:        true,
				RequestTimeout: 3 * time.Second,
			},
		},
		Log: logx.Config{
			Level:           "info",
			Format:          "json",
			MaxPayloadBytes: 2048,
		},
	}
}

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

		return cfg, nil
	}

	if err := configx.LoadYAML(path, &cfg); err != nil {
		return Config{}, err
	}

	if err := cfg.Normalize(); err != nil {
		return Config{}, err
	}

	return cfg, nil
}

// Normalize fills policy defaults and rejects unsupported RTC room/app modes at startup.
func (cfg *Config) Normalize() error {
	cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
	if cfg.ServiceName == "" {
		cfg.ServiceName = "gateway-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.CORS.AllowedOrigins = trimNonEmpty(cfg.CORS.AllowedOrigins)
	cfg.CORS.AllowedMethods = upperNonEmpty(cfg.CORS.AllowedMethods)
	cfg.CORS.AllowedHeaders = trimNonEmpty(cfg.CORS.AllowedHeaders)
	cfg.CORS.ExposeHeaders = trimNonEmpty(cfg.CORS.ExposeHeaders)
	if len(cfg.CORS.AllowedMethods) == 0 {
		cfg.CORS.AllowedMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
	}
	if len(cfg.CORS.AllowedHeaders) == 0 {
		// 设备资料头需要进入 CORS 允许集，H5 客户端才能与原生保持同一套设备上报协议。
		cfg.CORS.AllowedHeaders = []string{"Authorization", "Content-Type", "X-Request-ID", "X-App-Code", "X-HY-App-Code", "X-App-Package", "X-Package-Name", "X-App-Bundle-ID", "X-Bundle-ID", "X-App-Platform", "X-Platform", "X-App-Version", "X-App-Build-Number", "X-Device-Model", "X-Device-Manufacturer", "X-OS-Version", "X-Device-IMEI"}
	}
	if len(cfg.CORS.ExposeHeaders) == 0 {
		cfg.CORS.ExposeHeaders = []string{"X-Request-ID"}
	}
	if cfg.CORS.MaxAgeSec <= 0 {
		cfg.CORS.MaxAgeSec = 600
	}
	cfg.RoomServiceAddr = strings.TrimSpace(cfg.RoomServiceAddr)
	cfg.UserServiceAddr = strings.TrimSpace(cfg.UserServiceAddr)
	cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr)
	cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr)
	cfg.LuckyGiftServiceAddr = strings.TrimSpace(cfg.LuckyGiftServiceAddr)
	cfg.GameServiceAddr = strings.TrimSpace(cfg.GameServiceAddr)
	cfg.Statistics.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.Statistics.BaseURL), "/")
	cfg.applyFinanceNotificationEnvOverrides()
	if cfg.WalletServiceAddr == "" {
		cfg.WalletServiceAddr = "127.0.0.1:13004"
	}
	if cfg.ActivityServiceAddr == "" {
		cfg.ActivityServiceAddr = "127.0.0.1:13006"
	}
	if cfg.LuckyGiftServiceAddr == "" {
		cfg.LuckyGiftServiceAddr = "127.0.0.1:13013"
	}
	if cfg.GameServiceAddr == "" {
		cfg.GameServiceAddr = "127.0.0.1:13008"
	}
	if cfg.Statistics.BaseURL == "" {
		cfg.Statistics.BaseURL = "http://127.0.0.1:13010"
	}
	if cfg.Statistics.Timeout <= 0 {
		cfg.Statistics.Timeout = 2 * time.Second
	}
	cfg.FinanceNotifications.DingTalk.WebhookURL = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.WebhookURL)
	cfg.FinanceNotifications.DingTalk.Secret = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.Secret)
	cfg.FinanceNotifications.DingTalk.AtMobiles = trimNonEmpty(cfg.FinanceNotifications.DingTalk.AtMobiles)
	if cfg.FinanceNotifications.DingTalk.RequestTimeout <= 0 {
		cfg.FinanceNotifications.DingTalk.RequestTimeout = 3 * time.Second
	}
	if cfg.FinanceNotifications.DingTalk.Enabled && cfg.FinanceNotifications.DingTalk.WebhookURL != "" {
		if !strings.HasPrefix(cfg.FinanceNotifications.DingTalk.WebhookURL, "http://") && !strings.HasPrefix(cfg.FinanceNotifications.DingTalk.WebhookURL, "https://") {
			return fmt.Errorf("finance_notifications.dingtalk.webhook_url must be an absolute url")
		}
	}
	runtimeGRPC := cfg.GRPCClient.Runtime()
	cfg.GRPCClient = GRPCClientConfig{
		DefaultTimeout:         runtimeGRPC.DefaultTimeout,
		ConnectTimeout:         runtimeGRPC.ConnectTimeout,
		KeepaliveTime:          runtimeGRPC.KeepaliveTime,
		KeepaliveTimeout:       runtimeGRPC.KeepaliveTimeout,
		RetryMaxAttempts:       runtimeGRPC.RetryMaxAttempts,
		RetryInitialBackoff:    runtimeGRPC.RetryInitialBackoff,
		RetryMaxBackoff:        runtimeGRPC.RetryMaxBackoff,
		RetryBackoffMultiplier: runtimeGRPC.RetryBackoffMultiplier,
		RetryableStatusCodes:   runtimeGRPC.RetryableStatusCodes,
	}
	cfg.AppConfig.MySQLDSN = strings.TrimSpace(cfg.AppConfig.MySQLDSN)
	cfg.AppConfig.RedisAddr = strings.TrimSpace(cfg.AppConfig.RedisAddr)
	cfg.AppConfig.KeyPrefix = strings.TrimSpace(cfg.AppConfig.KeyPrefix)
	if cfg.AppConfig.RedisAddr == "" {
		cfg.AppConfig.RedisAddr = strings.TrimSpace(cfg.AuthRateLimit.RedisAddr)
		cfg.AppConfig.RedisPassword = cfg.AuthRateLimit.RedisPassword
		cfg.AppConfig.RedisDB = cfg.AuthRateLimit.RedisDB
	}
	if cfg.AppConfig.KeyPrefix == "" {
		cfg.AppConfig.KeyPrefix = "gateway:app_config:"
	}
	if cfg.AppConfig.CacheTTL <= 0 {
		cfg.AppConfig.CacheTTL = 5 * time.Minute
	}
	if cfg.AppConfig.RoomDiscoveryCacheTTL <= 0 {
		cfg.AppConfig.RoomDiscoveryCacheTTL = 30 * time.Second
	}
	cfg.Leaderboard.WalletMySQLDSN = strings.TrimSpace(cfg.Leaderboard.WalletMySQLDSN)
	cfg.Leaderboard.RedisAddr = strings.TrimSpace(cfg.Leaderboard.RedisAddr)
	cfg.Leaderboard.KeyPrefix = strings.TrimSpace(cfg.Leaderboard.KeyPrefix)
	if cfg.Leaderboard.RedisAddr == "" {
		cfg.Leaderboard.RedisAddr = strings.TrimSpace(cfg.AuthRateLimit.RedisAddr)
		cfg.Leaderboard.RedisPassword = cfg.AuthRateLimit.RedisPassword
		cfg.Leaderboard.RedisDB = cfg.AuthRateLimit.RedisDB
	}
	if cfg.Leaderboard.KeyPrefix == "" {
		cfg.Leaderboard.KeyPrefix = "activity:user_leaderboard"
	}
	cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.LoginRisk.RedisAddr)
	cfg.LoginRisk.KeyPrefix = strings.TrimSpace(cfg.LoginRisk.KeyPrefix)
	// 地区风控可关闭，但同一 Redis 还承载 mandatory session denylist；始终继承 auth rate-limit 连接。
	if cfg.LoginRisk.RedisAddr == "" {
		cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.AuthRateLimit.RedisAddr)
		cfg.LoginRisk.RedisPassword = cfg.AuthRateLimit.RedisPassword
		cfg.LoginRisk.RedisDB = cfg.AuthRateLimit.RedisDB
	}
	if cfg.LoginRisk.KeyPrefix == "" {
		cfg.LoginRisk.KeyPrefix = "login_risk:ip:"
	}
	cfg.GiftCapacityLimit.RedisAddr = strings.TrimSpace(cfg.GiftCapacityLimit.RedisAddr)
	cfg.GiftCapacityLimit.KeyPrefix = strings.TrimSpace(cfg.GiftCapacityLimit.KeyPrefix)
	if cfg.GiftCapacityLimit.Enabled && cfg.GiftCapacityLimit.RedisAddr == "" {
		// 送礼保护与 auth 频控具有同样的强一致入口语义，默认复用已部署的共享 Redis，
		// 但使用独立 key_prefix，避免引入新的基础设施依赖。
		cfg.GiftCapacityLimit.RedisAddr = strings.TrimSpace(cfg.AuthRateLimit.RedisAddr)
		cfg.GiftCapacityLimit.RedisPassword = cfg.AuthRateLimit.RedisPassword
		cfg.GiftCapacityLimit.RedisDB = cfg.AuthRateLimit.RedisDB
	}
	if cfg.GiftCapacityLimit.KeyPrefix == "" {
		cfg.GiftCapacityLimit.KeyPrefix = "gateway:gift:capacity:"
	}
	if cfg.GiftCapacityLimit.UserBucketCapacity <= 0 {
		cfg.GiftCapacityLimit.UserBucketCapacity = 5_000
	}
	if cfg.GiftCapacityLimit.UserRefillTokensPerSecond <= 0 {
		cfg.GiftCapacityLimit.UserRefillTokensPerSecond = 500
	}
	if cfg.GiftCapacityLimit.RoomBucketCapacity <= 0 {
		cfg.GiftCapacityLimit.RoomBucketCapacity = 20_000
	}
	if cfg.GiftCapacityLimit.RoomRefillTokensPerSecond <= 0 {
		cfg.GiftCapacityLimit.RoomRefillTokensPerSecond = 5_000
	}
	if cfg.GiftCapacityLimit.AppBucketCapacity <= 0 {
		cfg.GiftCapacityLimit.AppBucketCapacity = 100_000
	}
	if cfg.GiftCapacityLimit.AppRefillTokensPerSecond <= 0 {
		cfg.GiftCapacityLimit.AppRefillTokensPerSecond = 25_000
	}
	if cfg.GiftCapacityLimit.MaxTargetsPerRequest <= 0 {
		cfg.GiftCapacityLimit.MaxTargetsPerRequest = 30
	}
	if cfg.GiftCapacityLimit.MaxGiftCountPerRequest <= 0 {
		cfg.GiftCapacityLimit.MaxGiftCountPerRequest = int(giftlimits.MaxGiftCount)
	}
	if cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest <= 0 {
		cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest = int(giftlimits.MaxTotalGiftUnits)
	}
	if int64(cfg.GiftCapacityLimit.MaxGiftCountPerRequest) > giftlimits.MaxGiftCount || int64(cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest) > giftlimits.MaxTotalGiftUnits {
		return fmt.Errorf("gift_capacity_limit request limits exceed cross-service hard ceiling")
	}
	if cfg.GiftCapacityLimit.MaxInFlight <= 0 {
		cfg.GiftCapacityLimit.MaxInFlight = 2
	}
	if cfg.GiftCapacityLimit.InFlightLease <= 0 {
		cfg.GiftCapacityLimit.InFlightLease = 10 * time.Second
	}
	if cfg.GiftCapacityLimit.RedisOperationTimeout <= 0 {
		cfg.GiftCapacityLimit.RedisOperationTimeout = 500 * time.Millisecond
	}
	if cfg.GiftCapacityLimit.RequestTimeout <= 0 {
		cfg.GiftCapacityLimit.RequestTimeout = 5 * time.Second
	}
	if cfg.GiftCapacityLimit.UserBucketCapacity < cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest || cfg.GiftCapacityLimit.RoomBucketCapacity < cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest || cfg.GiftCapacityLimit.AppBucketCapacity < cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest {
		return fmt.Errorf("gift_capacity_limit bucket capacities must be >= max_gift_units_per_request")
	}
	if cfg.GiftCapacityLimit.InFlightLease <= cfg.GiftCapacityLimit.RequestTimeout+giftCapacityLeaseSafetyMargin {
		// lease 从用户层获准时开始计时；严格大于 RPC 总预算和 2s 收尾余量，确保正常请求期间槽位不会提前复用。
		return fmt.Errorf("gift_capacity_limit.in_flight_lease must exceed request_timeout by more than %s", giftCapacityLeaseSafetyMargin)
	}
	cfg.TencentCOS.SecretID = strings.TrimSpace(cfg.TencentCOS.SecretID)
	cfg.TencentCOS.SecretKey = strings.TrimSpace(cfg.TencentCOS.SecretKey)
	cfg.TencentCOS.BucketName = strings.TrimSpace(cfg.TencentCOS.BucketName)
	cfg.TencentCOS.Region = strings.TrimSpace(cfg.TencentCOS.Region)
	cfg.TencentCOS.AccessURL = strings.TrimRight(strings.TrimSpace(cfg.TencentCOS.AccessURL), "/")
	if cfg.TencentCOS.Enabled {
		if cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" {
			return fmt.Errorf("tencent-cos config is incomplete")
		}
		if !strings.HasPrefix(cfg.TencentCOS.AccessURL, "http://") && !strings.HasPrefix(cfg.TencentCOS.AccessURL, "https://") {
			return fmt.Errorf("tencent-cos.access-url must be an absolute url")
		}
	}

	cfg.TencentRTC.RoomIDType = strings.TrimSpace(cfg.TencentRTC.RoomIDType)
	if cfg.TencentRTC.RoomIDType == "" {
		cfg.TencentRTC.RoomIDType = tencentrtc.RoomIDTypeString
	}
	if cfg.TencentRTC.RoomIDType != tencentrtc.RoomIDTypeString {
		// Numeric TRTC roomId would fork users into a different room than internal string room_id.
		return fmt.Errorf("tencent_rtc.room_id_type must be %q", tencentrtc.RoomIDTypeString)
	}

	cfg.TencentRTC.AppScene = strings.TrimSpace(cfg.TencentRTC.AppScene)
	if cfg.TencentRTC.AppScene == "" {
		cfg.TencentRTC.AppScene = tencentrtc.AppSceneVoiceChatRoom
	}
	if cfg.TencentRTC.AppScene != tencentrtc.AppSceneVoiceChatRoom {
		// v1 has no gateway policy surface for multiple RTC scenes, so reject drift early.
		return fmt.Errorf("tencent_rtc.app_scene must be %q", tencentrtc.AppSceneVoiceChatRoom)
	}
	cfg.TencentRTC.CallbackURL = strings.TrimSpace(cfg.TencentRTC.CallbackURL)
	cfg.TencentRTC.CallbackSignKey = strings.TrimSpace(cfg.TencentRTC.CallbackSignKey)
	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.AdminUserSigTTL <= 0 {
		cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour
	}
	if cfg.TencentIM.RequestTimeout <= 0 {
		cfg.TencentIM.RequestTimeout = 5 * time.Second
	}
	cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix)
	cfg.TencentIM.CallbackURL = strings.TrimSpace(cfg.TencentIM.CallbackURL)
	cfg.TencentIM.CallbackAuthToken = strings.TrimSpace(cfg.TencentIM.CallbackAuthToken)

	return nil
}

func (cfg *Config) applyFinanceNotificationEnvOverrides() {
	if value := strings.TrimSpace(os.Getenv("HYAPP_GATEWAY_FINANCE_DINGTALK_ENABLED")); value != "" {
		cfg.FinanceNotifications.DingTalk.Enabled = parseBoolEnv(value)
	}
	if value := strings.TrimSpace(firstEnv("HYAPP_GATEWAY_FINANCE_DINGTALK_WEBHOOK_URL", "FINANCE_DINGTALK_WEBHOOK_URL")); value != "" {
		// Webhook URL 包含 access_token，只允许通过运行环境注入；示例配置只保留占位值。
		cfg.FinanceNotifications.DingTalk.WebhookURL = value
		cfg.FinanceNotifications.DingTalk.Enabled = true
	}
	if value := strings.TrimSpace(firstEnv("HYAPP_GATEWAY_FINANCE_DINGTALK_SECRET", "FINANCE_DINGTALK_SECRET")); value != "" {
		cfg.FinanceNotifications.DingTalk.Secret = value
	}
	if value := strings.TrimSpace(os.Getenv("HYAPP_GATEWAY_FINANCE_DINGTALK_AT_MOBILES")); value != "" {
		cfg.FinanceNotifications.DingTalk.AtMobiles = strings.Split(value, ",")
	}
	if value := strings.TrimSpace(os.Getenv("HYAPP_GATEWAY_FINANCE_DINGTALK_AT_ALL")); value != "" {
		cfg.FinanceNotifications.DingTalk.AtAll = parseBoolEnv(value)
	}
}

func firstEnv(keys ...string) string {
	for _, key := range keys {
		if value := os.Getenv(key); value != "" {
			return value
		}
	}
	return ""
}

func parseBoolEnv(value string) bool {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "1", "true", "yes", "y", "on", "enabled":
		return true
	default:
		return false
	}
}

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

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