package idp

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"

	"github.com/dexidp/dex/storage"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/idp/dex"
	"github.com/netbirdio/netbird/management/server/telemetry"
	nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
)

const (
	StaticClientDashboard      = "netbird-dashboard"
	StaticClientCLI            = "netbird-cli"
	DefaultTOTPAuthenticatorID = "default-totp"
	LocalConnectorID           = dex.LocalConnectorID

	defaultCLIRedirectURL1        = "http://localhost:53000/"
	defaultCLIRedirectURL2        = "http://localhost:54000/"
	defaultScopes                 = "openid profile email groups"
	defaultUserIDClaim            = "sub"
	sessionCookieEncryptionKeyEnv = "NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY"
)

// EmbeddedIdPConfig contains configuration for the embedded Dex OIDC identity provider
type EmbeddedIdPConfig struct {
	// Enabled indicates whether the embedded IDP is enabled
	Enabled bool
	// Issuer is the OIDC issuer URL (e.g., "https://management.netbird.io/oauth2")
	Issuer string
	// LocalAddress is the management server's local listen address (e.g., ":8080" or "localhost:8080")
	// Used for internal JWT validation to avoid external network calls
	LocalAddress string
	// Storage configuration for the IdP database
	Storage EmbeddedStorageConfig
	// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
	DashboardRedirectURIs []string
	// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
	CLIRedirectURIs []string
	// Owner is the initial owner/admin user (optional, can be nil)
	Owner *OwnerConfig
	// SignKeyRefreshEnabled enables automatic key rotation for signing keys
	SignKeyRefreshEnabled bool
	// LocalAuthDisabled disables the local (email/password) authentication connector.
	// When true, users cannot authenticate via email/password, only via external identity providers.
	// Existing local users are preserved and will be able to login again if re-enabled.
	// Cannot be enabled if no external identity provider connectors are configured.
	LocalAuthDisabled bool
	// MfaSessionMaxLifetime is the maximum MFA session duration from creation (e.g. "24h").
	// Defaults to "24h" if empty.
	MfaSessionMaxLifetime string
	// MfaSessionIdleTimeout is the idle timeout after which the MFA session expires (e.g. "1h").
	// Defaults to "1h" if empty.
	MfaSessionIdleTimeout string
	// MfaSessionRememberMe controls the default state of the "remember me" checkbox on the
	// login screen. When true, the session cookie persists across browser tabs/restarts so
	// MFA is not re-prompted until the session expires. Defaults to false.
	MfaSessionRememberMe bool
	// SessionCookieEncryptionKey is the optional AES key used to encrypt embedded IdP session cookies.
	// It can also be set with NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY. The value must be 16, 24, or 32
	// bytes when provided as a raw string, or base64-encoded to one of those lengths.
	SessionCookieEncryptionKey string
	// Dashboard Post logout redirect URIs, these are required to tell
	// Dex what to allow when an RP-Initiated logout is started by the frontend
	// at least one of these must match the dashboard base URL or the dashboard
	// DASHBOARD_POST_LOGOUT_URL environment variable
	// WARNING: Dex only uses exact match, not wildcards
	DashboardPostLogoutRedirectURIs []string
	// StaticConnectors are additional connectors to seed during initialization
	StaticConnectors []dex.Connector
	// GrantTypes restricts allowed OAuth2 grants; empty means all (Dex default). Omit the
	// device_code grant to disable the device flow; keep authorization_code and refresh_token.
	GrantTypes []string
}

// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
type EmbeddedStorageConfig struct {
	// Type is the storage type: "sqlite3" (default) or "postgres"
	Type string
	// Config contains type-specific configuration
	Config EmbeddedStorageTypeConfig
}

// EmbeddedStorageTypeConfig contains type-specific storage configuration.
type EmbeddedStorageTypeConfig struct {
	// File is the path to the SQLite database file (for sqlite3 type)
	File string
	// DSN is the connection string for postgres
	DSN string
}

// OwnerConfig represents the initial owner/admin user for the embedded IdP.
type OwnerConfig struct {
	// Email is the user's email address (required)
	Email string
	// Hash is the bcrypt hash of the user's password (required)
	Hash string
	// Username is the display name for the user (optional, defaults to email)
	Username string
}

