package command

import (
	"net/http"
	"reflect"
	"slices"
	"time"

	"github.com/zitadel/logging"
	"github.com/zitadel/oidc/v3/pkg/client/rp"
	"golang.org/x/oauth2"

	"github.com/zitadel/zitadel/internal/crypto"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	providers "github.com/zitadel/zitadel/internal/idp"
	"github.com/zitadel/zitadel/internal/idp/providers/apple"
	"github.com/zitadel/zitadel/internal/idp/providers/azuread"
	"github.com/zitadel/zitadel/internal/idp/providers/github"
	"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
	"github.com/zitadel/zitadel/internal/idp/providers/google"
	"github.com/zitadel/zitadel/internal/idp/providers/jwt"
	"github.com/zitadel/zitadel/internal/idp/providers/ldap"
	"github.com/zitadel/zitadel/internal/idp/providers/oauth"
	"github.com/zitadel/zitadel/internal/idp/providers/oidc"
	saml2 "github.com/zitadel/zitadel/internal/idp/providers/saml"
	"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
	"github.com/zitadel/zitadel/internal/repository/idp"
	"github.com/zitadel/zitadel/internal/repository/idpconfig"
	"github.com/zitadel/zitadel/internal/repository/instance"
	"github.com/zitadel/zitadel/internal/repository/org"
	"github.com/zitadel/zitadel/internal/zerrors"
)

type OAuthIDPWriteModel struct {
	eventstore.WriteModel

	Name                  string
	ID                    string
	ClientID              string
	ClientSecret          *crypto.CryptoValue
	AuthorizationEndpoint string
	TokenEndpoint         string
	UserEndpoint          string
	Scopes                []string
	IDAttribute           string
	idp.Options

	State domain.IDPState
}

func (wm *OAuthIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.OAuthIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.OAuthIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *OAuthIDPWriteModel) reduceAddedEvent(e *idp.OAuthIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.AuthorizationEndpoint = e.AuthorizationEndpoint
	wm.TokenEndpoint = e.TokenEndpoint
	wm.UserEndpoint = e.UserEndpoint
	wm.Scopes = e.Scopes
	wm.IDAttribute = e.IDAttribute
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *OAuthIDPWriteModel) reduceChangedEvent(e *idp.OAuthIDPChangedEvent) {
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.AuthorizationEndpoint != nil {
		wm.AuthorizationEndpoint = *e.AuthorizationEndpoint
	}
	if e.TokenEndpoint != nil {
		wm.TokenEndpoint = *e.TokenEndpoint
	}
	if e.UserEndpoint != nil {
		wm.UserEndpoint = *e.UserEndpoint
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	if e.IDAttribute != nil {
		wm.IDAttribute = *e.IDAttribute
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *OAuthIDPWriteModel) NewChanges(
	name,
	clientID,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	authorizationEndpoint,
	tokenEndpoint,
	userEndpoint,
	idAttribute string,
	scopes []string,
	options idp.Options,
) ([]idp.OAuthIDPChanges, error) {
	changes := make([]idp.OAuthIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeOAuthClientSecret(clientSecret))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeOAuthClientID(clientID))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeOAuthName(name))
	}
	if wm.AuthorizationEndpoint != authorizationEndpoint {
		changes = append(changes, idp.ChangeOAuthAuthorizationEndpoint(authorizationEndpoint))
	}
	if wm.TokenEndpoint != tokenEndpoint {
		changes = append(changes, idp.ChangeOAuthTokenEndpoint(tokenEndpoint))
	}
	if wm.UserEndpoint != userEndpoint {
		changes = append(changes, idp.ChangeOAuthUserEndpoint(userEndpoint))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeOAuthScopes(scopes))
	}
	if wm.IDAttribute != idAttribute {
		changes = append(changes, idp.ChangeOAuthIDAttribute(idAttribute))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeOAuthOptions(opts))
	}
	return changes, nil
}

func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	config := &oauth2.Config{
		ClientID:     wm.ClientID,
		ClientSecret: secret,
		Endpoint: oauth2.Endpoint{
			AuthURL:  wm.AuthorizationEndpoint,
			TokenURL: wm.TokenEndpoint,
		},
		RedirectURL: callbackURL,
		Scopes:      wm.Scopes,
	}
	opts := make([]oauth.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		opts = append(opts, oauth.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oauth.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oauth.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oauth.WithAutoUpdate())
	}
	return oauth.New(
		config,
		wm.Name,
		wm.UserEndpoint,
		func() providers.User {
			return oauth.NewUserMapper(wm.IDAttribute)
		},
		opts...,
	)
}

func (wm *OAuthIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type OIDCIDPWriteModel struct {
	eventstore.WriteModel

	Name             string
	ID               string
	Issuer           string
	ClientID         string
	ClientSecret     *crypto.CryptoValue
	Scopes           []string
	IsIDTokenMapping bool
	idp.Options

	State domain.IDPState
}

func (wm *OIDCIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.OIDCIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.OIDCIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.OIDCIDPMigratedAzureADEvent:
			wm.State = domain.IDPStateMigrated
		case *idp.OIDCIDPMigratedGoogleEvent:
			wm.State = domain.IDPStateMigrated
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		case *idpconfig.IDPConfigAddedEvent:
			wm.reduceIDPConfigAddedEvent(e)
		case *idpconfig.IDPConfigChangedEvent:
			wm.reduceIDPConfigChangedEvent(e)
		case *idpconfig.OIDCConfigAddedEvent:
			wm.reduceOIDCConfigAddedEvent(e)
		case *idpconfig.OIDCConfigChangedEvent:
			wm.reduceOIDCConfigChangedEvent(e)
		case *idpconfig.IDPConfigRemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *OIDCIDPWriteModel) reduceAddedEvent(e *idp.OIDCIDPAddedEvent) {
	wm.Name = e.Name
	wm.Issuer = e.Issuer
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.IsIDTokenMapping = e.IsIDTokenMapping
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *OIDCIDPWriteModel) reduceChangedEvent(e *idp.OIDCIDPChangedEvent) {
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Issuer != nil {
		wm.Issuer = *e.Issuer
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	if e.IsIDTokenMapping != nil {
		wm.IsIDTokenMapping = *e.IsIDTokenMapping
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *OIDCIDPWriteModel) NewChanges(
	name,
	issuer,
	clientID,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	idTokenMapping bool,
	options idp.Options,
) ([]idp.OIDCIDPChanges, error) {
	changes := make([]idp.OIDCIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeOIDCClientSecret(clientSecret))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeOIDCClientID(clientID))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeOIDCName(name))
	}
	if wm.Issuer != issuer {
		changes = append(changes, idp.ChangeOIDCIssuer(issuer))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeOIDCScopes(scopes))
	}
	if wm.IsIDTokenMapping != idTokenMapping {
		changes = append(changes, idp.ChangeOIDCIsIDTokenMapping(idTokenMapping))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeOIDCOptions(opts))
	}
	return changes, nil
}

