package config

import (
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"strings"
	"time"

	"hyapp-admin-server/internal/platform/logging"

	"gopkg.in/yaml.v3"
)

type Config struct {
	ServiceName        string                    `yaml:"service_name"`
	NodeID             string                    `yaml:"node_id"`
	Environment        string                    `yaml:"environment"`
	Log                logging.Config            `yaml:"log"`
	HTTPAddr           string                    `yaml:"http_addr"`
	TrustedProxies     []string                  `yaml:"trusted_proxies"`
	MySQLDSN           string                    `yaml:"mysql_dsn"`
	UserMySQLDSN       string                    `yaml:"user_mysql_dsn"`
	WalletMySQLDSN     string                    `yaml:"wallet_mysql_dsn"`
	RobotProfileSource RobotProfileSourceConfig  `yaml:"robot_profile_source"`
	MoneyRegionSources []MoneyRegionSourceConfig `yaml:"money_region_sources"`
	FinanceBillSources []FinanceBillSourceConfig `yaml:"finance_bill_sources"`
	// FinanceGooglePaidSync controls the durable polling loop that fills Google Orders API paid details for legacy bill sources.
	FinanceGooglePaidSync           FinanceGooglePaidSyncConfig            `yaml:"finance_google_paid_sync"`
	MySQLAutoMigrate                bool                                   `yaml:"mysql_auto_migrate"`
	Migrations                      MigrationConfig                        `yaml:"migrations"`
	JWTSecret                       string                                 `yaml:"jwt_secret"`
	AccessTokenTTL                  time.Duration                          `yaml:"access_token_ttl"`
	RefreshTokenTTL                 time.Duration                          `yaml:"refresh_token_ttl"`
	CORSAllowedOrigins              []string                               `yaml:"cors_allowed_origins"`
	RefreshCookieSecure             bool                                   `yaml:"refresh_cookie_secure"`
	RefreshCookieSameSite           string                                 `yaml:"refresh_cookie_same_site"`
	ExternalAdmin                   ExternalAdminConfig                    `yaml:"external_admin"`
	Bootstrap                       BootstrapConfig                        `yaml:"bootstrap"`
	BootstrapPassword               string                                 `yaml:"bootstrap_password"`
	UserService                     UserServiceConfig                      `yaml:"user_service"`
	TencentCOS                      TencentCOSConfig                       `yaml:"tencent-cos"`
	Redis                           RedisConfig                            `yaml:"redis"`
	Jobs                            JobsConfig                             `yaml:"jobs"`
	WalletService                   WalletServiceConfig                    `yaml:"wallet_service"`
	RoomService                     RoomServiceConfig                      `yaml:"room_service"`
	RobotService                    RobotServiceConfig                     `yaml:"robot_service"`
	ActivityService                 ActivityServiceConfig                  `yaml:"activity_service"`
	ActivityTemplateH5              ActivityTemplateH5Config               `yaml:"activity_template_h5"`
	LuckyGiftService                LuckyGiftServiceConfig                 `yaml:"lucky_gift_service"`
	GameService                     GameServiceConfig                      `yaml:"game_service"`
	StatisticsService               StatisticsServiceConfig                `yaml:"statistics_service"`
	FinanceNotifications            FinanceNotificationsConfig             `yaml:"finance_notifications"`
	DashboardExternalSources        []DashboardExternalSourceConfig        `yaml:"dashboard_external_sources"`
	LegacyCoinSellerRechargeSources []LegacyCoinSellerRechargeSourceConfig `yaml:"legacy_coin_seller_recharge_sources"`
	WithdrawalSources               []WithdrawalSourceConfig               `yaml:"withdrawal_sources"`
}

type MigrationConfig struct {
	Enabled             bool   `yaml:"enabled"`
	Dir                 string `yaml:"dir"`
	AllowChecksumRepair bool   `yaml:"allow_checksum_repair"`
}

type ExternalAdminConfig struct {
	SessionTTL       time.Duration                     `yaml:"session_ttl"`
	MaxLoginFailures uint                              `yaml:"max_login_failures"`
	LockDuration     time.Duration                     `yaml:"lock_duration"`
	LoginRateLimit   ExternalAdminLoginRateLimitConfig `yaml:"login_rate_limit"`
}

type ExternalAdminLoginRateLimitConfig struct {
	Window        time.Duration `yaml:"window"`
	SystemLimit   uint64        `yaml:"system_limit"`
	AppLimit      uint64        `yaml:"app_limit"`
	IPLimit       uint64        `yaml:"ip_limit"`
	IdentityLimit uint64        `yaml:"identity_limit"`
}

type UserServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type WalletServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type RoomServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type RobotServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type ActivityServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

// ActivityTemplateH5Config 指向只部署一次的通用活动 H5 壳。每个模板的落地页
// 通过 query 绑定 app_code/template_code，模板发布不会覆盖 COS HTML 对象。
type ActivityTemplateH5Config struct {
	BaseURL string `yaml:"base_url"`
}

type LuckyGiftServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type GameServiceConfig struct {
	Addr           string        `yaml:"addr"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type StatisticsServiceConfig struct {
	BaseURL        string        `yaml:"base_url"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

// FinanceNotificationsConfig 只保存审核协作通知配置；提现状态和资金终态仍由 Admin/Wallet 的持久事实决定。
type FinanceNotificationsConfig struct {
	DingTalk DingTalkRobotConfig `yaml:"dingtalk"`
}

// DingTalkRobotConfig 的 webhook 和签名密钥属于运行时凭证，可提交配置只能保留空值。
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"`
}

