// Package config provides typed configuration loaded from a YAML file or
// environment variables.
package config

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

// Config holds all server configuration.
type Config struct {
	Port       string
	BaseURL    string
	DBdsn      string
	GitRepoDir string

	// TenantID and the flat resource fields are the legacy single-tenant input
	// format. Normalize materializes them into exactly one TenantConfig and does
	// not retain them in the returned canonical Config.
	TenantID            string
	TenantDomainAliases []string
	SecretEncryptionKey string
	CORSAllowedOrigins  []string

	// Tenants is the normalized business-tenant configuration. Structured
	// multitenant input must not be combined with legacy tenant fields.
	Tenants []TenantConfig

	// TrustForwardedHost permits tenant resolution from X-Forwarded-Host.
	// Enable only behind an ingress that removes client-supplied values.
	TrustForwardedHost bool

	// ListenMode controls listener setup: "development" (default) starts
	// multiple listeners with TLS; "production" starts a single HTTP listener.
	ListenMode string

	// AllowAnyToken, when true, accepts any non-empty token when no
	// tokens exist in the database (dev-mode convenience).
	// Default is false (production-secure).
	AllowAnyToken bool

	// AdminLogin and AdminToken override the default seed credentials.
	// When both are empty the legacy testadmin / mytoken values are used.
	AdminLogin string
	AdminToken string

	// Environment controls operational behaviour that differs between
	// deployments. Allowed values: "production" (default, fail-closed) and
	// "development". When set to "development", test seed data is inserted
	// at startup. The default is "production" so that an unset variable
	// never silently seeds credentials.
	Environment string

	// Embedding provider configuration (all optional).
	// When EmbeddingAPIKey is empty, vector search is disabled and
	// search falls back to lexical-only matching.
	EmbeddingAPIKey  string
	EmbeddingBaseURL string
	EmbeddingModel   string
	// EmbeddingDimensions overrides the embedding vector size (0 = auto-detect).
	EmbeddingDimensions int

	OIDCProvider          string
	OIDCIssuer            string
	OIDCDiscoveryURL      string
	OIDCClientID          string
	OIDCClientSecret      string
	OIDCAudience          string
	OIDCScopes            string
	OIDCAllowInsecureHTTP bool

	// ConnectedLogin configures an OAuth-style browser login provider that does
	// not expose enough standard OIDC surface for the generic OIDC client.
	// Origin, APIOrigin, ClientID, and ClientSecret must be set together to
	// enable /auth/connected/login and /auth/connected/callback.
	ConnectedLoginProvider                  string
	ConnectedLoginOrigin                    string
	ConnectedLoginAPIOrigin                 string
	ConnectedLoginClientID                  string
	ConnectedLoginClientSecret              string
	ConnectedLoginLoginPath                 string
	ConnectedLoginTokenPath                 string
	ConnectedLoginUserinfoPath              string
	ConnectedLoginReturnToParam             string
	ConnectedLoginSubjectClaim              string
	ConnectedLoginSubjectNamespaceClaim     string
	ConnectedLoginSubjectNamespaceSlugClaim string
	ConnectedLoginActorTypeClaim            string
	ConnectedLoginHumanTypeValue            string
	ConnectedLoginAgentTypeValue            string
	ConnectedLoginClientIDClaim             string
	ConnectedLoginClientNameClaim           string
	ConnectedLoginPreferredUsernameClaim    string
	ConnectedLoginNameClaim                 string
	ConnectedLoginPictureClaim              string
	ConnectedLoginAvatarURLClaim            string
	ConnectedLoginDescriptionClaim          string
	ConnectedLoginScopeClaim                string

	// ConsoleBaseURL is the base URL of the console frontend used for browser redirects.
	ConsoleBaseURL string
	// OAuthDeviceVerificationURL is the optional external console URL shown to
	// device-flow users. When empty, the built-in /login/device fallback is used.
	OAuthDeviceVerificationURL string

	// Workflow execution sandbox configuration. Execution is fail-closed by
	// default and only enabled when ENABLE_WORKFLOW_EXEC is set.
	EnableWorkflowExec    bool
	WorkflowExecImage     string
	WorkflowExecTimeout   time.Duration
	WorkflowExecCPUs      string
	WorkflowExecMemory    string
	WorkflowExecPidsLimit int
	WorkflowExecNoFile    int
	WorkflowExecTmpfsSize string

	EnableRepositorySync             bool
	RepositorySyncGitHubAPIURL       string
	RepositorySyncGitHubWebURL       string
	RepositorySyncGitHubAppSlug      string
	RepositorySyncGitHubAppID        int64
	RepositorySyncGitHubPrivateKey   string
	RepositorySyncGitHubClientID     string
	RepositorySyncGitHubClientSecret string
	RepositorySyncPollInterval       time.Duration
	RepositorySyncLeaseTTL           time.Duration
	RepositorySyncFullAuditInterval  time.Duration
	RepositorySyncMaxConcurrency     int
	RepositorySyncMaxAssetBytes      int64
}