// reduceIDPConfigAddedEvent handles old idpConfig events
func (wm *OIDCIDPWriteModel) reduceIDPConfigAddedEvent(e *idpconfig.IDPConfigAddedEvent) {
	wm.Name = e.Name
	wm.Options.IsCreationAllowed = true
	wm.Options.IsLinkingAllowed = true
	wm.Options.IsAutoCreation = e.AutoRegister
	wm.Options.IsAutoUpdate = false
	wm.State = domain.IDPStateActive
}

// reduceIDPConfigChangedEvent handles old idpConfig changes
func (wm *OIDCIDPWriteModel) reduceIDPConfigChangedEvent(e *idpconfig.IDPConfigChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.AutoRegister != nil {
		wm.Options.IsAutoCreation = *e.AutoRegister
	}
}

// reduceOIDCConfigAddedEvent handles old OIDC idpConfig events
func (wm *OIDCIDPWriteModel) reduceOIDCConfigAddedEvent(e *idpconfig.OIDCConfigAddedEvent) {
	wm.Issuer = e.Issuer
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
}

// reduceOIDCConfigChangedEvent handles old OIDC idpConfig changes
func (wm *OIDCIDPWriteModel) reduceOIDCConfigChangedEvent(e *idpconfig.OIDCConfigChangedEvent) {
	if e.Issuer != nil {
		wm.Issuer = *e.Issuer
	}
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
}

func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]oidc.ProviderOpts, 1, 6)
	opts[0] = oidc.WithSelectAccount()
	if wm.IsIDTokenMapping {
		opts = append(opts, oidc.WithIDTokenMapping())
	}
	if wm.IsCreationAllowed {
		opts = append(opts, oidc.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oidc.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oidc.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oidc.WithAutoUpdate())
	}
	return oidc.New(
		wm.Name,
		wm.Issuer,
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		oidc.DefaultMapper,
		opts...,
	)
}

func (wm *OIDCIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type JWTIDPWriteModel struct {
	eventstore.WriteModel

	ID           string
	Name         string
	Issuer       string
	JWTEndpoint  string
	KeysEndpoint string
	HeaderName   string
	idp.Options

	State domain.IDPState
}

func (wm *JWTIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.JWTIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.JWTIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		case *idpconfig.IDPConfigAddedEvent:
			wm.reduceIDPConfigAddedEvent(e)
		case *idpconfig.IDPConfigChangedEvent:
			wm.reduceIDPConfigChangedEvent(e)
		case *idpconfig.JWTConfigAddedEvent:
			wm.reduceJWTConfigAddedEvent(e)
		case *idpconfig.JWTConfigChangedEvent:
			wm.reduceJWTConfigChangedEvent(e)
		case *idpconfig.IDPConfigRemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *JWTIDPWriteModel) reduceAddedEvent(e *idp.JWTIDPAddedEvent) {
	wm.Name = e.Name
	wm.Issuer = e.Issuer
	wm.JWTEndpoint = e.JWTEndpoint
	wm.KeysEndpoint = e.KeysEndpoint
	wm.HeaderName = e.HeaderName
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *JWTIDPWriteModel) reduceChangedEvent(e *idp.JWTIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Issuer != nil {
		wm.Issuer = *e.Issuer
	}
	if e.JWTEndpoint != nil {
		wm.JWTEndpoint = *e.JWTEndpoint
	}
	if e.KeysEndpoint != nil {
		wm.KeysEndpoint = *e.KeysEndpoint
	}
	if e.HeaderName != nil {
		wm.HeaderName = *e.HeaderName
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *JWTIDPWriteModel) NewChanges(
	name,
	issuer,
	jwtEndpoint,
	keysEndpoint,
	headerName string,
	options idp.Options,
) ([]idp.JWTIDPChanges, error) {
	changes := make([]idp.JWTIDPChanges, 0)
	if wm.Name != name {
		changes = append(changes, idp.ChangeJWTName(name))
	}
	if wm.Issuer != issuer {
		changes = append(changes, idp.ChangeJWTIssuer(issuer))
	}
	if wm.JWTEndpoint != jwtEndpoint {
		changes = append(changes, idp.ChangeJWTEndpoint(jwtEndpoint))
	}
	if wm.KeysEndpoint != keysEndpoint {
		changes = append(changes, idp.ChangeJWTKeysEndpoint(keysEndpoint))
	}
	if wm.HeaderName != headerName {
		changes = append(changes, idp.ChangeJWTHeaderName(headerName))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeJWTOptions(opts))
	}
	return changes, nil
}

// reduceIDPConfigAddedEvent handles old idpConfig events
func (wm *JWTIDPWriteModel) reduceIDPConfigAddedEvent(e *idpconfig.IDPConfigAddedEvent) {
	wm.Name = e.Name
	wm.Options.IsCreationAllowed = true
	wm.Options.IsLinkingAllowed = true
	wm.Options.IsAutoCreation = e.AutoRegister
	wm.Options.IsAutoUpdate = false
	wm.State = domain.IDPStateActive
}

// reduceIDPConfigChangedEvent handles old idpConfig changes
func (wm *JWTIDPWriteModel) reduceIDPConfigChangedEvent(e *idpconfig.IDPConfigChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.AutoRegister != nil {
		wm.Options.IsAutoCreation = *e.AutoRegister
	}
}

// reduceJWTConfigAddedEvent handles old JWT idpConfig events
func (wm *JWTIDPWriteModel) reduceJWTConfigAddedEvent(e *idpconfig.JWTConfigAddedEvent) {
	wm.Issuer = e.Issuer
	wm.JWTEndpoint = e.JWTEndpoint
	wm.KeysEndpoint = e.KeysEndpoint
	wm.HeaderName = e.HeaderName
}

// reduceJWTConfigChangedEvent handles old JWT idpConfig changes
func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigChangedEvent) {
	if e.Issuer != nil {
		wm.Issuer = *e.Issuer
	}
	if e.JWTEndpoint != nil {
		wm.JWTEndpoint = *e.JWTEndpoint
	}
	if e.KeysEndpoint != nil {
		wm.KeysEndpoint = *e.KeysEndpoint
	}
	if e.HeaderName != nil {
		wm.HeaderName = *e.HeaderName
	}
}

