package config

import (
	"context"
	"net/url"
	"os"
	"strings"
)

type AuthConfig struct {
	// Browser social sign-in. The redirect URIs are where the provider sends
	// the browser back, which is the API, not the dashboard: both default to
	// API_PUBLIC_URL plus /v1/auth/<provider>/callback when left unset.
	GoogleClientID     string
	GoogleRedirectURI  string
	GoogleClientSecret string

	// AppleAppID is the Services ID (the web identifier), not the app's
	// bundle ID.
	AppleAppID       string
	AppleTeamID      string
	AppleKeyID       string
	AppleKeySecret   string
	AppleRedirectURI string

	// Native-app (iOS) social sign-in. ID-token verification only needs the
	// expected audiences: the app's bundle ID for Sign in with Apple (fixed by
	// the shipped binary) and the iOS OAuth client ID for Google (empty
	// disables the Google option and hides its button in the app).
	AppleIOSBundleID  string
	GoogleIOSClientID string

	AuthSecret      string
	TurnstileSecret string
	TurnstileBypass string

	// TwoFASecret is the server-wide key the TOTP secret is sealed with (AES-GCM).
	// Must be set when 2FA is in use; falls back to AuthSecret when unset so a
	// deployment without the env var still functions (sealed under the auth key).
	TwoFASecret string

	// WebAuthn / passkey relying-party configuration.
	//
	// WebAuthnRPID is the relying-party ID a passkey is cryptographically
	// bound to — the registrable domain the dashboard is served from
	// (e.g. "app.warmbly.com"), with no scheme or port. WebAuthnRPOrigins
	// are the full scheme-qualified origins allowed to run ceremonies
	// (e.g. "https://app.warmbly.com"). Both are derived from APP_URL /
	// CORS_ALLOW_ORIGINS so self-hosted and local-dev deployments work out
	// of the box, and can be overridden explicitly with WEBAUTHN_RP_ID /
	// WEBAUTHN_RP_ORIGINS. Changing the RP ID invalidates every enrolled
	// passkey, so it must stay stable for a given deployment.
	WebAuthnRPID          string
	WebAuthnRPDisplayName string
	WebAuthnRPOrigins     []string
}

func (c *Config) LoadAuthConfig(ctx context.Context) (*AuthConfig, error) {
	// Social sign-in (Google/Apple) and captcha (Turnstile) are optional: a
	// self-hosted install running only email+password / passkeys needs none of
	// them, so leaving these unset disables the corresponding provider rather
	// than failing boot. AUTH_SECRET (JWT/session signing) stays required.
	googleClientID := c.GetStringOptional(ctx, "GOOGLE_CLIENT_ID", "google-auth/client_id", "")
	googleRedirectURI := c.GetStringOptional(ctx, "GOOGLE_REDIRECT_URI", "google-auth/redirect_uri", "")
	googleClientSecret := c.GetSecretOptional(ctx, "GOOGLE_CLIENT_SECRET", "google-auth/client_secret", "")

	appleAppID := c.GetStringOptional(ctx, "APPLE_APP_ID", "apple-auth/app_id", "")
	appleRedirectURI := c.GetStringOptional(ctx, "APPLE_REDIRECT_URI", "apple-auth/redirect_uri", "")
	appleTeamID := c.GetStringOptional(ctx, "APPLE_TEAM_ID", "apple-auth/team_id", "")
	appleKeyID := c.GetStringOptional(ctx, "APPLE_KEY_ID", "apple-auth/key_id", "")
	appleKeySecret := c.GetSecretOptional(ctx, "APPLE_KEY_SECRET", "apple-auth/key_secret", "")

	appleIOSBundleID := c.GetStringOptional(ctx, "APPLE_IOS_BUNDLE_ID", "apple-auth/ios_bundle_id", "com.warmbly.app")
	googleIOSClientID := c.GetStringOptional(ctx, "GOOGLE_IOS_CLIENT_ID", "google-auth/ios_client_id", "")

	authSecret, err := c.GetSecret(ctx, "AUTH_SECRET", "auth_secret")
	if err != nil {
		return nil, err
	}

	turnstileSecret := c.GetSecretOptional(ctx, "TURNSTILE_SECRET", "turnstile/secret", "")
	turnstileBypass := c.GetSecretOptional(ctx, "TURNSTILE_BYPASS_TOKEN", "turnstile/bypass_token", "")

	// 2FA sealing key. Optional: falls back to AUTH_SECRET so existing
	// deployments keep working; rotating it invalidates enrolled TOTP secrets.
	twoFASecret := c.GetSecretOptional(ctx, "TWOFA_SECRET", "twofa_secret", authSecret)

	rpDisplayName := c.GetStringOptional(ctx, "WEBAUTHN_RP_DISPLAY_NAME", "webauthn/rp_display_name", "Warmbly")
	rpIDRaw := c.GetStringOptional(ctx, "WEBAUTHN_RP_ID", "webauthn/rp_id", "")
	rpOriginsRaw := c.GetStringOptional(ctx, "WEBAUTHN_RP_ORIGINS", "webauthn/rp_origins", "")
	rpID, rpOrigins := resolveWebAuthnRP(rpIDRaw, rpOriginsRaw)

	return &AuthConfig{
		GoogleClientID:     googleClientID,
		GoogleClientSecret: googleClientSecret,
		GoogleRedirectURI:  googleRedirectURI,

		AppleAppID:       appleAppID,
		AppleTeamID:      appleTeamID,
		AppleKeyID:       appleKeyID,
		AppleKeySecret:   appleKeySecret,
		AppleRedirectURI: appleRedirectURI,

		AppleIOSBundleID:  appleIOSBundleID,
		GoogleIOSClientID: googleIOSClientID,

		AuthSecret:      authSecret,
		TurnstileSecret: turnstileSecret,
		TurnstileBypass: turnstileBypass,
		TwoFASecret:     twoFASecret,

		WebAuthnRPID:          rpID,
		WebAuthnRPDisplayName: rpDisplayName,
		WebAuthnRPOrigins:     rpOrigins,
	}, nil
}

// resolveWebAuthnRP derives the passkey relying-party ID and allowed origins.
//
// Origins come from (in order): an explicit WEBAUTHN_RP_ORIGINS list, then
// CORS_ALLOW_ORIGINS, then APP_URL, then a localhost dev fallback. The RP ID
// is an explicit override if given, otherwise the host of the first origin
// (scheme and port stripped), otherwise "localhost". This keeps prod, self
// hosted, and local-dev deployments correct without per-environment code.
func resolveWebAuthnRP(explicitRPID, explicitOrigins string) (string, []string) {
	originsRaw := explicitOrigins
	if originsRaw == "" {
		originsRaw = os.Getenv("CORS_ALLOW_ORIGINS")
	}
	if originsRaw == "" {
		originsRaw = os.Getenv("APP_URL")
	}

	origins := splitCSV(originsRaw)
	if len(origins) == 0 {
		origins = []string{"http://localhost:5173"}
	}

	rpID := strings.TrimSpace(explicitRPID)
	if rpID == "" {
		rpID = hostFromOrigin(origins[0])
	}
	if rpID == "" {
		rpID = "localhost"
	}

	return rpID, origins
}

// hostFromOrigin returns the bare hostname of an origin (no scheme, no port),
// suitable for use as a WebAuthn RP ID. Returns "" if the origin can't be
// parsed into a host (e.g. a wildcard "*").
func hostFromOrigin(origin string) string {
	u, err := url.Parse(strings.TrimSpace(origin))
	if err != nil || u.Hostname() == "" {
		return ""
	}
	return u.Hostname()
}