// New loads and normalizes application configuration. AGS_CONFIG_FILE selects
// the strict YAML format; otherwise the compatibility environment format is
// used.
func New() (Config, error) {
	if path := strings.TrimSpace(os.Getenv("AGS_CONFIG_FILE")); path != "" {
		if conflicts := configuredConfigFileEnvironment(); len(conflicts) > 0 {
			return Config{}, fmt.Errorf("AGS_CONFIG_FILE cannot be combined with environment configuration: %v", conflicts)
		}
		return loadConfigFile(path)
	}
	return newFromEnvironment()
}

func newFromEnvironment() (Config, error) {
	cfg := Config{
		Port:                                    os.Getenv("PORT"),
		TenantID:                                os.Getenv("TENANT_ID"),
		BaseURL:                                 os.Getenv("BASE_URL"),
		TenantDomainAliases:                     splitTenantCSV(os.Getenv("TENANT_DOMAIN_ALIASES")),
		ConsoleBaseURL:                          os.Getenv("CONSOLE_BASE_URL"),
		CORSAllowedOrigins:                      splitTenantCSV(os.Getenv("CORS_ALLOWED_ORIGINS")),
		OAuthDeviceVerificationURL:              os.Getenv("OAUTH_DEVICE_VERIFICATION_URL"),
		DBdsn:                                   os.Getenv("DB_DSN"),
		GitRepoDir:                              os.Getenv("GIT_REPO_DIR"),
		SecretEncryptionKey:                     os.Getenv("SECRET_ENCRYPTION_KEY"),
		TrustForwardedHost:                      os.Getenv("TRUST_FORWARDED_HOST") == "true" || os.Getenv("TRUST_FORWARDED_HOST") == "1",
		ListenMode:                              os.Getenv("LISTEN_MODE"),
		AllowAnyToken:                           os.Getenv("ALLOW_ANY_TOKEN") == "true" || os.Getenv("ALLOW_ANY_TOKEN") == "1",
		AdminLogin:                              os.Getenv("ADMIN_LOGIN"),
		AdminToken:                              os.Getenv("ADMIN_TOKEN"),
		Environment:                             os.Getenv("ENVIRONMENT"),
		EmbeddingAPIKey:                         os.Getenv("EMBEDDING_API_KEY"),
		EmbeddingBaseURL:                        os.Getenv("EMBEDDING_BASE_URL"),
		EmbeddingModel:                          os.Getenv("EMBEDDING_MODEL"),
		OIDCProvider:                            os.Getenv("OIDC_PROVIDER"),
		OIDCIssuer:                              os.Getenv("OIDC_ISSUER"),
		OIDCDiscoveryURL:                        os.Getenv("OIDC_DISCOVERY_URL"),
		OIDCClientID:                            os.Getenv("OIDC_CLIENT_ID"),
		OIDCClientSecret:                        os.Getenv("OIDC_CLIENT_SECRET"),
		OIDCAudience:                            os.Getenv("OIDC_AUDIENCE"),
		OIDCScopes:                              os.Getenv("OIDC_SCOPES"),
		OIDCAllowInsecureHTTP:                   os.Getenv("OIDC_ALLOW_INSECURE_HTTP") == "true" || os.Getenv("OIDC_ALLOW_INSECURE_HTTP") == "1",
		ConnectedLoginProvider:                  os.Getenv("CONNECTED_LOGIN_PROVIDER"),
		ConnectedLoginOrigin:                    os.Getenv("CONNECTED_LOGIN_ORIGIN"),
		ConnectedLoginAPIOrigin:                 os.Getenv("CONNECTED_LOGIN_API_ORIGIN"),
		ConnectedLoginClientID:                  os.Getenv("CONNECTED_LOGIN_CLIENT_ID"),
		ConnectedLoginClientSecret:              os.Getenv("CONNECTED_LOGIN_CLIENT_SECRET"),
		ConnectedLoginLoginPath:                 os.Getenv("CONNECTED_LOGIN_LOGIN_PATH"),
		ConnectedLoginTokenPath:                 os.Getenv("CONNECTED_LOGIN_TOKEN_PATH"),
		ConnectedLoginUserinfoPath:              os.Getenv("CONNECTED_LOGIN_USERINFO_PATH"),
		ConnectedLoginReturnToParam:             os.Getenv("CONNECTED_LOGIN_RETURN_TO_PARAM"),
		ConnectedLoginSubjectClaim:              os.Getenv("CONNECTED_LOGIN_SUBJECT_CLAIM"),
		ConnectedLoginSubjectNamespaceClaim:     os.Getenv("CONNECTED_LOGIN_SUBJECT_NAMESPACE_CLAIM"),
		ConnectedLoginSubjectNamespaceSlugClaim: os.Getenv("CONNECTED_LOGIN_SUBJECT_NAMESPACE_SLUG_CLAIM"),
		ConnectedLoginActorTypeClaim:            os.Getenv("CONNECTED_LOGIN_ACTOR_TYPE_CLAIM"),
		ConnectedLoginHumanTypeValue:            os.Getenv("CONNECTED_LOGIN_HUMAN_TYPE_VALUE"),
		ConnectedLoginAgentTypeValue:            os.Getenv("CONNECTED_LOGIN_AGENT_TYPE_VALUE"),
		ConnectedLoginClientIDClaim:             os.Getenv("CONNECTED_LOGIN_CLIENT_ID_CLAIM"),
		ConnectedLoginClientNameClaim:           os.Getenv("CONNECTED_LOGIN_CLIENT_NAME_CLAIM"),
		ConnectedLoginPreferredUsernameClaim:    os.Getenv("CONNECTED_LOGIN_PREFERRED_USERNAME_CLAIM"),
		ConnectedLoginNameClaim:                 os.Getenv("CONNECTED_LOGIN_NAME_CLAIM"),
		ConnectedLoginPictureClaim:              os.Getenv("CONNECTED_LOGIN_PICTURE_CLAIM"),
		ConnectedLoginAvatarURLClaim:            os.Getenv("CONNECTED_LOGIN_AVATAR_URL_CLAIM"),
		ConnectedLoginDescriptionClaim:          os.Getenv("CONNECTED_LOGIN_DESCRIPTION_CLAIM"),
		ConnectedLoginScopeClaim:                os.Getenv("CONNECTED_LOGIN_SCOPE_CLAIM"),
		EnableWorkflowExec:                      os.Getenv("ENABLE_WORKFLOW_EXEC") == "true" || os.Getenv("ENABLE_WORKFLOW_EXEC") == "1",
		WorkflowExecImage:                       os.Getenv("WORKFLOW_EXEC_IMAGE"),
		WorkflowExecCPUs:                        os.Getenv("WORKFLOW_EXEC_CPUS"),
		WorkflowExecMemory:                      os.Getenv("WORKFLOW_EXEC_MEMORY"),
		WorkflowExecTmpfsSize:                   os.Getenv("WORKFLOW_EXEC_TMPFS_SIZE"),
		EnableRepositorySync:                    os.Getenv("ENABLE_REPOSITORY_SYNC") == "true" || os.Getenv("ENABLE_REPOSITORY_SYNC") == "1",
		RepositorySyncGitHubAPIURL:              os.Getenv("REPOSITORY_SYNC_GITHUB_API_URL"),
		RepositorySyncGitHubWebURL:              os.Getenv("REPOSITORY_SYNC_GITHUB_WEB_URL"),
		RepositorySyncGitHubAppSlug:             os.Getenv("REPOSITORY_SYNC_GITHUB_APP_SLUG"),
		RepositorySyncGitHubPrivateKey:          os.Getenv("REPOSITORY_SYNC_GITHUB_APP_PRIVATE_KEY"),
		RepositorySyncGitHubClientID:            os.Getenv("REPOSITORY_SYNC_GITHUB_CLIENT_ID"),
		RepositorySyncGitHubClientSecret:        os.Getenv("REPOSITORY_SYNC_GITHUB_CLIENT_SECRET"),
	}
	if raw := strings.TrimSpace(os.Getenv("TENANT_CONFIG")); raw != "" {
		if conflicts := configuredLegacyTenantEnvironment(); len(conflicts) > 0 {
			return Config{}, fmt.Errorf("TENANT_CONFIG cannot be combined with legacy tenant environment variables: %v", conflicts)
		}
		tenants, err := parseTenantConfigJSON(raw)
		if err != nil {
			return Config{}, err
		}
		cfg.Tenants = tenants
	}
	if v := os.Getenv("EMBEDDING_DIMENSIONS"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, fmt.Errorf("invalid EMBEDDING_DIMENSIONS %q: must be a non-negative integer", v)
		}
		cfg.EmbeddingDimensions = n
	}
	if v := os.Getenv("WORKFLOW_EXEC_TIMEOUT"); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil {
			return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_TIMEOUT %q: must be a positive duration", v)
		}
		cfg.WorkflowExecTimeout = d
	}
	if v := os.Getenv("WORKFLOW_EXEC_PIDS_LIMIT"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n <= 0 {
			return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_PIDS_LIMIT %q: must be a positive integer", v)
		}
		cfg.WorkflowExecPidsLimit = n
	}
	if v := os.Getenv("WORKFLOW_EXEC_NOFILE"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n <= 0 {
			return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_NOFILE %q: must be a positive integer", v)
		}
		cfg.WorkflowExecNoFile = n
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_GITHUB_APP_ID")); v != "" {
		n, err := strconv.ParseInt(v, 10, 64)
		if err != nil || n <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_GITHUB_APP_ID %q", v)
		}
		cfg.RepositorySyncGitHubAppID = n
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_POLL_INTERVAL")); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil || d <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_POLL_INTERVAL %q", v)
		}
		cfg.RepositorySyncPollInterval = d
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_LEASE_TTL")); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil || d <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_LEASE_TTL %q", v)
		}
		cfg.RepositorySyncLeaseTTL = d
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_FULL_AUDIT_INTERVAL")); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil || d <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_FULL_AUDIT_INTERVAL %q", v)
		}
		cfg.RepositorySyncFullAuditInterval = d
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_MAX_CONCURRENCY")); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_MAX_CONCURRENCY %q", v)
		}
		cfg.RepositorySyncMaxConcurrency = n
	}
	if v := strings.TrimSpace(os.Getenv("REPOSITORY_SYNC_MAX_ASSET_BYTES")); v != "" {
		n, err := strconv.ParseInt(v, 10, 64)
		if err != nil || n <= 0 {
			return Config{}, fmt.Errorf("invalid REPOSITORY_SYNC_MAX_ASSET_BYTES %q", v)
		}
		cfg.RepositorySyncMaxAssetBytes = n
	}
	return Normalize(cfg)
}

