// Package config loads process-level gateway configuration from environment
// variables.
package config

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

	"galaxy/redisconn"
)

const gatewayRedisEnvPrefix = "GATEWAY"

const (
	// shutdownTimeoutEnvVar names the environment variable that controls the
	// maximum time granted to each component shutdown call.
	shutdownTimeoutEnvVar = "GATEWAY_SHUTDOWN_TIMEOUT"

	// logLevelEnvVar names the environment variable that configures the process
	// log level used by structured JSON logging.
	logLevelEnvVar = "GATEWAY_LOG_LEVEL"

	// publicHTTPAddrEnvVar names the environment variable that configures the
	// public REST listener address.
	publicHTTPAddrEnvVar = "GATEWAY_PUBLIC_HTTP_ADDR"

	// publicHTTPReadHeaderTimeoutEnvVar names the environment variable that
	// configures the maximum time allowed to read public REST request headers.
	publicHTTPReadHeaderTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_HEADER_TIMEOUT"

	// publicHTTPReadTimeoutEnvVar names the environment variable that configures
	// the maximum time allowed to read the full public REST request.
	publicHTTPReadTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_TIMEOUT"

	// publicHTTPIdleTimeoutEnvVar names the environment variable that configures
	// the keep-alive idle timeout for the public REST listener.
	publicHTTPIdleTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_IDLE_TIMEOUT"

	// publicHTTPCORSAllowedOriginsEnvVar names the environment variable that
	// configures the comma-separated list of browser origins permitted to
	// call the public REST surface. An empty value disables CORS entirely;
	// requests without an Origin header still pass through normally.
	publicHTTPCORSAllowedOriginsEnvVar = "GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS"

	// publicAuthUpstreamTimeoutEnvVar names the environment variable that
	// configures the timeout budget used for public auth upstream calls.
	publicAuthUpstreamTimeoutEnvVar = "GATEWAY_PUBLIC_AUTH_UPSTREAM_TIMEOUT"

	// backendHTTPURLEnvVar names the environment variable that configures
	// the absolute base URL of the consolidated backend HTTP listener used
	// for public auth, internal session lookup, and authenticated user /
	// lobby commands.
	backendHTTPURLEnvVar = "GATEWAY_BACKEND_HTTP_URL"

	// backendGRPCPushURLEnvVar names the environment variable that
	// configures the dial target of backend's gRPC `Push.SubscribePush`
	// listener.
	backendGRPCPushURLEnvVar = "GATEWAY_BACKEND_GRPC_PUSH_URL"

	// backendGatewayClientIDEnvVar names the environment variable that
	// configures the durable identifier this gateway instance presents to
	// backend in `GatewaySubscribeRequest.gateway_client_id`.
	backendGatewayClientIDEnvVar = "GATEWAY_BACKEND_GATEWAY_CLIENT_ID"

	// backendHTTPTimeoutEnvVar names the environment variable that
	// configures the per-call timeout applied to backend HTTP requests.
	backendHTTPTimeoutEnvVar = "GATEWAY_BACKEND_HTTP_TIMEOUT"

	// backendPushReconnectBaseBackoffEnvVar names the environment variable
	// that configures the starting delay between reconnect attempts of the
	// gRPC SubscribePush stream.
	backendPushReconnectBaseBackoffEnvVar = "GATEWAY_BACKEND_PUSH_RECONNECT_BASE_BACKOFF"

	// backendPushReconnectMaxBackoffEnvVar names the environment variable
	// that configures the upper bound for exponential reconnect delays.
	backendPushReconnectMaxBackoffEnvVar = "GATEWAY_BACKEND_PUSH_RECONNECT_MAX_BACKOFF"

	// adminHTTPAddrEnvVar names the environment variable that configures the
	// private admin HTTP listener address. When it is empty, the admin listener
	// remains disabled.
	adminHTTPAddrEnvVar = "GATEWAY_ADMIN_HTTP_ADDR"

	// adminHTTPReadHeaderTimeoutEnvVar names the environment variable that
	// configures the maximum time allowed to read admin listener request
	// headers.
	adminHTTPReadHeaderTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_HEADER_TIMEOUT"

	// adminHTTPReadTimeoutEnvVar names the environment variable that configures
	// the maximum time allowed to read one admin listener request.
	adminHTTPReadTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_TIMEOUT"

	// adminHTTPIdleTimeoutEnvVar names the environment variable that configures
	// the keep-alive idle timeout for the admin listener.
	adminHTTPIdleTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_IDLE_TIMEOUT"

	// authenticatedGRPCAddrEnvVar names the environment variable that configures
	// the authenticated gRPC listener address.
	authenticatedGRPCAddrEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ADDR"

	// authenticatedGRPCConnectionTimeoutEnvVar names the environment variable
	// that configures the inbound connection handshake timeout for the
	// authenticated gRPC listener.
	authenticatedGRPCConnectionTimeoutEnvVar = "GATEWAY_AUTHENTICATED_GRPC_CONNECTION_TIMEOUT"

	// authenticatedGRPCDownstreamTimeoutEnvVar names the environment variable
	// that configures the timeout budget used for downstream unary execution.
	authenticatedGRPCDownstreamTimeoutEnvVar = "GATEWAY_AUTHENTICATED_DOWNSTREAM_TIMEOUT"

	// authenticatedGRPCFreshnessWindowEnvVar names the environment variable that
	// configures the accepted client timestamp skew window for authenticated
	// gRPC requests.
	authenticatedGRPCFreshnessWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_FRESHNESS_WINDOW"

	// authenticatedGRPCIPRateLimitRequestsEnvVar names the environment
	// variable that configures the authenticated gRPC per-IP request budget per
	// window.
	authenticatedGRPCIPRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS"

	// authenticatedGRPCIPRateLimitWindowEnvVar names the environment variable
	// that configures the authenticated gRPC per-IP rate-limit window.
	authenticatedGRPCIPRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_WINDOW"

	// authenticatedGRPCIPRateLimitBurstEnvVar names the environment variable
	// that configures the authenticated gRPC per-IP rate-limit burst.
	authenticatedGRPCIPRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST"

	// authenticatedGRPCSessionRateLimitRequestsEnvVar names the environment
	// variable that configures the authenticated gRPC per-session request
	// budget per window.
	authenticatedGRPCSessionRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS"

	// authenticatedGRPCSessionRateLimitWindowEnvVar names the environment
	// variable that configures the authenticated gRPC per-session rate-limit
	// window.
	authenticatedGRPCSessionRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_WINDOW"

	// authenticatedGRPCSessionRateLimitBurstEnvVar names the environment
	// variable that configures the authenticated gRPC per-session rate-limit
	// burst.
	authenticatedGRPCSessionRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST"

	// authenticatedGRPCUserRateLimitRequestsEnvVar names the environment
	// variable that configures the authenticated gRPC per-user request budget
	// per window.
	authenticatedGRPCUserRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS"

	// authenticatedGRPCUserRateLimitWindowEnvVar names the environment
	// variable that configures the authenticated gRPC per-user rate-limit
	// window.
	authenticatedGRPCUserRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_WINDOW"

	// authenticatedGRPCUserRateLimitBurstEnvVar names the environment variable
	// that configures the authenticated gRPC per-user rate-limit burst.
	authenticatedGRPCUserRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST"

	// authenticatedGRPCMessageClassRateLimitRequestsEnvVar names the
	// environment variable that configures the authenticated gRPC per-message
	// class request budget per window.
	authenticatedGRPCMessageClassRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS"

	// authenticatedGRPCMessageClassRateLimitWindowEnvVar names the environment
	// variable that configures the authenticated gRPC per-message-class
	// rate-limit window.
	authenticatedGRPCMessageClassRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_WINDOW"

	// authenticatedGRPCMessageClassRateLimitBurstEnvVar names the environment
	// variable that configures the authenticated gRPC per-message-class
	// rate-limit burst.
	authenticatedGRPCMessageClassRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST"

	// sessionCacheMaxEntriesEnvVar names the environment variable that configures
	// the in-memory session cache LRU bound (entries).
	sessionCacheMaxEntriesEnvVar = "GATEWAY_SESSION_CACHE_MAX_ENTRIES"

	// sessionCacheTTLEnvVar names the environment variable that configures the
	// in-memory session cache safety-net TTL applied to every cached entry.
	sessionCacheTTLEnvVar = "GATEWAY_SESSION_CACHE_TTL"

	// replayRedisKeyPrefixEnvVar names the environment variable that configures
	// the Redis key prefix used for authenticated replay reservations.
	replayRedisKeyPrefixEnvVar = "GATEWAY_REPLAY_REDIS_KEY_PREFIX"

	// replayRedisReserveTimeoutEnvVar names the environment variable that
	// configures the timeout used for authenticated replay reservations and
	// startup connectivity checks.
	replayRedisReserveTimeoutEnvVar = "GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT"

	// responseSignerPrivateKeyPEMPathEnvVar names the environment variable that
	// configures the path to the PKCS#8 PEM-encoded Ed25519 private key used to
	// sign authenticated unary responses and stream events.
	responseSignerPrivateKeyPEMPathEnvVar = "GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH"

	// publicAuthMaxBodyBytesEnvVar names the environment variable that
	// configures the maximum accepted request body size for public_auth.
	publicAuthMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_MAX_BODY_BYTES"

	// publicAuthRateLimitRequestsEnvVar names the environment variable that
	// configures the public_auth request budget per window.
	publicAuthRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS"

	// publicAuthRateLimitWindowEnvVar names the environment variable that
	// configures the public_auth rate-limit window.
	publicAuthRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_WINDOW"

	// publicAuthRateLimitBurstEnvVar names the environment variable that
	// configures the public_auth rate-limit burst.
	publicAuthRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST"

	// browserBootstrapMaxBodyBytesEnvVar names the environment variable that
	// configures the maximum accepted request body size for browser_bootstrap.
	browserBootstrapMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_MAX_BODY_BYTES"

	// browserBootstrapRateLimitRequestsEnvVar names the environment variable
	// that configures the browser_bootstrap request budget per window.
	browserBootstrapRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_REQUESTS"

	// browserBootstrapRateLimitWindowEnvVar names the environment variable that
	// configures the browser_bootstrap rate-limit window.
	browserBootstrapRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_WINDOW"

	// browserBootstrapRateLimitBurstEnvVar names the environment variable that
	// configures the browser_bootstrap rate-limit burst.
	browserBootstrapRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_BURST"

	// browserAssetMaxBodyBytesEnvVar names the environment variable that
	// configures the maximum accepted request body size for browser_asset.
	browserAssetMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_MAX_BODY_BYTES"

	// browserAssetRateLimitRequestsEnvVar names the environment variable that
	// configures the browser_asset request budget per window.
	browserAssetRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_REQUESTS"

	// browserAssetRateLimitWindowEnvVar names the environment variable that
	// configures the browser_asset rate-limit window.
	browserAssetRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_WINDOW"

	// browserAssetRateLimitBurstEnvVar names the environment variable that
	// configures the browser_asset rate-limit burst.
	browserAssetRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_BURST"

	// publicMiscMaxBodyBytesEnvVar names the environment variable that
	// configures the maximum accepted request body size for public_misc.
	publicMiscMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_MAX_BODY_BYTES"

	// publicMiscRateLimitRequestsEnvVar names the environment variable that
	// configures the public_misc request budget per window.
	publicMiscRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_REQUESTS"

	// publicMiscRateLimitWindowEnvVar names the environment variable that
	// configures the public_misc rate-limit window.
	publicMiscRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_WINDOW"

	// publicMiscRateLimitBurstEnvVar names the environment variable that
	// configures the public_misc rate-limit burst.
	publicMiscRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_BURST"

	// sendEmailCodeIdentityRateLimitRequestsEnvVar names the environment
	// variable that configures the send-email-code identity request budget per
	// window.
	sendEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS"

	// sendEmailCodeIdentityRateLimitWindowEnvVar names the environment variable
	// that configures the send-email-code identity rate-limit window.
	sendEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW"

	// sendEmailCodeIdentityRateLimitBurstEnvVar names the environment variable
	// that configures the send-email-code identity rate-limit burst.
	sendEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST"

	// confirmEmailCodeIdentityRateLimitRequestsEnvVar names the environment
	// variable that configures the confirm-email-code identity request budget
	// per window.
	confirmEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS"

	// confirmEmailCodeIdentityRateLimitWindowEnvVar names the environment
	// variable that configures the confirm-email-code identity rate-limit
	// window.
	confirmEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW"

	// confirmEmailCodeIdentityRateLimitBurstEnvVar names the environment
	// variable that configures the confirm-email-code identity rate-limit burst.
	confirmEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST"

	// defaultShutdownTimeout is applied when shutdownTimeoutEnvVar is absent.
	defaultShutdownTimeout = 5 * time.Second

	// defaultLogLevel is applied when logLevelEnvVar is absent.
	defaultLogLevel = "info"

	// defaultPublicHTTPAddr is applied when publicHTTPAddrEnvVar is absent.
	defaultPublicHTTPAddr = ":8080"

	defaultPublicHTTPReadHeaderTimeout = 2 * time.Second
	defaultPublicHTTPReadTimeout = 10 * time.Second
	defaultPublicHTTPIdleTimeout = time.Minute
	defaultPublicAuthUpstreamTimeout = 3 * time.Second

	defaultAdminHTTPReadHeaderTimeout = 2 * time.Second
	defaultAdminHTTPReadTimeout = 10 * time.Second
	defaultAdminHTTPIdleTimeout = time.Minute

	// defaultAuthenticatedGRPCAddr is applied when
	// authenticatedGRPCAddrEnvVar is absent.
	defaultAuthenticatedGRPCAddr = ":9090"

	defaultAuthenticatedGRPCConnectionTimeout = 5 * time.Second
	defaultAuthenticatedGRPCDownstreamTimeout = 5 * time.Second
	defaultAuthenticatedGRPCFreshnessWindow = 5 * time.Minute

	defaultAuthenticatedGRPCIPRateLimitRequests = 120
	defaultAuthenticatedGRPCIPRateLimitBurst = 40

	defaultAuthenticatedGRPCSessionRateLimitRequests = 60
	defaultAuthenticatedGRPCSessionRateLimitBurst = 20

	defaultAuthenticatedGRPCUserRateLimitRequests = 120
	defaultAuthenticatedGRPCUserRateLimitBurst = 40

	defaultAuthenticatedGRPCMessageClassRateLimitRequests = 60
	defaultAuthenticatedGRPCMessageClassRateLimitBurst = 20

	defaultSessionCacheMaxEntries = 50_000
	defaultSessionCacheTTL = 10 * time.Minute

	defaultReplayRedisKeyPrefix = "gateway:replay:"
	defaultReplayRedisReserveTimeout = 250 * time.Millisecond

	defaultBackendHTTPTimeout = 5 * time.Second
	defaultBackendPushReconnectBaseBackoff = 250 * time.Millisecond
	defaultBackendPushReconnectMaxBackoff = 30 * time.Second

	defaultPublicAuthMaxBodyBytes = int64(8192)

	defaultPublicAuthRateLimitRequests = 30
	defaultPublicAuthRateLimitBurst = 10

	defaultBrowserBootstrapRateLimitRequests = 60
	defaultBrowserBootstrapRateLimitBurst = 20

	defaultBrowserAssetRateLimitRequests = 300
	defaultBrowserAssetRateLimitBurst = 80

	defaultPublicMiscRateLimitRequests = 30
	defaultPublicMiscRateLimitBurst = 10

	defaultSendEmailCodeIdentityRateLimitRequests = 3
	defaultSendEmailCodeIdentityRateLimitBurst = 1

	defaultConfirmEmailCodeIdentityRateLimitRequests = 6
	defaultConfirmEmailCodeIdentityRateLimitBurst = 2
)

