package config

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/url"
	"os"
	"strconv"
	"strings"
)

const (
	DefaultLocalGeneratedStorageDir = "data/static/generated"
	DefaultLocalUploadedStorageDir  = "data/static/uploaded"
)

type Config struct {
	AppEnv                                        string
	HTTPAddr                                      string
	DatabaseURL                                   string
	IdentityMode                                  string
	JWTSecret                                     string
	ServerMainBaseURL                             string
	ServerMainInternalToken                       string
	ServerMainInternalKey                         string
	ServerMainInternalSecret                      string
	IdentitySecretStore                           string
	IdentitySecretDir                             string
	IdentityKubernetesNamespace                   string
	IdentityKubernetesSecretName                  string
	IdentityKubernetesAPIServer                   string
	IdentityKubernetesTokenFile                   string
	IdentityKubernetesCAFile                      string
	IdentitySecurityEventHeartbeatIntervalSeconds int
	IdentitySecurityEventStaleAfterSeconds        int
	IdentitySecurityEventClockSkewSeconds         int
	PublicBaseURL                                 string
	WebBaseURL                                    string
	LocalGeneratedStorageDir                      string
	LocalUploadedStorageDir                       string
	LocalTempAssetTTLHours                        int
	LocalResultTTLHours                           int
	LocalResultMinFreeBytes                       int64
	LocalResultMaxBytes                           int64
	LocalResultMaxTaskBytes                       int64
	TaskProgressCallbackEnabled                   bool
	TaskProgressCallbackURL                       string
	TaskProgressCallbackTimeoutMS                 int
	TaskProgressCallbackMaxAttempts               int
	TaskCleanupEnabled                            bool
	TaskRetentionDays                             int
	TaskAnalysisRetentionDays                     int
	TaskCleanupIntervalSeconds                    int
	TaskCleanupBatchSize                          int
	CORSAllowedOrigin                             string
	GlobalHTTPProxy                               string
	GlobalHTTPProxySource                         string
	PlatformProxyBypassIDs                        string
	LogLevel                                      slog.Level
	BillingEngineMode                             string
	ProcessRole                                   string
	DatabaseMaxConns                              int
	DatabaseCriticalMaxConns                      int
	DatabaseRiverMaxConns                         int
	DatabaseMinIdleConns                          int
	DatabaseMaxConnIdleSeconds                    int
	DatabaseIdleInTransactionTimeoutSeconds       int
	DatabaseLockTimeoutSeconds                    int
	MediaRequestConcurrency                       int
	MediaMaterializationConcurrency               int
	MediaImageNormalizationConcurrency            int
	MediaOSSDirectEnabled                         bool
	MediaOSSEndpoint                              string
	MediaOSSBucket                                string
	MediaOSSAccessKeyID                           string
	MediaOSSAccessKeySecret                       string
	MediaOSSPublicBaseURL                         string
	MediaOSSObjectPrefix                          string
	AsyncQueueWorkerEnabled                       bool
	AsyncWorkerHardLimit                          int
	AsyncWorkerInstanceHardLimit                  int
	AsyncWorkerRefreshIntervalSeconds             int
	AsyncWorkerLoadMode                           string
	AsyncAdmissionMicrobatchSize                  int
	AsyncAdmissionDispatcherEnabled               bool
	AsyncAdmissionDispatcherConfigured            bool
	RoutingMode                                   string
	ExecutionPoolID                               string
	ExecutionPoolLabels                           string
	WorkerID                                      string
	WorkerAdvertiseEndpoint                       string
	WorkerOrchestratorInstanceRef                 string
	WorkerEndpointAllowedSuffixes                 string
	WorkerEndpointAllowPrivate                    bool
	WorkerExecutionSecret                         string
	WorkerExecutionCAFile                         string
	WorkerExecutionCertFile                       string
	WorkerExecutionKeyFile                        string
	RouteProbeEnabled                             bool
	RouteProbeHotIntervalSeconds                  int
	RouteProbeColdIntervalSeconds                 int
	RouteProbeTimeoutMS                           int
	RouteProbeConcurrency                         int
	WorkerAutoscalingEnabled                      bool
	CapacityOrchestratorAdapter                   string
	CapacityPoolsJSON                             string
	WorkerTargetOutstandingPerReplica             int
	WorkerScaleUpWindowSeconds                    int
	WorkerScaleDownStabilizationSeconds           int
	WorkerDrainTimeoutSeconds                     int
	NodeMemoryTargetPercent                       int
	NodeMemoryHardPercent                         int
	NodeCPUTargetPercent                          int
	PostgresConnectionBudget                      int
	PostgresNonWorkerConnectionBudget             int
	WorkerDatabaseMaxConns                        int
}

