package command

import (
	"context"

	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/internal/crypto"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/repository/user"
	"github.com/zitadel/zitadel/internal/telemetry/tracing"
	"github.com/zitadel/zitadel/internal/zerrors"
)

type ChangeHuman struct {
	ID       string
	Username *string
	Profile  *Profile
	Email    *Email
	Phone    *Phone

	Password *Password

	// Details are set after a successful execution of the command
	Details *domain.ObjectDetails

	// EmailCode is set by the command
	EmailCode *string

	// PhoneCode is set by the command
	PhoneCode *string
}

type Profile struct {
	FirstName         *string
	LastName          *string
	NickName          *string
	DisplayName       *string
	PreferredLanguage *language.Tag
	Gender            *domain.Gender
}

type Password struct {
	// Either you have to have permission, a password code or the old password to change
	PasswordCode        *string
	OldPassword         *string
	Password            *string
	EncodedPasswordHash *string

	ChangeRequired bool
}

func (h *ChangeHuman) Validate(hasher *crypto.Hasher) (err error) {
	if h.Email != nil && h.Email.Address != "" {
		if err := h.Email.Validate(); err != nil {
			return err
		}
	}

	if h.Phone != nil && h.Phone.Number != "" {
		if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
			return err
		}
	}

	if h.Password != nil {
		if err := h.Password.Validate(hasher); err != nil {
			return err
		}
	}
	return nil
}

func (p *Password) Validate(hasher *crypto.Hasher) error {
	if p.EncodedPasswordHash != nil {
		if !hasher.EncodingSupported(*p.EncodedPasswordHash) {
			return zerrors.ThrowInvalidArgument(nil, "USER-oz74onzvqr", "Errors.User.Password.NotSupported")
		}
	}
	if p.Password == nil && p.EncodedPasswordHash == nil {
		return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")
	}
	return nil
}

func (h *ChangeHuman) Changed() bool {
	if h.Username != nil {
		return true
	}
	if h.Profile != nil {
		return true
	}
	if h.Email != nil {
		return true
	}
	if h.Phone != nil {
		return true
	}
	if h.Password != nil {
		return true
	}
	return false
}

func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm) (err error) {
	if resourceOwner == "" {
		return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal")
	}

	if err := human.Validate(c.userPasswordHasher); err != nil {
		return err
	}

	if human.ID == "" {
		human.ID, err = c.idGenerator.Next()
		if err != nil {
			return err
		}
	}

	// only check if user is already existing
	existingHuman, err := c.userExistsWriteModel(
		ctx,
		human.ID,
	)
	if err != nil {
		return err
	}
	if isUserStateExists(existingHuman.UserState) {
		return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting")
	}
	// check for permission to create user on resourceOwner
	if !human.Register {
		if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil {
			return err
		}
	}
	// add resourceowner for the events with the aggregate
	existingHuman.ResourceOwner = resourceOwner

	domainPolicy, err := c.domainPolicyWriteModel(ctx, resourceOwner)
	if err != nil {
		return err
	}

	if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
		return err
	}
	var createCmd humanCreationCommand
	if human.Register {
		createCmd = user.NewHumanRegisteredEvent(
			ctx,
			&existingHuman.Aggregate().Aggregate,
			human.Username,
			human.FirstName,
			human.LastName,
			human.NickName,
			human.DisplayName,
			human.PreferredLanguage,
			human.Gender,
			human.Email.Address,
			domainPolicy.UserLoginMustBeDomain,
			human.UserAgentID,
		)
	} else {
		createCmd = user.NewHumanAddedEvent(
			ctx,
			&existingHuman.Aggregate().Aggregate,
			human.Username,
			human.FirstName,
			human.LastName,
			human.NickName,
			human.DisplayName,
			human.PreferredLanguage,
			human.Gender,
			human.Email.Address,
			domainPolicy.UserLoginMustBeDomain,
		)
	}

	if human.Phone.Number != "" {
		createCmd.AddPhoneData(human.Phone.Number)
	}

	// separated to change when old user logic is not used anymore
	filter := c.eventstore.Filter //nolint:staticcheck
	if err := addHumanCommandPassword(ctx, filter, createCmd, human, c.userPasswordHasher); err != nil {
		return err
	}

	cmds := make([]eventstore.Command, 0, 3)
	cmds = append(cmds, createCmd)

	cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail)
	if err != nil {
		return err
	}

	cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg)
	if err != nil {
		return err
	}

	for _, metadataEntry := range human.Metadata {
		cmds = append(cmds, user.NewMetadataSetEvent(
			ctx,
			&existingHuman.Aggregate().Aggregate,
			metadataEntry.Key,
			metadataEntry.Value,
		))
	}
	for _, link := range human.Links {
		cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link)
		if err != nil {
			return err
		}
		cmds = append(cmds, cmd)
	}

	if len(cmds) == 0 {
		human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
		return nil
	}

	err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
	if err != nil {
		return err
	}
	human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
	return nil
}