func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	opts := make([]jwt.ProviderOpts, 0)
	if wm.IsCreationAllowed {
		opts = append(opts, jwt.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, jwt.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, jwt.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, jwt.WithAutoUpdate())
	}
	return jwt.New(
		wm.Name,
		wm.Issuer,
		wm.JWTEndpoint,
		wm.KeysEndpoint,
		wm.HeaderName,
		idpAlg,
		opts...,
	)
}

func (wm *JWTIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type AzureADIDPWriteModel struct {
	eventstore.WriteModel

	ID              string
	Name            string
	ClientID        string
	ClientSecret    *crypto.CryptoValue
	Scopes          []string
	Tenant          string
	IsEmailVerified bool
	idp.Options

	State domain.IDPState
}

func (wm *AzureADIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.AzureADIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.OIDCIDPMigratedAzureADEvent:
			wm.reduceAddedEvent(&e.AzureADIDPAddedEvent)
		case *idp.AzureADIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *AzureADIDPWriteModel) reduceAddedEvent(e *idp.AzureADIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.Tenant = e.Tenant
	wm.IsEmailVerified = e.IsEmailVerified
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *AzureADIDPWriteModel) reduceChangedEvent(e *idp.AzureADIDPChangedEvent) {
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	if e.Tenant != nil {
		wm.Tenant = *e.Tenant
	}
	if e.IsEmailVerified != nil {
		wm.IsEmailVerified = *e.IsEmailVerified
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *AzureADIDPWriteModel) NewChanges(
	name string,
	clientID string,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	tenant string,
	isEmailVerified bool,
	options idp.Options,
) ([]idp.AzureADIDPChanges, error) {
	changes := make([]idp.AzureADIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeAzureADClientSecret(clientSecret))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeAzureADName(name))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeAzureADClientID(clientID))
	}
	if wm.Tenant != tenant {
		changes = append(changes, idp.ChangeAzureADTenant(tenant))
	}
	if wm.IsEmailVerified != isEmailVerified {
		changes = append(changes, idp.ChangeAzureADIsEmailVerified(isEmailVerified))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeAzureADScopes(scopes))
	}

	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeAzureADOptions(opts))
	}
	return changes, nil
}
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]azuread.ProviderOptions, 0, 3)
	if wm.IsEmailVerified {
		opts = append(opts, azuread.WithEmailVerified())
	}
	if wm.Tenant != "" {
		opts = append(opts, azuread.WithTenant(azuread.TenantType(wm.Tenant)))
	}
	oauthOpts := make([]oauth.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
	}
	if len(oauthOpts) > 0 {
		opts = append(opts, azuread.WithOAuthOptions(oauthOpts...))
	}
	return azuread.New(
		wm.Name,
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		opts...,
	)
}

func (wm *AzureADIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type GitHubIDPWriteModel struct {
	eventstore.WriteModel

	ID           string
	Name         string
	ClientID     string
	ClientSecret *crypto.CryptoValue
	Scopes       []string
	idp.Options

	State domain.IDPState
}

func (wm *GitHubIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.GitHubIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.GitHubIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *GitHubIDPWriteModel) reduceAddedEvent(e *idp.GitHubIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *GitHubIDPWriteModel) reduceChangedEvent(e *idp.GitHubIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *GitHubIDPWriteModel) NewChanges(
	name,
	clientID,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	options idp.Options,
) ([]idp.GitHubIDPChanges, error) {
	changes := make([]idp.GitHubIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeGitHubClientSecret(clientSecret))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeGitHubName(name))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeGitHubClientID(clientID))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeGitHubScopes(scopes))
	}

	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeGitHubOptions(opts))
	}
	return changes, nil
}
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	oauthOpts := make([]oauth.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
	}
	return github.New(
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		oauthOpts...,
	)
}

func (wm *GitHubIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type GitHubEnterpriseIDPWriteModel struct {
	eventstore.WriteModel

	ID                    string
	Name                  string
	ClientID              string
	ClientSecret          *crypto.CryptoValue
	AuthorizationEndpoint string
	TokenEndpoint         string
	UserEndpoint          string
	Scopes                []string
	idp.Options

	State domain.IDPState
}

func (wm *GitHubEnterpriseIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.GitHubEnterpriseIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.GitHubEnterpriseIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *GitHubEnterpriseIDPWriteModel) reduceAddedEvent(e *idp.GitHubEnterpriseIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.AuthorizationEndpoint = e.AuthorizationEndpoint
	wm.TokenEndpoint = e.TokenEndpoint
	wm.UserEndpoint = e.UserEndpoint
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *GitHubEnterpriseIDPWriteModel) reduceChangedEvent(e *idp.GitHubEnterpriseIDPChangedEvent) {
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.AuthorizationEndpoint != nil {
		wm.AuthorizationEndpoint = *e.AuthorizationEndpoint
	}
	if e.TokenEndpoint != nil {
		wm.TokenEndpoint = *e.TokenEndpoint
	}
	if e.UserEndpoint != nil {
		wm.UserEndpoint = *e.UserEndpoint
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *GitHubEnterpriseIDPWriteModel) NewChanges(
	name,
	clientID string,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	authorizationEndpoint,
	tokenEndpoint,
	userEndpoint string,
	scopes []string,
	options idp.Options,
) ([]idp.GitHubEnterpriseIDPChanges, error) {
	changes := make([]idp.GitHubEnterpriseIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeGitHubEnterpriseClientSecret(clientSecret))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeGitHubEnterpriseClientID(clientID))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeGitHubEnterpriseName(name))
	}
	if wm.AuthorizationEndpoint != authorizationEndpoint {
		changes = append(changes, idp.ChangeGitHubEnterpriseAuthorizationEndpoint(authorizationEndpoint))
	}
	if wm.TokenEndpoint != tokenEndpoint {
		changes = append(changes, idp.ChangeGitHubEnterpriseTokenEndpoint(tokenEndpoint))
	}
	if wm.UserEndpoint != userEndpoint {
		changes = append(changes, idp.ChangeGitHubEnterpriseUserEndpoint(userEndpoint))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeGitHubEnterpriseScopes(scopes))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeGitHubEnterpriseOptions(opts))
	}
	return changes, nil
}