var (
	defaultClassRateLimitWindow = time.Minute

	defaultIdentityRateLimitWindow = 10 * time.Minute
)

// RateLimitConfig describes a single rate-limit budget.
type RateLimitConfig struct {
	// Requests is the number of accepted requests replenished per Window.
	Requests int

	// Window is the interval over which Requests are replenished.
	Window time.Duration

	// Burst is the maximum number of immediately available tokens.
	Burst int
}

// PublicRateLimitConfig identifies the generic rate-limit budget shape used by
// public REST policy.
type PublicRateLimitConfig = RateLimitConfig

// AuthenticatedRateLimitConfig identifies the generic rate-limit budget shape
// used by authenticated gRPC policy.
type AuthenticatedRateLimitConfig = RateLimitConfig

// PublicRoutePolicyConfig describes the anti-abuse policy enforced for one
// stable public REST traffic class.
type PublicRoutePolicyConfig struct {
	// MaxBodyBytes is the maximum accepted request body size. Zero means that
	// the request must not carry a body.
	MaxBodyBytes int64

	// RateLimit configures the per-IP budget for the route class.
	RateLimit PublicRateLimitConfig
}

// PublicAuthIdentityPolicyConfig describes the additional identity-based
// limiter applied to one public auth command.
type PublicAuthIdentityPolicyConfig struct {
	// RateLimit configures the accepted request budget for one normalized public
	// auth identity key.
	RateLimit PublicRateLimitConfig
}