// buildIdpStorageConfig builds the Dex storage config map based on the storage type.
func buildIdpStorageConfig(storageType string, cfg EmbeddedStorageTypeConfig) (map[string]interface{}, error) {
	switch storageType {
	case "sqlite3":
		return map[string]interface{}{
			"file": cfg.File,
		}, nil
	case "postgres":
		return map[string]interface{}{
			"dsn": cfg.DSN,
		}, nil
	default:
		return nil, fmt.Errorf("unsupported IdP storage type: %s", storageType)
	}
}

// ToYAMLConfig converts EmbeddedIdPConfig to dex.YAMLConfig.
func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
	if c.Issuer == "" {
		return nil, fmt.Errorf("issuer is required")
	}
	if c.Storage.Type == "" {
		c.Storage.Type = "sqlite3"
	}
	if c.Storage.Type == "sqlite3" && c.Storage.Config.File == "" {
		return nil, fmt.Errorf("storage file is required for sqlite3")
	}
	if c.Storage.Type == "postgres" && c.Storage.Config.DSN == "" {
		return nil, fmt.Errorf("storage DSN is required for postgres")
	}

	storageConfig, err := buildIdpStorageConfig(c.Storage.Type, c.Storage.Config)
	if err != nil {
		return nil, fmt.Errorf("invalid IdP storage config: %w", err)
	}

	// Build CLI redirect URIs including the device callback. Dex uses the issuer-relative
	// path (for example, /oauth2/device/callback) when completing the device flow, so
	// include it explicitly in addition to the legacy bare path and absolute URL.
	cliRedirectURIs := c.CLIRedirectURIs
	cliRedirectURIs = append(cliRedirectURIs, "/device/callback")
	cliRedirectURIs = append(cliRedirectURIs, issuerRelativeDeviceCallback(c.Issuer))
	cliRedirectURIs = append(cliRedirectURIs, strings.TrimSuffix(c.Issuer, "/")+"/device/callback")

	// Build dashboard redirect URIs including the OAuth callback for proxy authentication
	dashboardRedirectURIs := c.DashboardRedirectURIs
	baseURL := strings.TrimSuffix(c.Issuer, "/oauth2")
	// todo: resolve import cycle
	dashboardRedirectURIs = append(dashboardRedirectURIs, baseURL+"/api/reverse-proxy/callback")

	dashboardPostLogoutRedirectURIs := c.DashboardPostLogoutRedirectURIs
	// It is safe to assume that most installations will share the location of the
	// MGMT api and the dashboard, adding baseURL means less configuration for the instance admin
	dashboardPostLogoutRedirectURIs = append(dashboardPostLogoutRedirectURIs, baseURL)

	redirectURIs := make([]string, 0)
	redirectURIs = append(redirectURIs, cliRedirectURIs...)
	redirectURIs = append(redirectURIs, dashboardRedirectURIs...)

	cfg := &dex.YAMLConfig{
		Issuer: c.Issuer,
		Storage: dex.Storage{
			Type:   c.Storage.Type,
			Config: storageConfig,
		},
		Web: dex.Web{
			AllowedOrigins: []string{"*"},
			AllowedHeaders: []string{"Authorization", "Content-Type"},
		},
		OAuth2: dex.OAuth2{
			SkipApprovalScreen: true,
			GrantTypes:         c.GrantTypes,
		},
		Frontend: dex.Frontend{
			Issuer: "NetBird",
			Theme:  "light",
		},
		// Always enable password DB initially - we disable the local connector after startup if needed.
		// This ensures Dex has at least one connector during initialization.
		EnablePasswordDB: true,
		StaticClients: []storage.Client{
			{
				ID:                     StaticClientDashboard,
				Name:                   "NetBird Dashboard",
				Public:                 true,
				RedirectURIs:           redirectURIs,
				PostLogoutRedirectURIs: sanitizePostLogoutRedirectURIs(dashboardPostLogoutRedirectURIs),
			},
			{
				ID:           StaticClientCLI,
				Name:         "NetBird CLI",
				Public:       true,
				RedirectURIs: redirectURIs,
			},
		},
		StaticConnectors: c.StaticConnectors,
	}

	// Always initialize MFA providers and sessions so TOTP can be toggled at runtime.
	// MFAChain on clients is NOT set here — it's synced from the DB setting on startup.
	if err := configureMFA(cfg, c.MfaSessionMaxLifetime, c.MfaSessionIdleTimeout, c.MfaSessionRememberMe, c.SessionCookieEncryptionKey); err != nil {
		return nil, err
	}

	// Add owner user if provided
	if c.Owner != nil && c.Owner.Email != "" && c.Owner.Hash != "" {
		username := c.Owner.Username
		if username == "" {
			username = c.Owner.Email
		}
		cfg.StaticPasswords = []dex.Password{
			{
				Email:    c.Owner.Email,
				Hash:     []byte(c.Owner.Hash),
				Username: username,
				UserID:   uuid.New().String(),
			},
		}
	}

	return cfg, nil
}