func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	oauthOpts := make([]oauth.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
	}
	return github.NewCustomURL(
		wm.Name,
		wm.ClientID,
		secret,
		callbackURL,
		wm.AuthorizationEndpoint,
		wm.TokenEndpoint,
		wm.UserEndpoint,
		wm.Scopes,
		oauthOpts...,
	)
}

func (wm *GitHubEnterpriseIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type GitLabIDPWriteModel struct {
	eventstore.WriteModel

	ID           string
	Name         string
	ClientID     string
	ClientSecret *crypto.CryptoValue
	Scopes       []string
	idp.Options

	State domain.IDPState
}

func (wm *GitLabIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.GitLabIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.GitLabIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *GitLabIDPWriteModel) reduceAddedEvent(e *idp.GitLabIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *GitLabIDPWriteModel) reduceChangedEvent(e *idp.GitLabIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *GitLabIDPWriteModel) NewChanges(
	name,
	clientID,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	options idp.Options,
) ([]idp.GitLabIDPChanges, error) {
	changes := make([]idp.GitLabIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeGitLabClientSecret(clientSecret))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeGitLabName(name))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeGitLabClientID(clientID))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeGitLabScopes(scopes))
	}

	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeGitLabOptions(opts))
	}
	return changes, nil
}

func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]oidc.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		opts = append(opts, oidc.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oidc.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oidc.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oidc.WithAutoUpdate())
	}
	return gitlab.New(
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		opts...,
	)
}

func (wm *GitLabIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type GitLabSelfHostedIDPWriteModel struct {
	eventstore.WriteModel

	ID           string
	Name         string
	Issuer       string
	ClientID     string
	ClientSecret *crypto.CryptoValue
	Scopes       []string
	idp.Options

	State domain.IDPState
}

func (wm *GitLabSelfHostedIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.GitLabSelfHostedIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.GitLabSelfHostedIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *GitLabSelfHostedIDPWriteModel) reduceAddedEvent(e *idp.GitLabSelfHostedIDPAddedEvent) {
	wm.Name = e.Name
	wm.Issuer = e.Issuer
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *GitLabSelfHostedIDPWriteModel) reduceChangedEvent(e *idp.GitLabSelfHostedIDPChangedEvent) {
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Issuer != nil {
		wm.Issuer = *e.Issuer
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *GitLabSelfHostedIDPWriteModel) NewChanges(
	name string,
	issuer string,
	clientID string,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	options idp.Options,
) ([]idp.GitLabSelfHostedIDPChanges, error) {
	changes := make([]idp.GitLabSelfHostedIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeGitLabSelfHostedClientSecret(clientSecret))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeGitLabSelfHostedClientID(clientID))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeGitLabSelfHostedName(name))
	}
	if wm.Issuer != issuer {
		changes = append(changes, idp.ChangeGitLabSelfHostedIssuer(issuer))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeGitLabSelfHostedScopes(scopes))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeGitLabSelfHostedOptions(opts))
	}
	return changes, nil
}

func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]oidc.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		opts = append(opts, oidc.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oidc.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oidc.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oidc.WithAutoUpdate())
	}
	return gitlab.NewCustomIssuer(
		wm.Name,
		wm.Issuer,
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		opts...,
	)
}

func (wm *GitLabSelfHostedIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type GoogleIDPWriteModel struct {
	eventstore.WriteModel

	ID           string
	Name         string
	ClientID     string
	ClientSecret *crypto.CryptoValue
	Scopes       []string
	idp.Options

	State domain.IDPState
}

func (wm *GoogleIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.GoogleIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.GoogleIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.OIDCIDPMigratedGoogleEvent:
			wm.reduceAddedEvent(&e.GoogleIDPAddedEvent)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *GoogleIDPWriteModel) reduceAddedEvent(e *idp.GoogleIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.ClientSecret = e.ClientSecret
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *GoogleIDPWriteModel) reduceChangedEvent(e *idp.GoogleIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.ClientSecret != nil {
		wm.ClientSecret = e.ClientSecret
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *GoogleIDPWriteModel) NewChanges(
	name string,
	clientID string,
	clientSecretString string,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	options idp.Options,
) ([]idp.GoogleIDPChanges, error) {
	changes := make([]idp.GoogleIDPChanges, 0)
	var clientSecret *crypto.CryptoValue
	var err error
	if clientSecretString != "" {
		clientSecret, err = crypto.Crypt([]byte(clientSecretString), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeGoogleClientSecret(clientSecret))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeGoogleName(name))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeGoogleClientID(clientID))
	}
	if !reflect.DeepEqual(wm.Scopes, scopes) {
		changes = append(changes, idp.ChangeGoogleScopes(scopes))
	}

	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeGoogleOptions(opts))
	}
	return changes, nil
}

func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
		logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
		rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
	}
	oidc.WithRelyingPartyOption(rp.WithErrorHandler(errorHandler))
	secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]oidc.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		opts = append(opts, oidc.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oidc.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oidc.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oidc.WithAutoUpdate())
	}
	return google.New(
		wm.ClientID,
		secret,
		callbackURL,
		wm.Scopes,
		opts...,
	)
}

