package config

import (
	"fmt"
	"io"
	"net/netip"
	"os"
	"strings"
	"time"

	"github.com/creasty/defaults"
	meteringsdkconfig "github.com/pingcap/metering_sdk/config"
	meteringsdkstorage "github.com/pingcap/metering_sdk/storage"
	meteringsdkmetawriter "github.com/pingcap/metering_sdk/writer/meta"
	meteringsdkmeteringwriter "github.com/pingcap/metering_sdk/writer/metering"
	"github.com/tidbcloud/pkgv2/gormutil"
	"github.com/tidbcloud/tiinf/tiinf"
	"gopkg.in/yaml.v2"
)

type Config struct {
	TiInfConfig  *tiinf.TiInfConfig `yaml:"tiinf"`
	ServerConfig *ServerConfig      `yaml:"server"`
}

type ServerConfig struct {
	CloudProvider        string               `yaml:"cloudProvider"` // aws,alicloud,azure,gcp
	Region               string               `yaml:"region"`
	TemplatePath         string               `yaml:"templatePath"`
	ImageConfig          ImageConfig          `yaml:"image"`
	BizConfig            BizConfig            `yaml:"biz"`
	MysqlConfig          gormutil.Config      `yaml:"mysql"`
	ExecutorGrpcAddr     string               `yaml:"executorGrpcAddr"`
	DataPlaneManagerAddr string               `yaml:"dataPlaneManagerAddr"`
	ConfigCenterAddr     string               `yaml:"configCenterAddr"`
	CronJob              CronJobConfig        `yaml:"cronJob"`
	RUConfig             *RuConfig            `yaml:"ruConfig"`
	ScaleOperation       ScaleOperationConfig `yaml:"scaleOperation"`

	ControlPlaneMetering *MeteringConfig `yaml:"controlPlaneMetering"`
	DataPlaneMetering    *MeteringConfig `yaml:"dataPlaneMetering"`

	// DevConfig contains development/local environment settings.
	// These options should NEVER be enabled in production.
	DevConfig *DevConfig `yaml:"dev,omitempty"`
}

// DevConfig contains settings for local development environment.
// All options default to false to ensure production safety.
type DevConfig struct {
	// MockConfigCenter enables mock config center client instead of real one.
	// When true, ConfigCenterAddr is ignored.
	MockConfigCenter bool `yaml:"mockConfigCenter" default:"false"`
	// MockDataplaneManager enables mock dataplane manager client.
	// When true, DataPlaneManagerAddr is ignored.
	MockDataplaneManager bool `yaml:"mockDataplaneManager" default:"false"`
}

type ImageConfig struct {
	Registry          string `yaml:"registry"`
	RepoPrefix        string `yaml:"repoPrefix"`
	ImagePullPolicy   string `yaml:"imagePullPolicy" default:"IfNotPresent"`
	SlowLogImage      string `yaml:"slowLogImage"`
	AuditLogImage     string `yaml:"auditLogImage"`
	DumplingImage     string `yaml:"dumplingImage"`
	TiKVPreStopImage  string `yaml:"tikvPreStopImage"`
	ClusterAgentImage string `yaml:"clusterAgentImage"`
	DataCleanerImage  string `yaml:"dataCleanerImage"`
	DmImage           string `yaml:"dmImage"`
	// CassandraProxyImage is the image:tag for the cassandra-proxy sidecar
	// component (e.g. "ghcr.io/pingcap/cassandra-proxy:v0.5.2"). Empty = feature off.
	CassandraProxyImage string `yaml:"cassandraProxyImage"`
}