func issuerRelativeDeviceCallback(issuer string) string {
	u, err := url.Parse(issuer)
	if err != nil || u.Path == "" {
		return "/device/callback"
	}
	return path.Join(u.Path, "/device/callback")
}

// Due to how the frontend generates the logout, sometimes it appends a trailing slash
// and because Dex only allows exact matches, we need to make sure we always have both
// versions of each provided uri
func sanitizePostLogoutRedirectURIs(uris []string) []string {
	result := make([]string, 0)
	for _, uri := range uris {
		if strings.HasSuffix(uri, "/") {
			result = append(result, uri)
			result = append(result, strings.TrimSuffix(uri, "/"))
		} else {
			result = append(result, uri)
			result = append(result, uri+"/")
		}
	}

	return result
}

func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout string, rememberMe bool, sessionCookieEncryptionKey string) error {
	cfg.MFA.Authenticators = []dex.MFAAuthenticator{{
		ID: DefaultTOTPAuthenticatorID,
		// Has to be caps otherwise it will fail
		Type: "TOTP",
		Config: map[string]interface{}{
			"issuer": "NetBird",
		},
		ConnectorTypes: []string{LocalConnectorID},
	}}

	if sessionMaxLifetime == "" {
		sessionMaxLifetime = "24h"
	}
	if sessionIdleTimeout == "" {
		sessionIdleTimeout = "1h"
	}

	cookieEncryptionKey, err := resolveSessionCookieEncryptionKey(sessionCookieEncryptionKey)
	if err != nil {
		return err
	}

	cfg.Sessions = &dex.Sessions{
		CookieName:                 "netbird-session",
		AbsoluteLifetime:           sessionMaxLifetime,
		ValidIfNotUsedFor:          sessionIdleTimeout,
		RememberMeCheckedByDefault: &rememberMe,
		SSOSharedWithDefault:       "all",
		CookieEncryptionKey:        cookieEncryptionKey,
	}
	// Absolutely required, otherwise the dex server will omit the MFA configuration entirely
	os.Setenv("DEX_SESSIONS_ENABLED", "true")

	// Note: MFAChain on clients is NOT set here.
	// It is toggled at runtime via SetMFAEnabled() based on the account settings DB value.
	return nil
}

func resolveSessionCookieEncryptionKey(configuredKey string) (string, error) {
	key := strings.TrimSpace(configuredKey)
	if key == "" {
		key = strings.TrimSpace(os.Getenv(sessionCookieEncryptionKeyEnv))
	}
	if key == "" {
		return "", nil
	}

	if validSessionCookieEncryptionKeyLength(len([]byte(key))) {
		return key, nil
	}

	for _, encoding := range []*base64.Encoding{
		base64.StdEncoding,
		base64.RawStdEncoding,
		base64.URLEncoding,
		base64.RawURLEncoding,
	} {
		decoded, err := encoding.DecodeString(key)
		if err == nil && validSessionCookieEncryptionKeyLength(len(decoded)) {
			return string(decoded), nil
		}
	}

	return "", fmt.Errorf("invalid embedded IdP session cookie encryption key:%s (or sessionCookieEncryptionKey) must be 16, 24, or 32 bytes as a raw string or base64-encoded to one of those lengths; got %d raw bytes", sessionCookieEncryptionKeyEnv, len([]byte(key)))
}