func (wm *GoogleIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type LDAPIDPWriteModel struct {
	eventstore.WriteModel

	ID                string
	Name              string
	Servers           []string
	StartTLS          bool
	BaseDN            string
	BindDN            string
	BindPassword      *crypto.CryptoValue
	UserBase          string
	UserObjectClasses []string
	UserFilters       []string
	Timeout           time.Duration
	idp.LDAPAttributes
	idp.Options

	State domain.IDPState
}

func (wm *LDAPIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.LDAPIDPAddedEvent:
			if wm.ID != e.ID {
				continue
			}
			wm.reduceAddedEvent(e)
		case *idp.LDAPIDPChangedEvent:
			if wm.ID != e.ID {
				continue
			}
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			if wm.ID != e.ID {
				continue
			}
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *LDAPIDPWriteModel) reduceAddedEvent(e *idp.LDAPIDPAddedEvent) {
	wm.Name = e.Name
	wm.Servers = e.Servers
	wm.StartTLS = e.StartTLS
	wm.BaseDN = e.BaseDN
	wm.BindDN = e.BindDN
	wm.BindPassword = e.BindPassword
	wm.UserBase = e.UserBase
	wm.UserObjectClasses = e.UserObjectClasses
	wm.UserFilters = e.UserFilters
	wm.Timeout = e.Timeout
	wm.LDAPAttributes = e.LDAPAttributes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *LDAPIDPWriteModel) reduceChangedEvent(e *idp.LDAPIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Servers != nil {
		wm.Servers = e.Servers
	}
	if e.StartTLS != nil {
		wm.StartTLS = *e.StartTLS
	}
	if e.BaseDN != nil {
		wm.BaseDN = *e.BaseDN
	}
	if e.BindDN != nil {
		wm.BindDN = *e.BindDN
	}
	if e.BindPassword != nil {
		wm.BindPassword = e.BindPassword
	}
	if e.UserBase != nil {
		wm.UserBase = *e.UserBase
	}
	if e.UserObjectClasses != nil {
		wm.UserObjectClasses = e.UserObjectClasses
	}
	if e.UserFilters != nil {
		wm.UserFilters = e.UserFilters
	}
	if e.Timeout != nil {
		wm.Timeout = *e.Timeout
	}
	wm.LDAPAttributes.ReduceChanges(e.LDAPAttributeChanges)
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *LDAPIDPWriteModel) NewChanges(
	name string,
	servers []string,
	startTLS bool,
	baseDN string,
	bindDN string,
	bindPassword string,
	userBase string,
	userObjectClasses []string,
	userFilters []string,
	timeout time.Duration,
	secretCrypto crypto.EncryptionAlgorithm,
	attributes idp.LDAPAttributes,
	options idp.Options,
) ([]idp.LDAPIDPChanges, error) {
	changes := make([]idp.LDAPIDPChanges, 0)
	var cryptedPassword *crypto.CryptoValue
	var err error
	if bindPassword != "" {
		cryptedPassword, err = crypto.Crypt([]byte(bindPassword), secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeLDAPBindPassword(cryptedPassword))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeLDAPName(name))
	}
	if !reflect.DeepEqual(wm.Servers, servers) {
		changes = append(changes, idp.ChangeLDAPServers(servers))
	}
	if wm.StartTLS != startTLS {
		changes = append(changes, idp.ChangeLDAPStartTLS(startTLS))
	}
	if wm.BaseDN != baseDN {
		changes = append(changes, idp.ChangeLDAPBaseDN(baseDN))
	}
	if wm.BindDN != bindDN {
		changes = append(changes, idp.ChangeLDAPBindDN(bindDN))
	}
	if wm.UserBase != userBase {
		changes = append(changes, idp.ChangeLDAPUserBase(userBase))
	}
	if !reflect.DeepEqual(wm.UserObjectClasses, userObjectClasses) {
		changes = append(changes, idp.ChangeLDAPUserObjectClasses(userObjectClasses))
	}
	if !reflect.DeepEqual(wm.UserFilters, userFilters) {
		changes = append(changes, idp.ChangeLDAPUserFilters(userFilters))
	}
	if wm.Timeout != timeout {
		changes = append(changes, idp.ChangeLDAPTimeout(timeout))
	}
	attrs := wm.LDAPAttributes.Changes(attributes)
	if !attrs.IsZero() {
		changes = append(changes, idp.ChangeLDAPAttributes(attrs))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeLDAPOptions(opts))
	}
	return changes, nil
}

func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	password, err := crypto.DecryptString(wm.BindPassword, idpAlg)
	if err != nil {
		return nil, err
	}
	var opts []ldap.ProviderOpts
	if !wm.StartTLS {
		opts = append(opts, ldap.WithoutStartTLS())
	}
	if wm.LDAPAttributes.IDAttribute != "" {
		opts = append(opts, ldap.WithCustomIDAttribute(wm.LDAPAttributes.IDAttribute))
	}
	if wm.LDAPAttributes.FirstNameAttribute != "" {
		opts = append(opts, ldap.WithFirstNameAttribute(wm.LDAPAttributes.FirstNameAttribute))
	}
	if wm.LDAPAttributes.LastNameAttribute != "" {
		opts = append(opts, ldap.WithLastNameAttribute(wm.LDAPAttributes.LastNameAttribute))
	}
	if wm.LDAPAttributes.DisplayNameAttribute != "" {
		opts = append(opts, ldap.WithDisplayNameAttribute(wm.LDAPAttributes.DisplayNameAttribute))
	}
	if wm.LDAPAttributes.NickNameAttribute != "" {
		opts = append(opts, ldap.WithNickNameAttribute(wm.LDAPAttributes.NickNameAttribute))
	}
	if wm.LDAPAttributes.PreferredUsernameAttribute != "" {
		opts = append(opts, ldap.WithPreferredUsernameAttribute(wm.LDAPAttributes.PreferredUsernameAttribute))
	}
	if wm.LDAPAttributes.EmailAttribute != "" {
		opts = append(opts, ldap.WithEmailAttribute(wm.LDAPAttributes.EmailAttribute))
	}
	if wm.LDAPAttributes.EmailVerifiedAttribute != "" {
		opts = append(opts, ldap.WithEmailVerifiedAttribute(wm.LDAPAttributes.EmailVerifiedAttribute))
	}
	if wm.LDAPAttributes.PhoneAttribute != "" {
		opts = append(opts, ldap.WithPhoneAttribute(wm.LDAPAttributes.PhoneAttribute))
	}
	if wm.LDAPAttributes.PhoneVerifiedAttribute != "" {
		opts = append(opts, ldap.WithPhoneVerifiedAttribute(wm.LDAPAttributes.PhoneVerifiedAttribute))
	}
	if wm.LDAPAttributes.PreferredLanguageAttribute != "" {
		opts = append(opts, ldap.WithPreferredLanguageAttribute(wm.LDAPAttributes.PreferredLanguageAttribute))
	}
	if wm.LDAPAttributes.AvatarURLAttribute != "" {
		opts = append(opts, ldap.WithAvatarURLAttribute(wm.LDAPAttributes.AvatarURLAttribute))
	}
	if wm.LDAPAttributes.ProfileAttribute != "" {
		opts = append(opts, ldap.WithProfileAttribute(wm.LDAPAttributes.ProfileAttribute))
	}
	if wm.IsCreationAllowed {
		opts = append(opts, ldap.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, ldap.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, ldap.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, ldap.WithAutoUpdate())
	}
	return ldap.New(
		wm.Name,
		wm.Servers,
		wm.BaseDN,
		wm.BindDN,
		password,
		wm.UserBase,
		wm.UserObjectClasses,
		wm.UserFilters,
		wm.Timeout,
		callbackURL,
		opts...,
	), nil
}