type BizConfig struct {
	DefaultDataplaneAccount string `yaml:"defaultDataplaneAccount"`
	// AzureResourceGroupName, when set, will be passed to dataplane-manager during dataplane creation.
	AzureResourceGroupName string            `yaml:"azureResourceGroupName"`
	O11yBizType            string            `yaml:"o11yBizType"`
	O11yClusterCIDR        string            `yaml:"o11yClusterCidr"`
	O11yAgentNamespace     string            `yaml:"o11yAgentNamespace"`
	StorageSizePolicy      StorageSizePolicy `yaml:"storageSizePolicy"`
	DisableO11y            bool              `yaml:"disableO11y"`
	EnableHpa              bool              `yaml:"enableHpa"`
	// AutoscalePolicy controls the policy subsystem independently from legacy
	// HPA and RuConfig autoscaling. The zero value is intentionally disabled.
	AutoscalePolicy AutoscalePolicyConfig `yaml:"autoscalePolicy"`
	// Kong plugins will be applied to the dataplane ingresses
	KongDataplanePlugins string `yaml:"kongDataplanePlugins"`
	// Kong plugins will be applied to the regional controlplane ingresses
	KongControlplanePlugins string `yaml:"kongControlplanePlugins"`
	// set replicas for placeholder
	PlaceholderReplicas int                     `yaml:"placeholderReplicas"`
	EnablePca           bool                    `yaml:"enablePca"`
	EnableSemv2         bool                    `yaml:"enableSemv2"`
	ClusterAgentAudit   ClusterAgentAuditConfig `yaml:"clusterAgentAudit"`
	// CertificateDuration overrides TiDB/TiProxy server certificate lifetimes.
	// Leave zero to use production defaults.
	CertificateDuration CertificateDurationConfig `yaml:"certificateDuration"`
	// PrivilegedTiDBDeactivationWait controls how long the cluster-agent waits
	// after a privileged TiDB SQL exec before deleting the pod. Lower values
	// improve throughput when many reset_password requests target different
	// keyspaces. Empty/zero leaves the cluster-agent default (30s) in effect.
	PrivilegedTiDBDeactivationWait time.Duration `yaml:"privilegedTiDBDeactivationWait"`
	DefaultTiBRConfig              TiBRConfig    `yaml:"defaultTiBRConfig"`
	DataCleanerCron                string        `yaml:"dataCleanerCron" default:"0 * * * *"` // default to run every hour
	// FIXME: only used before configuration center integration, will be deprecated after configuration center integration
	ClassicRestore        ClassicRestoreConfig `yaml:"classicRestore"`
	TemplateVersionMaxLag int                  `yaml:"templateVersionMaxLag" default:"3"`
}

type AutoscalePolicyMode string

const (
	AutoscalePolicyModeDisabled AutoscalePolicyMode = "disabled"
	AutoscalePolicyModeEnforce  AutoscalePolicyMode = "enforce"
)

// AutoscalePolicyConfig is a region-level safety gate. Enforce preserves the
// current policy implementation for all single-tenant pools in the region;
// tenant-scoped canary and shadow modes require a separate ownership-aware
// control plane.
type AutoscalePolicyConfig struct {
	Mode            AutoscalePolicyMode `yaml:"mode" default:"disabled"`
	AllowAllTenants bool                `yaml:"allowAllTenants" default:"false"`
}

func (c AutoscalePolicyConfig) EffectiveMode() AutoscalePolicyMode {
	if c.Mode == "" {
		return AutoscalePolicyModeDisabled
	}
	return c.Mode
}

func (c AutoscalePolicyConfig) IsEnforced() bool {
	return c.EffectiveMode() == AutoscalePolicyModeEnforce && c.AllowAllTenants
}

func (c AutoscalePolicyConfig) Validate() error {
	switch c.EffectiveMode() {
	case AutoscalePolicyModeDisabled:
		if c.AllowAllTenants {
			return fmt.Errorf("allowAllTenants requires mode %q", AutoscalePolicyModeEnforce)
		}
		return nil
	case AutoscalePolicyModeEnforce:
		if !c.AllowAllTenants {
			return fmt.Errorf("mode %q requires allowAllTenants=true", AutoscalePolicyModeEnforce)
		}
		return nil
	default:
		return fmt.Errorf("mode must be %q or %q", AutoscalePolicyModeDisabled, AutoscalePolicyModeEnforce)
	}
}

type StorageSizePolicy struct {
	TiKV           TiKVStorageSizePolicy    `yaml:"tikv"`
	TiFlashCompute TiFlashStorageSizePolicy `yaml:"tiflashCompute"`
}