// PublicHTTPAntiAbuseConfig describes the public REST anti-abuse policy used
// before route handling.
type PublicHTTPAntiAbuseConfig struct {
	// PublicAuth applies to the stable public_auth route class.
	PublicAuth PublicRoutePolicyConfig

	// BrowserBootstrap applies to the stable browser_bootstrap route class.
	BrowserBootstrap PublicRoutePolicyConfig

	// BrowserAsset applies to the stable browser_asset route class.
	BrowserAsset PublicRoutePolicyConfig

	// PublicMisc applies to the stable public_misc route class.
	PublicMisc PublicRoutePolicyConfig

	// SendEmailCodeIdentity applies the additional identity limiter for
	// send-email-code.
	SendEmailCodeIdentity PublicAuthIdentityPolicyConfig

	// ConfirmEmailCodeIdentity applies the additional identity limiter for
	// confirm-email-code.
	ConfirmEmailCodeIdentity PublicAuthIdentityPolicyConfig
}

// AuthenticatedGRPCAntiAbuseConfig describes the authenticated gRPC
// rate-limit budgets enforced after request authenticity has been established.
type AuthenticatedGRPCAntiAbuseConfig struct {
	// IP applies to the transport peer IP derived from the gRPC connection.
	IP AuthenticatedRateLimitConfig

	// Session applies to the authenticated device_session_id.
	Session AuthenticatedRateLimitConfig

	// User applies to the authenticated user_id resolved from SessionCache.
	User AuthenticatedRateLimitConfig

	// MessageClass applies to the current authenticated message class. The
	// gateway uses the full message_type literal as the stable v1 class key.
	MessageClass AuthenticatedRateLimitConfig
}

// PublicHTTPConfig describes the public unauthenticated REST listener exposed
// by the gateway.
type PublicHTTPConfig struct {
	// Addr is the TCP listen address used by the public REST server.
	Addr string

	// ReadHeaderTimeout bounds how long the listener may spend reading request
	// headers before the gateway rejects the connection.
	ReadHeaderTimeout time.Duration

	// ReadTimeout bounds how long the listener may spend reading one public
	// request.
	ReadTimeout time.Duration

	// IdleTimeout bounds how long the listener keeps an idle keep-alive
	// connection open.
	IdleTimeout time.Duration

	// AuthUpstreamTimeout bounds one public auth adapter call.
	AuthUpstreamTimeout time.Duration

	// AntiAbuse configures the public REST anti-abuse middleware.
	AntiAbuse PublicHTTPAntiAbuseConfig

	// CORSAllowedOrigins is the exact-match list of browser origins
	// permitted to call the public REST surface. Empty disables CORS:
	// requests without an Origin header continue to work, cross-origin
	// requests are subject to the browser's default same-origin policy.
	CORSAllowedOrigins []string
}