func (wm *LDAPIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type AppleIDPWriteModel struct {
	eventstore.WriteModel

	ID         string
	Name       string
	ClientID   string
	TeamID     string
	KeyID      string
	PrivateKey *crypto.CryptoValue
	Scopes     []string
	idp.Options

	State domain.IDPState
}

func (wm *AppleIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.AppleIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.AppleIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *AppleIDPWriteModel) reduceAddedEvent(e *idp.AppleIDPAddedEvent) {
	wm.Name = e.Name
	wm.ClientID = e.ClientID
	wm.TeamID = e.TeamID
	wm.KeyID = e.KeyID
	wm.PrivateKey = e.PrivateKey
	wm.Scopes = e.Scopes
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *AppleIDPWriteModel) reduceChangedEvent(e *idp.AppleIDPChangedEvent) {
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.ClientID != nil {
		wm.ClientID = *e.ClientID
	}
	if e.PrivateKey != nil {
		wm.PrivateKey = e.PrivateKey
	}
	if e.Scopes != nil {
		wm.Scopes = e.Scopes
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *AppleIDPWriteModel) NewChanges(
	name string,
	clientID string,
	teamID string,
	keyID string,
	privateKey []byte,
	secretCrypto crypto.EncryptionAlgorithm,
	scopes []string,
	options idp.Options,
) ([]idp.AppleIDPChanges, error) {
	changes := make([]idp.AppleIDPChanges, 0)
	var encryptedKey *crypto.CryptoValue
	var err error
	if len(privateKey) != 0 {
		encryptedKey, err = crypto.Crypt(privateKey, secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeApplePrivateKey(encryptedKey))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeAppleName(name))
	}
	if wm.ClientID != clientID {
		changes = append(changes, idp.ChangeAppleClientID(clientID))
	}
	if wm.TeamID != teamID {
		changes = append(changes, idp.ChangeAppleTeamID(teamID))
	}
	if wm.KeyID != keyID {
		changes = append(changes, idp.ChangeAppleKeyID(keyID))
	}
	if slices.Compare(wm.Scopes, scopes) != 0 {
		changes = append(changes, idp.ChangeAppleScopes(scopes))
	}

	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeAppleOptions(opts))
	}
	return changes, nil
}

func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	privateKey, err := crypto.Decrypt(wm.PrivateKey, idpAlg)
	if err != nil {
		return nil, err
	}
	opts := make([]oidc.ProviderOpts, 0, 4)
	if wm.IsCreationAllowed {
		opts = append(opts, oidc.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, oidc.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, oidc.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, oidc.WithAutoUpdate())
	}
	return apple.New(
		wm.ClientID,
		wm.TeamID,
		wm.KeyID,
		callbackURL,
		privateKey,
		wm.Scopes,
		opts...,
	)
}

func (wm *AppleIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type SAMLIDPWriteModel struct {
	eventstore.WriteModel

	Name                          string
	ID                            string
	Metadata                      []byte
	Key                           *crypto.CryptoValue
	Certificate                   []byte
	Binding                       string
	WithSignedRequest             bool
	NameIDFormat                  *domain.SAMLNameIDFormat
	TransientMappingAttributeName string
	idp.Options

	State domain.IDPState
}

func (wm *SAMLIDPWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.SAMLIDPAddedEvent:
			wm.reduceAddedEvent(e)
		case *idp.SAMLIDPChangedEvent:
			wm.reduceChangedEvent(e)
		case *idp.RemovedEvent:
			wm.State = domain.IDPStateRemoved
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) {
	wm.Name = e.Name
	wm.Metadata = e.Metadata
	wm.Key = e.Key
	wm.Certificate = e.Certificate
	wm.Binding = e.Binding
	wm.WithSignedRequest = e.WithSignedRequest
	wm.NameIDFormat = e.NameIDFormat
	wm.TransientMappingAttributeName = e.TransientMappingAttributeName
	wm.Options = e.Options
	wm.State = domain.IDPStateActive
}

func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) {
	if e.Key != nil {
		wm.Key = e.Key
	}
	if e.Certificate != nil {
		wm.Certificate = e.Certificate
	}
	if e.Name != nil {
		wm.Name = *e.Name
	}
	if e.Metadata != nil {
		wm.Metadata = e.Metadata
	}
	if e.Binding != nil {
		wm.Binding = *e.Binding
	}
	if e.WithSignedRequest != nil {
		wm.WithSignedRequest = *e.WithSignedRequest
	}
	if e.NameIDFormat != nil {
		wm.NameIDFormat = e.NameIDFormat
	}
	if e.TransientMappingAttributeName != nil {
		wm.TransientMappingAttributeName = *e.TransientMappingAttributeName
	}
	wm.Options.ReduceChanges(e.OptionChanges)
}

func (wm *SAMLIDPWriteModel) NewChanges(
	name string,
	metadata,
	key,
	certificate []byte,
	secretCrypto crypto.EncryptionAlgorithm,
	binding string,
	withSignedRequest bool,
	nameIDFormat *domain.SAMLNameIDFormat,
	transientMappingAttributeName string,
	options idp.Options,
) ([]idp.SAMLIDPChanges, error) {
	changes := make([]idp.SAMLIDPChanges, 0)
	if key != nil {
		keyEnc, err := crypto.Crypt(key, secretCrypto)
		if err != nil {
			return nil, err
		}
		changes = append(changes, idp.ChangeSAMLKey(keyEnc))
	}
	if certificate != nil {
		changes = append(changes, idp.ChangeSAMLCertificate(certificate))
	}
	if wm.Name != name {
		changes = append(changes, idp.ChangeSAMLName(name))
	}
	if !reflect.DeepEqual(wm.Metadata, metadata) {
		changes = append(changes, idp.ChangeSAMLMetadata(metadata))
	}
	if wm.Binding != binding {
		changes = append(changes, idp.ChangeSAMLBinding(binding))
	}
	if wm.WithSignedRequest != withSignedRequest {
		changes = append(changes, idp.ChangeSAMLWithSignedRequest(withSignedRequest))
	}
	if wm.NameIDFormat != nameIDFormat {
		changes = append(changes, idp.ChangeSAMLNameIDFormat(nameIDFormat))
	}
	if wm.TransientMappingAttributeName != transientMappingAttributeName {
		changes = append(changes, idp.ChangeSAMLTransientMappingAttributeName(transientMappingAttributeName))
	}
	opts := wm.Options.Changes(options)
	if !opts.IsZero() {
		changes = append(changes, idp.ChangeSAMLOptions(opts))
	}
	return changes, nil
}