func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) {
	if err := human.Validate(c.userPasswordHasher); err != nil {
		return err
	}

	existingHuman, err := c.userHumanWriteModel(
		ctx,
		human.ID,
		human.Profile != nil,
		human.Email != nil,
		human.Phone != nil,
		human.Password != nil,
		false, // avatar not updateable
		false, // IDPLinks not updateable
	)
	if err != nil {
		return err
	}
	if !isUserStateExists(existingHuman.UserState) {
		return zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")
	}

	if human.Changed() {
		if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
			return err
		}
	}

	cmds := make([]eventstore.Command, 0)
	if human.Username != nil {
		cmds, err = c.changeUsername(ctx, cmds, existingHuman, *human.Username)
		if err != nil {
			return err
		}
	}
	if human.Profile != nil {
		cmds, err = changeUserProfile(ctx, cmds, existingHuman, human.Profile)
		if err != nil {
			return err
		}
	}
	if human.Email != nil {
		cmds, human.EmailCode, err = c.changeUserEmail(ctx, cmds, existingHuman, human.Email, alg)
		if err != nil {
			return err
		}
	}
	if human.Phone != nil {
		cmds, human.PhoneCode, err = c.changeUserPhone(ctx, cmds, existingHuman, human.Phone, alg)
		if err != nil {
			return err
		}
	}
	if human.Password != nil {
		cmds, err = c.changeUserPassword(ctx, cmds, existingHuman, human.Password, alg)
		if err != nil {
			return err
		}
	}

	if len(cmds) == 0 {
		human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
		return nil
	}
	err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
	if err != nil {
		return err
	}
	human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
	return nil
}

func (c *Commands) changeUserEmail(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, code *string, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.End() }()

	if email.Address != "" && email.Address != wm.Email {
		cmds = append(cmds, user.NewHumanEmailChangedEvent(ctx, &wm.Aggregate().Aggregate, email.Address))

		if email.Verified {
			return append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
		} else {
			cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
			if err != nil {
				return cmds, code, err
			}
			cmds = append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, email.URLTemplate, email.ReturnCode))
			if email.ReturnCode {
				code = &cryptoCode.Plain
			}
			return cmds, code, nil
		}
	}
	// only create separate event of verified if email was not changed
	if email.Verified && wm.IsEmailVerified != email.Verified {
		return append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), nil, nil
	}
	return cmds, code, nil
}

func (c *Commands) changeUserPhone(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, code *string, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.End() }()

	if phone.Number != "" && phone.Number != wm.Phone {
		cmds = append(cmds, user.NewHumanPhoneChangedEvent(ctx, &wm.Aggregate().Aggregate, phone.Number))

		if phone.Verified {
			return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
		} else {
			cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
			if err != nil {
				return cmds, code, err
			}
			cmds = append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, phone.ReturnCode))
			if phone.ReturnCode {
				code = &cryptoCode.Plain
			}
			return cmds, code, nil
		}
	}
	// only create separate event of verified if email was not changed
	if phone.Verified && wm.IsPhoneVerified != phone.Verified {
		return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
	}
	return cmds, code, nil
}

func changeUserProfile(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, profile *Profile) ([]eventstore.Command, error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.End() }()

	cmd, err := wm.NewProfileChangedEvent(ctx, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, profile.Gender)
	if cmd != nil {
		return append(cmds, cmd), err
	}
	return cmds, err
}

func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, password *Password, alg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.End() }()

	// Either have a code to set the password
	if password.PasswordCode != nil {
		if err := crypto.VerifyCode(wm.PasswordCodeCreationDate, wm.PasswordCodeExpiry, wm.PasswordCode, *password.PasswordCode, alg); err != nil {
			return cmds, err
		}
	}
	var encodedPassword string
	// or have the old password to change it
	if password.OldPassword != nil {
		// newly encode old password if no new and already encoded password is set
		pw := *password.OldPassword
		if password.Password != nil {
			pw = *password.Password
		}
		alreadyEncodedPassword, err := c.verifyAndUpdatePassword(ctx, wm.PasswordEncodedHash, *password.OldPassword, pw)
		if err != nil {
			return cmds, err
		}
		encodedPassword = alreadyEncodedPassword
	}

	// password already hashed in request
	if password.EncodedPasswordHash != nil {
		cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.EncodedPasswordHash, "", password.ChangeRequired, true)
		if cmd != nil {
			return append(cmds, cmd), err
		}
		return cmds, err
	}
	// password already hashed in verify
	if encodedPassword != "" {
		cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, encodedPassword, "", password.ChangeRequired, true)
		if cmd != nil {
			return append(cmds, cmd), err
		}
		return cmds, err
	}
	// password still to be hashed
	if password.Password != nil {
		cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.Password, "", password.ChangeRequired, false)
		if cmd != nil {
			return append(cmds, cmd), err
		}
		return cmds, err
	}
	// no password changes necessary
	return cmds, nil
}

func (c *Commands) userExistsWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	writeModel = NewUserExistsWriteModel(userID, "")
	err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
	if err != nil {
		return nil, err
	}
	return writeModel, nil
}

func (c *Commands) userHumanWriteModel(ctx context.Context, userID string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM bool) (writeModel *UserV2WriteModel, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	writeModel = NewUserHumanWriteModel(userID, "", profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM)
	err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
	if err != nil {
		return nil, err
	}
	return writeModel, nil
}

func (c *Commands) orgDomainVerifiedWriteModel(ctx context.Context, domain string) (writeModel *OrgDomainVerifiedWriteModel, err error) {
	ctx, span := tracing.NewSpan(ctx)
	defer func() { span.EndWithError(err) }()

	writeModel = NewOrgDomainVerifiedWriteModel(domain)
	err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
	if err != nil {
		return nil, err
	}
	return writeModel, nil
}