// BackendConfig describes the consolidated backend service the gateway
// talks to. Every authenticated and public HTTP request is forwarded to
// `HTTPBaseURL`; the gRPC `Push.SubscribePush` stream is opened against
// `GRPCPushURL`.
type BackendConfig struct {
	// HTTPBaseURL is the absolute base URL of the backend HTTP listener
	// (`/api/v1/{public,user,internal}/*`). Required.
	HTTPBaseURL string

	// GRPCPushURL is the dial target of the backend `Push.SubscribePush`
	// listener (`host:port`). Required.
	GRPCPushURL string

	// GatewayClientID is the durable identifier this gateway instance
	// presents to backend in `GatewaySubscribeRequest.gateway_client_id`.
	// Required.
	GatewayClientID string

	// HTTPTimeout bounds individual REST calls. Must be positive.
	HTTPTimeout time.Duration

	// PushReconnectBaseBackoff is the starting delay between reconnect
	// attempts of `Push.SubscribePush`. Must be positive.
	PushReconnectBaseBackoff time.Duration

	// PushReconnectMaxBackoff is the upper bound for exponential
	// reconnect delays. Must be greater than or equal to
	// PushReconnectBaseBackoff.
	PushReconnectMaxBackoff time.Duration
}

// AdminHTTPConfig describes the private operational HTTP listener used for
// metrics exposure. The listener remains disabled when Addr is empty.
type AdminHTTPConfig struct {
	// Addr is the TCP listen address used by the admin HTTP server. An empty
	// value disables the listener.
	Addr string

	// ReadHeaderTimeout bounds how long the listener may spend reading request
	// headers before the gateway rejects the connection.
	ReadHeaderTimeout time.Duration

	// ReadTimeout bounds how long the listener may spend reading one admin
	// request.
	ReadTimeout time.Duration

	// IdleTimeout bounds how long the listener keeps an idle keep-alive
	// connection open.
	IdleTimeout time.Duration
}

// AuthenticatedGRPCConfig describes the authenticated gRPC listener exposed by
// the gateway.
type AuthenticatedGRPCConfig struct {
	// Addr is the TCP listen address used by the authenticated gRPC server.
	Addr string

	// ConnectionTimeout bounds one inbound connection handshake.
	ConnectionTimeout time.Duration

	// DownstreamTimeout bounds one downstream unary execution after the request
	// has passed the full authenticated ingress pipeline.
	DownstreamTimeout time.Duration

	// FreshnessWindow is the accepted skew window around current server time
	// used for client request timestamps.
	FreshnessWindow time.Duration

	// AntiAbuse configures the authenticated gRPC rate limits enforced after
	// the request passes the transport authenticity checks.
	AntiAbuse AuthenticatedGRPCAntiAbuseConfig
}

// SessionCacheConfig describes the bounds of the gateway's in-memory
// session cache. The cache fronts every authenticated request and
// falls back to a synchronous backend lookup on miss; push-event
// driven invalidations flip cached records to revoked status without
// a backend roundtrip.
type SessionCacheConfig struct {
	// MaxEntries bounds the LRU. Zero or negative values fall back to
	// the package default at construction time.
	MaxEntries int

	// TTL is the safety-net freshness window applied to every cached
	// entry. Zero or negative values fall back to the package default.
	TTL time.Duration
}

// ReplayRedisConfig describes the Redis namespace and timeout used for
// authenticated replay reservations.
type ReplayRedisConfig struct {
	// KeyPrefix is prepended to every ReplayStore Redis key.
	KeyPrefix string

	// ReserveTimeout bounds individual ReplayStore Redis operations.
	ReserveTimeout time.Duration
}

// ResponseSignerConfig describes the private-key material used to sign
// authenticated unary responses and stream events.
type ResponseSignerConfig struct {
	// PrivateKeyPEMPath is the filesystem path to the PKCS#8 PEM-encoded
	// Ed25519 private key loaded during startup.
	PrivateKeyPEMPath string
}

// LoggingConfig describes the process-wide structured logging settings.
type LoggingConfig struct {
	// Level is the configured minimum log level literal.
	Level string
}

// Config describes process-wide settings required to start and stop the
// gateway safely.
type Config struct {
	// ShutdownTimeout limits how long each component may spend in Shutdown
	// before the gateway reports a timeout.
	ShutdownTimeout time.Duration

	// Logging configures the process-wide structured logger.
	Logging LoggingConfig

	// PublicHTTP configures the public unauthenticated REST listener.
	PublicHTTP PublicHTTPConfig

	// Backend configures the consolidated backend the gateway forwards
	// every public auth and authenticated user/lobby request to and the
	// gRPC `Push.SubscribePush` stream consumed for inbound events.
	Backend BackendConfig

	// AdminHTTP configures the optional private admin listener used for metrics
	// exposure.
	AdminHTTP AdminHTTPConfig

	// AuthenticatedGRPC configures the authenticated gRPC listener.
	AuthenticatedGRPC AuthenticatedGRPCConfig

	// Redis carries the master/replica/password connection topology used
	// by the anti-replay reservation store, sourced from the
	// GATEWAY_REDIS_* environment variables managed by `pkg/redisconn`.
	// The implementation dropped session cache projection and the two Redis
	// Streams; Redis is now used only for replay reservations.
	Redis redisconn.Config

	// SessionCache configures the in-memory session cache fronting
	// every authenticated request.
	SessionCache SessionCacheConfig

	// ReplayRedis configures the Redis-backed authenticated ReplayStore.
	ReplayRedis ReplayRedisConfig

	// ResponseSigner configures the authenticated response and event signer
	// loaded during startup.
	ResponseSigner ResponseSignerConfig
}