func validSessionCookieEncryptionKeyLength(length int) bool {
	switch length {
	case 16, 24, 32:
		return true
	default:
		return false
	}
}

// Compile-time check that EmbeddedIdPManager implements Manager interface
var _ Manager = (*EmbeddedIdPManager)(nil)

// Compile-time check that EmbeddedIdPManager implements OAuthConfigProvider interface
var _ OAuthConfigProvider = (*EmbeddedIdPManager)(nil)

// OAuthConfigProvider defines the interface for OAuth configuration needed by auth flows.
type OAuthConfigProvider interface {
	GetIssuer() string
	// GetKeysLocation returns the public JWKS endpoint URL (uses external issuer URL)
	GetKeysLocation() string
	// GetLocalKeysLocation returns the localhost JWKS endpoint URL for internal use.
	// Management server has embedded Dex and can validate tokens via localhost,
	// avoiding external network calls and DNS resolution issues during startup.
	GetLocalKeysLocation() string
	// GetKeyFetcher returns a KeyFetcher that reads keys directly from the IDP storage,
	// or nil if direct key fetching is not supported (falls back to HTTP).
	GetKeyFetcher() nbjwt.KeyFetcher
	GetClientIDs() []string
	GetUserIDClaim() string
	GetTokenEndpoint() string
	GetDeviceAuthEndpoint() string
	GetAuthorizationEndpoint() string
	GetDefaultScopes() string
	GetCLIClientID() string
	GetCLIRedirectURLs() []string
}

// EmbeddedIdPManager implements the Manager interface using the embedded Dex IdP.
type EmbeddedIdPManager struct {
	provider   *dex.Provider
	appMetrics telemetry.AppMetrics
	config     EmbeddedIdPConfig
	mfaEnabled bool
}

// NewEmbeddedIdPManager creates a new instance of EmbeddedIdPManager from a configuration.
// It instantiates the underlying Dex provider internally.
// Note: Storage defaults are applied in config loading (applyEmbeddedIdPConfig) based on Datadir.
func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMetrics telemetry.AppMetrics) (*EmbeddedIdPManager, error) {
	if config == nil {
		return nil, fmt.Errorf("embedded IdP config is required")
	}

	// Apply defaults for CLI redirect URIs
	if len(config.CLIRedirectURIs) == 0 {
		config.CLIRedirectURIs = []string{defaultCLIRedirectURL1, defaultCLIRedirectURL2}
	}

	// there are some properties create when creating YAML config (e.g., auth clients)
	yamlConfig, err := config.ToYAMLConfig()
	if err != nil {
		return nil, err
	}

	log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v", config)

	provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err)
	}

	// If local auth is disabled, validate that other connectors exist
	if config.LocalAuthDisabled {
		hasOthers, err := provider.HasNonLocalConnectors(ctx)
		if err != nil {
			_ = provider.Stop(ctx)
			return nil, fmt.Errorf("failed to check connectors: %w", err)
		}
		if !hasOthers {
			_ = provider.Stop(ctx)
			return nil, fmt.Errorf("cannot disable local authentication: no other identity providers configured")
		}
		// Ensure local connector is removed (it might exist from a previous run)
		if err := provider.DisableLocalAuth(ctx); err != nil {
			_ = provider.Stop(ctx)
			return nil, fmt.Errorf("failed to disable local auth: %w", err)
		}
		log.WithContext(ctx).Info("local authentication disabled - only external identity providers can be used")
	}

	log.WithContext(ctx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)

	return &EmbeddedIdPManager{
		provider:   provider,
		appMetrics: appMetrics,
		config:     *config,
	}, nil
}

// Handler returns the HTTP handler for serving OIDC requests.
func (m *EmbeddedIdPManager) Handler() http.Handler {
	return m.provider.Handler()
}

