package config

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"
)

// Duration unmarshals a Go duration string (e.g. "30s", "5m", "150ms") from JSON.
// An empty string is treated as zero duration.
type Duration struct{ time.Duration }

func (d *Duration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	if s == "" {
		d.Duration = 0
		return nil
	}
	dur, err := time.ParseDuration(s)
	if err != nil {
		return fmt.Errorf("invalid duration %q: %w", s, err)
	}
	d.Duration = dur
	return nil
}

// AppConfig is the unified config loaded from a JSON file.
type AppConfig struct {
	HTTPPort     int
	MySQL        MySQLConfig
	Logging      LoggingConfig
	Audit        AuditConfig
	Auth         AuthConfig
	SES          SESConfig
	Email        EmailDeliveryConfig
	MicrosoftSSO MicrosoftSSOConfig
	HTTP         HTTPConfig
	Resilience   ResilienceConfig
	Queue        QueueConfig
	FileQueue    FileQueueConfig
	SQS          SQSConfig
	FileSQS      SQSConfig
	FileStorage  FileStorageConfig
	FileUpload   FileManagerUploadPolicyConfig
	SAML         SAMLConfig
	WOPI         WOPIConfig
	Gotenberg    GotenbergConfig
}

// LoadFromFile reads path, parses it as JSON, and returns a populated AppConfig.
// The file is typically produced by scripts/inject-secrets.sh.
func LoadFromFile(path string) (AppConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return AppConfig{}, fmt.Errorf("config: read %q: %w", path, err)
	}
	var raw jsonAppConfig
	if err := json.Unmarshal(data, &raw); err != nil {
		return AppConfig{}, fmt.Errorf("config: parse %q: %w", path, err)
	}
	return raw.toAppConfig(), nil
}

// ── JSON intermediate types ───────────────────────────────────────────────────

type jsonCircuitBreakerPolicy struct {
	Enabled             bool     `json:"enabled"`
	MaxRequests         uint32   `json:"maxRequests"`
	Interval            Duration `json:"interval"`
	BucketPeriod        Duration `json:"bucketPeriod"`
	Timeout             Duration `json:"timeout"`
	MinRequests         uint32   `json:"minRequests"`
	FailureRatio        float64  `json:"failureRatio"`
	ConsecutiveFailures uint32   `json:"consecutiveFailures"`
}

func (j jsonCircuitBreakerPolicy) toPolicy() CircuitBreakerPolicy {
	return CircuitBreakerPolicy{
		Enabled:             j.Enabled,
		MaxRequests:         j.MaxRequests,
		Interval:            j.Interval.Duration,
		BucketPeriod:        j.BucketPeriod.Duration,
		Timeout:             j.Timeout.Duration,
		MinRequests:         j.MinRequests,
		FailureRatio:        j.FailureRatio,
		ConsecutiveFailures: j.ConsecutiveFailures,
	}
}

type jsonWorker struct {
	Concurrency           int      `json:"concurrency"`
	MaxBatchSize          int      `json:"maxBatchSize"`
	ProcessTimeout        Duration `json:"processTimeout"`
	ShutdownTimeout       Duration `json:"shutdownTimeout"`
	BackoffMin            Duration `json:"backoffMin"`
	BackoffMax            Duration `json:"backoffMax"`
	PermanentFailureDelay Duration `json:"permanentFailureDelay"`
	MonitorAddr           string   `json:"monitorAddr"`
	HealthErrorThreshold  int      `json:"healthErrorThreshold"`
	DepthPollInterval     Duration `json:"depthPollInterval"`
}

type jsonOutbox struct {
	Enabled         bool     `json:"enabled"`
	PollInterval    Duration `json:"pollInterval"`
	BatchSize       int      `json:"batchSize"`
	DispatchTimeout Duration `json:"dispatchTimeout"`
	LockTTL         Duration `json:"lockTTL"`
	MaxAttempts     int      `json:"maxAttempts"`
}

