package user

import (
	"context"
	"time"

	"github.com/zitadel/zitadel/internal/api/http"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/zerrors"
)

const (
	UniqueUsername            = "usernames"
	userEventTypePrefix       = eventstore.EventType("user.")
	UserLockedType            = userEventTypePrefix + "locked"
	UserUnlockedType          = userEventTypePrefix + "unlocked"
	UserDeactivatedType       = userEventTypePrefix + "deactivated"
	UserReactivatedType       = userEventTypePrefix + "reactivated"
	UserRemovedType           = userEventTypePrefix + "removed"
	UserTokenAddedType        = userEventTypePrefix + "token.added"
	UserTokenV2AddedType      = userEventTypePrefix + "token.v2.added"
	UserTokenRemovedType      = userEventTypePrefix + "token.removed"
	UserImpersonatedType      = userEventTypePrefix + "impersonated"
	UserDomainClaimedType     = userEventTypePrefix + "domain.claimed"
	UserDomainClaimedSentType = userEventTypePrefix + "domain.claimed.sent"
	UserUserNameChangedType   = userEventTypePrefix + "username.changed"
)

func NewAddUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint {
	uniqueUserName := userName
	if userLoginMustBeDomain {
		uniqueUserName = userName + resourceOwner
	}
	return eventstore.NewAddEventUniqueConstraint(
		UniqueUsername,
		uniqueUserName,
		"Errors.User.AlreadyExists")
}

func NewRemoveUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint {
	uniqueUserName := userName
	if userLoginMustBeDomain {
		uniqueUserName = userName + resourceOwner
	}
	return eventstore.NewRemoveUniqueConstraint(
		UniqueUsername,
		uniqueUserName)
}

type UserLockedEvent struct {
	eventstore.BaseEvent `json:"-"`
}

func (e *UserLockedEvent) Payload() interface{} {
	return nil
}

func (e *UserLockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserLockedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserLockedEvent {
	return &UserLockedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserLockedType,
		),
	}
}

func UserLockedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &UserLockedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UserUnlockedEvent struct {
	eventstore.BaseEvent `json:"-"`
}

func (e *UserUnlockedEvent) Payload() interface{} {
	return nil
}

func (e *UserUnlockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserUnlockedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserUnlockedEvent {
	return &UserUnlockedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserUnlockedType,
		),
	}
}

func UserUnlockedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &UserUnlockedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UserDeactivatedEvent struct {
	eventstore.BaseEvent `json:"-"`
}

func (e *UserDeactivatedEvent) Payload() interface{} {
	return nil
}

func (e *UserDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserDeactivatedEvent {
	return &UserDeactivatedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserDeactivatedType,
		),
	}
}

func UserDeactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &UserDeactivatedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UserReactivatedEvent struct {
	eventstore.BaseEvent `json:"-"`
}

func (e *UserReactivatedEvent) Payload() interface{} {
	return nil
}

func (e *UserReactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserReactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserReactivatedEvent {
	return &UserReactivatedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserReactivatedType,
		),
	}
}

func UserReactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &UserReactivatedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UserRemovedEvent struct {
	eventstore.BaseEvent `json:"-"`

	userName          string
	externalIDPs      []*domain.UserIDPLink
	loginMustBeDomain bool
}

func (e *UserRemovedEvent) Payload() interface{} {
	return nil
}

func (e *UserRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	events := make([]*eventstore.UniqueConstraint, 0)
	if e.userName != "" {
		events = append(events, NewRemoveUsernameUniqueConstraint(e.userName, e.Aggregate().ResourceOwner, e.loginMustBeDomain))
	}
	for _, idp := range e.externalIDPs {
		events = append(events, NewRemoveUserIDPLinkUniqueConstraint(idp.IDPConfigID, idp.ExternalUserID))
	}
	return events
}

func NewUserRemovedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	userName string,
	externalIDPs []*domain.UserIDPLink,
	userLoginMustBeDomain bool,
) *UserRemovedEvent {
	return &UserRemovedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserRemovedType,
		),
		userName:          userName,
		externalIDPs:      externalIDPs,
		loginMustBeDomain: userLoginMustBeDomain,
	}
}

func UserRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &UserRemovedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UserTokenAddedEvent struct {
	eventstore.BaseEvent `json:"-"`

	TokenID               string             `json:"tokenId,omitempty"`
	ApplicationID         string             `json:"applicationId,omitempty"`
	UserAgentID           string             `json:"userAgentId,omitempty"`
	RefreshTokenID        string             `json:"refreshTokenID,omitempty"`
	Audience              []string           `json:"audience,omitempty"`
	Scopes                []string           `json:"scopes,omitempty"`
	AuthMethodsReferences []string           `json:"authMethodsReferences,omitempty"`
	AuthTime              time.Time          `json:"authTime,omitempty"`
	Expiration            time.Time          `json:"expiration,omitempty"`
	PreferredLanguage     string             `json:"preferredLanguage,omitempty"`
	Reason                domain.TokenReason `json:"reason,omitempty"`
	Actor                 *domain.TokenActor `json:"actor,omitempty"`
}

func (e *UserTokenAddedEvent) Payload() interface{} {
	return e
}

func (e *UserTokenAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserTokenAddedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	tokenID,
	applicationID,
	userAgentID,
	preferredLanguage,
	refreshTokenID string,
	audience,
	scopes,
	authMethodsReferences []string,
	authTime,
	expiration time.Time,
	reason domain.TokenReason,
	actor *domain.TokenActor,
) *UserTokenAddedEvent {
	return &UserTokenAddedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserTokenAddedType,
		),
		TokenID:           tokenID,
		ApplicationID:     applicationID,
		UserAgentID:       userAgentID,
		RefreshTokenID:    refreshTokenID,
		Audience:          audience,
		Scopes:            scopes,
		Expiration:        expiration,
		PreferredLanguage: preferredLanguage,
		Reason:            reason,
		Actor:             actor,
	}
}

func UserTokenAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	tokenAdded := &UserTokenAddedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}
	err := event.Unmarshal(tokenAdded)
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "USER-7M9sd", "unable to unmarshal token added")
	}

	return tokenAdded, nil
}

type UserTokenV2AddedEvent struct {
	*eventstore.BaseEvent `json:"-"`

	TokenID string `json:"tokenId,omitempty"`
}

func (e *UserTokenV2AddedEvent) Payload() interface{} {
	return e
}

func (e *UserTokenV2AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
	e.BaseEvent = b
}

func (e *UserTokenV2AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserTokenV2AddedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	tokenID string,
) *UserTokenV2AddedEvent {
	return &UserTokenV2AddedEvent{
		BaseEvent: eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserTokenV2AddedType,
		),
		TokenID: tokenID,
	}
}

type UserImpersonatedEvent struct {
	eventstore.BaseEvent `json:"-"`

	ApplicationID string             `json:"applicationId,omitempty"`
	Actor         *domain.TokenActor `json:"actor,omitempty"`
}

func (e *UserImpersonatedEvent) Payload() interface{} {
	return e
}

func (e *UserImpersonatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func (e *UserImpersonatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
	e.BaseEvent = *base
}

func NewUserImpersonatedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	applicationID string,
	actor *domain.TokenActor,
) *UserImpersonatedEvent {
	return &UserImpersonatedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserImpersonatedType,
		),
		ApplicationID: applicationID,
		Actor:         actor,
	}
}

type UserTokenRemovedEvent struct {
	eventstore.BaseEvent `json:"-"`

	TokenID string `json:"tokenId"`
}

func (e *UserTokenRemovedEvent) Payload() interface{} {
	return e
}

func (e *UserTokenRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewUserTokenRemovedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	tokenID string,
) *UserTokenRemovedEvent {
	return &UserTokenRemovedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserTokenRemovedType,
		),
		TokenID: tokenID,
	}
}

func UserTokenRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	tokenRemoved := &UserTokenRemovedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}
	err := event.Unmarshal(tokenRemoved)
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "USER-7M9sd", "unable to unmarshal token added")
	}

	return tokenRemoved, nil
}

type DomainClaimedEvent struct {
	eventstore.BaseEvent `json:"-"`

	UserName              string `json:"userName"`
	TriggeredAtOrigin     string `json:"triggerOrigin,omitempty"`
	oldUserName           string
	userLoginMustBeDomain bool
}

func (e *DomainClaimedEvent) Payload() interface{} {
	return e
}

func (e *DomainClaimedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return []*eventstore.UniqueConstraint{
		NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
		NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
	}
}

func (e *DomainClaimedEvent) TriggerOrigin() string {
	return e.TriggeredAtOrigin
}

func NewDomainClaimedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	userName,
	oldUserName string,
	userLoginMustBeDomain bool,
) *DomainClaimedEvent {
	return &DomainClaimedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserDomainClaimedType,
		),
		UserName:              userName,
		oldUserName:           oldUserName,
		userLoginMustBeDomain: userLoginMustBeDomain,
		TriggeredAtOrigin:     http.ComposedOrigin(ctx),
	}
}

func DomainClaimedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	domainClaimed := &DomainClaimedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}
	err := event.Unmarshal(domainClaimed)
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "USER-aR8jc", "unable to unmarshal domain claimed")
	}

	return domainClaimed, nil
}

type DomainClaimedSentEvent struct {
	eventstore.BaseEvent `json:"-"`
}

func (e *DomainClaimedSentEvent) Payload() interface{} {
	return nil
}

func (e *DomainClaimedSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return nil
}

func NewDomainClaimedSentEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
) *DomainClaimedSentEvent {
	return &DomainClaimedSentEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserDomainClaimedSentType,
		),
	}
}

func DomainClaimedSentEventMapper(event eventstore.Event) (eventstore.Event, error) {
	return &DomainClaimedSentEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}, nil
}

type UsernameChangedEvent struct {
	eventstore.BaseEvent `json:"-"`

	UserName                 string `json:"userName"`
	oldUserName              string
	userLoginMustBeDomain    bool
	oldUserLoginMustBeDomain bool
}

func (e *UsernameChangedEvent) Payload() interface{} {
	return e
}

func (e *UsernameChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
	return []*eventstore.UniqueConstraint{
		NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.oldUserLoginMustBeDomain),
		NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
	}
}

func NewUsernameChangedEvent(
	ctx context.Context,
	aggregate *eventstore.Aggregate,
	oldUserName,
	newUserName string,
	userLoginMustBeDomain bool,
	opts ...UsernameChangedEventOption,
) *UsernameChangedEvent {
	event := &UsernameChangedEvent{
		BaseEvent: *eventstore.NewBaseEventForPush(
			ctx,
			aggregate,
			UserUserNameChangedType,
		),
		UserName:                 newUserName,
		oldUserName:              oldUserName,
		userLoginMustBeDomain:    userLoginMustBeDomain,
		oldUserLoginMustBeDomain: userLoginMustBeDomain,
	}
	for _, opt := range opts {
		opt(event)
	}
	return event
}

type UsernameChangedEventOption func(*UsernameChangedEvent)

// UsernameChangedEventWithPolicyChange signals that the change occurs because of / during a domain policy change
// (will ensure the unique constraint change is handled correctly)
func UsernameChangedEventWithPolicyChange() UsernameChangedEventOption {
	return func(e *UsernameChangedEvent) {
		e.oldUserLoginMustBeDomain = !e.userLoginMustBeDomain
	}
}

func UsernameChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
	domainClaimed := &UsernameChangedEvent{
		BaseEvent: *eventstore.BaseEventFromRepo(event),
	}
	err := event.Unmarshal(domainClaimed)
	if err != nil {
		return nil, zerrors.ThrowInternal(err, "USER-4Bm9s", "unable to unmarshal username changed")
	}

	return domainClaimed, nil
}