func Load() Config {
	globalProxy := LoadGlobalHTTPProxyStatus()
	appEnv := env("APP_ENV", "development")
	return Config{
		AppEnv:       appEnv,
		HTTPAddr:     env("HTTP_ADDR", ":8088"),
		DatabaseURL:  gatewayDatabaseURL(),
		IdentityMode: env("IDENTITY_MODE", "hybrid"),
		JWTSecret:    env("CONFIG_JWT_SECRET", "this is a very secret secret"),
		ServerMainBaseURL: strings.TrimRight(
			env("SERVER_MAIN_BASE_URL", "http://localhost:3000"),
			"/",
		),
		ServerMainInternalToken:                       env("SERVER_MAIN_INTERNAL_TOKEN", ""),
		ServerMainInternalKey:                         env("SERVER_MAIN_INTERNAL_KEY", "gateway"),
		ServerMainInternalSecret:                      env("SERVER_MAIN_INTERNAL_SECRET", env("SERVER_MAIN_INTERNAL_TOKEN", "")),
		IdentitySecretStore:                           env("IDENTITY_SECRET_STORE", "file"),
		IdentitySecretDir:                             env("IDENTITY_SECRET_DIR", ".local-secrets/identity"),
		IdentityKubernetesNamespace:                   env("IDENTITY_KUBERNETES_NAMESPACE", env("POD_NAMESPACE", "")),
		IdentityKubernetesSecretName:                  env("IDENTITY_KUBERNETES_SECRET_NAME", "easyai-gateway-identity"),
		IdentityKubernetesAPIServer:                   env("IDENTITY_KUBERNETES_API_SERVER", "https://kubernetes.default.svc"),
		IdentityKubernetesTokenFile:                   env("IDENTITY_KUBERNETES_TOKEN_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
		IdentityKubernetesCAFile:                      env("IDENTITY_KUBERNETES_CA_FILE", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"),
		IdentitySecurityEventHeartbeatIntervalSeconds: envInt("IDENTITY_SECURITY_EVENTS_HEARTBEAT_INTERVAL_SECONDS", 60),
		IdentitySecurityEventStaleAfterSeconds:        envInt("IDENTITY_SECURITY_EVENTS_STALE_AFTER_SECONDS", 180),
		IdentitySecurityEventClockSkewSeconds:         envInt("IDENTITY_SECURITY_EVENTS_CLOCK_SKEW_SECONDS", 60),
		PublicBaseURL:                                 strings.TrimRight(env("AI_GATEWAY_PUBLIC_BASE_URL", env("PUBLIC_BASE_URL", "")), "/"),
		WebBaseURL:                                    strings.TrimRight(env("AI_GATEWAY_WEB_BASE_URL", env("GATEWAY_WEB_BASE_URL", env("PUBLIC_WEB_BASE_URL", ""))), "/"),
		LocalGeneratedStorageDir:                      env("AI_GATEWAY_GENERATED_STORAGE_DIR", env("LOCAL_GENERATED_STORAGE_DIR", env("AI_GATEWAY_STATIC_STORAGE_DIR", DefaultLocalGeneratedStorageDir))),
		LocalUploadedStorageDir:                       env("AI_GATEWAY_UPLOADED_STORAGE_DIR", env("LOCAL_UPLOADED_STORAGE_DIR", DefaultLocalUploadedStorageDir)),
		LocalTempAssetTTLHours:                        envInt("AI_GATEWAY_LOCAL_TEMP_ASSET_TTL_HOURS", 24),
		LocalResultTTLHours:                           envIntValidated("AI_GATEWAY_LOCAL_RESULT_TTL_HOURS", 24),
		LocalResultMinFreeBytes:                       envInt64Validated("AI_GATEWAY_LOCAL_RESULT_MIN_FREE_BYTES", 10*1024*1024*1024),
		LocalResultMaxBytes:                           envInt64Validated("AI_GATEWAY_LOCAL_RESULT_MAX_BYTES", 256*1024*1024),
		LocalResultMaxTaskBytes:                       envInt64Validated("AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES", 512*1024*1024),
		TaskProgressCallbackEnabled:                   env("TASK_PROGRESS_CALLBACK_ENABLED", "true") == "true",
		TaskProgressCallbackURL: env("TASK_PROGRESS_CALLBACK_URL",
			strings.TrimRight(env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), "/")+"/internal/platform/task-progress-callbacks",
		),
		TaskProgressCallbackTimeoutMS:   envIntValidated("TASK_PROGRESS_CALLBACK_TIMEOUT_MS", 5000),
		TaskProgressCallbackMaxAttempts: envIntValidated("TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS", 10),
		TaskCleanupEnabled:              env("AI_GATEWAY_TASK_CLEANUP_ENABLED", "false") == "true",
		TaskRetentionDays:               envIntValidated("AI_GATEWAY_TASK_RETENTION_DAYS", 30),
		TaskAnalysisRetentionDays:       envIntValidated("AI_GATEWAY_TASK_ANALYSIS_RETENTION_DAYS", 7),
		TaskCleanupIntervalSeconds:      envIntValidated("AI_GATEWAY_TASK_CLEANUP_INTERVAL_SECONDS", 300),
		TaskCleanupBatchSize:            envIntValidated("AI_GATEWAY_TASK_CLEANUP_BATCH_SIZE", 1000),
		CORSAllowedOrigin:               env("CORS_ALLOWED_ORIGIN", "http://localhost:5178,http://127.0.0.1:5178"),
		GlobalHTTPProxy:                 globalProxy.HTTPProxy,
		GlobalHTTPProxySource:           globalProxy.Source,
		PlatformProxyBypassIDs:          env("AI_GATEWAY_PLATFORM_PROXY_BYPASS_IDS", ""),
		LogLevel:                        logLevel(env("LOG_LEVEL", "info")),
		BillingEngineMode:               strings.ToLower(env("BILLING_ENGINE_MODE", "observe")),
		ProcessRole:                     strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_PROCESS_ROLE", "all"))),
		DatabaseMaxConns:                envInt("AI_GATEWAY_DATABASE_MAX_CONNS", 0),
		DatabaseCriticalMaxConns:        envOptionalIntValidated("AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS", 0),
		DatabaseRiverMaxConns:           envOptionalIntValidated("AI_GATEWAY_DATABASE_RIVER_MAX_CONNS", 0),
		DatabaseMinIdleConns:            envOptionalIntValidated("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS", 0),
		DatabaseMaxConnIdleSeconds:      envOptionalIntValidated("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS", 0),

		DatabaseIdleInTransactionTimeoutSeconds: envOptionalIntValidated(
			"AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS",
			60,
		),
		DatabaseLockTimeoutSeconds: envOptionalIntValidated("AI_GATEWAY_DATABASE_LOCK_TIMEOUT_SECONDS", 30),

		MediaRequestConcurrency:         envIntValidated("AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY", 16),
		MediaMaterializationConcurrency: envIntValidated("AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY", 8),
		MediaImageNormalizationConcurrency: envIntValidated(
			"AI_GATEWAY_MEDIA_IMAGE_NORMALIZATION_CONCURRENCY",
			2,
		),
		MediaOSSDirectEnabled:   env("AI_GATEWAY_MEDIA_OSS_DIRECT_ENABLED", "false") == "true",
		MediaOSSEndpoint:        strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_ENDPOINT", ""), "/"),
		MediaOSSBucket:          strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_BUCKET", "")),
		MediaOSSAccessKeyID:     strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_ID", "")),
		MediaOSSAccessKeySecret: strings.TrimSpace(env("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_SECRET", "")),
		MediaOSSPublicBaseURL:   strings.TrimRight(env("AI_GATEWAY_MEDIA_OSS_PUBLIC_BASE_URL", ""), "/"),
		MediaOSSObjectPrefix:    strings.Trim(env("AI_GATEWAY_MEDIA_OSS_OBJECT_PREFIX", "easyai-ai-gateway/media"), "/"),
		AsyncQueueWorkerEnabled: env("AI_GATEWAY_ASYNC_QUEUE_WORKER_ENABLED", "true") == "true",
		AsyncWorkerHardLimit: envIntValidated(
			"AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT",
			envOptionalIntValidated("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT", 2048),
		),
		AsyncWorkerInstanceHardLimit:      envIntValidated("AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT", 32),
		AsyncWorkerRefreshIntervalSeconds: envIntValidated("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS", 5),
		AsyncWorkerLoadMode:               strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_WORKER_LOAD_MODE", "adaptive"))),
		AsyncAdmissionMicrobatchSize:      envIntValidated("AI_GATEWAY_ASYNC_ADMISSION_MICROBATCH_SIZE", 8),
		AsyncAdmissionDispatcherEnabled:   envValue("AI_GATEWAY_ASYNC_ADMISSION_DISPATCHER_ENABLED") == "true",
		AsyncAdmissionDispatcherConfigured: envValue(
			"AI_GATEWAY_ASYNC_ADMISSION_DISPATCHER_ENABLED",
		) != "",
		RoutingMode:                       strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_ROUTING_MODE", "legacy"))),
		ExecutionPoolID:                   strings.TrimSpace(env("AI_GATEWAY_EXECUTION_POOL_ID", "legacy-default")),
		ExecutionPoolLabels:               strings.TrimSpace(env("AI_GATEWAY_EXECUTION_POOL_LABELS", "{}")),
		WorkerID:                          strings.TrimSpace(env("AI_GATEWAY_WORKER_ID", "")),
		WorkerAdvertiseEndpoint:           workerAdvertiseEndpoint(),
		WorkerOrchestratorInstanceRef:     strings.TrimSpace(env("AI_GATEWAY_ORCHESTRATOR_INSTANCE_REF", "")),
		WorkerEndpointAllowedSuffixes:     strings.TrimSpace(env("AI_GATEWAY_WORKER_ENDPOINT_ALLOWED_SUFFIXES", "svc,cluster.local")),
		WorkerEndpointAllowPrivate:        env("AI_GATEWAY_WORKER_ENDPOINT_ALLOW_PRIVATE", "true") == "true",
		WorkerExecutionSecret:             env("AI_GATEWAY_WORKER_EXECUTION_SECRET", env("SERVER_MAIN_INTERNAL_SECRET", env("SERVER_MAIN_INTERNAL_TOKEN", ""))),
		WorkerExecutionCAFile:             strings.TrimSpace(env("AI_GATEWAY_WORKER_EXECUTION_CA_FILE", "")),
		WorkerExecutionCertFile:           strings.TrimSpace(env("AI_GATEWAY_WORKER_EXECUTION_CERT_FILE", "")),
		WorkerExecutionKeyFile:            strings.TrimSpace(env("AI_GATEWAY_WORKER_EXECUTION_KEY_FILE", "")),
		RouteProbeEnabled:                 env("AI_GATEWAY_ROUTE_PROBE_ENABLED", "true") == "true",
		RouteProbeHotIntervalSeconds:      envIntValidated("AI_GATEWAY_ROUTE_PROBE_HOT_INTERVAL_SECONDS", 15),
		RouteProbeColdIntervalSeconds:     envIntValidated("AI_GATEWAY_ROUTE_PROBE_COLD_INTERVAL_SECONDS", 60),
		RouteProbeTimeoutMS:               envIntValidated("AI_GATEWAY_ROUTE_PROBE_TIMEOUT_MS", 3000),
		RouteProbeConcurrency:             envIntValidated("AI_GATEWAY_ROUTE_PROBE_CONCURRENCY", 8),
		WorkerAutoscalingEnabled:          env("AI_GATEWAY_WORKER_AUTOSCALING_ENABLED", "false") == "true",
		CapacityOrchestratorAdapter:       strings.ToLower(strings.TrimSpace(env("AI_GATEWAY_CAPACITY_ORCHESTRATOR_ADAPTER", "kubernetes"))),
		CapacityPoolsJSON:                 strings.TrimSpace(env("AI_GATEWAY_CAPACITY_POOLS", "")),
		WorkerTargetOutstandingPerReplica: envOptionalIntValidated("AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA", 0),
		WorkerScaleUpWindowSeconds:        envIntValidated("AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS", 20),
		WorkerScaleDownStabilizationSeconds: envIntValidated(
			"AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS",
			600,
		),
		WorkerDrainTimeoutSeconds: envIntValidated("AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS", 600),
		NodeMemoryTargetPercent:   envIntValidated("AI_GATEWAY_NODE_MEMORY_TARGET_PERCENT", 75),
		NodeMemoryHardPercent:     envIntValidated("AI_GATEWAY_NODE_MEMORY_HARD_PERCENT", 85),
		NodeCPUTargetPercent:      envIntValidated("AI_GATEWAY_NODE_CPU_TARGET_PERCENT", 70),
		PostgresConnectionBudget:  envIntValidated("AI_GATEWAY_POSTGRES_CONNECTION_BUDGET", 150),
		PostgresNonWorkerConnectionBudget: envIntValidated(
			"AI_GATEWAY_POSTGRES_NON_WORKER_CONNECTION_BUDGET",
			72,
		),
		WorkerDatabaseMaxConns: envIntValidated("AI_GATEWAY_WORKER_DATABASE_MAX_CONNS", 32),
	}
}

func (c Config) Validate() error {
	switch strings.ToLower(strings.TrimSpace(c.ProcessRole)) {
	case "", "all", "api", "worker", "capacity-controller":
	default:
		return errors.New("AI_GATEWAY_PROCESS_ROLE must be all, api, worker, or capacity-controller")
	}
	if c.DatabaseMaxConns < 0 || c.DatabaseMaxConns > 1000 {
		return errors.New("AI_GATEWAY_DATABASE_MAX_CONNS must be between 1 and 1000 when configured")
	}
	if c.DatabaseMinIdleConns < 0 || c.DatabaseMinIdleConns > 1000 ||
		(c.DatabaseMaxConns > 0 && c.DatabaseMinIdleConns > c.DatabaseMaxConns) {
		return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must be between 0 and AI_GATEWAY_DATABASE_MAX_CONNS")
	}
	if c.DatabaseCriticalMaxConns < 0 || c.DatabaseCriticalMaxConns > 64 ||
		(c.DatabaseMaxConns > 0 && c.DatabaseCriticalMaxConns >= c.DatabaseMaxConns) {
		return errors.New("AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS must be between 0 and less than AI_GATEWAY_DATABASE_MAX_CONNS")
	}
	if c.DatabaseRiverMaxConns < 0 || c.DatabaseRiverMaxConns > 256 ||
		(c.DatabaseMaxConns > 0 &&
			c.DatabaseCriticalMaxConns+c.DatabaseRiverMaxConns >= c.DatabaseMaxConns) {
		return errors.New("critical and River database pools must leave at least one execution connection")
	}
	if c.DatabaseMaxConns > 0 &&
		c.DatabaseMinIdleConns > c.DatabaseMaxConns-c.DatabaseCriticalMaxConns-c.DatabaseRiverMaxConns {
		return errors.New("AI_GATEWAY_DATABASE_MIN_IDLE_CONNS must not exceed the execution pool after reserving critical and River connections")
	}
	if c.DatabaseMaxConnIdleSeconds < 0 || c.DatabaseMaxConnIdleSeconds > 3600 {
		return errors.New("AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS must be between 0 and 3600")
	}
	if c.DatabaseIdleInTransactionTimeoutSeconds < 0 || c.DatabaseIdleInTransactionTimeoutSeconds > 3600 {
		return errors.New("AI_GATEWAY_DATABASE_IDLE_IN_TRANSACTION_TIMEOUT_SECONDS must be between 0 and 3600")
	}
	if c.DatabaseLockTimeoutSeconds < 0 || c.DatabaseLockTimeoutSeconds > 3600 {
		return errors.New("AI_GATEWAY_DATABASE_LOCK_TIMEOUT_SECONDS must be between 0 and 3600")
	}
	if c.MediaMaterializationConcurrency != 0 && (c.MediaMaterializationConcurrency < 1 || c.MediaMaterializationConcurrency > 256) {
		return errors.New("AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY must be between 1 and 256")
	}
	if c.MediaRequestConcurrency != 0 && (c.MediaRequestConcurrency < 1 || c.MediaRequestConcurrency > 1024) {
		return errors.New("AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY must be between 1 and 1024")
	}
	if c.MediaImageNormalizationConcurrency != 0 && (c.MediaImageNormalizationConcurrency < 1 || c.MediaImageNormalizationConcurrency > 64) {
		return errors.New("AI_GATEWAY_MEDIA_IMAGE_NORMALIZATION_CONCURRENCY must be between 1 and 64")
	}
	if c.MediaOSSDirectEnabled && !c.RunsCapacityController() {
		if strings.TrimSpace(c.MediaOSSAccessKeyID) == "" || strings.TrimSpace(c.MediaOSSAccessKeySecret) == "" {
			return errors.New("AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_ID and AI_GATEWAY_MEDIA_OSS_ACCESS_KEY_SECRET are required when direct media OSS is enabled")
		}
		if !validMediaOSSBucket(c.MediaOSSBucket) {
			return errors.New("AI_GATEWAY_MEDIA_OSS_BUCKET is invalid")
		}
		if !validMediaOSSURL(c.MediaOSSEndpoint, c.AppEnv) {
			return errors.New("AI_GATEWAY_MEDIA_OSS_ENDPOINT must be an HTTPS URL")
		}
		if !validMediaOSSURL(c.MediaOSSPublicBaseURL, c.AppEnv) {
			return errors.New("AI_GATEWAY_MEDIA_OSS_PUBLIC_BASE_URL must be an HTTPS URL")
		}
		if !validMediaOSSObjectPrefix(c.MediaOSSObjectPrefix) {
			return errors.New("AI_GATEWAY_MEDIA_OSS_OBJECT_PREFIX is invalid")
		}
	}
	switch strings.ToLower(strings.TrimSpace(c.BillingEngineMode)) {
	case "", "observe", "enforce", "hold":
	default:
		return errors.New("BILLING_ENGINE_MODE must be observe, enforce, or hold")
	}
	if c.AsyncWorkerHardLimit < 1 || c.AsyncWorkerHardLimit > 10000 {
		return errors.New("AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT must be between 1 and 10000")
	}
	if c.AsyncWorkerInstanceHardLimit < 1 || c.AsyncWorkerInstanceHardLimit > 10000 {
		return errors.New("AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT must be between 1 and 10000")
	}
	if c.AsyncWorkerRefreshIntervalSeconds < 1 {
		return errors.New("AI_GATEWAY_ASYNC_WORKER_REFRESH_INTERVAL_SECONDS must be positive")
	}
	switch strings.ToLower(strings.TrimSpace(c.AsyncWorkerLoadMode)) {
	case "", "adaptive", "legacy":
	default:
		return errors.New("AI_GATEWAY_WORKER_LOAD_MODE must be adaptive or legacy")
	}
	switch strings.ToLower(strings.TrimSpace(c.RoutingMode)) {
	case "", "legacy", "shadow", "enforced":
	default:
		return errors.New("AI_GATEWAY_ROUTING_MODE must be legacy, shadow, or enforced")
	}
	if strings.TrimSpace(c.ExecutionPoolID) != "" && !validExecutionPoolID(c.ExecutionPoolID) {
		return errors.New("AI_GATEWAY_EXECUTION_POOL_ID is invalid")
	}
	if strings.TrimSpace(c.ExecutionPoolLabels) != "" && !jsonObject(c.ExecutionPoolLabels) {
		return errors.New("AI_GATEWAY_EXECUTION_POOL_LABELS must be a JSON object")
	}
	if c.RouteProbeHotIntervalSeconds != 0 && (c.RouteProbeHotIntervalSeconds < 5 || c.RouteProbeHotIntervalSeconds > 300) {
		return errors.New("AI_GATEWAY_ROUTE_PROBE_HOT_INTERVAL_SECONDS must be between 5 and 300")
	}
	if c.RouteProbeColdIntervalSeconds != 0 && (c.RouteProbeColdIntervalSeconds < c.RouteProbeHotIntervalSeconds || c.RouteProbeColdIntervalSeconds > 3600) {
		return errors.New("AI_GATEWAY_ROUTE_PROBE_COLD_INTERVAL_SECONDS must be between the hot interval and 3600")
	}
	if c.RouteProbeTimeoutMS != 0 && (c.RouteProbeTimeoutMS < 250 || c.RouteProbeTimeoutMS > 10000) {
		return errors.New("AI_GATEWAY_ROUTE_PROBE_TIMEOUT_MS must be between 250 and 10000")
	}
	if c.RouteProbeConcurrency != 0 && (c.RouteProbeConcurrency < 1 || c.RouteProbeConcurrency > 64) {
		return errors.New("AI_GATEWAY_ROUTE_PROBE_CONCURRENCY must be between 1 and 64")
	}
	if strings.EqualFold(c.RoutingMode, "enforced") && len(c.WorkerExecutionSecret) < 32 {
		return errors.New("AI_GATEWAY_WORKER_EXECUTION_SECRET must be at least 32 bytes in enforced routing mode")
	}
	if (c.WorkerExecutionCertFile == "") != (c.WorkerExecutionKeyFile == "") {
		return errors.New("AI_GATEWAY_WORKER_EXECUTION_CERT_FILE and AI_GATEWAY_WORKER_EXECUTION_KEY_FILE must be configured together")
	}
	if c.AsyncAdmissionMicrobatchSize < 1 || c.AsyncAdmissionMicrobatchSize > 32 {
		return errors.New("AI_GATEWAY_ASYNC_ADMISSION_MICROBATCH_SIZE must be between 1 and 32")
	}
	pools, err := c.CapacityPools()
	if err != nil {
		return err
	}
	if c.RunsCapacityController() && len(pools) == 0 {
		return errors.New("AI_GATEWAY_CAPACITY_POOLS must contain at least one pool for the capacity-controller role")
	}
	switch strings.ToLower(strings.TrimSpace(c.CapacityOrchestratorAdapter)) {
	case "", "kubernetes", "static":
	default:
		return errors.New("AI_GATEWAY_CAPACITY_ORCHESTRATOR_ADAPTER must be kubernetes or static")
	}
	if c.WorkerTargetOutstandingPerReplica < 0 || c.WorkerTargetOutstandingPerReplica > 100000 {
		return errors.New("AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA must be between 0 and 100000")
	}
	if c.WorkerScaleUpWindowSeconds != 0 && (c.WorkerScaleUpWindowSeconds < 5 || c.WorkerScaleUpWindowSeconds > 600) {
		return errors.New("AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS must be between 5 and 600")
	}
	if c.WorkerScaleDownStabilizationSeconds != 0 &&
		(c.WorkerScaleDownStabilizationSeconds < 60 || c.WorkerScaleDownStabilizationSeconds > 86400) {
		return errors.New("AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS must be between 60 and 86400")
	}
	if c.WorkerDrainTimeoutSeconds != 0 && (c.WorkerDrainTimeoutSeconds < 60 || c.WorkerDrainTimeoutSeconds > 86400) {
		return errors.New("AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS must be between 60 and 86400")
	}
	if (c.NodeMemoryTargetPercent != 0 || c.NodeMemoryHardPercent != 0) &&
		(c.NodeMemoryTargetPercent < 50 || c.NodeMemoryTargetPercent > 90 ||
			c.NodeMemoryHardPercent <= c.NodeMemoryTargetPercent || c.NodeMemoryHardPercent > 95) {
		return errors.New("Worker node memory target/hard percentages must be ordered within 50..95")
	}
	if c.NodeCPUTargetPercent != 0 && (c.NodeCPUTargetPercent < 40 || c.NodeCPUTargetPercent > 90) {
		return errors.New("AI_GATEWAY_NODE_CPU_TARGET_PERCENT must be between 40 and 90")
	}
	if c.PostgresConnectionBudget < 0 || c.PostgresConnectionBudget > 1000 {
		return errors.New("AI_GATEWAY_POSTGRES_CONNECTION_BUDGET must be between 1 and 1000")
	}
	if c.PostgresNonWorkerConnectionBudget < 0 ||
		(c.PostgresConnectionBudget > 0 &&
			c.PostgresNonWorkerConnectionBudget >= c.PostgresConnectionBudget) {
		return errors.New("AI_GATEWAY_POSTGRES_NON_WORKER_CONNECTION_BUDGET must be below the PostgreSQL connection budget")
	}
	if c.WorkerDatabaseMaxConns < 0 || c.WorkerDatabaseMaxConns > 256 {
		return errors.New("AI_GATEWAY_WORKER_DATABASE_MAX_CONNS must be between 1 and 256")
	}
	if c.TaskProgressCallbackTimeoutMS != 0 && (c.TaskProgressCallbackTimeoutMS < 100 || c.TaskProgressCallbackTimeoutMS > 60000) {
		return errors.New("TASK_PROGRESS_CALLBACK_TIMEOUT_MS must be between 100 and 60000")
	}
	if c.TaskProgressCallbackMaxAttempts != 0 && (c.TaskProgressCallbackMaxAttempts < 1 || c.TaskProgressCallbackMaxAttempts > 100) {
		return errors.New("TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS must be between 1 and 100")
	}
	if c.TaskRetentionDays != 0 && (c.TaskRetentionDays < 1 || c.TaskRetentionDays > 3650) {
		return errors.New("AI_GATEWAY_TASK_RETENTION_DAYS must be between 1 and 3650")
	}
	if c.TaskAnalysisRetentionDays != 0 && (c.TaskAnalysisRetentionDays < 1 || (c.TaskRetentionDays != 0 && c.TaskAnalysisRetentionDays > c.TaskRetentionDays)) {
		return errors.New("AI_GATEWAY_TASK_ANALYSIS_RETENTION_DAYS must be between 1 and AI_GATEWAY_TASK_RETENTION_DAYS")
	}
	if c.TaskCleanupIntervalSeconds != 0 && c.TaskCleanupIntervalSeconds < 60 {
		return errors.New("AI_GATEWAY_TASK_CLEANUP_INTERVAL_SECONDS must be at least 60")
	}
	if c.TaskCleanupBatchSize != 0 && (c.TaskCleanupBatchSize < 100 || c.TaskCleanupBatchSize > 5000) {
		return errors.New("AI_GATEWAY_TASK_CLEANUP_BATCH_SIZE must be between 100 and 5000")
	}
	if c.LocalResultTTLHours != 0 && (c.LocalResultTTLHours < 1 || c.LocalResultTTLHours > 24*30) {
		return errors.New("AI_GATEWAY_LOCAL_RESULT_TTL_HOURS must be between 1 and 720")
	}
	if c.LocalResultMinFreeBytes < 0 {
		return errors.New("AI_GATEWAY_LOCAL_RESULT_MIN_FREE_BYTES must not be negative")
	}
	if c.LocalResultMaxBytes != 0 && c.LocalResultMaxBytes < 1024 {
		return errors.New("AI_GATEWAY_LOCAL_RESULT_MAX_BYTES must be at least 1024")
	}
	if c.LocalResultMaxTaskBytes != 0 && c.LocalResultMaxTaskBytes < c.LocalResultMaxBytes {
		return errors.New("AI_GATEWAY_LOCAL_RESULT_MAX_TASK_BYTES must be at least AI_GATEWAY_LOCAL_RESULT_MAX_BYTES")
	}
	switch strings.ToLower(strings.TrimSpace(c.IdentitySecretStore)) {
	case "":
	case "file":
		if strings.TrimSpace(c.IdentitySecretDir) == "" {
			return errors.New("IDENTITY_SECRET_DIR is required for the file SecretStore")
		}
	case "kubernetes":
		if strings.TrimSpace(c.IdentityKubernetesNamespace) == "" || strings.TrimSpace(c.IdentityKubernetesSecretName) == "" {
			return errors.New("Kubernetes identity SecretStore requires namespace and Secret name")
		}
	default:
		return errors.New("IDENTITY_SECRET_STORE must be file or kubernetes")
	}
	if c.IdentitySecurityEventHeartbeatIntervalSeconds != 0 || c.IdentitySecurityEventStaleAfterSeconds != 0 || c.IdentitySecurityEventClockSkewSeconds != 0 {
		if c.IdentitySecurityEventHeartbeatIntervalSeconds <= 0 ||
			c.IdentitySecurityEventStaleAfterSeconds < 2*c.IdentitySecurityEventHeartbeatIntervalSeconds ||
			c.IdentitySecurityEventClockSkewSeconds < 0 || c.IdentitySecurityEventClockSkewSeconds > 300 {
			return errors.New("identity security event heartbeat, stale threshold, or clock skew is invalid")
		}
	}
	return nil
}

func validMediaOSSBucket(value string) bool {
	value = strings.TrimSpace(value)
	if len(value) < 3 || len(value) > 63 {
		return false
	}
	for index, item := range value {
		if (item >= 'a' && item <= 'z') || (item >= '0' && item <= '9') || (item == '-' && index > 0 && index < len(value)-1) {
			continue
		}
		return false
	}
	return true
}

func validMediaOSSURL(value string, appEnv string) bool {
	parsed, err := url.Parse(strings.TrimSpace(value))
	if err != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.User != nil {
		return false
	}
	if parsed.Scheme == "https" {
		return true
	}
	host := strings.ToLower(parsed.Hostname())
	return !strings.EqualFold(strings.TrimSpace(appEnv), "production") &&
		parsed.Scheme == "http" && (host == "127.0.0.1" || host == "localhost" || host == "::1")
}

func validMediaOSSObjectPrefix(value string) bool {
	value = strings.Trim(value, "/")
	if value == "" || len(value) > 256 || strings.Contains(value, "..") {
		return false
	}
	for _, item := range value {
		if (item >= 'a' && item <= 'z') || (item >= 'A' && item <= 'Z') || (item >= '0' && item <= '9') ||
			item == '-' || item == '_' || item == '/' {
			continue
		}
		return false
	}
	return true
}

func (c Config) EffectiveProcessRole() string {
	role := strings.ToLower(strings.TrimSpace(c.ProcessRole))
	if role == "" {
		return "all"
	}
	return role
}

func (c Config) RunsPublicHTTP() bool {
	switch c.EffectiveProcessRole() {
	case "worker", "capacity-controller":
		return false
	default:
		return true
	}
}

func (c Config) RunsAsyncExecutionWorker() bool {
	switch c.EffectiveProcessRole() {
	case "api", "capacity-controller":
		return false
	case "worker":
		return true
	default:
		return c.AsyncQueueWorkerEnabled
	}
}

// RunsAsyncAdmissionDispatcher keeps the legacy all-in-one/Worker behaviour
// unless a deployment explicitly separates admission coordination from task
// execution. Production enables this on both API sites so the instance nearest
// the current PostgreSQL primary wins the existing database scope locks, while
// remote Worker replicas only execute River jobs.
func (c Config) RunsAsyncAdmissionDispatcher() bool {
	if c.AsyncAdmissionDispatcherConfigured {
		return c.AsyncAdmissionDispatcherEnabled
	}
	return c.RunsAsyncExecutionWorker()
}

func (c Config) RunsBackgroundWorkers() bool {
	switch c.EffectiveProcessRole() {
	case "api", "capacity-controller":
		return false
	default:
		return true
	}
}

func (c Config) RunsCapacityController() bool {
	return c.EffectiveProcessRole() == "capacity-controller"
}

func (c Config) RunsRouteProber() bool {
	return c.RunsAsyncExecutionWorker() && c.RouteProbeEnabled && !strings.EqualFold(c.RoutingMode, "legacy")
}

func workerAdvertiseEndpoint() string {
	return strings.TrimRight(strings.TrimSpace(os.Getenv("AI_GATEWAY_WORKER_ADVERTISE_ENDPOINT")), "/")
}

func validExecutionPoolID(value string) bool {
	value = strings.TrimSpace(value)
	if value == "" || len(value) > 128 {
		return false
	}
	for index, character := range value {
		valid := (character >= 'a' && character <= 'z') ||
			(character >= 'A' && character <= 'Z') ||
			(character >= '0' && character <= '9') ||
			character == '.' || character == '_' || character == ':' || character == '/' || character == '-'
		if !valid || (index == 0 && !((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9'))) {
			return false
		}
	}
	return true
}

func jsonObject(value string) bool {
	var object map[string]any
	return json.Unmarshal([]byte(value), &object) == nil && object != nil
}

type GlobalHTTPProxyStatus struct {
	HTTPProxy string
	Source    string
}

type ExecutionPoolCapacityConfig struct {
	ID                string `json:"id"`
	AdapterRef        string `json:"adapterRef"`
	BootstrapReplicas int    `json:"bootstrapReplicas"`
	MinReplicas       int    `json:"minReplicas"`
	MaxReplicas       int    `json:"maxReplicas"`
}

func (c Config) CapacityPools() ([]ExecutionPoolCapacityConfig, error) {
	raw := strings.TrimSpace(c.CapacityPoolsJSON)
	if raw == "" {
		return nil, nil
	}
	var pools []ExecutionPoolCapacityConfig
	if err := json.Unmarshal([]byte(raw), &pools); err != nil {
		return nil, errors.New("AI_GATEWAY_CAPACITY_POOLS must be a JSON array")
	}
	if len(pools) == 0 {
		return nil, errors.New("AI_GATEWAY_CAPACITY_POOLS must contain at least one pool")
	}
	seen := make(map[string]struct{}, len(pools))
	for index := range pools {
		pools[index].ID = strings.TrimSpace(pools[index].ID)
		pools[index].AdapterRef = strings.TrimSpace(pools[index].AdapterRef)
		if pools[index].AdapterRef == "" {
			pools[index].AdapterRef = pools[index].ID
		}
		if !validExecutionPoolID(pools[index].ID) || pools[index].MinReplicas < 0 || pools[index].MaxReplicas < pools[index].MinReplicas || pools[index].MaxReplicas > 64 || pools[index].BootstrapReplicas < pools[index].MinReplicas || pools[index].BootstrapReplicas > pools[index].MaxReplicas {
			return nil, errors.New("AI_GATEWAY_CAPACITY_POOLS contains an invalid pool definition")
		}
		if _, duplicate := seen[pools[index].ID]; duplicate {
			return nil, errors.New("AI_GATEWAY_CAPACITY_POOLS contains duplicate pool IDs")
		}
		seen[pools[index].ID] = struct{}{}
	}
	return pools, nil
}

func LoadGlobalHTTPProxyStatus() GlobalHTTPProxyStatus {
	for _, key := range []string{
		"AI_GATEWAY_GLOBAL_HTTP_PROXY",
		"GLOBAL_HTTP_PROXY",
		"HTTPS_PROXY",
		"https_proxy",
		"HTTP_PROXY",
		"http_proxy",
		"ALL_PROXY",
		"all_proxy",
	} {
		if value := envValue(key); value != "" {
			return GlobalHTTPProxyStatus{HTTPProxy: value, Source: key}
		}
	}
	return GlobalHTTPProxyStatus{}
}

func gatewayDatabaseURL() string {
	if value := envValue("AI_GATEWAY_DATABASE_URL"); value != "" {
		return normalizePostgresURL(value)
	}
	if value := envValue("DATABASE_URL"); value != "" {
		return normalizePostgresURL(value)
	}
	if memoryURL := envValue("MEMORY_DATABASE_URL"); memoryURL != "" {
		return normalizePostgresURL(withDatabase(memoryURL, env("AI_GATEWAY_DATABASE_NAME", "easyai_ai_gateway")))
	}
	return normalizePostgresURL("postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable")
}

func normalizePostgresURL(raw string) string {
	parsed, err := url.Parse(raw)
	if err != nil {
		return raw
	}
	values := parsed.Query()
	schema := values.Get("schema")
	if schema == "" {
		return raw
	}
	values.Del("schema")
	if values.Get("search_path") == "" {
		values.Set("search_path", schema)
	}
	parsed.RawQuery = values.Encode()
	return parsed.String()
}

func withDatabase(raw string, databaseName string) string {
	parsed, err := url.Parse(raw)
	if err != nil || databaseName == "" {
		return raw
	}
	parsed.Path = "/" + databaseName
	return parsed.String()
}

func envValue(key string) string {
	return strings.TrimSpace(os.Getenv(key))
}

func env(key string, fallback string) string {
	if value := envValue(key); value != "" {
		return value
	}
	return fallback
}

func envInt(key string, fallback int) int {
	value := envValue(key)
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return fallback
	}
	return parsed
}

func envIntValidated(key string, fallback int) int {
	value := envValue(key)
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return 0
	}
	return parsed
}

func envOptionalIntValidated(key string, fallback int) int {
	value := envValue(key)
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return -1
	}
	return parsed
}

func envInt64Validated(key string, fallback int64) int64 {
	value := envValue(key)
	if value == "" {
		return fallback
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return 0
	}
	return parsed
}

func logLevel(value string) slog.Level {
	switch strings.ToLower(value) {
	case "debug":
		return slog.LevelDebug
	case "warn", "warning":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}