type jsonSQS struct {
	Region                string   `json:"region"`
	Endpoint              string   `json:"endpoint"`
	AllowInsecureEndpoint bool     `json:"allowInsecureEndpoint"`
	QueueURL              string   `json:"queueUrl"`
	QueueType             string   `json:"queueType"`
	MessageGroupID        string   `json:"messageGroupId"`
	MaxMessages           int      `json:"maxMessages"`
	WaitTimeSeconds       int32    `json:"waitTimeSeconds"`
	VisibilityTimeout     Duration `json:"visibilityTimeout"`
}

func (j jsonSQS) toSQSConfig(regionFallback, endpointFallback, accessKeyID, secretAccessKey string) SQSConfig {
	region := j.Region
	if region == "" {
		region = regionFallback
	}
	endpoint := j.Endpoint
	if endpoint == "" {
		endpoint = endpointFallback
	}
	return SQSConfig{
		Region:                region,
		AccessKeyID:           strings.TrimSpace(accessKeyID),
		SecretAccessKey:       strings.TrimSpace(secretAccessKey),
		Endpoint:              endpoint,
		QueueURL:              j.QueueURL,
		QueueType:             j.QueueType,
		MessageGroupID:        j.MessageGroupID,
		MaxMessages:           j.MaxMessages,
		WaitTimeSeconds:       j.WaitTimeSeconds,
		VisibilityTimeout:     j.VisibilityTimeout.Duration,
		AllowInsecureEndpoint: j.AllowInsecureEndpoint,
	}
}