// Stop gracefully shuts down the embedded IdP provider.
func (m *EmbeddedIdPManager) Stop(ctx context.Context) error {
	return m.provider.Stop(ctx)
}

// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
func (m *EmbeddedIdPManager) UpdateUserAppMetadata(ctx context.Context, userID string, appMetadata AppMetadata) error {
	// TODO: implement
	return nil
}

// GetUserDataByID requests user data from the embedded IdP via user ID.
func (m *EmbeddedIdPManager) GetUserDataByID(ctx context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
	user, err := m.provider.GetUserByID(ctx, userID)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to get user by ID: %w", err)
	}

	return &UserData{
		Email:       user.Email,
		Name:        user.Username,
		ID:          user.UserID,
		AppMetadata: appMetadata,
	}, nil
}

// GetAccount returns all the users for a given account.
// Note: Embedded dex doesn't store account metadata, so this returns all users.
func (m *EmbeddedIdPManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
	users, err := m.provider.ListUsers(ctx)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to list users: %w", err)
	}

	result := make([]*UserData, 0, len(users))
	for _, user := range users {
		result = append(result, &UserData{
			Email: user.Email,
			Name:  user.Username,
			ID:    user.UserID,
			AppMetadata: AppMetadata{
				WTAccountID: accountID,
			},
		})
	}

	return result, nil
}

// GetAllAccounts gets all registered accounts with corresponding user data.
// Note: Embedded dex doesn't store account metadata, so all users are indexed under UnsetAccountID.
func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
	if m.appMetrics != nil {
		m.appMetrics.IDPMetrics().CountGetAllAccounts()
	}

	users, err := m.provider.ListUsers(ctx)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to list users: %w", err)
	}

	log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(users))

	indexedUsers := make(map[string][]*UserData)
	for _, user := range users {
		indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
			Email: user.Email,
			Name:  user.Username,
			ID:    user.UserID,
		})
	}

	log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(indexedUsers[UnsetAccountID]))

	return indexedUsers, nil
}

// CreateUser creates a new user in the embedded IdP.
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
	if m.config.LocalAuthDisabled {
		return nil, fmt.Errorf("local user creation is disabled")
	}

	if m.appMetrics != nil {
		m.appMetrics.IDPMetrics().CountCreateUser()
	}

	// Check if user already exists
	_, err := m.provider.GetUser(ctx, email)
	if err == nil {
		return nil, fmt.Errorf("user with email %s already exists", email)
	}
	if !errors.Is(err, storage.ErrNotFound) {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to check existing user: %w", err)
	}

	// Generate a random password for the new user
	password := GeneratePassword(16, 2, 2, 2)

	// Create the user via provider (handles hashing and ID generation)
	// The provider returns an encoded user ID in Dex's format (base64 protobuf with connector ID)
	userID, err := m.provider.CreateUser(ctx, email, name, password)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to create user in embedded IdP: %w", err)
	}

	log.WithContext(ctx).Debugf("created user %s in embedded IdP", email)

	return &UserData{
		Email:    email,
		Name:     name,
		ID:       userID,
		Password: password,
		AppMetadata: AppMetadata{
			WTAccountID: accountID,
			WTInvitedBy: invitedByEmail,
		},
	}, nil
}

// GetUserByEmail searches users with a given email.
func (m *EmbeddedIdPManager) GetUserByEmail(ctx context.Context, email string) ([]*UserData, error) {
	user, err := m.provider.GetUser(ctx, email)
	if err != nil {
		if errors.Is(err, storage.ErrNotFound) {
			return nil, nil // Return empty slice for not found
		}
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to get user by email: %w", err)
	}

	return []*UserData{
		{
			Email: user.Email,
			Name:  user.Username,
			ID:    user.UserID,
		},
	}, nil
}