type TiKVStorageSizePolicy struct {
	Data    StorageSizeRange `yaml:"data"`
	Raftlog StorageSizeRange `yaml:"raftlog"`
}

type TiFlashStorageSizePolicy struct {
	Data StorageSizeRange `yaml:"data"`
}

type StorageSizeRange struct {
	DefaultSizeGi uint32 `yaml:"defaultSizeGi"`
	MinSizeGi     uint32 `yaml:"minSizeGi"`
	MaxSizeGi     uint32 `yaml:"maxSizeGi"`
}

func DefaultStorageSizePolicy() StorageSizePolicy {
	return StorageSizePolicy{
		TiKV: TiKVStorageSizePolicy{
			Data:    StorageSizeRange{DefaultSizeGi: 500, MinSizeGi: 100, MaxSizeGi: 4000},
			Raftlog: StorageSizeRange{DefaultSizeGi: 50, MinSizeGi: 20, MaxSizeGi: 500},
		},
		TiFlashCompute: TiFlashStorageSizePolicy{
			Data: StorageSizeRange{DefaultSizeGi: 500, MinSizeGi: 100, MaxSizeGi: 4000},
		},
	}
}

func (p StorageSizePolicy) Validate() error {
	ranges := []struct {
		name  string
		value StorageSizeRange
	}{
		{name: "tikv.data", value: p.TiKV.Data},
		{name: "tikv.raftlog", value: p.TiKV.Raftlog},
		{name: "tiflashCompute.data", value: p.TiFlashCompute.Data},
	}
	for _, storageRange := range ranges {
		if err := storageRange.value.Validate(); err != nil {
			return fmt.Errorf("%s: %w", storageRange.name, err)
		}
	}
	return nil
}

func (r StorageSizeRange) Validate() error {
	if r.MinSizeGi == 0 {
		return fmt.Errorf("minSizeGi must be positive")
	}
	if r.DefaultSizeGi < r.MinSizeGi {
		return fmt.Errorf("defaultSizeGi must be greater than or equal to minSizeGi")
	}
	if r.DefaultSizeGi > r.MaxSizeGi {
		return fmt.Errorf("defaultSizeGi must be less than or equal to maxSizeGi")
	}
	return nil
}

// ParseO11yClusterCIDR parses the deployment-scoped observability CIDR.
// Empty means the deployment has not enabled BYOC resource-pool provisioning.
func ParseO11yClusterCIDR(value string) (netip.Prefix, bool, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return netip.Prefix{}, false, nil
	}

	prefix, err := netip.ParsePrefix(value)
	if err != nil {
		return netip.Prefix{}, false, fmt.Errorf("parse o11y cluster cidr: %w", err)
	}
	if !prefix.Addr().Is4() {
		return netip.Prefix{}, false, fmt.Errorf("o11y cluster cidr must be an IPv4 prefix")
	}
	if prefix != prefix.Masked() {
		return netip.Prefix{}, false, fmt.Errorf("o11y cluster cidr must be a canonical masked prefix")
	}

	return prefix, true, nil
}

type CertificateDurationConfig struct {
	TiDBServer    time.Duration `yaml:"tidbServer"`
	TiProxyServer time.Duration `yaml:"tiProxyServer"`
	RenewBefore   time.Duration `yaml:"renewBefore"`
}

func (c CertificateDurationConfig) HasOverride() bool {
	return c.TiDBServer > 0 || c.TiProxyServer > 0 || c.RenewBefore > 0
}

func (c CertificateDurationConfig) Validate() error {
	if !c.HasOverride() {
		return nil
	}
	if c.TiDBServer <= 0 {
		return fmt.Errorf("tidbServer must be > 0 when certificateDuration is configured")
	}
	if c.TiProxyServer <= 0 {
		return fmt.Errorf("tiProxyServer must be > 0 when certificateDuration is configured")
	}
	if c.RenewBefore <= 0 {
		return fmt.Errorf("renewBefore must be > 0 when certificateDuration is configured")
	}
	if c.RenewBefore >= c.TiDBServer {
		return fmt.Errorf("renewBefore must be less than tidbServer")
	}
	if c.RenewBefore >= c.TiProxyServer {
		return fmt.Errorf("renewBefore must be less than tiProxyServer")
	}
	return nil
}