type jsonAppConfig struct {
	HTTP struct {
		Port      int `json:"port"`
		BodyLimit int `json:"bodyLimit"`
		Timeouts  struct {
			Read             Duration `json:"read"`
			Write            Duration `json:"write"`
			Idle             Duration `json:"idle"`
			Request          Duration `json:"request"`
			RequestOverrides string   `json:"requestOverrides"`
			Shutdown         Duration `json:"shutdown"`
		} `json:"timeouts"`
		CORS struct {
			AllowOrigins     string `json:"allowOrigins"`
			AllowMethods     string `json:"allowMethods"`
			AllowHeaders     string `json:"allowHeaders"`
			ExposeHeaders    string `json:"exposeHeaders"`
			AllowCredentials bool   `json:"allowCredentials"`
			MaxAge           int    `json:"maxAge"`
		} `json:"cors"`
		RateLimit struct {
			Max       int      `json:"max"`
			Window    Duration `json:"window"`
			Endpoints string   `json:"endpoints"`
		} `json:"rateLimit"`
		Security struct {
			HSTSMaxAge int `json:"hstsMaxAge"`
		} `json:"security"`
	} `json:"http"`

	Database struct {
		MySQL struct {
			DSN             string   `json:"dsn"`
			MaxOpenConns    int      `json:"maxOpenConns"`
			MaxIdleConns    int      `json:"maxIdleConns"`
			ConnMaxLifetime Duration `json:"connMaxLifetime"`
			ConnMaxIdleTime Duration `json:"connMaxIdleTime"`
		} `json:"mysql"`
	} `json:"database"`

	Logging struct {
		Level  string `json:"level"`
		Format string `json:"format"`
	} `json:"logging"`

	Audit struct {
		QueueSize int `json:"queueSize"`
		Workers   int `json:"workers"`
	} `json:"audit"`

	Email struct {
		Provider string `json:"provider"`
		MockDir  string `json:"mockDir"`
	} `json:"email"`

	CircuitBreaker struct {
		Default      jsonCircuitBreakerPolicy `json:"default"`
		MicrosoftSSO jsonCircuitBreakerPolicy `json:"microsoftSSO"`
		SES          jsonCircuitBreakerPolicy `json:"ses"`
		SQS          jsonCircuitBreakerPolicy `json:"sqs"`
	} `json:"circuitBreaker"`

	Auth struct {
		PublicBaseURL   string `json:"publicBaseURL"`
		PasswordPepper  string `json:"passwordPepper"`
		IPEncryptionKey string `json:"ipEncryptionKey"`
		DefaultRole     string `json:"defaultRole"`
		DisableRegister bool   `json:"disableRegister"`
		Hash            struct {
			Time     uint32 `json:"time"`
			MemoryKB uint32 `json:"memoryKB"`
			Threads  uint8  `json:"threads"`
			KeyLen   uint32 `json:"keyLen"`
		} `json:"hash"`
		Email struct {
			VerifyTTL Duration `json:"verifyTTL"`
			VerifyURL string   `json:"verifyURL"`
			InviteTTL Duration `json:"inviteTTL"`
		} `json:"email"`
		Password struct {
			SetURL              string   `json:"setURL"`
			ResetTTL            Duration `json:"resetTTL"`
			ResetURL            string   `json:"resetURL"`
			ForgotEmailCooldown Duration `json:"forgotEmailCooldown"`
			ForgotMinDuration   Duration `json:"forgotMinDuration"`
			ForgotIPRateMax     int      `json:"forgotIPRateMax"`
			ForgotIPRateWindow  Duration `json:"forgotIPRateWindow"`
			LoginIPRateMax      int      `json:"loginIPRateMax"`
			LoginIPRateWindow   Duration `json:"loginIPRateWindow"`
		} `json:"password"`
		SecurityPin struct {
			ResetTTL            Duration `json:"resetTTL"`
			ResetURL            string   `json:"resetURL"`
			ForgotEmailCooldown Duration `json:"forgotEmailCooldown"`
			ForgotMinDuration   Duration `json:"forgotMinDuration"`
			MaxAttempts         int      `json:"maxAttempts"`
			LockDuration        Duration `json:"lockDuration"`
			ActionTokenTTL      Duration `json:"actionTokenTTL"`
		} `json:"securityPin"`
		JWT struct {
			AccessSecret    string   `json:"accessSecret"`
			RefreshSecret   string   `json:"refreshSecret"`
			AccessTTL       Duration `json:"accessTTL"`
			RefreshTTL      Duration `json:"refreshTTL"`
			RefreshTotpTTL  Duration `json:"refreshTotpTTL"`
			LocalRefreshTTL Duration `json:"localRefreshTTL"`
			SessionIdleTTL  Duration `json:"sessionIdleTTL"`
			Cookie          struct {
				Domain      string `json:"domain"`
				Secure      bool   `json:"secure"`
				SameSite    string `json:"sameSite"`
				AccessName  string `json:"accessName"`
				RefreshName string `json:"refreshName"`
			} `json:"cookie"`
		} `json:"jwt"`
		TOTP struct {
			ChallengeTTL Duration `json:"challengeTTL"`
		} `json:"totp"`
		SSO struct {
			StateTTL        Duration `json:"stateTTL"`
			SuccessRedirect string   `json:"successRedirect"`
		} `json:"sso"`
		WebAuthn struct {
			RPID          string   `json:"rpId"`
			RPDisplayName string   `json:"rpDisplayName"`
			RPOrigins     string   `json:"rpOrigins"`
			ChallengeTTL  Duration `json:"challengeTTL"`
		} `json:"webauthn"`
	} `json:"auth"`

	TOTP struct {
		Issuer    string `json:"issuer"`
		SecretKey string `json:"secretKey"`
	} `json:"totp"`

	MicrosoftSSO struct {
		TenantID     string `json:"tenantId"`
		ClientID     string `json:"clientId"`
		ClientSecret string `json:"clientSecret"`
		RedirectURL  string `json:"redirectUrl"`
		Authority    string `json:"authority"`
		Scopes       string `json:"scopes"`
	} `json:"microsoftSSO"`
	SAML struct {
		CertFile     string `json:"certFile"`
		KeyFile      string `json:"keyFile"`
		MetadataFile string `json:"metadataFile"`
		RootURL      string `json:"rootUrl"`
		FrontendURL  string `json:"frontendUrl"`
		CookieDomain string `json:"cookieDomain"`
		CookieSecure bool   `json:"cookieSecure"`
		SessionTTLHr int    `json:"sessionTTLHour"`
	} `json:"saml"`

	Queue struct {
		MessageSchemaVersion        string     `json:"messageSchemaVersion"`
		EnableLegacyPayloadFallback bool       `json:"enableLegacyPayloadFallback"`
		IdempotencyKeyPrefix        string     `json:"idempotencyKeyPrefix"`
		IdempotencyTTL              Duration   `json:"idempotencyTTL"`
		ProcessingTTL               Duration   `json:"processingTTL"`
		Worker                      jsonWorker `json:"worker"`
		Outbox                      jsonOutbox `json:"outbox"`
	} `json:"queue"`

	FileQueue struct {
		MessageSchemaVersion string     `json:"messageSchemaVersion"`
		IdempotencyKeyPrefix string     `json:"idempotencyKeyPrefix"`
		IdempotencyTTL       Duration   `json:"idempotencyTTL"`
		ProcessingTTL        Duration   `json:"processingTTL"`
		Worker               jsonWorker `json:"worker"`
		Outbox               jsonOutbox `json:"outbox"`
	} `json:"fileQueue"`

	AWS struct {
		Region          string  `json:"region"`
		AccessKeyID     string  `json:"accessKeyId"`
		SecretAccessKey string  `json:"secretAccessKey"`
		SQS             jsonSQS `json:"sqs"`
		FileSQS         jsonSQS `json:"fileSqs"`
		SES             struct {
			Region                string   `json:"region"`
			Endpoint              string   `json:"endpoint"`
			From                  string   `json:"from"`
			ConfigurationSet      string   `json:"configurationSet"`
			SendTimeout           Duration `json:"sendTimeout"`
			AllowInsecureEndpoint bool     `json:"allowInsecureEndpoint"`
		} `json:"ses"`
	} `json:"aws"`

	File struct {
		Provider string `json:"provider"`
		S3       struct {
			Bucket                string   `json:"bucket"`
			Region                string   `json:"region"`
			Endpoint              string   `json:"endpoint"`
			ForcePathStyle        bool     `json:"forcePathStyle"`
			AllowInsecureEndpoint bool     `json:"allowInsecureEndpoint"`
			AutoCreateBucket      bool     `json:"autoCreateBucket"`
			ObjectKeyPrefix       string   `json:"objectKeyPrefix"`
			PresignPutTTL         Duration `json:"presignPutTTL"`
			PresignGetTTL         Duration `json:"presignGetTTL"`
		} `json:"s3"`
		Upload struct {
			MaxSize           int64  `json:"maxSize"`
			AllowedMimeTypes  string `json:"allowedMimeTypes"`
			AllowedExtensions string `json:"allowedExtensions"`
			AllowEmptyFile    bool   `json:"allowEmptyFile"`
			MaxFilenameLength int    `json:"maxFilenameLength"`
		} `json:"upload"`
	} `json:"file"`

	WOPI struct {
		TokenSecret   string   `json:"tokenSecret"`
		ProofSecret   string   `json:"proofSecret"`
		DiscoveryURL  string   `json:"discoveryUrl"`
		PublicBaseURL string   `json:"publicBaseUrl"`
		EditActionURL string   `json:"editActionUrl"`
		TokenTTL      Duration `json:"tokenTTL"`
		MaxBodyBytes  int      `json:"maxBodyBytes"`
		RequireProof  bool     `json:"requireProof"`
	} `json:"wopi"`

	Gotenberg struct {
		URL string `json:"url"`
	} `json:"gotenberg"`
}