// DefaultPublicHTTPConfig returns the default listener and anti-abuse settings
// for the public REST surface.
func DefaultPublicHTTPConfig() PublicHTTPConfig {
	return PublicHTTPConfig{
		Addr: defaultPublicHTTPAddr,
		ReadHeaderTimeout: defaultPublicHTTPReadHeaderTimeout,
		ReadTimeout: defaultPublicHTTPReadTimeout,
		IdleTimeout: defaultPublicHTTPIdleTimeout,
		AuthUpstreamTimeout: defaultPublicAuthUpstreamTimeout,
		AntiAbuse: PublicHTTPAntiAbuseConfig{
			PublicAuth: PublicRoutePolicyConfig{
				MaxBodyBytes: defaultPublicAuthMaxBodyBytes,
				RateLimit: PublicRateLimitConfig{
					Requests: defaultPublicAuthRateLimitRequests,
					Window: defaultClassRateLimitWindow,
					Burst: defaultPublicAuthRateLimitBurst,
				},
			},
			BrowserBootstrap: PublicRoutePolicyConfig{
				RateLimit: PublicRateLimitConfig{
					Requests: defaultBrowserBootstrapRateLimitRequests,
					Window: defaultClassRateLimitWindow,
					Burst: defaultBrowserBootstrapRateLimitBurst,
				},
			},
			BrowserAsset: PublicRoutePolicyConfig{
				RateLimit: PublicRateLimitConfig{
					Requests: defaultBrowserAssetRateLimitRequests,
					Window: defaultClassRateLimitWindow,
					Burst: defaultBrowserAssetRateLimitBurst,
				},
			},
			PublicMisc: PublicRoutePolicyConfig{
				RateLimit: PublicRateLimitConfig{
					Requests: defaultPublicMiscRateLimitRequests,
					Window: defaultClassRateLimitWindow,
					Burst: defaultPublicMiscRateLimitBurst,
				},
			},
			SendEmailCodeIdentity: PublicAuthIdentityPolicyConfig{
				RateLimit: PublicRateLimitConfig{
					Requests: defaultSendEmailCodeIdentityRateLimitRequests,
					Window: defaultIdentityRateLimitWindow,
					Burst: defaultSendEmailCodeIdentityRateLimitBurst,
				},
			},
			ConfirmEmailCodeIdentity: PublicAuthIdentityPolicyConfig{
				RateLimit: PublicRateLimitConfig{
					Requests: defaultConfirmEmailCodeIdentityRateLimitRequests,
					Window: defaultIdentityRateLimitWindow,
					Burst: defaultConfirmEmailCodeIdentityRateLimitBurst,
				},
			},
		},
	}
}

// DefaultAdminHTTPConfig returns the default settings for the optional private
// admin listener. The zero address keeps the listener disabled by default.
func DefaultAdminHTTPConfig() AdminHTTPConfig {
	return AdminHTTPConfig{
		ReadHeaderTimeout: defaultAdminHTTPReadHeaderTimeout,
		ReadTimeout: defaultAdminHTTPReadTimeout,
		IdleTimeout: defaultAdminHTTPIdleTimeout,
	}
}

// DefaultAuthenticatedGRPCConfig returns the default listener, freshness, and
// anti-abuse settings for the authenticated gRPC surface.
func DefaultAuthenticatedGRPCConfig() AuthenticatedGRPCConfig {
	return AuthenticatedGRPCConfig{
		Addr: defaultAuthenticatedGRPCAddr,
		ConnectionTimeout: defaultAuthenticatedGRPCConnectionTimeout,
		DownstreamTimeout: defaultAuthenticatedGRPCDownstreamTimeout,
		FreshnessWindow: defaultAuthenticatedGRPCFreshnessWindow,
		AntiAbuse: AuthenticatedGRPCAntiAbuseConfig{
			IP: AuthenticatedRateLimitConfig{
				Requests: defaultAuthenticatedGRPCIPRateLimitRequests,
				Window: defaultClassRateLimitWindow,
				Burst: defaultAuthenticatedGRPCIPRateLimitBurst,
			},
			Session: AuthenticatedRateLimitConfig{
				Requests: defaultAuthenticatedGRPCSessionRateLimitRequests,
				Window: defaultClassRateLimitWindow,
				Burst: defaultAuthenticatedGRPCSessionRateLimitBurst,
			},
			User: AuthenticatedRateLimitConfig{
				Requests: defaultAuthenticatedGRPCUserRateLimitRequests,
				Window: defaultClassRateLimitWindow,
				Burst: defaultAuthenticatedGRPCUserRateLimitBurst,
			},
			MessageClass: AuthenticatedRateLimitConfig{
				Requests: defaultAuthenticatedGRPCMessageClassRateLimitRequests,
				Window: defaultClassRateLimitWindow,
				Burst: defaultAuthenticatedGRPCMessageClassRateLimitBurst,
			},
		},
	}
}

// DefaultLoggingConfig returns the default structured logging settings.
func DefaultLoggingConfig() LoggingConfig {
	return LoggingConfig{Level: defaultLogLevel}
}

// DefaultReplayRedisConfig returns the default Redis key namespace and timeout
// used for authenticated replay reservations.
func DefaultReplayRedisConfig() ReplayRedisConfig {
	return ReplayRedisConfig{
		KeyPrefix: defaultReplayRedisKeyPrefix,
		ReserveTimeout: defaultReplayRedisReserveTimeout,
	}
}

// DefaultSessionCacheConfig returns the default LRU bound and safety-net TTL
// used by the in-memory session cache.
func DefaultSessionCacheConfig() SessionCacheConfig {
	return SessionCacheConfig{
		MaxEntries: defaultSessionCacheMaxEntries,
		TTL: defaultSessionCacheTTL,
	}
}

// DefaultBackendConfig returns the default backend settings used for the
// gateway → backend HTTP and gRPC conversation. URL fields stay empty and
// must be supplied explicitly via env vars.
func DefaultBackendConfig() BackendConfig {
	return BackendConfig{
		HTTPTimeout: defaultBackendHTTPTimeout,
		PushReconnectBaseBackoff: defaultBackendPushReconnectBaseBackoff,
		PushReconnectMaxBackoff: defaultBackendPushReconnectMaxBackoff,
	}
}

// DefaultResponseSignerConfig returns the default response-signer settings.
// The private key path remains empty and must be supplied explicitly.
func DefaultResponseSignerConfig() ResponseSignerConfig {
	return ResponseSignerConfig{}
}