type ClusterAgentAuditConfig struct {
	Enabled      bool   `yaml:"enabled"`
	StorageClass string `yaml:"storageClass" default:"cloud-ssd"`
	StorageSize  string `yaml:"storageSize" default:"1Gi"`
}

type CronJobConfig struct {
	EnableCleanSharedPool              bool          `yaml:"enableCleanSharedPool" default:"false"`
	CleanSharedPoolCron                string        `yaml:"cleanSharedPoolCron" default:"0 */6 * * *"`                // default to run every 6 hours
	CleanDataBucketCron                string        `yaml:"cleanDataBucketCron" default:"0 */6 * * *"`                // default to run every 6 hours
	CleanInternalBucketCron            string        `yaml:"cleanInternalBucketCron" default:"0 */6 * * *"`            // default to run every 6 hours
	CleanExpiredAutoBackupDataCron     string        `yaml:"cleanExpiredAutoBackupDataCron" default:"0 */6 * * *"`     // default to run every 6 hours
	CleanManualBackupDeletionRulesCron string        `yaml:"cleanManualBackupDeletionRulesCron" default:"0 */6 * * *"` // default to run every 6 hours
	EnableTemplateVersionUsageDetector bool          `yaml:"enableTemplateVersionUsageDetector" default:"false"`
	TemplateVersionUsageDetectorCron   string        `yaml:"templateVersionUsageDetectorCron" default:"0 0 */8 * * *"` // every 8 hours; uses per-component DB lookup so keep frequency low
	EnableShortageDetector             bool          `yaml:"enableShortageDetector" default:"false"`
	ShortageDetectorCron               string        `yaml:"shortageDetectorCron" default:"0 */5 * * * *"` // every 5 minutes; scans shared pools for capacity shortage
	EnableRetiringProcessor            bool          `yaml:"enableRetiringProcessor" default:"false"`
	RetiringProcessorCron              string        `yaml:"retiringProcessorCron" default:"0 */10 * * * *"` // every 10 minutes; processes RETIRING node instance groups
	EnableRcuScaleDown                 bool          `yaml:"enableRcuScaleDown" default:"true"`
	RcuScaleDownCron                   string        `yaml:"rcuScaleDownCron" default:"0 0 */6 * * *"` // every 6 hours
	RcuScaleDownMaxRu                  uint32        `yaml:"rcuScaleDownMaxRu" default:"50000"`        // skip scale-down for components whose max RU exceeds this ceiling
	EnableDMWorkerAutoscaler           bool          `yaml:"enableDMWorkerAutoscaler" default:"false"`
	DMWorkerAutoscalerCron             string        `yaml:"dmWorkerAutoscalerCron" default:"*/30 * * * * *"`
	DMWorkerScaleDownStability         time.Duration `yaml:"dmWorkerScaleDownStability" default:"5m"`
}

// ScaleOperationConfig is scoped to M1 (horizontal scale-out cancel).
// Retention cleanup, vertical precheck (M3 warmupPodImage/verticalPrecheckTimeout),
// and the automatic policy layer (M4 autoPolicy) add their own fields when a
// caller actually needs them.
type ScaleOperationConfig struct {
	Enabled               bool          `yaml:"enabled" default:"false"`
	WorkerInterval        time.Duration `yaml:"workerInterval" default:"30s"`
	StaleOperationTimeout time.Duration `yaml:"staleOperationTimeout" default:"30m"`
}