func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
	key, err := crypto.Decrypt(wm.Key, idpAlg)
	if err != nil {
		return nil, err
	}

	opts := make([]saml2.ProviderOpts, 0, 7)
	if wm.IsCreationAllowed {
		opts = append(opts, saml2.WithCreationAllowed())
	}
	if wm.IsLinkingAllowed {
		opts = append(opts, saml2.WithLinkingAllowed())
	}
	if wm.IsAutoCreation {
		opts = append(opts, saml2.WithAutoCreation())
	}
	if wm.IsAutoUpdate {
		opts = append(opts, saml2.WithAutoUpdate())
	}
	if wm.WithSignedRequest {
		opts = append(opts, saml2.WithSignedRequest())
	}
	if wm.Binding != "" {
		opts = append(opts, saml2.WithBinding(wm.Binding))
	}
	if wm.NameIDFormat != nil {
		opts = append(opts, saml2.WithNameIDFormat(*wm.NameIDFormat))
	}
	if wm.TransientMappingAttributeName != "" {
		opts = append(opts, saml2.WithTransientMappingAttributeName(wm.TransientMappingAttributeName))
	}
	opts = append(opts, saml2.WithCustomRequestTracker(
		requesttracker.New(
			addRequest,
			getRequest,
		),
	))
	return saml2.New(
		wm.Name,
		callbackURL,
		wm.Metadata,
		wm.Certificate,
		key,
		opts...,
	)
}

func (wm *SAMLIDPWriteModel) GetProviderOptions() idp.Options {
	return wm.Options
}

type IDPRemoveWriteModel struct {
	eventstore.WriteModel

	ID    string
	State domain.IDPState
}

func (wm *IDPRemoveWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *idp.OAuthIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.OIDCIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.JWTIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.AzureADIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.GitHubIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.GitHubEnterpriseIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.GitLabIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.GitLabSelfHostedIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.GoogleIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.LDAPIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.AppleIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.SAMLIDPAddedEvent:
			wm.reduceAdded(e.ID)
		case *idp.RemovedEvent:
			wm.reduceRemoved(e.ID)
		case *idpconfig.IDPConfigAddedEvent:
			wm.reduceAdded(e.ConfigID)
		case *idpconfig.IDPConfigRemovedEvent:
			wm.reduceRemoved(e.ConfigID)
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *IDPRemoveWriteModel) reduceAdded(id string) {
	if wm.ID != id {
		return
	}
	wm.State = domain.IDPStateActive
}

func (wm *IDPRemoveWriteModel) reduceRemoved(id string) {
	if wm.ID != id {
		return
	}
	wm.State = domain.IDPStateRemoved
}

type IDPTypeWriteModel struct {
	eventstore.WriteModel

	ID    string
	Type  domain.IDPType
	State domain.IDPState
}

func NewIDPTypeWriteModel(id string) *IDPTypeWriteModel {
	return &IDPTypeWriteModel{
		ID: id,
	}
}

func (wm *IDPTypeWriteModel) Reduce() error {
	for _, event := range wm.Events {
		switch e := event.(type) {
		case *instance.OAuthIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
		case *org.OAuthIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
		case *instance.OIDCIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
		case *org.OIDCIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
		case *instance.JWTIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
		case *org.JWTIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
		case *instance.AzureADIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
		case *org.AzureADIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
		case *instance.GitHubIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
		case *org.GitHubIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
		case *instance.GitHubEnterpriseIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
		case *org.GitHubEnterpriseIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
		case *instance.GitLabIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
		case *org.GitLabIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
		case *instance.GitLabSelfHostedIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
		case *org.GitLabSelfHostedIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
		case *instance.GoogleIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
		case *org.GoogleIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
		case *instance.LDAPIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
		case *org.LDAPIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
		case *instance.AppleIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
		case *org.AppleIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
		case *instance.SAMLIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeSAML, e.Aggregate())
		case *org.SAMLIDPAddedEvent:
			wm.reduceAdded(e.ID, domain.IDPTypeSAML, e.Aggregate())
		case *instance.OIDCIDPMigratedAzureADEvent:
			wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
		case *org.OIDCIDPMigratedAzureADEvent:
			wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
		case *instance.OIDCIDPMigratedGoogleEvent:
			wm.reduceChanged(e.ID, domain.IDPTypeGoogle)
		case *org.OIDCIDPMigratedGoogleEvent:
			wm.reduceChanged(e.ID, domain.IDPTypeGoogle)
		case *instance.IDPRemovedEvent:
			wm.reduceRemoved(e.ID)
		case *org.IDPRemovedEvent:
			wm.reduceRemoved(e.ID)
		case *instance.IDPConfigAddedEvent:
			if e.Typ == domain.IDPConfigTypeOIDC {
				wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
			} else if e.Typ == domain.IDPConfigTypeJWT {
				wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
			}
		case *org.IDPConfigAddedEvent:
			if e.Typ == domain.IDPConfigTypeOIDC {
				wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
			} else if e.Typ == domain.IDPConfigTypeJWT {
				wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
			}
		case *instance.IDPConfigRemovedEvent:
			wm.reduceRemoved(e.ConfigID)
		case *org.IDPConfigRemovedEvent:
			wm.reduceRemoved(e.ConfigID)
		}
	}
	return wm.WriteModel.Reduce()
}

func (wm *IDPTypeWriteModel) reduceAdded(id string, t domain.IDPType, agg *eventstore.Aggregate) {
	if wm.ID != id {
		return
	}
	wm.Type = t
	wm.State = domain.IDPStateActive
	wm.ResourceOwner = agg.ResourceOwner
	wm.InstanceID = agg.InstanceID
}