// LoadFromEnv loads Config from the process environment, applies defaults for
// omitted settings, and validates the resulting values.
func LoadFromEnv() (Config, error) {
	cfg := Config{
		ShutdownTimeout: defaultShutdownTimeout,
		Logging: DefaultLoggingConfig(),
		PublicHTTP: DefaultPublicHTTPConfig(),
		Backend: DefaultBackendConfig(),
		AdminHTTP: DefaultAdminHTTPConfig(),
		AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
		Redis: redisconn.DefaultConfig(),
		SessionCache: DefaultSessionCacheConfig(),
		ReplayRedis: DefaultReplayRedisConfig(),
		ResponseSigner: DefaultResponseSignerConfig(),
	}

	rawShutdownTimeout, ok := os.LookupEnv(shutdownTimeoutEnvVar)
	if ok {
		shutdownTimeout, err := time.ParseDuration(rawShutdownTimeout)
		if err != nil {
			return Config{}, fmt.Errorf("load gateway config: parse %s: %w", shutdownTimeoutEnvVar, err)
		}
		cfg.ShutdownTimeout = shutdownTimeout
	}

	rawLogLevel, ok := os.LookupEnv(logLevelEnvVar)
	if ok {
		cfg.Logging.Level = rawLogLevel
	}

	rawPublicHTTPAddr, ok := os.LookupEnv(publicHTTPAddrEnvVar)
	if ok {
		cfg.PublicHTTP.Addr = rawPublicHTTPAddr
	}

	publicHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(publicHTTPReadHeaderTimeoutEnvVar, cfg.PublicHTTP.ReadHeaderTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.ReadHeaderTimeout = publicHTTPReadHeaderTimeout

	publicHTTPReadTimeout, err := loadDurationEnvWithDefault(publicHTTPReadTimeoutEnvVar, cfg.PublicHTTP.ReadTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.ReadTimeout = publicHTTPReadTimeout

	publicHTTPIdleTimeout, err := loadDurationEnvWithDefault(publicHTTPIdleTimeoutEnvVar, cfg.PublicHTTP.IdleTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.IdleTimeout = publicHTTPIdleTimeout

	publicAuthUpstreamTimeout, err := loadDurationEnvWithDefault(publicAuthUpstreamTimeoutEnvVar, cfg.PublicHTTP.AuthUpstreamTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AuthUpstreamTimeout = publicAuthUpstreamTimeout

	if v, ok := os.LookupEnv(publicHTTPCORSAllowedOriginsEnvVar); ok {
		origins := make([]string, 0)
		for part := range strings.SplitSeq(v, ",") {
			if trimmed := strings.TrimSpace(part); trimmed != "" {
				origins = append(origins, trimmed)
			}
		}
		cfg.PublicHTTP.CORSAllowedOrigins = origins
	}

	if v, ok := os.LookupEnv(backendHTTPURLEnvVar); ok {
		cfg.Backend.HTTPBaseURL = v
	}
	if v, ok := os.LookupEnv(backendGRPCPushURLEnvVar); ok {
		cfg.Backend.GRPCPushURL = v
	}
	if v, ok := os.LookupEnv(backendGatewayClientIDEnvVar); ok {
		cfg.Backend.GatewayClientID = v
	}
	backendHTTPTimeout, err := loadDurationEnvWithDefault(backendHTTPTimeoutEnvVar, cfg.Backend.HTTPTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.Backend.HTTPTimeout = backendHTTPTimeout
	backendPushReconnectBaseBackoff, err := loadDurationEnvWithDefault(backendPushReconnectBaseBackoffEnvVar, cfg.Backend.PushReconnectBaseBackoff)
	if err != nil {
		return Config{}, err
	}
	cfg.Backend.PushReconnectBaseBackoff = backendPushReconnectBaseBackoff
	backendPushReconnectMaxBackoff, err := loadDurationEnvWithDefault(backendPushReconnectMaxBackoffEnvVar, cfg.Backend.PushReconnectMaxBackoff)
	if err != nil {
		return Config{}, err
	}
	cfg.Backend.PushReconnectMaxBackoff = backendPushReconnectMaxBackoff

	rawAdminHTTPAddr, ok := os.LookupEnv(adminHTTPAddrEnvVar)
	if ok {
		cfg.AdminHTTP.Addr = rawAdminHTTPAddr
	}

	adminHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(adminHTTPReadHeaderTimeoutEnvVar, cfg.AdminHTTP.ReadHeaderTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.AdminHTTP.ReadHeaderTimeout = adminHTTPReadHeaderTimeout

	adminHTTPReadTimeout, err := loadDurationEnvWithDefault(adminHTTPReadTimeoutEnvVar, cfg.AdminHTTP.ReadTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.AdminHTTP.ReadTimeout = adminHTTPReadTimeout

	adminHTTPIdleTimeout, err := loadDurationEnvWithDefault(adminHTTPIdleTimeoutEnvVar, cfg.AdminHTTP.IdleTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.AdminHTTP.IdleTimeout = adminHTTPIdleTimeout

	rawAuthenticatedGRPCAddr, ok := os.LookupEnv(authenticatedGRPCAddrEnvVar)
	if ok {
		cfg.AuthenticatedGRPC.Addr = rawAuthenticatedGRPCAddr
	}

	authenticatedGRPCConnectionTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCConnectionTimeoutEnvVar, cfg.AuthenticatedGRPC.ConnectionTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.ConnectionTimeout = authenticatedGRPCConnectionTimeout

	authenticatedGRPCDownstreamTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCDownstreamTimeoutEnvVar, cfg.AuthenticatedGRPC.DownstreamTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.DownstreamTimeout = authenticatedGRPCDownstreamTimeout

	authenticatedGRPCFreshnessWindow, err := loadDurationEnvWithDefault(authenticatedGRPCFreshnessWindowEnvVar, cfg.AuthenticatedGRPC.FreshnessWindow)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.FreshnessWindow = authenticatedGRPCFreshnessWindow

	authenticatedGRPCIPRateLimit, err := loadRateLimitConfigFromEnv(
		cfg.AuthenticatedGRPC.AntiAbuse.IP,
		authenticatedGRPCIPRateLimitRequestsEnvVar,
		authenticatedGRPCIPRateLimitWindowEnvVar,
		authenticatedGRPCIPRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.AntiAbuse.IP = authenticatedGRPCIPRateLimit

	authenticatedGRPCSessionRateLimit, err := loadRateLimitConfigFromEnv(
		cfg.AuthenticatedGRPC.AntiAbuse.Session,
		authenticatedGRPCSessionRateLimitRequestsEnvVar,
		authenticatedGRPCSessionRateLimitWindowEnvVar,
		authenticatedGRPCSessionRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.AntiAbuse.Session = authenticatedGRPCSessionRateLimit

	authenticatedGRPCUserRateLimit, err := loadRateLimitConfigFromEnv(
		cfg.AuthenticatedGRPC.AntiAbuse.User,
		authenticatedGRPCUserRateLimitRequestsEnvVar,
		authenticatedGRPCUserRateLimitWindowEnvVar,
		authenticatedGRPCUserRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.AntiAbuse.User = authenticatedGRPCUserRateLimit

	messageClassRateLimit, err := loadRateLimitConfigFromEnv(
		cfg.AuthenticatedGRPC.AntiAbuse.MessageClass,
		authenticatedGRPCMessageClassRateLimitRequestsEnvVar,
		authenticatedGRPCMessageClassRateLimitWindowEnvVar,
		authenticatedGRPCMessageClassRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.AuthenticatedGRPC.AntiAbuse.MessageClass = messageClassRateLimit

	redisConn, err := redisconn.LoadFromEnv(gatewayRedisEnvPrefix)
	if err != nil {
		return Config{}, err
	}
	cfg.Redis = redisConn

	sessionCacheMaxEntries, err := loadIntEnvWithDefault(sessionCacheMaxEntriesEnvVar, cfg.SessionCache.MaxEntries)
	if err != nil {
		return Config{}, err
	}
	cfg.SessionCache.MaxEntries = sessionCacheMaxEntries

	sessionCacheTTL, err := loadDurationEnvWithDefault(sessionCacheTTLEnvVar, cfg.SessionCache.TTL)
	if err != nil {
		return Config{}, err
	}
	cfg.SessionCache.TTL = sessionCacheTTL

	rawReplayRedisKeyPrefix, ok := os.LookupEnv(replayRedisKeyPrefixEnvVar)
	if ok {
		cfg.ReplayRedis.KeyPrefix = rawReplayRedisKeyPrefix
	}

	replayRedisReserveTimeout, err := loadDurationEnvWithDefault(replayRedisReserveTimeoutEnvVar, cfg.ReplayRedis.ReserveTimeout)
	if err != nil {
		return Config{}, err
	}
	cfg.ReplayRedis.ReserveTimeout = replayRedisReserveTimeout

	rawSignerKeyPath, ok := os.LookupEnv(responseSignerPrivateKeyPEMPathEnvVar)
	if ok {
		cfg.ResponseSigner.PrivateKeyPEMPath = rawSignerKeyPath
	}

	publicAuthPolicy, err := loadPublicRoutePolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.PublicAuth,
		publicAuthMaxBodyBytesEnvVar,
		publicAuthRateLimitRequestsEnvVar,
		publicAuthRateLimitWindowEnvVar,
		publicAuthRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.PublicAuth = publicAuthPolicy

	browserBootstrapPolicy, err := loadPublicRoutePolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.BrowserBootstrap,
		browserBootstrapMaxBodyBytesEnvVar,
		browserBootstrapRateLimitRequestsEnvVar,
		browserBootstrapRateLimitWindowEnvVar,
		browserBootstrapRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.BrowserBootstrap = browserBootstrapPolicy

	browserAssetPolicy, err := loadPublicRoutePolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.BrowserAsset,
		browserAssetMaxBodyBytesEnvVar,
		browserAssetRateLimitRequestsEnvVar,
		browserAssetRateLimitWindowEnvVar,
		browserAssetRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.BrowserAsset = browserAssetPolicy

	publicMiscPolicy, err := loadPublicRoutePolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.PublicMisc,
		publicMiscMaxBodyBytesEnvVar,
		publicMiscRateLimitRequestsEnvVar,
		publicMiscRateLimitWindowEnvVar,
		publicMiscRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.PublicMisc = publicMiscPolicy

	sendIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity,
		sendEmailCodeIdentityRateLimitRequestsEnvVar,
		sendEmailCodeIdentityRateLimitWindowEnvVar,
		sendEmailCodeIdentityRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity = sendIdentityPolicy

	confirmIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv(
		cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity,
		confirmEmailCodeIdentityRateLimitRequestsEnvVar,
		confirmEmailCodeIdentityRateLimitWindowEnvVar,
		confirmEmailCodeIdentityRateLimitBurstEnvVar,
	)
	if err != nil {
		return Config{}, err
	}
	cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity = confirmIdentityPolicy

	if cfg.ShutdownTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", shutdownTimeoutEnvVar)
	}
	if err := validateLogLevel(cfg.Logging.Level); err != nil {
		return Config{}, fmt.Errorf("load gateway config: %w", err)
	}
	if strings.TrimSpace(cfg.PublicHTTP.Addr) == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", publicHTTPAddrEnvVar)
	}
	if cfg.PublicHTTP.ReadHeaderTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadHeaderTimeoutEnvVar)
	}
	if cfg.PublicHTTP.ReadTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadTimeoutEnvVar)
	}
	if cfg.PublicHTTP.IdleTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPIdleTimeoutEnvVar)
	}
	if cfg.PublicHTTP.AuthUpstreamTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicAuthUpstreamTimeoutEnvVar)
	}
	cfg.Backend.HTTPBaseURL = strings.TrimSpace(cfg.Backend.HTTPBaseURL)
	if cfg.Backend.HTTPBaseURL == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendHTTPURLEnvVar)
	}
	parsedBackendHTTP, err := url.Parse(strings.TrimRight(cfg.Backend.HTTPBaseURL, "/"))
	if err != nil {
		return Config{}, fmt.Errorf("load gateway config: parse %s: %w", backendHTTPURLEnvVar, err)
	}
	if parsedBackendHTTP.Scheme == "" || parsedBackendHTTP.Host == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must be an absolute URL", backendHTTPURLEnvVar)
	}
	cfg.Backend.HTTPBaseURL = parsedBackendHTTP.String()
	cfg.Backend.GRPCPushURL = strings.TrimSpace(cfg.Backend.GRPCPushURL)
	if cfg.Backend.GRPCPushURL == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendGRPCPushURLEnvVar)
	}
	cfg.Backend.GatewayClientID = strings.TrimSpace(cfg.Backend.GatewayClientID)
	if cfg.Backend.GatewayClientID == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendGatewayClientIDEnvVar)
	}
	if cfg.Backend.HTTPTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", backendHTTPTimeoutEnvVar)
	}
	if cfg.Backend.PushReconnectBaseBackoff <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", backendPushReconnectBaseBackoffEnvVar)
	}
	if cfg.Backend.PushReconnectMaxBackoff < cfg.Backend.PushReconnectBaseBackoff {
		return Config{}, fmt.Errorf("load gateway config: %s must be >= %s", backendPushReconnectMaxBackoffEnvVar, backendPushReconnectBaseBackoffEnvVar)
	}
	if addr := strings.TrimSpace(cfg.AdminHTTP.Addr); addr != "" {
		cfg.AdminHTTP.Addr = addr
	}
	if cfg.AdminHTTP.ReadHeaderTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadHeaderTimeoutEnvVar)
	}
	if cfg.AdminHTTP.ReadTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadTimeoutEnvVar)
	}
	if cfg.AdminHTTP.IdleTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPIdleTimeoutEnvVar)
	}
	if strings.TrimSpace(cfg.AuthenticatedGRPC.Addr) == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", authenticatedGRPCAddrEnvVar)
	}
	if cfg.AuthenticatedGRPC.ConnectionTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCConnectionTimeoutEnvVar)
	}
	if cfg.AuthenticatedGRPC.DownstreamTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCDownstreamTimeoutEnvVar)
	}
	if cfg.AuthenticatedGRPC.FreshnessWindow <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCFreshnessWindowEnvVar)
	}
	if err := validateRateLimitConfig(
		cfg.AuthenticatedGRPC.AntiAbuse.IP,
		authenticatedGRPCIPRateLimitRequestsEnvVar,
		authenticatedGRPCIPRateLimitWindowEnvVar,
		authenticatedGRPCIPRateLimitBurstEnvVar,
	); err != nil {
		return Config{}, err
	}
	if err := validateRateLimitConfig(
		cfg.AuthenticatedGRPC.AntiAbuse.Session,
		authenticatedGRPCSessionRateLimitRequestsEnvVar,
		authenticatedGRPCSessionRateLimitWindowEnvVar,
		authenticatedGRPCSessionRateLimitBurstEnvVar,
	); err != nil {
		return Config{}, err
	}
	if err := validateRateLimitConfig(
		cfg.AuthenticatedGRPC.AntiAbuse.User,
		authenticatedGRPCUserRateLimitRequestsEnvVar,
		authenticatedGRPCUserRateLimitWindowEnvVar,
		authenticatedGRPCUserRateLimitBurstEnvVar,
	); err != nil {
		return Config{}, err
	}
	if err := validateRateLimitConfig(
		cfg.AuthenticatedGRPC.AntiAbuse.MessageClass,
		authenticatedGRPCMessageClassRateLimitRequestsEnvVar,
		authenticatedGRPCMessageClassRateLimitWindowEnvVar,
		authenticatedGRPCMessageClassRateLimitBurstEnvVar,
	); err != nil {
		return Config{}, err
	}
	if err := cfg.Redis.Validate(); err != nil {
		return Config{}, fmt.Errorf("load gateway config: redis: %w", err)
	}
	if strings.TrimSpace(cfg.ReplayRedis.KeyPrefix) == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", replayRedisKeyPrefixEnvVar)
	}
	if cfg.ReplayRedis.ReserveTimeout <= 0 {
		return Config{}, fmt.Errorf("load gateway config: %s must be positive", replayRedisReserveTimeoutEnvVar)
	}
	if strings.TrimSpace(cfg.ResponseSigner.PrivateKeyPEMPath) == "" {
		return Config{}, fmt.Errorf("load gateway config: %s must not be empty", responseSignerPrivateKeyPEMPathEnvVar)
	}
	if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicAuth, publicAuthMaxBodyBytesEnvVar, publicAuthRateLimitRequestsEnvVar, publicAuthRateLimitWindowEnvVar, publicAuthRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}
	if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserBootstrap, browserBootstrapMaxBodyBytesEnvVar, browserBootstrapRateLimitRequestsEnvVar, browserBootstrapRateLimitWindowEnvVar, browserBootstrapRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}
	if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserAsset, browserAssetMaxBodyBytesEnvVar, browserAssetRateLimitRequestsEnvVar, browserAssetRateLimitWindowEnvVar, browserAssetRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}
	if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicMisc, publicMiscMaxBodyBytesEnvVar, publicMiscRateLimitRequestsEnvVar, publicMiscRateLimitWindowEnvVar, publicMiscRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}
	if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity, sendEmailCodeIdentityRateLimitRequestsEnvVar, sendEmailCodeIdentityRateLimitWindowEnvVar, sendEmailCodeIdentityRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}
	if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity, confirmEmailCodeIdentityRateLimitRequestsEnvVar, confirmEmailCodeIdentityRateLimitWindowEnvVar, confirmEmailCodeIdentityRateLimitBurstEnvVar); err != nil {
		return Config{}, err
	}

	return cfg, nil
}