type TiBRConfig struct {
	OSSRateLimitGbps        float64 `yaml:"oss_rate_limit_gbps" default:"30"`
	CopyBackupMaxThroughput string  `yaml:"copy_backup_max_throughput"`
	RestoreMaxThroughput    string  `yaml:"restore_max_throughput"`
	ArchiveMaxThroughput    string  `yaml:"archive_max_throughput"`
	DisableFlushWAL         bool    `yaml:"disable_flush_wal" default:"false"`
	// common br storage resource
	StorageClass string `yaml:"storage_class" default:"cloud-ssd"`
	// default tibr cpu and memory
	CPU    string `yaml:"cpu" default:"2"`
	Memory string `yaml:"memory" default:"8Gi"`
	// for restore of tibr
	StorageSize string `yaml:"storage_size" default:"500Gi"`
	// will be used in restore to evalute if need to scale tikv
	TiKVUsagePercentageThreshold uint32 `yaml:"tikv_usage_percentage_threshold" default:"70"`

	// for archive backup of tibr gc
	ArchiveBackupStorageSize string `yaml:"archive_backup_storage_size" default:"250Gi"`
	// defult tibrgc settings
	// only age of backup data older than this will be moved to T2
	MinT2DataAgeDays uint32 `yaml:"min_t2_data_age_days"`
	// only age of backup data older than this will be packed and moved to T3
	MinT3DataAgeDays uint32 `yaml:"min_t3_data_age_days" default:"5"`
	// max lifetimes of data in buckets, will deleted by S3 lifecycle rule if exceeded
	MaxT1MetaLifetimeDays   uint32 `yaml:"max_t1_meta_lifetime_days" default:"7"`
	MaxT1WalLifetimeDays    uint32 `yaml:"max_t1_wal_lifetime_days" default:"7"`
	MaxT1PackedLifetimeDays uint32 `yaml:"max_t1_packed_lifetime_days" default:"4"`
	MaxT2SstLifetimeDays    uint32 `yaml:"max_t2_sst_lifetime_days" default:"30"`
	MaxT3DataLifetimeDays   uint32 `yaml:"max_t3_data_lifetime_days" default:"90"`
	// max use face auto backup life time is 34 days, so we set the default value to 34
	MaxRetainT1AfterSPDeletedDays uint32 `yaml:"max_retain_t1_after_sp_deleted_days" default:"34"`
	MaxRetainT2AfterSPDeletedDays uint32 `yaml:"max_retain_t2_after_sp_deleted_days" default:"34"`
	MaxRetainT3AfterSPDeletedDays uint32 `yaml:"max_retain_t3_after_sp_deleted_days" default:"34"`
	IntegrateConfigCenter         bool   `yaml:"integrate_config_center" default:"false"`
}

type ClassicRestoreConfig struct {
	ImageTag string            `yaml:"imageTag"`
	Options  map[string]string `yaml:"options"`
}

type RuConfig struct {
	EnableScaleSharedPoolByRu           bool                          `yaml:"enableScaleSharedPoolByRu" default:"false"`
	EnableScaleTiDBGroupByRu            bool                          `yaml:"enableScaleTiDBGroupByRu" default:"false"`
	EnableAutoscaleTikvCpuScaleOut      bool                          `yaml:"enableAutoscaleTikvCpuScaleOut" default:"false"`
	EnableAutoscaleDiskExpand           bool                          `yaml:"enableAutoscaleDiskExpand" default:"false"`
	EnableAutoscaleIopsThroughputExpand bool                          `yaml:"enableAutoscaleIopsThroughputExpand" default:"false"`
	EnableAutoscaleTiproxyScaleIn       bool                          `yaml:"enableAutoscaleTiproxyScaleIn" default:"false"`
	Components                          map[string]map[string]RuRange `yaml:"components"`
	NodeCountQuota                      map[string]uint32             `yaml:"nodeCountQuota"`
	DefaultStandbyReplicas              int                           `yaml:"defaultStandbyReplicas"`
	TenantStandbyReplicas               map[string]int                `yaml:"tenantStandbyReplicas"`
}

type RuRange struct {
	Min              uint32  `yaml:"min"`
	Max              uint32  `yaml:"max"`
	DefaultNodeCount *uint32 `yaml:"defaultNodeCount"`
}

type MeteringAWSConfig struct {
	S3ForcePathStyle bool   `yaml:"s3ForcePathStyle,omitempty"`
	AssumeRoleARN    string `yaml:"assumeRoleArn,omitempty"`
}