// CreateUserWithPassword creates a new user in the embedded IdP with a provided password.
// Unlike CreateUser which auto-generates a password, this method uses the provided password.
// This is useful for instance setup where the user provides their own password.
func (m *EmbeddedIdPManager) CreateUserWithPassword(ctx context.Context, email, password, name string) (*UserData, error) {
	if m.config.LocalAuthDisabled {
		return nil, fmt.Errorf("local user creation is disabled")
	}

	if m.appMetrics != nil {
		m.appMetrics.IDPMetrics().CountCreateUser()
	}

	// Check if user already exists
	_, err := m.provider.GetUser(ctx, email)
	if err == nil {
		return nil, fmt.Errorf("user with email %s already exists", email)
	}
	if !errors.Is(err, storage.ErrNotFound) {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to check existing user: %w", err)
	}

	// Create the user via provider with the provided password
	userID, err := m.provider.CreateUser(ctx, email, name, password)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return nil, fmt.Errorf("failed to create user in embedded IdP: %w", err)
	}

	log.WithContext(ctx).Debugf("created user %s in embedded IdP with provided password", email)

	return &UserData{
		Email: email,
		Name:  name,
		ID:    userID,
	}, nil
}

// InviteUserByID resends an invitation to a user.
func (m *EmbeddedIdPManager) InviteUserByID(ctx context.Context, userID string) error {
	return fmt.Errorf("not implemented")
}

// DeleteUser deletes a user from the embedded IdP by user ID.
func (m *EmbeddedIdPManager) DeleteUser(ctx context.Context, userID string) error {
	if m.appMetrics != nil {
		m.appMetrics.IDPMetrics().CountDeleteUser()
	}

	// Get user by ID to retrieve email (provider.DeleteUser requires email)
	user, err := m.provider.GetUserByID(ctx, userID)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return fmt.Errorf("failed to get user for deletion: %w", err)
	}

	err = m.provider.DeleteUser(ctx, user.Email)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return fmt.Errorf("failed to delete user from embedded IdP: %w", err)
	}

	log.WithContext(ctx).Debugf("deleted user %s from embedded IdP", user.Email)

	return nil
}

// UpdateUserPassword updates the password for a user in the embedded IdP.
// It verifies that the current user is changing their own password and
// validates the current password before updating to the new password.
func (m *EmbeddedIdPManager) UpdateUserPassword(ctx context.Context, currentUserID, targetUserID string, oldPassword, newPassword string) error {
	// Verify the user is changing their own password
	if currentUserID != targetUserID {
		return fmt.Errorf("users can only change their own password")
	}

	// Verify the new password is different from the old password
	if oldPassword == newPassword {
		return fmt.Errorf("new password must be different from current password")
	}

	err := m.provider.UpdateUserPassword(ctx, targetUserID, oldPassword, newPassword)
	if err != nil {
		if m.appMetrics != nil {
			m.appMetrics.IDPMetrics().CountRequestError()
		}
		return err
	}

	log.WithContext(ctx).Debugf("updated password for user %s in embedded IdP", targetUserID)

	return nil
}

// CreateConnector creates a new identity provider connector in Dex.
// Returns the created connector config with the redirect URL populated.
func (m *EmbeddedIdPManager) CreateConnector(ctx context.Context, cfg *dex.ConnectorConfig) (*dex.ConnectorConfig, error) {
	return m.provider.CreateConnector(ctx, cfg)
}

// GetConnector retrieves an identity provider connector by ID.
func (m *EmbeddedIdPManager) GetConnector(ctx context.Context, id string) (*dex.ConnectorConfig, error) {
	return m.provider.GetConnector(ctx, id)
}

// ListConnectors returns all identity provider connectors.
func (m *EmbeddedIdPManager) ListConnectors(ctx context.Context) ([]*dex.ConnectorConfig, error) {
	return m.provider.ListConnectors(ctx)
}

// UpdateConnector updates an existing identity provider connector.
// Field preservation for partial updates is handled by Provider.UpdateConnector.
func (m *EmbeddedIdPManager) UpdateConnector(ctx context.Context, cfg *dex.ConnectorConfig) error {
	return m.provider.UpdateConnector(ctx, cfg)
}

// DeleteConnector removes an identity provider connector.
func (m *EmbeddedIdPManager) DeleteConnector(ctx context.Context, id string) error {
	return m.provider.DeleteConnector(ctx, id)
}

// GetIssuer returns the OIDC issuer URL.
func (m *EmbeddedIdPManager) GetIssuer() string {
	return m.provider.GetIssuer()
}