func loadPublicRoutePolicyConfigFromEnv(defaults PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicRoutePolicyConfig, error) {
	policy := defaults

	maxBodyBytes, err := loadInt64EnvWithDefault(maxBodyEnvVar, defaults.MaxBodyBytes)
	if err != nil {
		return PublicRoutePolicyConfig{}, err
	}
	policy.MaxBodyBytes = maxBodyBytes

	rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar)
	if err != nil {
		return PublicRoutePolicyConfig{}, err
	}
	policy.RateLimit = rateLimit

	return policy, nil
}

func loadPublicAuthIdentityPolicyConfigFromEnv(defaults PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicAuthIdentityPolicyConfig, error) {
	rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar)
	if err != nil {
		return PublicAuthIdentityPolicyConfig{}, err
	}

	return PublicAuthIdentityPolicyConfig{RateLimit: rateLimit}, nil
}

func loadRateLimitConfigFromEnv(defaults RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (RateLimitConfig, error) {
	cfg := defaults

	requests, err := loadIntEnvWithDefault(requestsEnvVar, defaults.Requests)
	if err != nil {
		return RateLimitConfig{}, err
	}
	cfg.Requests = requests

	window, err := loadDurationEnvWithDefault(windowEnvVar, defaults.Window)
	if err != nil {
		return RateLimitConfig{}, err
	}
	cfg.Window = window

	burst, err := loadIntEnvWithDefault(burstEnvVar, defaults.Burst)
	if err != nil {
		return RateLimitConfig{}, err
	}
	cfg.Burst = burst

	return cfg, nil
}

func validateLogLevel(level string) error {
	switch strings.ToLower(strings.TrimSpace(level)) {
	case "debug", "info", "warn", "error", "dpanic", "panic", "fatal":
		return nil
	default:
		return fmt.Errorf("%s must be one of debug, info, warn, error, dpanic, panic, fatal", logLevelEnvVar)
	}
}

func loadIntEnvWithDefault(envVar string, fallback int) (int, error) {
	rawValue, ok := os.LookupEnv(envVar)
	if !ok {
		return fallback, nil
	}

	value, err := strconv.Atoi(rawValue)
	if err != nil {
		return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err)
	}

	return value, nil
}