type MeteringOSSConfig struct {
	AssumeRoleARN string `yaml:"assumeRoleArn,omitempty"`
}

type MeteringAzureConfig struct {
	AccountName string `yaml:"accountName,omitempty"`
}

type WorkerNodeMeteringConfig struct {
	// Enable controls whether worker node metering is enabled.
	// This item is independent of the main `Enable` switch in MeteringConfig,
	// allowing users to enable worker node metering separately from control plane metering if desired.
	Enable             bool          `yaml:"enable" default:"false"`
	CompensationWindow time.Duration `yaml:"compensationWindow" default:"30m"`
}

type ReservedNodeMeteringConfig struct {
	// Enable controls whether reserved-node metering is enabled.
	Enable             bool          `yaml:"enable" default:"false"`
	CompensationWindow time.Duration `yaml:"compensationWindow" default:"30m"`
}

type MeteringConfig struct {
	Enable   bool                 `yaml:"enable"`
	Type     string               `yaml:"type"`               // https://github.com/pingcap/metering_sdk/blob/main/storage/provider/types.go#L6
	Prefix   string               `yaml:"prefix,omitempty"`   // path prefix, all write paths will add this prefix
	Region   string               `yaml:"region,omitempty"`   // common region configuration
	Bucket   string               `yaml:"bucket,omitempty"`   // common bucket/container name
	Endpoint string               `yaml:"endpoint,omitempty"` // common endpoint configuration
	AWS      *MeteringAWSConfig   `yaml:"aws,omitempty"`      // AWS S3 specific configuration
	OSS      *MeteringOSSConfig   `yaml:"oss,omitempty"`      // Alicloud OSS specific configuration
	Azure    *MeteringAzureConfig `yaml:"azure,omitempty"`    // Azure Blob Storage specific configuration

	Plan string `yaml:"plan" default:"premium"` // metering plan, default to "premium"

	WorkerNode   *WorkerNodeMeteringConfig   `yaml:"workerNode,omitempty"`
	ReservedNode *ReservedNodeMeteringConfig `yaml:"reservedNode,omitempty"`
}

const PremiumReservedMeteringPlan = "premium_reserved"

func (cfg *MeteringConfig) MetadataEnabled() bool {
	if cfg == nil {
		return false
	}
	return cfg.Enable || cfg.WorkerNodeEnabled()
}

func (cfg *MeteringConfig) WorkerNodeEnabled() bool {
	if cfg == nil || cfg.WorkerNode == nil {
		return false
	}
	return cfg.WorkerNode.Enable
}

func (cfg *MeteringConfig) ReservedNodeEnabled() bool {
	if cfg == nil || cfg.ReservedNode == nil {
		return false
	}
	return cfg.ReservedNode.Enable
}

func (cfg *MeteringConfig) MetadataPlan() string {
	if cfg == nil || cfg.Plan == "" {
		return "premium"
	}
	return cfg.Plan
}

func (cfg *MeteringConfig) BuildMeteringMetaWriter() (*meteringsdkmetawriter.MetaWriter, error) {
	provider, err := meteringsdkstorage.NewObjectStorageProvider(cfg.ToMeteringSDKConfig().ToProviderConfig())
	if err != nil {
		return nil, fmt.Errorf("failed to create storage provider: %w", err)
	}

	writer := meteringsdkmetawriter.NewMetaWriter(provider, nil)
	return writer, nil
}

func (cfg *MeteringConfig) BuildMeteringDataWriter() (*meteringsdkmeteringwriter.MeteringWriter, error) {
	provider, err := meteringsdkstorage.NewObjectStorageProvider(cfg.ToMeteringSDKConfig().ToProviderConfig())
	if err != nil {
		return nil, fmt.Errorf("failed to create storage provider: %w", err)
	}
	writer := meteringsdkmeteringwriter.NewMeteringWriter(provider, nil)
	return writer, nil
}