func (wm *IDPTypeWriteModel) reduceChanged(id string, t domain.IDPType) {
	if wm.ID != id {
		return
	}
	wm.Type = t
}

func (wm *IDPTypeWriteModel) reduceRemoved(id string) {
	if wm.ID != id {
		return
	}
	wm.Type = domain.IDPTypeUnspecified
	wm.State = domain.IDPStateRemoved
	wm.ResourceOwner = ""
	wm.InstanceID = ""
}

func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
	return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
		AddQuery().
		AggregateTypes(instance.AggregateType).
		EventTypes(
			instance.OAuthIDPAddedEventType,
			instance.OIDCIDPAddedEventType,
			instance.JWTIDPAddedEventType,
			instance.AzureADIDPAddedEventType,
			instance.GitHubIDPAddedEventType,
			instance.GitHubEnterpriseIDPAddedEventType,
			instance.GitLabIDPAddedEventType,
			instance.GitLabSelfHostedIDPAddedEventType,
			instance.GoogleIDPAddedEventType,
			instance.LDAPIDPAddedEventType,
			instance.AppleIDPAddedEventType,
			instance.SAMLIDPAddedEventType,
			instance.OIDCIDPMigratedAzureADEventType,
			instance.OIDCIDPMigratedGoogleEventType,
			instance.IDPRemovedEventType,
		).
		EventData(map[string]interface{}{"id": wm.ID}).
		Or().
		AggregateTypes(org.AggregateType).
		EventTypes(
			org.OAuthIDPAddedEventType,
			org.OIDCIDPAddedEventType,
			org.JWTIDPAddedEventType,
			org.AzureADIDPAddedEventType,
			org.GitHubIDPAddedEventType,
			org.GitHubEnterpriseIDPAddedEventType,
			org.GitLabIDPAddedEventType,
			org.GitLabSelfHostedIDPAddedEventType,
			org.GoogleIDPAddedEventType,
			org.LDAPIDPAddedEventType,
			org.AppleIDPAddedEventType,
			org.SAMLIDPAddedEventType,
			org.OIDCIDPMigratedAzureADEventType,
			org.OIDCIDPMigratedGoogleEventType,
			org.IDPRemovedEventType,
		).
		EventData(map[string]interface{}{"id": wm.ID}).
		Or(). // old events
		AggregateTypes(instance.AggregateType).
		EventTypes(
			instance.IDPConfigAddedEventType,
			instance.IDPConfigRemovedEventType,
		).
		EventData(map[string]interface{}{"idpConfigId": wm.ID}).
		Or().
		AggregateTypes(org.AggregateType).
		EventTypes(
			org.IDPConfigAddedEventType,
			org.IDPConfigRemovedEventType,
		).
		EventData(map[string]interface{}{"idpConfigId": wm.ID}).
		Builder()
}

type IDP interface {
	eventstore.QueryReducer
	ToProvider(string, crypto.EncryptionAlgorithm) (providers.Provider, error)
	GetProviderOptions() idp.Options
}

type SAMLIDP interface {
	eventstore.QueryReducer
	ToProvider(string, crypto.EncryptionAlgorithm, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error)
	GetProviderOptions() idp.Options
}

type AllIDPWriteModel struct {
	model     IDP
	samlModel SAMLIDP

	ID            string
	IDPType       domain.IDPType
	ResourceOwner string
	Instance      bool
}

func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idpType domain.IDPType) (*AllIDPWriteModel, error) {
	writeModel := &AllIDPWriteModel{
		ID:            id,
		IDPType:       idpType,
		ResourceOwner: resourceOwner,
		Instance:      instanceBool,
	}

	if instanceBool {
		switch idpType {
		case domain.IDPTypeOIDC:
			writeModel.model = NewOIDCInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeJWT:
			writeModel.model = NewJWTInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeOAuth:
			writeModel.model = NewOAuthInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeLDAP:
			writeModel.model = NewLDAPInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeAzureAD:
			writeModel.model = NewAzureADInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitHub:
			writeModel.model = NewGitHubInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitHubEnterprise:
			writeModel.model = NewGitHubEnterpriseInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitLab:
			writeModel.model = NewGitLabInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitLabSelfHosted:
			writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGoogle:
			writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeApple:
			writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeSAML:
			writeModel.samlModel = NewSAMLInstanceIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeUnspecified:
			fallthrough
		default:
			return nil, zerrors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting")
		}
	} else {
		switch idpType {
		case domain.IDPTypeOIDC:
			writeModel.model = NewOIDCOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeJWT:
			writeModel.model = NewJWTOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeOAuth:
			writeModel.model = NewOAuthOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeLDAP:
			writeModel.model = NewLDAPOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeAzureAD:
			writeModel.model = NewAzureADOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitHub:
			writeModel.model = NewGitHubOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitHubEnterprise:
			writeModel.model = NewGitHubEnterpriseOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitLab:
			writeModel.model = NewGitLabOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGitLabSelfHosted:
			writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeGoogle:
			writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeApple:
			writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeSAML:
			writeModel.samlModel = NewSAMLOrgIDPWriteModel(resourceOwner, id)
		case domain.IDPTypeUnspecified:
			fallthrough
		default:
			return nil, zerrors.ThrowInternal(nil, "COMMAND-xw921111", "Errors.IDPConfig.NotExisting")
		}
	}
	return writeModel, nil
}

func (wm *AllIDPWriteModel) Reduce() error {
	if wm.model != nil {
		return wm.model.Reduce()
	}
	return wm.samlModel.Reduce()
}

func (wm *AllIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
	if wm.model != nil {
		return wm.model.Query()
	}
	return wm.samlModel.Query()
}

func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) {
	if wm.model != nil {
		wm.model.AppendEvents(events...)
		return
	}
	wm.samlModel.AppendEvents(events...)
}

func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
	if wm.model == nil {
		return nil, zerrors.ThrowInternal(nil, "COMMAND-afvf0gc9sa", "ErrorsIDPConfig.NotExisting")
	}
	return wm.model.ToProvider(callbackURL, idpAlg)
}

func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options {
	if wm.model != nil {
		return wm.model.GetProviderOptions()
	}
	return wm.samlModel.GetProviderOptions()
}

func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
	if wm.samlModel == nil {
		return nil, zerrors.ThrowInternal(nil, "COMMAND-csi30hdscv", "ErrorsIDPConfig.NotExisting")
	}
	return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest)
}