func loadInt64EnvWithDefault(envVar string, fallback int64) (int64, error) {
	rawValue, ok := os.LookupEnv(envVar)
	if !ok {
		return fallback, nil
	}

	value, err := strconv.ParseInt(rawValue, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err)
	}

	return value, nil
}

func loadDurationEnvWithDefault(envVar string, fallback time.Duration) (time.Duration, error) {
	rawValue, ok := os.LookupEnv(envVar)
	if !ok {
		return fallback, nil
	}

	value, err := time.ParseDuration(rawValue)
	if err != nil {
		return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err)
	}

	return value, nil
}

func loadBoolEnvWithDefault(envVar string, fallback bool) (bool, error) {
	rawValue, ok := os.LookupEnv(envVar)
	if !ok {
		return fallback, nil
	}

	value, err := strconv.ParseBool(rawValue)
	if err != nil {
		return false, fmt.Errorf("load gateway config: parse %s: %w", envVar, err)
	}

	return value, nil
}

func validatePublicRoutePolicyConfig(cfg PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error {
	if cfg.MaxBodyBytes < 0 {
		return fmt.Errorf("load gateway config: %s must not be negative", maxBodyEnvVar)
	}

	return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar)
}

func validatePublicAuthIdentityPolicyConfig(cfg PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error {
	return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar)
}

func validateRateLimitConfig(cfg RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error {
	if cfg.Requests <= 0 {
		return fmt.Errorf("load gateway config: %s must be positive", requestsEnvVar)
	}
	if cfg.Window <= 0 {
		return fmt.Errorf("load gateway config: %s must be positive", windowEnvVar)
	}
	if cfg.Burst <= 0 {
		return fmt.Errorf("load gateway config: %s must be positive", burstEnvVar)
	}

	return nil
}