func (cfg *MeteringConfig) ToMeteringSDKConfig() *meteringsdkconfig.MeteringConfig {
	meteringConf := new(meteringsdkconfig.MeteringConfig)
	meteringConf.Type = meteringsdkstorage.ProviderType(cfg.Type)
	meteringConf.Prefix = cfg.Prefix
	meteringConf.Region = cfg.Region
	meteringConf.Bucket = cfg.Bucket
	meteringConf.Endpoint = cfg.Endpoint
	if cfg.AWS != nil {
		meteringConf.AWS = new(meteringsdkconfig.MeteringAWSConfig)
		meteringConf.AWS.AssumeRoleARN = cfg.AWS.AssumeRoleARN
		meteringConf.AWS.S3ForcePathStyle = cfg.AWS.S3ForcePathStyle
	}
	if cfg.OSS != nil {
		meteringConf.OSS = new(meteringsdkconfig.MeteringOSSConfig)
		meteringConf.OSS.AssumeRoleARN = cfg.OSS.AssumeRoleARN
	}
	if cfg.Azure != nil {
		meteringConf.Azure = new(meteringsdkconfig.MeteringAzureConfig)
		meteringConf.Azure.AccountName = cfg.Azure.AccountName
	}
	return meteringConf
}

func (cfg *MeteringConfig) ValidateMeteringStorageConfig() error {
	switch meteringsdkstorage.ProviderType(cfg.Type) {
	case meteringsdkstorage.ProviderTypeS3:
		if cfg.Bucket == "" {
			return fmt.Errorf("bucket is required for S3 metering provider")
		}
		return nil
	case meteringsdkstorage.ProviderTypeLocalFS:
		return nil
	case meteringsdkstorage.ProviderTypeOSS:
		if cfg.Bucket == "" {
			return fmt.Errorf("bucket is required for OSS metering provider")
		}
		if cfg.Region == "" {
			return fmt.Errorf("region is required for OSS metering provider")
		}
		return nil
	case meteringsdkstorage.ProviderTypeAzure:
		if cfg.Bucket == "" {
			return fmt.Errorf("bucket is required for Azure metering provider")
		}
		if cfg.Endpoint == "" && (cfg.Azure == nil || cfg.Azure.AccountName == "") {
			return fmt.Errorf("azure account name or endpoint is required for Azure metering provider")
		}
		return nil
	case meteringsdkstorage.ProviderTypeGCS:
		return fmt.Errorf("provider GCS not implemented yet")
	default:
		return fmt.Errorf("unsupported provider type: %s", cfg.Type)
	}
}