// GetTokenEndpoint returns the OAuth2 token endpoint URL.
func (m *EmbeddedIdPManager) GetTokenEndpoint() string {
	return m.provider.GetTokenEndpoint()
}

// GetDeviceAuthEndpoint returns the OAuth2 device authorization endpoint URL.
func (m *EmbeddedIdPManager) GetDeviceAuthEndpoint() string {
	return m.provider.GetDeviceAuthEndpoint()
}

// GetAuthorizationEndpoint returns the OAuth2 authorization endpoint URL.
func (m *EmbeddedIdPManager) GetAuthorizationEndpoint() string {
	return m.provider.GetAuthorizationEndpoint()
}

// GetDefaultScopes returns the default OAuth2 scopes for authentication.
func (m *EmbeddedIdPManager) GetDefaultScopes() string {
	return defaultScopes
}

// GetCLIClientID returns the client ID for CLI authentication.
func (m *EmbeddedIdPManager) GetCLIClientID() string {
	return StaticClientCLI
}

// GetCLIRedirectURLs returns the redirect URLs configured for the CLI client.
func (m *EmbeddedIdPManager) GetCLIRedirectURLs() []string {
	if len(m.config.CLIRedirectURIs) == 0 {
		return []string{defaultCLIRedirectURL1, defaultCLIRedirectURL2}
	}
	return m.config.CLIRedirectURIs
}

// GetKeyFetcher returns a KeyFetcher that reads keys directly from Dex storage.
func (m *EmbeddedIdPManager) GetKeyFetcher() nbjwt.KeyFetcher {
	return m.provider.GetJWKS
}

// GetKeysLocation returns the JWKS endpoint URL for token validation.
func (m *EmbeddedIdPManager) GetKeysLocation() string {
	return m.provider.GetKeysLocation()
}

// GetLocalKeysLocation returns the localhost JWKS endpoint URL for internal token validation.
// Uses the LocalAddress from config (management server's listen address) since embedded Dex
// is served by the management HTTP server, not a standalone Dex server.
func (m *EmbeddedIdPManager) GetLocalKeysLocation() string {
	addr := m.config.LocalAddress
	if addr == "" {
		return ""
	}
	// Construct localhost URL from listen address
	// addr is in format ":port" or "host:port" or "localhost:port"
	if strings.HasPrefix(addr, ":") {
		return fmt.Sprintf("http://localhost%s/oauth2/keys", addr)
	}
	return fmt.Sprintf("http://%s/oauth2/keys", addr)
}

// GetClientIDs returns the OAuth2 client IDs configured for this provider.
func (m *EmbeddedIdPManager) GetClientIDs() []string {
	return []string{StaticClientDashboard, StaticClientCLI}
}

// GetUserIDClaim returns the JWT claim name used for user identification.
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
	return defaultUserIDClaim
}

// IsLocalAuthDisabled returns whether local authentication is disabled based on configuration.
func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool {
	return m.config.LocalAuthDisabled
}

// SetMFAEnabled enables or disables TOTP MFA for local users by updating the MFAChain on OAuth2 clients.
func (m *EmbeddedIdPManager) SetMFAEnabled(ctx context.Context, enabled bool) error {
	var mfaChain []string
	if enabled {
		mfaChain = []string{DefaultTOTPAuthenticatorID}
	}
	if err := m.provider.SetClientsMFAChain(ctx, []string{
		StaticClientCLI,
		StaticClientDashboard,
	}, mfaChain); err != nil {
		return fmt.Errorf("failed to set MFA enabled=%v: %w", enabled, err)
	}
	m.mfaEnabled = enabled
	return nil
}

// IsMFAEnabled returns whether TOTP MFA is currently enabled for local users.
func (m *EmbeddedIdPManager) IsMFAEnabled() bool {
	return m.mfaEnabled
}

// HasNonLocalConnectors checks if there are any identity provider connectors other than local.
func (m *EmbeddedIdPManager) HasNonLocalConnectors(ctx context.Context) (bool, error) {
	return m.provider.HasNonLocalConnectors(ctx)
}
