package config import ( "fmt" "github.com/joho/godotenv" "github.com/spf13/viper" "git.georgsen.dk/hwlab/internal/ai" ) type Config struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` LogLevel string `mapstructure:"log_level"` NetBoxURL string `mapstructure:"netbox_url"` NetBoxToken string `mapstructure:"netbox_token"` NetBoxTimeoutSeconds int `mapstructure:"netbox_timeout_seconds"` DragonflyURL string `mapstructure:"dragonfly_url"` WAQRetryIntervalSeconds int `mapstructure:"waq_retry_interval_seconds"` WAQMaxAttempts int `mapstructure:"waq_max_attempts"` QualityGateConfidenceThreshold float64 `mapstructure:"quality_gate_confidence_threshold"` AI ai.AIConfig `mapstructure:"ai"` } func Load() (*Config, error) { // Load .env file if present (ignore error — .env is optional in production) _ = godotenv.Load() v := viper.New() // Set defaults v.SetDefault("host", "0.0.0.0") v.SetDefault("port", 8080) v.SetDefault("log_level", "info") v.SetDefault("netbox_timeout_seconds", 10) v.SetDefault("waq_retry_interval_seconds", 30) v.SetDefault("waq_max_attempts", 5) v.SetDefault("quality_gate_confidence_threshold", 0.75) // AI tier defaults v.SetDefault("ai.tier1.base_url", "http://localhost:8000/v1") v.SetDefault("ai.tier1.api_key", "local") v.SetDefault("ai.tier1.model", "gemma-4-e4b") v.SetDefault("ai.tier1.timeout_seconds", 30) v.SetDefault("ai.tier2.base_url", "https://openrouter.ai/api/v1") v.SetDefault("ai.tier2.api_key", "") v.SetDefault("ai.tier2.model", "google/gemma-3-27b-it") v.SetDefault("ai.tier2.timeout_seconds", 60) v.SetDefault("ai.confidence_threshold", 0.75) v.SetDefault("ai.quick_add_enabled", false) v.SetDefault("ai.quick_add_threshold", 0.90) // Config file v.SetConfigName("config") v.SetConfigType("json") v.AddConfigPath(".") v.AddConfigPath("/etc/hwlab") // Environment variables: HWLAB_PORT -> port, HWLAB_NETBOX_URL -> netbox_url, etc. v.SetEnvPrefix("HWLAB") v.AutomaticEnv() // Explicit bindings ensure AutomaticEnv works correctly with mapstructure v2 unmarshalling. _ = v.BindEnv("host", "HWLAB_HOST") _ = v.BindEnv("port", "HWLAB_PORT") _ = v.BindEnv("log_level", "HWLAB_LOG_LEVEL") _ = v.BindEnv("netbox_url", "HWLAB_NETBOX_URL") _ = v.BindEnv("netbox_token", "HWLAB_NETBOX_TOKEN") _ = v.BindEnv("netbox_timeout_seconds", "HWLAB_NETBOX_TIMEOUT_SECONDS") _ = v.BindEnv("dragonfly_url", "HWLAB_DRAGONFLY_URL") _ = v.BindEnv("waq_retry_interval_seconds", "HWLAB_WAQ_RETRY_INTERVAL_SECONDS") _ = v.BindEnv("waq_max_attempts", "HWLAB_WAQ_MAX_ATTEMPTS") _ = v.BindEnv("quality_gate_confidence_threshold", "HWLAB_QUALITY_GATE_CONFIDENCE_THRESHOLD") // AI env bindings _ = v.BindEnv("ai.tier1.base_url", "HWLAB_AI_TIER1_BASE_URL") _ = v.BindEnv("ai.tier1.api_key", "HWLAB_AI_TIER1_API_KEY") _ = v.BindEnv("ai.tier1.model", "HWLAB_AI_TIER1_MODEL") _ = v.BindEnv("ai.tier2.base_url", "HWLAB_AI_TIER2_BASE_URL") _ = v.BindEnv("ai.tier2.api_key", "HWLAB_AI_TIER2_API_KEY") _ = v.BindEnv("ai.tier2.model", "HWLAB_AI_TIER2_MODEL") _ = v.BindEnv("ai.confidence_threshold", "HWLAB_AI_CONFIDENCE_THRESHOLD") _ = v.BindEnv("ai.quick_add_enabled", "HWLAB_AI_QUICK_ADD_ENABLED") // Read primary config file (non-fatal if missing) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return nil, fmt.Errorf("config file: %w", err) } } // Optionally merge ai_config.json as an override (separate viper instance to avoid // conflicts with the primary config file name). v2 := viper.New() v2.SetConfigName("ai_config") v2.SetConfigType("json") v2.AddConfigPath(".") v2.AddConfigPath("/etc/hwlab") if err := v2.ReadInConfig(); err == nil { // ai_config.json present — merge AI section into main viper if err := v.MergeConfigMap(v2.AllSettings()); err != nil { return nil, fmt.Errorf("merge ai_config: %w", err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } return &cfg, nil }