// Normalize applies defaults and validates a programmatically supplied config.
func Normalize(cfg Config) (Config, error) {
	explicitTenants := len(cfg.Tenants) > 0
	if explicitTenants {
		if fields := configuredLegacyTenantFields(cfg); len(fields) > 0 {
			return Config{}, fmt.Errorf("structured tenants cannot be combined with legacy tenant fields: %v", fields)
		}
	}
	cfg.Port = firstNonEmpty(cfg.Port, "8080")
	cfg.ListenMode = firstNonEmpty(cfg.ListenMode, "development")
	cfg.Environment = strings.ToLower(strings.TrimSpace(firstNonEmpty(cfg.Environment, "production")))
	if cfg.ListenMode != "production" && cfg.ListenMode != "development" {
		return Config{}, fmt.Errorf("invalid LISTEN_MODE %q: must be \"production\" or \"development\"", cfg.ListenMode)
	}
	if cfg.Environment != "production" && cfg.Environment != "development" {
		return Config{}, fmt.Errorf("invalid ENVIRONMENT %q: must be \"production\" or \"development\"", cfg.Environment)
	}
	if !explicitTenants {
		var err error
		cfg, err = normalizeLegacyTenantConfig(cfg)
		if err != nil {
			return Config{}, err
		}
	}
	tenants, err := normalizeTenantConfigs(cfg, explicitTenants)
	if err != nil {
		return Config{}, err
	}
	return Config{
		Port:               cfg.Port,
		Tenants:            tenants,
		TrustForwardedHost: cfg.TrustForwardedHost,
		ListenMode:         cfg.ListenMode,
		Environment:        cfg.Environment,
	}, nil
}