func InitConfigFromFile(cfgFilePath string) (*Config, error) {
	file, err := os.OpenFile(cfgFilePath, os.O_RDONLY, 0o644)
	if err != nil {
		return nil, fmt.Errorf("failed to open config file: %w", err)
	}
	defer func() { _ = file.Close() }()

	content, err := io.ReadAll(file)
	if err != nil {
		return nil, fmt.Errorf("failed to read config file: %w", err)
	}

	// Track whether ruConfig is actually present in YAML so we can both
	// preserve the "missing RU config" validation and pre-initialize the
	// pointer for default-tag population.
	var rawCfg struct {
		Server struct {
			RUConfig any `yaml:"ruConfig"`
		} `yaml:"server"`
	}
	if err = yaml.Unmarshal(content, &rawCfg); err != nil {
		return nil, fmt.Errorf("failed to unmarshal yaml config file: %w", err)
	}

	// define the config struct; initialise pointer fields so that
	// creasty/defaults can traverse into nested structs and apply
	// `default:` tags (nil pointers would be skipped).
	cfg := &Config{
		ServerConfig: &ServerConfig{
			BizConfig: BizConfig{
				StorageSizePolicy: DefaultStorageSizePolicy(),
			},
			RUConfig: &RuConfig{},
			ControlPlaneMetering: &MeteringConfig{
				WorkerNode:   &WorkerNodeMeteringConfig{},
				ReservedNode: &ReservedNodeMeteringConfig{},
			},
		},
	}

	// Apply default values from struct tags before unmarshaling.
	// yaml.Unmarshal does not process `default:` tags, so we use
	// creasty/defaults to populate them first; YAML values then override.
	if err = defaults.Set(cfg); err != nil {
		return nil, fmt.Errorf("failed to set config defaults: %w", err)
	}

	err = yaml.Unmarshal(content, cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal yaml config file: %w", err)
	}

	if err := cfg.ServerConfig.BizConfig.CertificateDuration.Validate(); err != nil {
		return nil, fmt.Errorf("invalid certificateDuration config: %w", err)
	}
	if err := cfg.ServerConfig.BizConfig.StorageSizePolicy.Validate(); err != nil {
		return nil, fmt.Errorf("invalid storageSizePolicy config: %w", err)
	}
	if err := cfg.ServerConfig.BizConfig.AutoscalePolicy.Validate(); err != nil {
		return nil, fmt.Errorf("invalid autoscalePolicy config: %w", err)
	}
	o11yPrefix, configured, err := ParseO11yClusterCIDR(cfg.ServerConfig.BizConfig.O11yClusterCIDR)
	if err != nil {
		return nil, fmt.Errorf("invalid o11yClusterCidr config: %w", err)
	}
	if configured {
		cfg.ServerConfig.BizConfig.O11yClusterCIDR = o11yPrefix.String()
	}

	// get rds config from env
	username := os.Getenv("RDS_USERNAME")
	password := os.Getenv("RDS_PASSWORD")
	if username != "" {
		cfg.ServerConfig.MysqlConfig.User = username
	}
	if password != "" {
		cfg.ServerConfig.MysqlConfig.Password = password
	}

	// check ru config
	if rawCfg.Server.RUConfig == nil {
		return nil, fmt.Errorf("missing RU config")
	}
	// validate RU config ranges
	for component, specs := range cfg.ServerConfig.RUConfig.Components {
		for spec, ruRange := range specs {
			// max == 0 means unlimited, so skip the check
			if ruRange.Max != 0 && ruRange.Min > ruRange.Max {
				return nil, fmt.Errorf("invalid RU config: component %s spec %s has min (%d) > max (%d)", component, spec, ruRange.Min, ruRange.Max)
			}
		}
	}
	// set default metering config if nil to avoid panic
	if cfg.ServerConfig.ControlPlaneMetering == nil {
		cfg.ServerConfig.ControlPlaneMetering = new(MeteringConfig)
	}
	if cfg.ServerConfig.ControlPlaneMetering.WorkerNode == nil {
		cfg.ServerConfig.ControlPlaneMetering.WorkerNode = new(WorkerNodeMeteringConfig)
	}
	if cfg.ServerConfig.ControlPlaneMetering.ReservedNode == nil {
		cfg.ServerConfig.ControlPlaneMetering.ReservedNode = new(ReservedNodeMeteringConfig)
	}
	if cfg.ServerConfig.DataPlaneMetering == nil {
		cfg.ServerConfig.DataPlaneMetering = new(MeteringConfig)
	}
	if cfg.ServerConfig.ControlPlaneMetering.WorkerNode.Enable && cfg.ServerConfig.ControlPlaneMetering.WorkerNode.CompensationWindow <= 0 {
		return nil, fmt.Errorf("invalid worker node metering compensation window: must be > 0")
	}
	if cfg.ServerConfig.ControlPlaneMetering.WorkerNode.Enable {
		if err := cfg.ServerConfig.ControlPlaneMetering.ValidateMeteringStorageConfig(); err != nil {
			return nil, fmt.Errorf("invalid control plane metering config for worker node metering: %w", err)
		}
	}
	if cfg.ServerConfig.ControlPlaneMetering.ReservedNode.Enable && cfg.ServerConfig.ControlPlaneMetering.ReservedNode.CompensationWindow <= 0 {
		return nil, fmt.Errorf("invalid reserved node metering compensation window: must be > 0")
	}
	if cfg.ServerConfig.ControlPlaneMetering.ReservedNode.Enable {
		if err := cfg.ServerConfig.ControlPlaneMetering.ValidateMeteringStorageConfig(); err != nil {
			return nil, fmt.Errorf("invalid control plane metering config for reserved node metering: %w", err)
		}
	}

	return cfg, nil
}