func (r *jsonAppConfig) toAppConfig() AppConfig {
	// IPEncryptionKey falls back to PasswordPepper when not set (mirrors env loader behaviour)
	ipKey := r.Auth.IPEncryptionKey
	if ipKey == "" {
		ipKey = r.Auth.PasswordPepper
	}

	// RefreshTotpTTL falls back to RefreshTTL when not set
	refreshTotpTTL := r.Auth.JWT.RefreshTotpTTL.Duration
	if refreshTotpTTL == 0 {
		refreshTotpTTL = r.Auth.JWT.RefreshTTL.Duration
	}

	// SES region falls back to aws.region
	sesRegion := r.AWS.SES.Region
	if sesRegion == "" {
		sesRegion = r.AWS.Region
	}

	return AppConfig{
		HTTPPort: r.HTTP.Port,

		MySQL: MySQLConfig{
			DSN:             r.Database.MySQL.DSN,
			MaxOpenConns:    r.Database.MySQL.MaxOpenConns,
			MaxIdleConns:    r.Database.MySQL.MaxIdleConns,
			ConnMaxLifetime: r.Database.MySQL.ConnMaxLifetime.Duration,
			ConnMaxIdleTime: r.Database.MySQL.ConnMaxIdleTime.Duration,
		},

		Logging: LoggingConfig{
			Level:  r.Logging.Level,
			Format: r.Logging.Format,
		},

		Audit: AuditConfig{
			QueueSize: r.Audit.QueueSize,
			Workers:   r.Audit.Workers,
		},

		Email: EmailDeliveryConfig{
			Provider: strings.ToLower(strings.TrimSpace(r.Email.Provider)),
			MockDir:  strings.TrimSpace(r.Email.MockDir),
		},

		SES: SESConfig{
			Region:                sesRegion,
			Endpoint:              r.AWS.SES.Endpoint,
			From:                  r.AWS.SES.From,
			ConfigurationSetName:  r.AWS.SES.ConfigurationSet,
			SendTimeout:           r.AWS.SES.SendTimeout.Duration,
			AllowInsecureEndpoint: r.AWS.SES.AllowInsecureEndpoint,
		},

		Auth: AuthConfig{
			PublicBaseURL:                r.Auth.PublicBaseURL,
			PasswordPepper:               r.Auth.PasswordPepper,
			IPEncryptionKey:              ipKey,
			HashTime:                     r.Auth.Hash.Time,
			HashMemoryKB:                 r.Auth.Hash.MemoryKB,
			HashThreads:                  r.Auth.Hash.Threads,
			HashKeyLen:                   r.Auth.Hash.KeyLen,
			EmailVerifyTTL:               r.Auth.Email.VerifyTTL.Duration,
			EmailVerifyURL:               r.Auth.Email.VerifyURL,
			InviteTTL:                    r.Auth.Email.InviteTTL.Duration,
			SetPasswordURL:               r.Auth.Password.SetURL,
			PasswordResetTTL:             r.Auth.Password.ResetTTL.Duration,
			PasswordResetURL:             r.Auth.Password.ResetURL,
			SecurityPINResetTTL:          r.Auth.SecurityPin.ResetTTL.Duration,
			SecurityPINResetURL:          r.Auth.SecurityPin.ResetURL,
			ForgotSecurityPINCooldown:    r.Auth.SecurityPin.ForgotEmailCooldown.Duration,
			ForgotSecurityPINMinDuration: r.Auth.SecurityPin.ForgotMinDuration.Duration,
			SecurityPINMaxAttempts:       r.Auth.SecurityPin.MaxAttempts,
			SecurityPINLockDuration:      r.Auth.SecurityPin.LockDuration.Duration,
			SecurityPINActionTokenTTL:    r.Auth.SecurityPin.ActionTokenTTL.Duration,
			ForgotPasswordEmailCooldown:  r.Auth.Password.ForgotEmailCooldown.Duration,
			ForgotPasswordMinDuration:    r.Auth.Password.ForgotMinDuration.Duration,
			ForgotPasswordIPRateMax:      r.Auth.Password.ForgotIPRateMax,
			ForgotPasswordIPRateWindow:   r.Auth.Password.ForgotIPRateWindow.Duration,
			LoginIPRateMax:               r.Auth.Password.LoginIPRateMax,
			LoginIPRateWindow:            r.Auth.Password.LoginIPRateWindow.Duration,
			TOTPChallengeTTL:             r.Auth.TOTP.ChallengeTTL.Duration,
			SSOStateTTL:                  r.Auth.SSO.StateTTL.Duration,
			TOTPIssuer:                   r.TOTP.Issuer,
			TOTPSecretKeyB64:             r.TOTP.SecretKey,
			JWTAccessSecret:              r.Auth.JWT.AccessSecret,
			JWTRefreshSecret:             r.Auth.JWT.RefreshSecret,
			JWTAccessTTL:                 r.Auth.JWT.AccessTTL.Duration,
			JWTRefreshTTL:                r.Auth.JWT.RefreshTTL.Duration,
			JWTRefreshTOTPTTL:            refreshTotpTTL,
			JWTLocalRefreshTTL:           r.Auth.JWT.LocalRefreshTTL.Duration,
			SessionIdleTTL:               r.Auth.JWT.SessionIdleTTL.Duration,
			JWTCookieDomain:              r.Auth.JWT.Cookie.Domain,
			JWTCookieSecure:              r.Auth.JWT.Cookie.Secure,
			JWTCookieSameSite:            r.Auth.JWT.Cookie.SameSite,
			JWTAccessCookieName:          r.Auth.JWT.Cookie.AccessName,
			JWTRefreshCookieName:         r.Auth.JWT.Cookie.RefreshName,
			WebAuthnRPID:                 r.Auth.WebAuthn.RPID,
			WebAuthnRPDisplayName:        r.Auth.WebAuthn.RPDisplayName,
			WebAuthnRPOrigins:            splitCSV(r.Auth.WebAuthn.RPOrigins),
			WebAuthnChallengeTTL:         r.Auth.WebAuthn.ChallengeTTL.Duration,
			SSOSuccessRedirectURL:        r.Auth.SSO.SuccessRedirect,
			DefaultRoleName:              r.Auth.DefaultRole,
			DisableRegister:              r.Auth.DisableRegister,
		},

		MicrosoftSSO: MicrosoftSSOConfig{
			TenantID:     r.MicrosoftSSO.TenantID,
			ClientID:     r.MicrosoftSSO.ClientID,
			ClientSecret: r.MicrosoftSSO.ClientSecret,
			RedirectURL:  r.MicrosoftSSO.RedirectURL,
			Authority:    r.MicrosoftSSO.Authority,
			Scopes:       splitCSV(r.MicrosoftSSO.Scopes),
		},
		SAML: SAMLConfig{
			CertFile:     strings.TrimSpace(r.SAML.CertFile),
			KeyFile:      strings.TrimSpace(r.SAML.KeyFile),
			MetadataFile: strings.TrimSpace(r.SAML.MetadataFile),
			RootURL:      strings.TrimSpace(r.SAML.RootURL),
			FrontendURL:  strings.TrimSpace(r.SAML.FrontendURL),
			CookieDomain: strings.TrimSpace(r.SAML.CookieDomain),
			CookieSecure: r.SAML.CookieSecure,
			SessionTTLHr: r.SAML.SessionTTLHr,
		},

		HTTP: HTTPConfig{
			BodyLimit:            r.HTTP.BodyLimit,
			ReadTimeout:          r.HTTP.Timeouts.Read.Duration,
			WriteTimeout:         r.HTTP.Timeouts.Write.Duration,
			IdleTimeout:          r.HTTP.Timeouts.Idle.Duration,
			RequestTimeout:       r.HTTP.Timeouts.Request.Duration,
			RequestTimeoutByPath: parseHTTPTimeoutOverrides(r.HTTP.Timeouts.RequestOverrides),
			ShutdownTimeout:      r.HTTP.Timeouts.Shutdown.Duration,
			CORSAllowOrigins:     r.HTTP.CORS.AllowOrigins,
			CORSAllowMethods:     r.HTTP.CORS.AllowMethods,
			CORSAllowHeaders:     r.HTTP.CORS.AllowHeaders,
			CORSExposeHeaders:    r.HTTP.CORS.ExposeHeaders,
			CORSAllowCredentials: r.HTTP.CORS.AllowCredentials,
			CORSMaxAge:           r.HTTP.CORS.MaxAge,
			RateLimitMax:         r.HTTP.RateLimit.Max,
			RateLimitWindow:      r.HTTP.RateLimit.Window.Duration,
			RateLimitByEndpoint:  parseHTTPEndpointRateLimitOverrides(r.HTTP.RateLimit.Endpoints),
			HSTSMaxAge:           r.HTTP.Security.HSTSMaxAge,
		},

		Resilience: ResilienceConfig{
			Default:      r.CircuitBreaker.Default.toPolicy(),
			MicrosoftSSO: r.CircuitBreaker.MicrosoftSSO.toPolicy(),
			SES:          r.CircuitBreaker.SES.toPolicy(),
			SQS:          r.CircuitBreaker.SQS.toPolicy(),
		},

		Queue: QueueConfig{
			MessageSchemaVersion:        r.Queue.MessageSchemaVersion,
			EnableLegacyPayloadFallback: r.Queue.EnableLegacyPayloadFallback,
			IdempotencyKeyPrefix:        r.Queue.IdempotencyKeyPrefix,
			IdempotencyTTL:              r.Queue.IdempotencyTTL.Duration,
			ProcessingTTL:               r.Queue.ProcessingTTL.Duration,
			WorkerConcurrency:           r.Queue.Worker.Concurrency,
			WorkerMaxBatchSize:          r.Queue.Worker.MaxBatchSize,
			WorkerProcessTimeout:        r.Queue.Worker.ProcessTimeout.Duration,
			WorkerShutdownTimeout:       r.Queue.Worker.ShutdownTimeout.Duration,
			WorkerBackoffMin:            r.Queue.Worker.BackoffMin.Duration,
			WorkerBackoffMax:            r.Queue.Worker.BackoffMax.Duration,
			WorkerPermanentFailureDelay: r.Queue.Worker.PermanentFailureDelay.Duration,
			WorkerMonitorAddr:           r.Queue.Worker.MonitorAddr,
			WorkerHealthErrorThreshold:  r.Queue.Worker.HealthErrorThreshold,
			WorkerDepthPollInterval:     r.Queue.Worker.DepthPollInterval.Duration,
			OutboxEnabled:               r.Queue.Outbox.Enabled,
			OutboxPollInterval:          r.Queue.Outbox.PollInterval.Duration,
			OutboxBatchSize:             r.Queue.Outbox.BatchSize,
			OutboxDispatchTimeout:       r.Queue.Outbox.DispatchTimeout.Duration,
			OutboxLockTTL:               r.Queue.Outbox.LockTTL.Duration,
			OutboxMaxAttempts:           r.Queue.Outbox.MaxAttempts,
		},

		FileQueue: FileQueueConfig{
			MessageSchemaVersion:        r.FileQueue.MessageSchemaVersion,
			IdempotencyKeyPrefix:        r.FileQueue.IdempotencyKeyPrefix,
			IdempotencyTTL:              r.FileQueue.IdempotencyTTL.Duration,
			ProcessingTTL:               r.FileQueue.ProcessingTTL.Duration,
			WorkerConcurrency:           r.FileQueue.Worker.Concurrency,
			WorkerMaxBatchSize:          r.FileQueue.Worker.MaxBatchSize,
			WorkerProcessTimeout:        r.FileQueue.Worker.ProcessTimeout.Duration,
			WorkerShutdownTimeout:       r.FileQueue.Worker.ShutdownTimeout.Duration,
			WorkerBackoffMin:            r.FileQueue.Worker.BackoffMin.Duration,
			WorkerBackoffMax:            r.FileQueue.Worker.BackoffMax.Duration,
			WorkerPermanentFailureDelay: r.FileQueue.Worker.PermanentFailureDelay.Duration,
			WorkerMonitorAddr:           r.FileQueue.Worker.MonitorAddr,
			WorkerHealthErrorThreshold:  r.FileQueue.Worker.HealthErrorThreshold,
			WorkerDepthPollInterval:     r.FileQueue.Worker.DepthPollInterval.Duration,
			OutboxEnabled:               r.FileQueue.Outbox.Enabled,
			OutboxPollInterval:          r.FileQueue.Outbox.PollInterval.Duration,
			OutboxBatchSize:             r.FileQueue.Outbox.BatchSize,
			OutboxDispatchTimeout:       r.FileQueue.Outbox.DispatchTimeout.Duration,
			OutboxLockTTL:               r.FileQueue.Outbox.LockTTL.Duration,
			OutboxMaxAttempts:           r.FileQueue.Outbox.MaxAttempts,
		},

		// SQS email queue: region falls back to aws.region
		SQS: r.AWS.SQS.toSQSConfig(r.AWS.Region, "", r.AWS.AccessKeyID, r.AWS.SecretAccessKey),

		// FileSQS: region + endpoint fall back to aws.region / aws.sqs.endpoint
		FileSQS: r.AWS.FileSQS.toSQSConfig(r.AWS.Region, r.AWS.SQS.Endpoint, r.AWS.AccessKeyID, r.AWS.SecretAccessKey),

		FileStorage: FileStorageConfig{
			Provider:              strings.ToLower(strings.TrimSpace(r.File.Provider)),
			Bucket:                strings.TrimSpace(r.File.S3.Bucket),
			Region:                strings.TrimSpace(r.File.S3.Region),
			Endpoint:              strings.TrimSpace(r.File.S3.Endpoint),
			AccessKeyID:           strings.TrimSpace(r.AWS.AccessKeyID),
			SecretAccessKey:       strings.TrimSpace(r.AWS.SecretAccessKey),
			ForcePathStyle:        r.File.S3.ForcePathStyle,
			AllowInsecureEndpoint: r.File.S3.AllowInsecureEndpoint,
			AutoCreateBucket:      r.File.S3.AutoCreateBucket,
			ObjectKeyPrefix:       strings.TrimSpace(r.File.S3.ObjectKeyPrefix),
			PresignPutTTL:         r.File.S3.PresignPutTTL.Duration,
			PresignGetTTL:         r.File.S3.PresignGetTTL.Duration,
		},

		FileUpload: FileManagerUploadPolicyConfig{
			MaxFileSizeBytes:  r.File.Upload.MaxSize,
			AllowedMimeTypes:  normalizeMimeTypes(splitCSV(r.File.Upload.AllowedMimeTypes)),
			AllowedExtensions: normalizeExtensions(splitCSV(r.File.Upload.AllowedExtensions)),
			AllowEmptyFile:    r.File.Upload.AllowEmptyFile,
			MaxFilenameLength: r.File.Upload.MaxFilenameLength,
		},

		WOPI: WOPIConfig{
			TokenSecret:   strings.TrimSpace(r.WOPI.TokenSecret),
			ProofSecret:   strings.TrimSpace(r.WOPI.ProofSecret),
			DiscoveryURL:  strings.TrimSpace(r.WOPI.DiscoveryURL),
			PublicBaseURL: strings.TrimSpace(r.WOPI.PublicBaseURL),
			EditActionURL: strings.TrimSpace(r.WOPI.EditActionURL),
			TokenTTL:      r.WOPI.TokenTTL.Duration,
			MaxBodyBytes:  r.WOPI.MaxBodyBytes,
			RequireProof:  r.WOPI.RequireProof,
		},

		Gotenberg: GotenbergConfig{
			URL: strings.TrimSpace(r.Gotenberg.URL),
		},
	}
}

func splitCSV(s string) []string {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil
	}
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}