func normalizeLegacyTenantConfig(cfg Config) (Config, error) {
	cfg.BaseURL = firstNonEmpty(cfg.BaseURL, "http://localhost:8080")
	cfg.ConsoleBaseURL = firstNonEmpty(cfg.ConsoleBaseURL, "http://localhost:5173")
	cfg.OAuthDeviceVerificationURL = strings.TrimSpace(cfg.OAuthDeviceVerificationURL)
	cfg.GitRepoDir = firstNonEmpty(cfg.GitRepoDir, "gitrepos")
	cfg.EmbeddingBaseURL = firstNonEmpty(cfg.EmbeddingBaseURL, "https://api.openai.com")
	cfg.EmbeddingModel = firstNonEmpty(cfg.EmbeddingModel, "text-embedding-3-small")
	cfg.OIDCScopes = firstNonEmpty(strings.TrimSpace(cfg.OIDCScopes), "openid profile email")
	cfg.WorkflowExecImage = firstNonEmpty(cfg.WorkflowExecImage, "bash:5.2")
	if cfg.WorkflowExecTimeout == 0 {
		cfg.WorkflowExecTimeout = 2 * time.Minute
	}
	if cfg.WorkflowExecCPUs == "" {
		cfg.WorkflowExecCPUs = "1.0"
	}
	if cfg.WorkflowExecMemory == "" {
		cfg.WorkflowExecMemory = "256m"
	}
	if cfg.WorkflowExecPidsLimit == 0 {
		cfg.WorkflowExecPidsLimit = 128
	}
	if cfg.WorkflowExecNoFile == 0 {
		cfg.WorkflowExecNoFile = 1024
	}
	if cfg.WorkflowExecTmpfsSize == "" {
		cfg.WorkflowExecTmpfsSize = "64m"
	}
	if cfg.DBdsn == "" {
		return Config{}, fmt.Errorf("required environment variable not set: DB_DSN")
	}
	if strings.TrimSpace(cfg.TenantID) == "" {
		return Config{}, fmt.Errorf("required environment variable not set: TENANT_ID")
	}
	if cfg.EmbeddingDimensions < 0 {
		return Config{}, fmt.Errorf("invalid EMBEDDING_DIMENSIONS %d: must be a non-negative integer", cfg.EmbeddingDimensions)
	}
	if cfg.WorkflowExecTimeout <= 0 {
		return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_TIMEOUT %q: must be a positive duration", cfg.WorkflowExecTimeout)
	}
	if cfg.WorkflowExecPidsLimit <= 0 {
		return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_PIDS_LIMIT %d: must be a positive integer", cfg.WorkflowExecPidsLimit)
	}
	if cfg.WorkflowExecNoFile <= 0 {
		return Config{}, fmt.Errorf("invalid WORKFLOW_EXEC_NOFILE %d: must be a positive integer", cfg.WorkflowExecNoFile)
	}
	if cfg.OAuthDeviceVerificationURL != "" {
		parsed, err := url.Parse(cfg.OAuthDeviceVerificationURL)
		if err != nil || parsed.Scheme == "" || parsed.Host == "" {
			return Config{}, fmt.Errorf("invalid OAUTH_DEVICE_VERIFICATION_URL %q: must be an absolute HTTP(S) URL", cfg.OAuthDeviceVerificationURL)
		}
		if parsed.Scheme != "http" && parsed.Scheme != "https" {
			return Config{}, fmt.Errorf("invalid OAUTH_DEVICE_VERIFICATION_URL %q: must use http or https", cfg.OAuthDeviceVerificationURL)
		}
	}
	if strings.TrimSpace(cfg.OIDCProvider) == "" && (cfg.OIDCIssuer != "" || cfg.OIDCDiscoveryURL != "" || cfg.OIDCClientID != "") {
		cfg.OIDCProvider = defaultOIDCProvider(cfg.OIDCIssuer, cfg.OIDCDiscoveryURL)
	}
	cfg.ConnectedLoginProvider = strings.TrimSpace(cfg.ConnectedLoginProvider)
	cfg.ConnectedLoginOrigin = strings.TrimSpace(cfg.ConnectedLoginOrigin)
	cfg.ConnectedLoginAPIOrigin = strings.TrimSpace(cfg.ConnectedLoginAPIOrigin)
	cfg.ConnectedLoginClientID = strings.TrimSpace(cfg.ConnectedLoginClientID)
	cfg.ConnectedLoginClientSecret = strings.TrimSpace(cfg.ConnectedLoginClientSecret)
	cfg.ConnectedLoginLoginPath = strings.TrimSpace(cfg.ConnectedLoginLoginPath)
	cfg.ConnectedLoginTokenPath = strings.TrimSpace(cfg.ConnectedLoginTokenPath)
	cfg.ConnectedLoginUserinfoPath = strings.TrimSpace(cfg.ConnectedLoginUserinfoPath)
	cfg.ConnectedLoginReturnToParam = strings.TrimSpace(cfg.ConnectedLoginReturnToParam)
	cfg.ConnectedLoginSubjectClaim = strings.TrimSpace(cfg.ConnectedLoginSubjectClaim)
	cfg.ConnectedLoginSubjectNamespaceClaim = strings.TrimSpace(cfg.ConnectedLoginSubjectNamespaceClaim)
	cfg.ConnectedLoginSubjectNamespaceSlugClaim = strings.TrimSpace(cfg.ConnectedLoginSubjectNamespaceSlugClaim)
	cfg.ConnectedLoginActorTypeClaim = strings.TrimSpace(cfg.ConnectedLoginActorTypeClaim)
	cfg.ConnectedLoginHumanTypeValue = strings.TrimSpace(cfg.ConnectedLoginHumanTypeValue)
	cfg.ConnectedLoginAgentTypeValue = strings.TrimSpace(cfg.ConnectedLoginAgentTypeValue)
	cfg.ConnectedLoginClientIDClaim = strings.TrimSpace(cfg.ConnectedLoginClientIDClaim)
	cfg.ConnectedLoginClientNameClaim = strings.TrimSpace(cfg.ConnectedLoginClientNameClaim)
	cfg.ConnectedLoginPreferredUsernameClaim = strings.TrimSpace(cfg.ConnectedLoginPreferredUsernameClaim)
	cfg.ConnectedLoginNameClaim = strings.TrimSpace(cfg.ConnectedLoginNameClaim)
	cfg.ConnectedLoginPictureClaim = strings.TrimSpace(cfg.ConnectedLoginPictureClaim)
	cfg.ConnectedLoginAvatarURLClaim = strings.TrimSpace(cfg.ConnectedLoginAvatarURLClaim)
	cfg.ConnectedLoginDescriptionClaim = strings.TrimSpace(cfg.ConnectedLoginDescriptionClaim)
	cfg.ConnectedLoginScopeClaim = strings.TrimSpace(cfg.ConnectedLoginScopeClaim)
	if err := validateConnectedLoginConfig(cfg); err != nil {
		return Config{}, err
	}
	if cfg.ConnectedLoginEnabled() {
		cfg.ConnectedLoginProvider = firstNonEmpty(cfg.ConnectedLoginProvider, "connected")
		cfg.ConnectedLoginLoginPath = firstNonEmpty(cfg.ConnectedLoginLoginPath, "/oauth/login")
		cfg.ConnectedLoginTokenPath = firstNonEmpty(cfg.ConnectedLoginTokenPath, "/api/oauth/token")
		cfg.ConnectedLoginUserinfoPath = firstNonEmpty(cfg.ConnectedLoginUserinfoPath, "/api/oauth/userinfo")
		cfg.ConnectedLoginReturnToParam = firstNonEmpty(cfg.ConnectedLoginReturnToParam, "return_to")
		cfg.ConnectedLoginSubjectClaim = firstNonEmpty(cfg.ConnectedLoginSubjectClaim, "sub")
		cfg.ConnectedLoginActorTypeClaim = firstNonEmpty(cfg.ConnectedLoginActorTypeClaim, "type")
		cfg.ConnectedLoginHumanTypeValue = firstNonEmpty(cfg.ConnectedLoginHumanTypeValue, "human")
		cfg.ConnectedLoginAgentTypeValue = firstNonEmpty(cfg.ConnectedLoginAgentTypeValue, "agent")
		cfg.ConnectedLoginClientIDClaim = firstNonEmpty(cfg.ConnectedLoginClientIDClaim, "client_id")
		cfg.ConnectedLoginClientNameClaim = firstNonEmpty(cfg.ConnectedLoginClientNameClaim, "client_name")
		cfg.ConnectedLoginPreferredUsernameClaim = firstNonEmpty(cfg.ConnectedLoginPreferredUsernameClaim, "preferred_username")
		cfg.ConnectedLoginNameClaim = firstNonEmpty(cfg.ConnectedLoginNameClaim, "name")
		cfg.ConnectedLoginPictureClaim = firstNonEmpty(cfg.ConnectedLoginPictureClaim, "picture")
		cfg.ConnectedLoginAvatarURLClaim = firstNonEmpty(cfg.ConnectedLoginAvatarURLClaim, "avatar_url")
		cfg.ConnectedLoginDescriptionClaim = firstNonEmpty(cfg.ConnectedLoginDescriptionClaim, "description")
		cfg.ConnectedLoginScopeClaim = firstNonEmpty(cfg.ConnectedLoginScopeClaim, "scope")
	}
	return cfg, nil
}