// WithdrawalSourceConfig 描述接入统一双审工作台的外部钱包系统。
// submit_token 只允许来源 App 创建自己的申请；callback_token 用于 admin 终审时回调来源钱包，二者分离避免任一方向泄漏后扩大权限。
type WithdrawalSourceConfig struct {
	Enabled        bool          `yaml:"enabled"`
	AppCode        string        `yaml:"app_code"`
	SourceSystem   string        `yaml:"source_system"`
	SubmitToken    string        `yaml:"submit_token"`
	CallbackURL    string        `yaml:"callback_url"`
	CallbackToken  string        `yaml:"callback_token"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type DashboardExternalSourceConfig struct {
	Enabled              bool          `yaml:"enabled"`
	SourceType           string        `yaml:"type"`
	AppCode              string        `yaml:"app_code"`
	AppName              string        `yaml:"app_name"`
	SysOrigin            string        `yaml:"sys_origin"`
	MySQLDSN             string        `yaml:"mysql_dsn"`
	LegacyMySQLDSN       string        `yaml:"legacy_mysql_dsn"`
	LegacyWalletDatabase string        `yaml:"legacy_wallet_database"`
	MongoURI             string        `yaml:"mongo_uri"`
	MongoDatabase        string        `yaml:"mongo_database"`
	BaseURL              string        `yaml:"base_url"`
	OverviewPath         string        `yaml:"overview_path"`
	StatTimezone         string        `yaml:"stat_timezone"`
	RequestTimeout       time.Duration `yaml:"request_timeout"`
}

// LegacyCoinSellerRechargeSourceConfig 是 Aslan/Yumi 等独立账套的币商进货写入源。
// 它与 dashboard_external_sources 分离，避免把只读报表连接误当成可写钱包连接。
type LegacyCoinSellerRechargeSourceConfig struct {
	Enabled        bool          `yaml:"enabled"`
	AppCode        string        `yaml:"app_code"`
	AppName        string        `yaml:"app_name"`
	SysOrigin      string        `yaml:"sys_origin"`
	AppMySQLDSN    string        `yaml:"app_mysql_dsn"`
	WalletMySQLDSN string        `yaml:"wallet_mysql_dsn"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type RobotProfileSourceConfig struct {
	MySQLDSN       string        `yaml:"mysql_dsn"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type MoneyRegionSourceConfig struct {
	Enabled         bool          `yaml:"enabled"`
	AppCode         string        `yaml:"app_code"`
	AppName         string        `yaml:"app_name"`
	LogoURL         string        `yaml:"logo_url"`
	SysOrigin       string        `yaml:"sys_origin"`
	MongoURI        string        `yaml:"mongo_uri"`
	MongoDatabase   string        `yaml:"mongo_database"`
	MongoCollection string        `yaml:"mongo_collection"`
	RequestTimeout  time.Duration `yaml:"request_timeout"`
}

// FinanceBillSourceConfig 描述 legacy App（Yumi/Aslan 等 likei 平台）的充值账单外部 Mongo 源；
// 财务系统 APP 充值详情按 app_code 命中后直接读 in_app_purchase_details，不经过 wallet-service。
type FinanceBillSourceConfig struct {
	Enabled         bool          `yaml:"enabled"`
	AppCode         string        `yaml:"app_code"`
	AppName         string        `yaml:"app_name"`
	LogoURL         string        `yaml:"logo_url"`
	SysOrigin       string        `yaml:"sys_origin"`
	MongoURI        string        `yaml:"mongo_uri"`
	MongoDatabase   string        `yaml:"mongo_database"`
	MongoCollection string        `yaml:"mongo_collection"`
	RequestTimeout  time.Duration `yaml:"request_timeout"`
	// Google 实付同步（可选）：配置该 App 在 Play Console 的包名与服务账号后，
	// 财务页“查询谷歌实付”按钮即可对 legacy 谷歌账单拉取实付币种/总额/税/净收入。
	// 服务账号需要对应 Play Console 的“查看财务数据”权限；JSON 优先于文件路径。
	GooglePackageName        string `yaml:"google_package_name"`
	GoogleServiceAccountJSON string `yaml:"google_service_account_json"`
	GoogleServiceAccountFile string `yaml:"google_service_account_file"`
}

// FinanceGooglePaidSyncConfig keeps legacy Google paid-detail polling independent from HTTP requests.
// New successful Mongo orders are picked every PollInterval; failed orders use persisted retry backoff.
type FinanceGooglePaidSyncConfig struct {
	Enabled        bool          `yaml:"enabled"`
	PollInterval   time.Duration `yaml:"poll_interval"`
	BatchSize      int           `yaml:"batch_size"`
	RequestTimeout time.Duration `yaml:"request_timeout"`
}

type TencentCOSConfig struct {
	Enabled      bool   `yaml:"enabled"`
	SecretID     string `yaml:"secret-id"`
	SecretKey    string `yaml:"secret-key"`
	BucketName   string `yaml:"bucket-name"`
	Region       string `yaml:"region"`
	AccessURL    string `yaml:"access-url"`
	ObjectPrefix string `yaml:"object-prefix"`
}

type BootstrapConfig struct {
	Enabled      bool   `yaml:"enabled"`
	SeedDemoData bool   `yaml:"seed_demo_data"`
	Username     string `yaml:"username"`
	Name         string `yaml:"name"`
	Team         string `yaml:"team"`
	Password     string `yaml:"password"`
}

type RedisConfig struct {
	Enabled   bool   `yaml:"enabled"`
	Addr      string `yaml:"addr"`
	Password  string `yaml:"password"`
	DB        int    `yaml:"db"`
	KeyPrefix string `yaml:"key_prefix"`
}

type JobsConfig struct {
	Enabled        bool          `yaml:"enabled"`
	WorkerInterval time.Duration `yaml:"worker_interval"`
	LeaseTTL       time.Duration `yaml:"lease_ttl"`
	MaxAttempts    int           `yaml:"max_attempts"`
	ArtifactDir    string        `yaml:"artifact_dir"`
}

func Default() Config {
	return Config{
		ServiceName:    "admin-server",
		NodeID:         "admin-local",
		Environment:    "local",
		Log:            logging.DefaultConfig(),
		HTTPAddr:       ":13100",
		TrustedProxies: []string{"127.0.0.0/8", "::1"},
		MySQLDSN:       "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC",
		UserMySQLDSN:   "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC",
		WalletMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC",
		RobotProfileSource: RobotProfileSourceConfig{
			RequestTimeout: 5 * time.Second,
		},
		MoneyRegionSources: []MoneyRegionSourceConfig{
			{
				Enabled:         false,
				AppCode:         "yumi",
				AppName:         "Yumi",
				LogoURL:         "https://media.haiyihy.com/admin/apps/logos/yumi.png",
				SysOrigin:       "LIKEI",
				MongoDatabase:   "tarab_all",
				MongoCollection: "sys_region_config",
				RequestTimeout:  5 * time.Second,
			},
			{
				Enabled:         false,
				AppCode:         "aslan",
				AppName:         "Aslan",
				LogoURL:         "https://media.haiyihy.com/admin/apps/logos/aslan.png",
				SysOrigin:       "ATYOU",
				MongoDatabase:   "tarab_all",
				MongoCollection: "sys_region_config",
				RequestTimeout:  5 * time.Second,
			},
		},
		FinanceBillSources: []FinanceBillSourceConfig{
			{
				Enabled:         false,
				AppCode:         "yumi",
				AppName:         "Yumi",
				LogoURL:         "https://media.haiyihy.com/admin/apps/logos/yumi.png",
				SysOrigin:       "LIKEI",
				MongoDatabase:   "tarab_all",
				MongoCollection: "in_app_purchase_details",
				RequestTimeout:  5 * time.Second,
			},
			{
				Enabled: false,
				AppCode: "aslan",
				AppName: "Aslan",
				LogoURL: "https://media.haiyihy.com/admin/apps/logos/aslan.png",
				// Aslan（likei-services 项目）与 Yumi 同一套平台代码，但独立 Mongo 集群；业务库是 atyou。
				SysOrigin:       "ATYOU",
				MongoDatabase:   "atyou",
				MongoCollection: "in_app_purchase_details",
				RequestTimeout:  5 * time.Second,
			},
		},
		MySQLAutoMigrate: true,
		Migrations: MigrationConfig{
			Enabled:             true,
			Dir:                 "migrations",
			AllowChecksumRepair: true,
		},
		JWTSecret:       "dev-shared-secret",
		AccessTokenTTL:  12 * time.Hour,
		RefreshTokenTTL: 7 * 24 * time.Hour,
		CORSAllowedOrigins: []string{
			"http://localhost:7001",
			"http://127.0.0.1:7001",
		},
		RefreshCookieSecure:   false,
		RefreshCookieSameSite: "lax",
		ExternalAdmin: ExternalAdminConfig{
			SessionTTL:       12 * time.Hour,
			MaxLoginFailures: 5,
			LockDuration:     15 * time.Minute,
			LoginRateLimit: ExternalAdminLoginRateLimitConfig{
				Window: 60 * time.Second, SystemLimit: 300, AppLimit: 120, IPLimit: 30, IdentityLimit: 10,
			},
		},
		Bootstrap: BootstrapConfig{
			Enabled:      true,
			SeedDemoData: true,
			Username:     "admin",
			Name:         "admin",
			Team:         "运营团队",
			Password:     "admin123",
		},
		BootstrapPassword: "admin123",
		UserService: UserServiceConfig{
			Addr:           "127.0.0.1:13005",
			RequestTimeout: 3 * time.Second,
		},
		WalletService: WalletServiceConfig{
			Addr:           "127.0.0.1:13004",
			RequestTimeout: 3 * time.Second,
		},
		RoomService: RoomServiceConfig{
			Addr:           "127.0.0.1:13001",
			RequestTimeout: 3 * time.Second,
		},
		RobotService: RobotServiceConfig{
			Addr:           "127.0.0.1:13011",
			RequestTimeout: 3 * time.Second,
		},
		ActivityService: ActivityServiceConfig{
			Addr:           "127.0.0.1:13006",
			RequestTimeout: 3 * time.Second,
		},
		ActivityTemplateH5: ActivityTemplateH5Config{
			BaseURL: "https://media.haiyihy.com/activity/gift-challenge/template.html",
		},
		LuckyGiftService: LuckyGiftServiceConfig{
			Addr:           "127.0.0.1:13013",
			RequestTimeout: 3 * time.Second,
		},
		GameService: GameServiceConfig{
			Addr:           "127.0.0.1:13008",
			RequestTimeout: 3 * time.Second,
		},
		StatisticsService: StatisticsServiceConfig{
			BaseURL:        "http://127.0.0.1:13010",
			RequestTimeout: 3 * time.Second,
		},
		FinanceNotifications: FinanceNotificationsConfig{
			DingTalk: DingTalkRobotConfig{
				Enabled:        true,
				RequestTimeout: 3 * time.Second,
			},
		},
		DashboardExternalSources: []DashboardExternalSourceConfig{
			{
				Enabled:        false,
				SourceType:     "mysql",
				AppCode:        "yumi",
				AppName:        "Yumi",
				SysOrigin:      "LIKEI",
				StatTimezone:   "Asia/Riyadh",
				RequestTimeout: 5 * time.Second,
			},
			{
				Enabled:        false,
				SourceType:     "aslan_mongo",
				AppCode:        "aslan",
				AppName:        "Aslan",
				SysOrigin:      "ATYOU",
				MongoDatabase:  "test",
				StatTimezone:   "Asia/Shanghai",
				RequestTimeout: 5 * time.Second,
			},
		},
		LegacyCoinSellerRechargeSources: []LegacyCoinSellerRechargeSourceConfig{
			{
				Enabled:        false,
				AppCode:        "yumi",
				AppName:        "Yumi",
				SysOrigin:      "LIKEI",
				RequestTimeout: 5 * time.Second,
			},
			{
				Enabled:        false,
				AppCode:        "aslan",
				AppName:        "Aslan",
				SysOrigin:      "ATYOU",
				RequestTimeout: 5 * time.Second,
			},
		},
		WithdrawalSources: []WithdrawalSourceConfig{
			{
				Enabled:        false,
				AppCode:        "yumi",
				SourceSystem:   "chatapp3",
				RequestTimeout: 5 * time.Second,
			},
			{
				Enabled:        false,
				AppCode:        "aslan",
				SourceSystem:   "likei",
				RequestTimeout: 5 * time.Second,
			},
		},
		FinanceGooglePaidSync: FinanceGooglePaidSyncConfig{
			Enabled:        true,
			PollInterval:   15 * time.Second,
			BatchSize:      100,
			RequestTimeout: 2 * time.Minute,
		},
		TencentCOS: TencentCOSConfig{
			Enabled:      false,
			ObjectPrefix: "admin",
		},
		Redis: RedisConfig{
			Enabled:   true,
			Addr:      "127.0.0.1:13379",
			KeyPrefix: "admin:",
		},
		Jobs: JobsConfig{
			Enabled:        true,
			WorkerInterval: 5 * time.Second,
			LeaseTTL:       2 * time.Minute,
			MaxAttempts:    3,
			ArtifactDir:    "storage/exports",
		},
	}
}

func Load(path string) (Config, error) {
	cfg := Default()
	if path == "" {
		return cfg, nil
	}

	body, err := os.ReadFile(path)
	if err != nil {
		return Config{}, err
	}

	if err := yaml.Unmarshal(body, &cfg); err != nil {
		return Config{}, err
	}

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

	return cfg, nil
}

func (cfg *Config) Normalize() {
	cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
	if cfg.Environment == "" {
		cfg.Environment = "local"
	}
	if cfg.Log.Level == "" {
		cfg.Log.Level = "info"
	}
	if cfg.Log.Format == "" {
		cfg.Log.Format = "json"
	}
	if cfg.Log.MaxPayloadBytes <= 0 {
		cfg.Log.MaxPayloadBytes = 2048
	}
	// Gin must never retain its trust-all proxy default: a forged X-Forwarded-For
	// would otherwise bypass IP login limits and corrupt every audit record. Keep
	// only explicit ingress hops and preserve an empty list as "trust no proxy".
	trustedProxies := make([]string, 0, len(cfg.TrustedProxies))
	seenTrustedProxy := make(map[string]struct{}, len(cfg.TrustedProxies))
	for _, proxy := range cfg.TrustedProxies {
		proxy = strings.TrimSpace(proxy)
		if proxy == "" {
			continue
		}
		if _, exists := seenTrustedProxy[proxy]; exists {
			continue
		}
		seenTrustedProxy[proxy] = struct{}{}
		trustedProxies = append(trustedProxies, proxy)
	}
	cfg.TrustedProxies = trustedProxies
	cfg.RefreshCookieSameSite = strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite))
	if cfg.RefreshCookieSameSite == "" {
		cfg.RefreshCookieSameSite = "lax"
	}
	if cfg.ExternalAdmin.SessionTTL <= 0 {
		cfg.ExternalAdmin.SessionTTL = 12 * time.Hour
	}
	if cfg.ExternalAdmin.MaxLoginFailures == 0 {
		cfg.ExternalAdmin.MaxLoginFailures = 5
	}
	if cfg.ExternalAdmin.LockDuration <= 0 {
		cfg.ExternalAdmin.LockDuration = 15 * time.Minute
	}
	if cfg.ExternalAdmin.LoginRateLimit.Window <= 0 {
		cfg.ExternalAdmin.LoginRateLimit.Window = 60 * time.Second
	}
	if cfg.ExternalAdmin.LoginRateLimit.SystemLimit == 0 {
		cfg.ExternalAdmin.LoginRateLimit.SystemLimit = 300
	}
	if cfg.ExternalAdmin.LoginRateLimit.AppLimit == 0 {
		cfg.ExternalAdmin.LoginRateLimit.AppLimit = 120
	}
	if cfg.ExternalAdmin.LoginRateLimit.IPLimit == 0 {
		cfg.ExternalAdmin.LoginRateLimit.IPLimit = 30
	}
	if cfg.ExternalAdmin.LoginRateLimit.IdentityLimit == 0 {
		cfg.ExternalAdmin.LoginRateLimit.IdentityLimit = 10
	}
	cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
	cfg.NodeID = strings.TrimSpace(cfg.NodeID)
	if cfg.NodeID == "" {
		cfg.NodeID = cfg.ServiceName
	}
	if cfg.Bootstrap.Password == "" && cfg.BootstrapPassword != "" {
		cfg.Bootstrap.Password = cfg.BootstrapPassword
	}
	if cfg.Bootstrap.Username == "" {
		cfg.Bootstrap.Username = "admin"
	}
	if cfg.Bootstrap.Name == "" {
		cfg.Bootstrap.Name = cfg.Bootstrap.Username
	}
	if cfg.Bootstrap.Team == "" {
		cfg.Bootstrap.Team = "运营团队"
	}
	if cfg.Jobs.LeaseTTL <= 0 {
		cfg.Jobs.LeaseTTL = 2 * time.Minute
	}
	if cfg.Jobs.MaxAttempts <= 0 {
		cfg.Jobs.MaxAttempts = 3
	}
	if cfg.Jobs.ArtifactDir == "" {
		cfg.Jobs.ArtifactDir = "storage/exports"
	}
	cfg.UserService.Addr = strings.TrimSpace(cfg.UserService.Addr)
	if cfg.UserService.RequestTimeout <= 0 {
		cfg.UserService.RequestTimeout = 3 * time.Second
	}
	cfg.WalletService.Addr = strings.TrimSpace(cfg.WalletService.Addr)
	if cfg.WalletService.RequestTimeout <= 0 {
		cfg.WalletService.RequestTimeout = 3 * time.Second
	}
	cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
	if cfg.RoomService.Addr == "" {
		cfg.RoomService.Addr = "127.0.0.1:13001"
	}
	if cfg.RoomService.RequestTimeout <= 0 {
		cfg.RoomService.RequestTimeout = 3 * time.Second
	}
	cfg.RobotService.Addr = strings.TrimSpace(cfg.RobotService.Addr)
	if cfg.RobotService.Addr == "" {
		cfg.RobotService.Addr = "127.0.0.1:13011"
	}
	if cfg.RobotService.RequestTimeout <= 0 {
		cfg.RobotService.RequestTimeout = 3 * time.Second
	}
	cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
	cfg.ActivityTemplateH5.BaseURL = strings.TrimSpace(cfg.ActivityTemplateH5.BaseURL)
	if cfg.ActivityService.RequestTimeout <= 0 {
		cfg.ActivityService.RequestTimeout = 3 * time.Second
	}
	cfg.LuckyGiftService.Addr = strings.TrimSpace(cfg.LuckyGiftService.Addr)
	if cfg.LuckyGiftService.Addr == "" {
		cfg.LuckyGiftService.Addr = "127.0.0.1:13013"
	}
	if cfg.LuckyGiftService.RequestTimeout <= 0 {
		cfg.LuckyGiftService.RequestTimeout = 3 * time.Second
	}
	cfg.GameService.Addr = strings.TrimSpace(cfg.GameService.Addr)
	if cfg.GameService.Addr == "" {
		cfg.GameService.Addr = "127.0.0.1:13008"
	}
	if cfg.GameService.RequestTimeout <= 0 {
		cfg.GameService.RequestTimeout = 3 * time.Second
	}
	cfg.StatisticsService.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.StatisticsService.BaseURL), "/")
	if cfg.StatisticsService.BaseURL == "" {
		cfg.StatisticsService.BaseURL = "http://127.0.0.1:13010"
	}
	if cfg.StatisticsService.RequestTimeout <= 0 {
		cfg.StatisticsService.RequestTimeout = 3 * 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 = compactStrings(cfg.FinanceNotifications.DingTalk.AtMobiles)
	if cfg.FinanceNotifications.DingTalk.RequestTimeout <= 0 {
		cfg.FinanceNotifications.DingTalk.RequestTimeout = 3 * time.Second
	}
	cfg.applyFinanceDingTalkEnvOverrides()
	for index := range cfg.DashboardExternalSources {
		source := &cfg.DashboardExternalSources[index]
		// 外接 App 大屏的数据源类型在配置层归一化；查询层只根据类型读取对应的外部聚合事实，不回退到 hyapp statistics-service。
		source.SourceType = strings.ToLower(strings.TrimSpace(source.SourceType))
		source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
		source.AppName = strings.TrimSpace(source.AppName)
		source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
		source.MySQLDSN = strings.TrimSpace(source.MySQLDSN)
		source.LegacyMySQLDSN = strings.TrimSpace(source.LegacyMySQLDSN)
		source.LegacyWalletDatabase = strings.Trim(strings.TrimSpace(source.LegacyWalletDatabase), "/")
		source.MongoURI = strings.TrimSpace(source.MongoURI)
		source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
		source.BaseURL = strings.TrimRight(strings.TrimSpace(source.BaseURL), "/")
		source.OverviewPath = "/" + strings.TrimLeft(strings.TrimSpace(source.OverviewPath), "/")
		if source.OverviewPath == "/" {
			source.OverviewPath = ""
		}
		if source.SourceType == "" {
			if source.MongoURI != "" || source.MongoDatabase != "" {
				source.SourceType = "aslan_mongo"
			} else if source.BaseURL != "" || source.OverviewPath != "" {
				source.SourceType = "aslan_http"
			} else {
				source.SourceType = "mysql"
			}
		}
		if source.SourceType == "aslan_mongo" && source.MongoDatabase == "" {
			source.MongoDatabase = "test"
		}
		if source.SourceType == "aslan_mongo" && source.LegacyWalletDatabase == "" && source.MongoDatabase != "" {
			// Aslan legacy RDS 把用户资料放在 atyou，把金币代理钱包流水放在 atyou_wallet；默认按主库名派生。
			source.LegacyWalletDatabase = source.MongoDatabase + "_wallet"
		}
		source.StatTimezone = strings.TrimSpace(source.StatTimezone)
		if source.StatTimezone == "" {
			if source.SourceType == "aslan_http" || source.SourceType == "aslan_mongo" {
				source.StatTimezone = "Asia/Shanghai"
			} else {
				source.StatTimezone = "Asia/Riyadh"
			}
		}
		if source.RequestTimeout <= 0 {
			source.RequestTimeout = 5 * time.Second
		}
	}
	cfg.applyDashboardExternalSourceEnvOverrides()
	for index := range cfg.LegacyCoinSellerRechargeSources {
		source := &cfg.LegacyCoinSellerRechargeSources[index]
		source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
		source.AppName = strings.TrimSpace(source.AppName)
		source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
		source.AppMySQLDSN = strings.TrimSpace(source.AppMySQLDSN)
		source.WalletMySQLDSN = strings.TrimSpace(source.WalletMySQLDSN)
		if source.RequestTimeout <= 0 {
			source.RequestTimeout = 5 * time.Second
		}
	}
	cfg.applyLegacyCoinSellerRechargeSourceEnvOverrides()
	for index := range cfg.WithdrawalSources {
		source := &cfg.WithdrawalSources[index]
		source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
		source.SourceSystem = strings.ToLower(strings.TrimSpace(source.SourceSystem))
		source.SubmitToken = strings.TrimSpace(source.SubmitToken)
		source.CallbackURL = strings.TrimRight(strings.TrimSpace(source.CallbackURL), "/")
		source.CallbackToken = strings.TrimSpace(source.CallbackToken)
		if source.RequestTimeout <= 0 {
			source.RequestTimeout = 5 * time.Second
		}
	}
	cfg.applyWithdrawalSourceEnvOverrides()
	cfg.RobotProfileSource.MySQLDSN = strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN)
	if cfg.RobotProfileSource.RequestTimeout <= 0 {
		cfg.RobotProfileSource.RequestTimeout = 5 * time.Second
	}
	for index := range cfg.MoneyRegionSources {
		source := &cfg.MoneyRegionSources[index]
		// legacy 区域来源只服务财务范围目录；这里先规整配置，handler 层只处理业务过滤，不重复处理大小写和默认集合名。
		source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
		source.AppName = strings.TrimSpace(source.AppName)
		source.LogoURL = strings.TrimSpace(source.LogoURL)
		source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
		source.MongoURI = strings.TrimSpace(source.MongoURI)
		source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
		if source.MongoDatabase == "" {
			source.MongoDatabase = "tarab_all"
		}
		source.MongoCollection = strings.TrimSpace(source.MongoCollection)
		if source.MongoCollection == "" {
			source.MongoCollection = "sys_region_config"
		}
		if source.RequestTimeout <= 0 {
			source.RequestTimeout = 5 * time.Second
		}
	}
	cfg.applyMoneyRegionSourceEnvOverrides()
	for index := range cfg.FinanceBillSources {
		source := &cfg.FinanceBillSources[index]
		// legacy 账单源只服务财务充值明细；配置层统一大小写和默认集合，handler 只按 app_code 命中。
		source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
		source.AppName = strings.TrimSpace(source.AppName)
		source.LogoURL = strings.TrimSpace(source.LogoURL)
		source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
		source.MongoURI = strings.TrimSpace(source.MongoURI)
		source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
		if source.MongoDatabase == "" {
			// 线上 likei Mongo（10.2.21.9）的账单和区域目录都在 tarab_all 库，与 money_region_sources 保持一致。
			source.MongoDatabase = "tarab_all"
		}
		source.MongoCollection = strings.TrimSpace(source.MongoCollection)
		if source.MongoCollection == "" {
			source.MongoCollection = "in_app_purchase_details"
		}
		source.GooglePackageName = strings.TrimSpace(source.GooglePackageName)
		source.GoogleServiceAccountJSON = strings.TrimSpace(source.GoogleServiceAccountJSON)
		source.GoogleServiceAccountFile = strings.TrimSpace(source.GoogleServiceAccountFile)
		if source.RequestTimeout <= 0 {
			source.RequestTimeout = 5 * time.Second
		}
	}
	cfg.applyFinanceBillSourceEnvOverrides()
	// The sync feature is enabled by default; normalization only bounds operator-provided cadence and batch size.
	if cfg.FinanceGooglePaidSync.PollInterval <= 0 {
		cfg.FinanceGooglePaidSync.PollInterval = Default().FinanceGooglePaidSync.PollInterval
	}
	if cfg.FinanceGooglePaidSync.BatchSize <= 0 {
		cfg.FinanceGooglePaidSync.BatchSize = Default().FinanceGooglePaidSync.BatchSize
	}
	if cfg.FinanceGooglePaidSync.BatchSize > 1000 {
		cfg.FinanceGooglePaidSync.BatchSize = 1000
	}
	if cfg.FinanceGooglePaidSync.RequestTimeout <= 0 {
		cfg.FinanceGooglePaidSync.RequestTimeout = Default().FinanceGooglePaidSync.RequestTimeout
	}
	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), "/")
	cfg.TencentCOS.ObjectPrefix = strings.Trim(strings.TrimSpace(cfg.TencentCOS.ObjectPrefix), "/")
	if cfg.TencentCOS.ObjectPrefix == "" {
		cfg.TencentCOS.ObjectPrefix = "admin"
	}
}

func (cfg Config) Validate() error {
	if !validEnvironment(cfg.Environment) {
		return errors.New("environment must be one of local, dev, staging, prod")
	}
	if strings.TrimSpace(cfg.ServiceName) == "" {
		return errors.New("service_name is required")
	}
	if strings.TrimSpace(cfg.NodeID) == "" {
		return errors.New("node_id is required")
	}
	if strings.TrimSpace(cfg.HTTPAddr) == "" {
		return errors.New("http_addr is required")
	}
	for _, proxy := range cfg.TrustedProxies {
		if net.ParseIP(proxy) != nil {
			continue
		}
		if _, _, err := net.ParseCIDR(proxy); err != nil {
			return fmt.Errorf("trusted_proxies contains invalid IP or CIDR %q", proxy)
		}
	}
	if strings.TrimSpace(cfg.MySQLDSN) == "" {
		return errors.New("mysql_dsn is required")
	}
	if strings.TrimSpace(cfg.UserMySQLDSN) == "" {
		return errors.New("user_mysql_dsn is required")
	}
	if strings.TrimSpace(cfg.WalletMySQLDSN) == "" {
		return errors.New("wallet_mysql_dsn is required")
	}
	if cfg.Migrations.Enabled && strings.TrimSpace(cfg.Migrations.Dir) == "" {
		return errors.New("migrations.dir is required when migrations are enabled")
	}
	if isLockedEnvironment(cfg.Environment) && cfg.Migrations.AllowChecksumRepair {
		return errors.New("migrations.allow_checksum_repair must be false outside local/dev")
	}
	if strings.TrimSpace(cfg.JWTSecret) == "" {
		return errors.New("jwt_secret is required")
	}
	if cfg.AccessTokenTTL <= 0 {
		return errors.New("access_token_ttl must be greater than 0")
	}
	if cfg.RefreshTokenTTL <= 0 {
		return errors.New("refresh_token_ttl must be greater than 0")
	}
	if cfg.ExternalAdmin.SessionTTL <= 0 {
		return errors.New("external_admin.session_ttl must be greater than 0")
	}
	if cfg.ExternalAdmin.MaxLoginFailures == 0 || cfg.ExternalAdmin.MaxLoginFailures > 20 {
		return errors.New("external_admin.max_login_failures must be between 1 and 20")
	}
	if cfg.ExternalAdmin.LockDuration <= 0 {
		return errors.New("external_admin.lock_duration must be greater than 0")
	}
	if cfg.ExternalAdmin.LoginRateLimit.Window < time.Second || cfg.ExternalAdmin.LoginRateLimit.Window > time.Hour {
		return errors.New("external_admin.login_rate_limit.window must be between 1s and 1h")
	}
	if cfg.ExternalAdmin.LoginRateLimit.SystemLimit == 0 || cfg.ExternalAdmin.LoginRateLimit.AppLimit == 0 || cfg.ExternalAdmin.LoginRateLimit.IPLimit == 0 || cfg.ExternalAdmin.LoginRateLimit.IdentityLimit == 0 {
		return errors.New("external_admin.login_rate_limit limits must be greater than 0")
	}
	if cfg.ExternalAdmin.LoginRateLimit.SystemLimit < cfg.ExternalAdmin.LoginRateLimit.AppLimit {
		return errors.New("external_admin.login_rate_limit.system_limit must be greater than or equal to app_limit")
	}
	if cfg.ExternalAdmin.LoginRateLimit.AppLimit < cfg.ExternalAdmin.LoginRateLimit.IPLimit || cfg.ExternalAdmin.LoginRateLimit.AppLimit < cfg.ExternalAdmin.LoginRateLimit.IdentityLimit {
		return errors.New("external_admin.login_rate_limit.app_limit must cover ip_limit and identity_limit")
	}
	switch strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite)) {
	case "lax", "strict", "none":
	default:
		return errors.New("refresh_cookie_same_site must be one of lax, strict, none")
	}
	if strings.EqualFold(strings.TrimSpace(cfg.RefreshCookieSameSite), "none") && !cfg.RefreshCookieSecure {
		return errors.New("refresh_cookie_secure must be true when refresh_cookie_same_site is none")
	}
	if isLockedEnvironment(cfg.Environment) {
		if cfg.JWTSecret == "dev-shared-secret" || len(cfg.JWTSecret) < 32 {
			return errors.New("jwt_secret must be a non-default value with at least 32 characters outside local/dev")
		}
		if cfg.MySQLAutoMigrate {
			return errors.New("mysql_auto_migrate must be false outside local/dev")
		}
		if cfg.Bootstrap.Enabled && cfg.Bootstrap.Password == "admin123" {
			return errors.New("bootstrap.password must not use the default value outside local/dev")
		}
	}
	for _, source := range cfg.LegacyCoinSellerRechargeSources {
		if !source.Enabled {
			continue
		}
		if strings.TrimSpace(source.AppCode) == "" {
			return errors.New("legacy_coin_seller_recharge_sources app_code is required when enabled")
		}
		if strings.TrimSpace(source.SysOrigin) == "" {
			return errors.New("legacy_coin_seller_recharge_sources sys_origin is required when enabled")
		}
		if strings.TrimSpace(source.AppMySQLDSN) == "" || strings.TrimSpace(source.WalletMySQLDSN) == "" {
			return errors.New("legacy_coin_seller_recharge_sources app_mysql_dsn and wallet_mysql_dsn are required when enabled")
		}
	}
	if len(cfg.CORSAllowedOrigins) == 0 {
		return errors.New("cors_allowed_origins must not be empty")
	}
	if cfg.Bootstrap.Enabled && strings.TrimSpace(cfg.Bootstrap.Password) == "" {
		return errors.New("bootstrap.password is required when bootstrap is enabled")
	}
	if strings.TrimSpace(cfg.UserService.Addr) == "" {
		return errors.New("user_service.addr is required")
	}
	if cfg.UserService.RequestTimeout <= 0 {
		return errors.New("user_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.WalletService.Addr) == "" {
		return errors.New("wallet_service.addr is required")
	}
	if cfg.WalletService.RequestTimeout <= 0 {
		return errors.New("wallet_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.RoomService.Addr) == "" {
		return errors.New("room_service.addr is required")
	}
	if cfg.RoomService.RequestTimeout <= 0 {
		return errors.New("room_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.RobotService.Addr) == "" {
		return errors.New("robot_service.addr is required")
	}
	if cfg.RobotService.RequestTimeout <= 0 {
		return errors.New("robot_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
		return errors.New("activity_service.addr is required")
	}
	if cfg.ActivityService.RequestTimeout <= 0 {
		return errors.New("activity_service.request_timeout must be greater than 0")
	}
	landingURL, err := url.ParseRequestURI(cfg.ActivityTemplateH5.BaseURL)
	if err != nil || landingURL.Scheme == "" || landingURL.Host == "" || (landingURL.Scheme != "http" && landingURL.Scheme != "https") {
		return errors.New("activity_template_h5.base_url must be an absolute http(s) url")
	}
	if strings.TrimSpace(cfg.LuckyGiftService.Addr) == "" {
		return errors.New("lucky_gift_service.addr is required")
	}
	if cfg.LuckyGiftService.RequestTimeout <= 0 {
		return errors.New("lucky_gift_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.GameService.Addr) == "" {
		return errors.New("game_service.addr is required")
	}
	if cfg.GameService.RequestTimeout <= 0 {
		return errors.New("game_service.request_timeout must be greater than 0")
	}
	if strings.TrimSpace(cfg.StatisticsService.BaseURL) == "" {
		return errors.New("statistics_service.base_url is required")
	}
	if cfg.StatisticsService.RequestTimeout <= 0 {
		return errors.New("statistics_service.request_timeout must be greater than 0")
	}
	for index, source := range cfg.DashboardExternalSources {
		if !source.Enabled {
			continue
		}
		if strings.TrimSpace(source.AppCode) == "" {
			return fmt.Errorf("dashboard_external_sources[%d].app_code is required when enabled", index)
		}
		switch strings.ToLower(strings.TrimSpace(source.SourceType)) {
		case "mysql":
			if strings.TrimSpace(source.SysOrigin) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].sys_origin is required when mysql source is enabled", index)
			}
			if strings.TrimSpace(source.MySQLDSN) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].mysql_dsn is required when mysql source is enabled", index)
			}
		case "aslan_http":
			if strings.TrimSpace(source.BaseURL) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].base_url is required when aslan_http source is enabled", index)
			}
			if !strings.HasPrefix(source.BaseURL, "http://") && !strings.HasPrefix(source.BaseURL, "https://") {
				return fmt.Errorf("dashboard_external_sources[%d].base_url must be an absolute url", index)
			}
			if strings.TrimSpace(source.OverviewPath) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].overview_path is required when aslan_http source is enabled", index)
			}
		case "aslan_mongo":
			if strings.TrimSpace(source.SysOrigin) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].sys_origin is required when aslan_mongo source is enabled", index)
			}
			if strings.TrimSpace(source.MongoURI) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].mongo_uri is required when aslan_mongo source is enabled", index)
			}
			if strings.TrimSpace(source.MongoDatabase) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].mongo_database is required when aslan_mongo source is enabled", index)
			}
			if strings.TrimSpace(source.LegacyMySQLDSN) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].legacy_mysql_dsn is required when aslan_mongo source is enabled", index)
			}
			if containsConfigPlaceholder(source.LegacyMySQLDSN) {
				return fmt.Errorf("dashboard_external_sources[%d].legacy_mysql_dsn must be injected by environment, not left as a placeholder", index)
			}
			if strings.TrimSpace(source.LegacyWalletDatabase) == "" {
				return fmt.Errorf("dashboard_external_sources[%d].legacy_wallet_database is required when aslan_mongo source is enabled", index)
			}
			if !isMySQLIdentifier(source.LegacyWalletDatabase) {
				return fmt.Errorf("dashboard_external_sources[%d].legacy_wallet_database must be a mysql identifier", index)
			}
		default:
			return fmt.Errorf("dashboard_external_sources[%d].type must be one of mysql, aslan_http, aslan_mongo", index)
		}
		if source.RequestTimeout <= 0 {
			return fmt.Errorf("dashboard_external_sources[%d].request_timeout must be greater than 0", index)
		}
	}
	for index, source := range cfg.WithdrawalSources {
		if !source.Enabled {
			continue
		}
		if strings.TrimSpace(source.AppCode) == "" || strings.TrimSpace(source.SourceSystem) == "" {
			return fmt.Errorf("withdrawal_sources[%d] app_code and source_system are required when enabled", index)
		}
		if strings.TrimSpace(source.SubmitToken) == "" || containsConfigPlaceholder(source.SubmitToken) {
			return fmt.Errorf("withdrawal_sources[%d].submit_token must be injected when enabled", index)
		}
		if strings.TrimSpace(source.CallbackToken) == "" || containsConfigPlaceholder(source.CallbackToken) {
			return fmt.Errorf("withdrawal_sources[%d].callback_token must be injected when enabled", index)
		}
		if !strings.HasPrefix(source.CallbackURL, "http://") && !strings.HasPrefix(source.CallbackURL, "https://") {
			return fmt.Errorf("withdrawal_sources[%d].callback_url must be an absolute url", index)
		}
		if source.RequestTimeout <= 0 {
			return fmt.Errorf("withdrawal_sources[%d].request_timeout must be greater than 0", index)
		}
	}
	if isLockedEnvironment(cfg.Environment) && strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN) == "" {
		return errors.New("robot_profile_source.mysql_dsn is required outside local/dev")
	}
	if cfg.RobotProfileSource.RequestTimeout <= 0 {
		return errors.New("robot_profile_source.request_timeout must be greater than 0")
	}
	for index, source := range cfg.MoneyRegionSources {
		if !source.Enabled {
			continue
		}
		// 线上 likei Mongo 是外部只读事实来源；启用但缺 URI/库/集合时必须启动失败，不能让财务范围静默漏掉 Yumi 区域。
		if strings.TrimSpace(source.AppCode) == "" {
			return fmt.Errorf("money_region_sources[%d].app_code is required when enabled", index)
		}
		if strings.TrimSpace(source.SysOrigin) == "" {
			return fmt.Errorf("money_region_sources[%d].sys_origin is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoURI) == "" {
			return fmt.Errorf("money_region_sources[%d].mongo_uri is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoDatabase) == "" {
			return fmt.Errorf("money_region_sources[%d].mongo_database is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoCollection) == "" {
			return fmt.Errorf("money_region_sources[%d].mongo_collection is required when enabled", index)
		}
		if source.RequestTimeout <= 0 {
			return fmt.Errorf("money_region_sources[%d].request_timeout must be greater than 0", index)
		}
	}
	for index, source := range cfg.FinanceBillSources {
		if !source.Enabled {
			continue
		}
		// legacy 账单源启用但配置不全时必须启动失败，避免财务充值明细静默漏掉 Yumi 账单。
		if strings.TrimSpace(source.AppCode) == "" {
			return fmt.Errorf("finance_bill_sources[%d].app_code is required when enabled", index)
		}
		if strings.TrimSpace(source.SysOrigin) == "" {
			return fmt.Errorf("finance_bill_sources[%d].sys_origin is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoURI) == "" {
			return fmt.Errorf("finance_bill_sources[%d].mongo_uri is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoDatabase) == "" {
			return fmt.Errorf("finance_bill_sources[%d].mongo_database is required when enabled", index)
		}
		if strings.TrimSpace(source.MongoCollection) == "" {
			return fmt.Errorf("finance_bill_sources[%d].mongo_collection is required when enabled", index)
		}
		if source.RequestTimeout <= 0 {
			return fmt.Errorf("finance_bill_sources[%d].request_timeout must be greater than 0", index)
		}
	}
	if cfg.TencentCOS.Enabled {
		if cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" {
			return errors.New("tencent-cos config is incomplete")
		}
		if !strings.HasPrefix(cfg.TencentCOS.AccessURL, "http://") && !strings.HasPrefix(cfg.TencentCOS.AccessURL, "https://") {
			return errors.New("tencent-cos.access-url must be an absolute url")
		}
	}
	if cfg.Redis.Enabled && strings.TrimSpace(cfg.Redis.Addr) == "" {
		return errors.New("redis.addr is required when redis is enabled")
	}
	if isLockedEnvironment(cfg.Environment) && !cfg.Redis.Enabled {
		return errors.New("redis.enabled must be true outside local/dev for external login rate limiting")
	}
	if cfg.Redis.DB < 0 {
		return fmt.Errorf("redis.db must be greater than or equal to 0")
	}
	if cfg.Jobs.Enabled && cfg.Jobs.WorkerInterval <= 0 {
		return errors.New("jobs.worker_interval must be greater than 0")
	}
	if cfg.Jobs.Enabled && cfg.Jobs.LeaseTTL <= 0 {
		return errors.New("jobs.lease_ttl must be greater than 0")
	}
	if cfg.Jobs.Enabled && cfg.Jobs.MaxAttempts <= 0 {
		return errors.New("jobs.max_attempts must be greater than 0")
	}
	if cfg.Jobs.Enabled && strings.TrimSpace(cfg.Jobs.ArtifactDir) == "" {
		return errors.New("jobs.artifact_dir is required when jobs are enabled")
	}
	return nil
}

func (cfg *Config) applyDashboardExternalSourceEnvOverrides() {
	for index := range cfg.DashboardExternalSources {
		source := &cfg.DashboardExternalSources[index]
		appKey := envAppKey(source.AppCode)
		if appKey == "" {
			continue
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_DASHBOARD_MONGO_URI",
			"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
		)); value != "" {
			// Mongo URI 含线上密码；仓库 YAML 只放占位，真实值由运行环境注入后覆盖。
			source.MongoURI = value
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_DASHBOARD_MONGO_DATABASE",
			"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
		)); value != "" {
			previousMongoDatabase := source.MongoDatabase
			usesDerivedWalletDatabase := source.LegacyWalletDatabase == "" || source.LegacyWalletDatabase == previousMongoDatabase+"_wallet"
			source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
			if source.SourceType == "aslan_mongo" && usesDerivedWalletDatabase && source.MongoDatabase != "" {
				source.LegacyWalletDatabase = source.MongoDatabase + "_wallet"
			}
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_DASHBOARD_LEGACY_MYSQL_DSN",
			"HYAPP_ADMIN_"+appKey+"_LEGACY_MYSQL_DSN",
		)); value != "" {
			// Aslan Mongo 大屏只把 legacy MySQL 作为补充事实源，真实 likei RDS 凭证由运行环境注入。
			source.LegacyMySQLDSN = value
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_DASHBOARD_LEGACY_WALLET_DATABASE",
			"HYAPP_ADMIN_"+appKey+"_LEGACY_WALLET_DATABASE",
		)); value != "" {
			source.LegacyWalletDatabase = strings.Trim(value, "/")
		}
	}
}

func (cfg *Config) applyMoneyRegionSourceEnvOverrides() {
	for index := range cfg.MoneyRegionSources {
		source := &cfg.MoneyRegionSources[index]
		appKey := envAppKey(source.AppCode)
		if appKey == "" {
			continue
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_MONEY_REGION_MONGO_URI",
			"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
		)); value != "" {
			// legacy 区域源和 Aslan 大屏可以共享同一个 Mongo 集群；只在运行环境注入真实密码。
			source.MongoURI = value
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_MONEY_REGION_MONGO_DATABASE",
			"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
		)); value != "" {
			source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
		}
	}
}

func (cfg *Config) applyFinanceBillSourceEnvOverrides() {
	for index := range cfg.FinanceBillSources {
		source := &cfg.FinanceBillSources[index]
		appKey := envAppKey(source.AppCode)
		if appKey == "" {
			continue
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_FINANCE_BILL_MONGO_URI",
			"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
		)); value != "" {
			// legacy 账单源与区域源/大屏共享同一个 likei Mongo 集群；真实密码只由运行环境注入。
			source.MongoURI = value
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_FINANCE_BILL_MONGO_DATABASE",
			"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
		)); value != "" {
			source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
		}
		if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_PACKAGE_NAME")); value != "" {
			source.GooglePackageName = value
		}
		// 服务账号 JSON 含私钥；生产优先用环境变量注入，仓库 YAML 只放文件路径或占位。
		if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_SERVICE_ACCOUNT_JSON")); value != "" {
			source.GoogleServiceAccountJSON = value
		}
		if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_SERVICE_ACCOUNT_FILE")); value != "" {
			source.GoogleServiceAccountFile = value
		}
	}
}

func (cfg *Config) applyLegacyCoinSellerRechargeSourceEnvOverrides() {
	for index := range cfg.LegacyCoinSellerRechargeSources {
		source := &cfg.LegacyCoinSellerRechargeSources[index]
		appKey := envAppKey(source.AppCode)
		if appKey == "" {
			continue
		}
		if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_LEGACY_COIN_SELLER_RECHARGE_ENABLED")); value != "" {
			source.Enabled = parseBoolEnv(value)
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_COIN_SELLER_RECHARGE_APP_MYSQL_DSN",
			"HYAPP_ADMIN_"+appKey+"_LEGACY_APP_MYSQL_DSN",
		)); value != "" {
			// legacy 用户库 DSN 带可写权限，只能由运行环境注入；仓库配置保持占位。
			source.AppMySQLDSN = value
		}
		if value := strings.TrimSpace(firstEnv(
			"HYAPP_ADMIN_"+appKey+"_COIN_SELLER_RECHARGE_WALLET_MYSQL_DSN",
			"HYAPP_ADMIN_"+appKey+"_LEGACY_WALLET_MYSQL_DSN",
		)); value != "" {
			// legacy 钱包库 DSN 用于写 user_freight_balance_running_water，不能复用只读报表源。
			source.WalletMySQLDSN = value
		}
	}
}

func (cfg *Config) applyWithdrawalSourceEnvOverrides() {
	for index := range cfg.WithdrawalSources {
		source := &cfg.WithdrawalSources[index]
		appKey := envAppKey(source.AppCode)
		if appKey == "" {
			continue
		}
		prefix := "HYAPP_ADMIN_" + appKey + "_WITHDRAWAL_"
		if value := strings.TrimSpace(os.Getenv(prefix + "ENABLED")); value != "" {
			source.Enabled = parseBoolEnv(value)
		}
		if value := strings.TrimSpace(os.Getenv(prefix + "SOURCE_SYSTEM")); value != "" {
			source.SourceSystem = value
		}
		if value := strings.TrimSpace(os.Getenv(prefix + "SUBMIT_TOKEN")); value != "" {
			// 双向令牌都属于资金系统密钥，只允许通过运行环境注入，不能写进仓库配置。
			source.SubmitToken = value
		}
		if value := strings.TrimSpace(os.Getenv(prefix + "CALLBACK_URL")); value != "" {
			source.CallbackURL = value
		}
		if value := strings.TrimSpace(os.Getenv(prefix + "CALLBACK_TOKEN")); value != "" {
			source.CallbackToken = value
		}
	}
}

func (cfg *Config) applyFinanceDingTalkEnvOverrides() {
	dingTalk := &cfg.FinanceNotifications.DingTalk
	if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_ENABLED")); value != "" {
		dingTalk.Enabled = parseBoolEnv(value)
	}
	if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_WEBHOOK_URL", "FINANCE_DINGTALK_WEBHOOK_URL")); value != "" {
		// webhook query 中包含 access_token，只允许运行环境覆盖，不能复制到 Admin YAML 或日志。
		dingTalk.WebhookURL = value
		dingTalk.Enabled = true
	}
	if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_SECRET", "FINANCE_DINGTALK_SECRET")); value != "" {
		dingTalk.Secret = value
	}
	if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_MOBILES")); value != "" {
		dingTalk.AtMobiles = compactStrings(strings.Split(value, ","))
	}
	if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_ALL")); value != "" {
		dingTalk.AtAll = parseBoolEnv(value)
	}
}

func envAppKey(appCode string) string {
	appCode = strings.ToUpper(strings.TrimSpace(appCode))
	if appCode == "" {
		return ""
	}
	return strings.NewReplacer("-", "_", ".", "_").Replace(appCode)
}

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

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

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

func containsConfigPlaceholder(value string) bool {
	upper := strings.ToUpper(strings.TrimSpace(value))
	return strings.Contains(upper, "REPLACE_ME") || strings.Contains(upper, "CHANGEME") || strings.Contains(upper, "<")
}

func isMySQLIdentifier(value string) bool {
	if value == "" {
		return false
	}
	for _, r := range value {
		if r == '_' || r == '$' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
			continue
		}
		return false
	}
	return true
}

func validEnvironment(value string) bool {
	switch value {
	case "local", "dev", "staging", "prod":
		return true
	default:
		return false
	}
}

func isLockedEnvironment(value string) bool {
	return value == "staging" || value == "prod"
}
