package config import ( "errors" "fmt" "os" "strconv" ) const ( AIProviderAvalAI = "avalai" AIProviderAgentRouter = "agentrouter" ) type Provider struct { Name string APIKey string BaseURL string Model string } type Config struct { AppEnv string HTTPAddr string DatabaseURL string RedisAddr string RedisPassword string ImportPath string DemoUsername string DemoPassword string SessionSecret string CursorSecret string FrontendOrigin string CookieSecure bool AIGrowthEnabled bool AICoachEnabled bool AIPageInsightsEnabled bool AIProvider string AvalAIAPIKey string AvalAIBaseURL string AvalAIModel string AgentRouterAPIKey string AgentRouterBaseURL string AgentRouterModel string AIDailyRequestBudget int64 AIDailyTokenBudget int64 AIMaxConcurrent int AICreatePerMinute int64 AIUncachedScopesPerDay int64 AICoachDailyRequestBudget int64 AICoachDailyTokenBudget int64 AIPageInsightsDailyRequestBudget int64 AIPageInsightsDailyTokenBudget int64 } func Load() (Config, error) { cfg := Config{ AppEnv: value("APP_ENV", "development"), HTTPAddr: value("HTTP_ADDR", "127.0.0.1:8080"), DatabaseURL: os.Getenv("DATABASE_URL"), RedisAddr: value("REDIS_ADDR", "localhost:6379"), RedisPassword: os.Getenv("REDIS_PASSWORD"), ImportPath: value("IMPORT_PATH", "data/incoming/challenge_data.csv.gz"), DemoUsername: value("DEMO_USERNAME", "merchant-0001"), DemoPassword: os.Getenv("DEMO_PASSWORD"), SessionSecret: os.Getenv("SESSION_SECRET"), CursorSecret: os.Getenv("CURSOR_SECRET"), FrontendOrigin: value("FRONTEND_ORIGIN", "http://localhost:5173"), AIProvider: value("AI_PROVIDER", AIProviderAvalAI), AvalAIAPIKey: os.Getenv("AVALAI_API_KEY"), AvalAIBaseURL: value("AVALAI_BASE_URL", "https://api.avalai.ir"), AvalAIModel: value("AVALAI_MODEL", "gpt-5-mini-2025-08-07"), AgentRouterAPIKey: os.Getenv("AGENTROUTER_API_KEY"), AgentRouterBaseURL: value("AGENTROUTER_BASE_URL", "https://agentrouter.org/v1"), AgentRouterModel: value("AGENTROUTER_MODEL", "gpt-5.6-sol"), } if cfg.AIProvider != AIProviderAvalAI && cfg.AIProvider != AIProviderAgentRouter { return Config{}, fmt.Errorf("config: unsupported AI_PROVIDER %q", cfg.AIProvider) } var err error cfg.CookieSecure, err = strconv.ParseBool(value("COOKIE_SECURE", "false")) if err != nil { return Config{}, fmt.Errorf("parsing cookie secure: %w", err) } cfg.AIGrowthEnabled, err = strconv.ParseBool(value("AI_GROWTH_ENABLED", "false")) if err != nil { return Config{}, fmt.Errorf("parsing AI growth enabled: %w", err) } cfg.AICoachEnabled, err = strconv.ParseBool(value("AI_COACH_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled))) if err != nil { return Config{}, fmt.Errorf("parsing AI coach enabled: %w", err) } cfg.AIPageInsightsEnabled, err = strconv.ParseBool(value("AI_PAGE_INSIGHTS_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled))) if err != nil { return Config{}, fmt.Errorf("parsing AI page insights enabled: %w", err) } cfg.AIDailyRequestBudget, err = parseInt64("AI_DAILY_REQUEST_BUDGET", 0) if err != nil { return Config{}, err } cfg.AIDailyTokenBudget, err = parseInt64("AI_DAILY_TOKEN_BUDGET", 0) if err != nil { return Config{}, err } cfg.AICoachDailyRequestBudget, err = parseInt64("AI_COACH_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget) if err != nil { return Config{}, err } cfg.AICoachDailyTokenBudget, err = parseInt64("AI_COACH_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget) if err != nil { return Config{}, err } cfg.AIPageInsightsDailyRequestBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget) if err != nil { return Config{}, err } cfg.AIPageInsightsDailyTokenBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget) if err != nil { return Config{}, err } maxConcurrent, err := parseInt64("AI_MAX_CONCURRENT", 4) if err != nil { return Config{}, err } cfg.AIMaxConcurrent = int(maxConcurrent) cfg.AICreatePerMinute, err = parseInt64("AI_CREATE_PER_MINUTE", 10) if err != nil { return Config{}, err } cfg.AIUncachedScopesPerDay, err = parseInt64("AI_UNCACHED_SCOPES_PER_DAY", 5) if err != nil { return Config{}, err } if cfg.DatabaseURL == "" { return Config{}, errors.New("config: DATABASE_URL is required") } return cfg, nil } func (c Config) ValidateAPI() error { if len(c.SessionSecret) < 32 { return errors.New("config: SESSION_SECRET must contain at least 32 characters") } if len(c.CursorSecret) < 32 { return errors.New("config: CURSOR_SECRET must contain at least 32 characters") } if c.CursorSecret == c.SessionSecret { return errors.New("config: CURSOR_SECRET must differ from SESSION_SECRET") } if c.AIMaxConcurrent < 1 || c.AICreatePerMinute < 1 || c.AIUncachedScopesPerDay < 1 { return errors.New("config: AI limits must be positive") } if c.AIProvider != "" && c.AIProvider != AIProviderAvalAI && c.AIProvider != AIProviderAgentRouter { return errors.New("config: unsupported AI provider") } if c.AppEnv == "production" && (c.AIGrowthEnabled || c.AICoachEnabled || c.AIPageInsightsEnabled) { provider := c.SelectedAIProvider() if provider.APIKey == "" || provider.Model == "" { return errors.New("config: enabled production AI requires provider API key and model") } if c.AIGrowthEnabled && (c.AIDailyRequestBudget < 1 || c.AIDailyTokenBudget < 1) { return errors.New("config: enabled production AI requires positive daily budgets") } if c.AICoachEnabled && (c.AICoachDailyRequestBudget < 1 || c.AICoachDailyTokenBudget < 1) { return errors.New("config: enabled production coach requires positive coach budgets") } if c.AIPageInsightsEnabled && (c.AIPageInsightsDailyRequestBudget < 1 || c.AIPageInsightsDailyTokenBudget < 1) { return errors.New("config: enabled production page insights require positive page-insight budgets") } } return nil } func (c Config) SelectedAIProvider() Provider { if c.AIProvider == AIProviderAgentRouter { return Provider{Name: AIProviderAgentRouter, APIKey: c.AgentRouterAPIKey, BaseURL: c.AgentRouterBaseURL, Model: c.AgentRouterModel} } return Provider{Name: AIProviderAvalAI, APIKey: c.AvalAIAPIKey, BaseURL: c.AvalAIBaseURL, Model: c.AvalAIModel} } func (c Config) ValidateSeed() error { if len(c.DemoPassword) < 12 { return errors.New("config: DEMO_PASSWORD must contain at least 12 characters") } return nil } func value(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback } func parseInt64(name string, fallback int64) (int64, error) { raw := value(name, strconv.FormatInt(fallback, 10)) parsed, err := strconv.ParseInt(raw, 10, 64) if err != nil || parsed < 0 { return 0, fmt.Errorf("config: %s must be a non-negative integer", name) } return parsed, nil }