// ConnectedLoginEnabled reports whether connected login is configured.
func (c Config) ConnectedLoginEnabled() bool {
	return strings.TrimSpace(c.ConnectedLoginOrigin) != "" &&
		strings.TrimSpace(c.ConnectedLoginAPIOrigin) != "" &&
		strings.TrimSpace(c.ConnectedLoginClientID) != "" &&
		strings.TrimSpace(c.ConnectedLoginClientSecret) != ""
}

func firstNonEmpty(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func defaultOIDCProvider(issuer, discoveryURL string) string {
	if looksLikeAuth0Issuer(issuer) || looksLikeAuth0Issuer(discoveryURL) {
		return "auth0"
	}
	return "oidc"
}

func looksLikeAuth0Issuer(raw string) bool {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return false
	}
	u, err := url.Parse(raw)
	if err != nil {
		return false
	}
	host := strings.ToLower(strings.TrimSpace(u.Hostname()))
	return host == "auth0.com" || strings.HasSuffix(host, ".auth0.com")
}

func validateConnectedLoginConfig(cfg Config) error {
	type envValue struct {
		name  string
		value string
	}
	required := []envValue{
		{name: "CONNECTED_LOGIN_ORIGIN", value: cfg.ConnectedLoginOrigin},
		{name: "CONNECTED_LOGIN_API_ORIGIN", value: cfg.ConnectedLoginAPIOrigin},
		{name: "CONNECTED_LOGIN_CLIENT_ID", value: cfg.ConnectedLoginClientID},
		{name: "CONNECTED_LOGIN_CLIENT_SECRET", value: cfg.ConnectedLoginClientSecret},
	}
	optional := []envValue{
		{name: "CONNECTED_LOGIN_PROVIDER", value: cfg.ConnectedLoginProvider},
		{name: "CONNECTED_LOGIN_LOGIN_PATH", value: cfg.ConnectedLoginLoginPath},
		{name: "CONNECTED_LOGIN_TOKEN_PATH", value: cfg.ConnectedLoginTokenPath},
		{name: "CONNECTED_LOGIN_USERINFO_PATH", value: cfg.ConnectedLoginUserinfoPath},
		{name: "CONNECTED_LOGIN_RETURN_TO_PARAM", value: cfg.ConnectedLoginReturnToParam},
		{name: "CONNECTED_LOGIN_SUBJECT_CLAIM", value: cfg.ConnectedLoginSubjectClaim},
		{name: "CONNECTED_LOGIN_SUBJECT_NAMESPACE_CLAIM", value: cfg.ConnectedLoginSubjectNamespaceClaim},
		{name: "CONNECTED_LOGIN_SUBJECT_NAMESPACE_SLUG_CLAIM", value: cfg.ConnectedLoginSubjectNamespaceSlugClaim},
		{name: "CONNECTED_LOGIN_ACTOR_TYPE_CLAIM", value: cfg.ConnectedLoginActorTypeClaim},
		{name: "CONNECTED_LOGIN_HUMAN_TYPE_VALUE", value: cfg.ConnectedLoginHumanTypeValue},
		{name: "CONNECTED_LOGIN_AGENT_TYPE_VALUE", value: cfg.ConnectedLoginAgentTypeValue},
		{name: "CONNECTED_LOGIN_CLIENT_ID_CLAIM", value: cfg.ConnectedLoginClientIDClaim},
		{name: "CONNECTED_LOGIN_CLIENT_NAME_CLAIM", value: cfg.ConnectedLoginClientNameClaim},
		{name: "CONNECTED_LOGIN_PREFERRED_USERNAME_CLAIM", value: cfg.ConnectedLoginPreferredUsernameClaim},
		{name: "CONNECTED_LOGIN_NAME_CLAIM", value: cfg.ConnectedLoginNameClaim},
		{name: "CONNECTED_LOGIN_PICTURE_CLAIM", value: cfg.ConnectedLoginPictureClaim},
		{name: "CONNECTED_LOGIN_AVATAR_URL_CLAIM", value: cfg.ConnectedLoginAvatarURLClaim},
		{name: "CONNECTED_LOGIN_DESCRIPTION_CLAIM", value: cfg.ConnectedLoginDescriptionClaim},
		{name: "CONNECTED_LOGIN_SCOPE_CLAIM", value: cfg.ConnectedLoginScopeClaim},
	}
	var set, missing []string
	for _, item := range required {
		if strings.TrimSpace(item.value) == "" {
			missing = append(missing, item.name)
			continue
		}
		set = append(set, item.name)
	}
	for _, item := range optional {
		if strings.TrimSpace(item.value) != "" {
			set = append(set, item.name)
		}
	}
	if len(set) > 0 && len(missing) > 0 {
		return fmt.Errorf("connected login: partial configuration; set %v, missing %v", set, missing)
	}
	return nil
}
