package models

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"fmt"
	"strings"
	"time"

	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/gobuffalo/pop/v6"
	"github.com/gofrs/uuid"
	"github.com/pkg/errors"
	"github.com/supabase/auth/internal/crypto"
	"github.com/supabase/auth/internal/storage"
	"golang.org/x/crypto/bcrypt"
)

// User respresents a registered user with email/password authentication
type User struct {
	ID uuid.UUID `json:"id" db:"id"`

	Aud       string             `json:"aud" db:"aud"`
	Role      string             `json:"role" db:"role"`
	Email     storage.NullString `json:"email" db:"email"`
	IsSSOUser bool               `json:"-" db:"is_sso_user"`

	EncryptedPassword *string    `json:"-" db:"encrypted_password"`
	EmailConfirmedAt  *time.Time `json:"email_confirmed_at,omitempty" db:"email_confirmed_at"`
	InvitedAt         *time.Time `json:"invited_at,omitempty" db:"invited_at"`

	Phone            storage.NullString `json:"phone" db:"phone"`
	PhoneConfirmedAt *time.Time         `json:"phone_confirmed_at,omitempty" db:"phone_confirmed_at"`

	ConfirmationToken  string     `json:"-" db:"confirmation_token"`
	ConfirmationSentAt *time.Time `json:"confirmation_sent_at,omitempty" db:"confirmation_sent_at"`

	// For backward compatibility only. Use EmailConfirmedAt or PhoneConfirmedAt instead.
	ConfirmedAt *time.Time `json:"confirmed_at,omitempty" db:"confirmed_at" rw:"r"`

	RecoveryToken  string     `json:"-" db:"recovery_token"`
	RecoverySentAt *time.Time `json:"recovery_sent_at,omitempty" db:"recovery_sent_at"`

	EmailChangeTokenCurrent  string     `json:"-" db:"email_change_token_current"`
	EmailChangeTokenNew      string     `json:"-" db:"email_change_token_new"`
	EmailChange              string     `json:"new_email,omitempty" db:"email_change"`
	EmailChangeSentAt        *time.Time `json:"email_change_sent_at,omitempty" db:"email_change_sent_at"`
	EmailChangeConfirmStatus int        `json:"-" db:"email_change_confirm_status"`

	PhoneChangeToken  string     `json:"-" db:"phone_change_token"`
	PhoneChange       string     `json:"new_phone,omitempty" db:"phone_change"`
	PhoneChangeSentAt *time.Time `json:"phone_change_sent_at,omitempty" db:"phone_change_sent_at"`

	ReauthenticationToken  string     `json:"-" db:"reauthentication_token"`
	ReauthenticationSentAt *time.Time `json:"reauthentication_sent_at,omitempty" db:"reauthentication_sent_at"`

	LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"`

	AppMetaData  JSONMap `json:"app_metadata" db:"raw_app_meta_data"`
	UserMetaData JSONMap `json:"user_metadata" db:"raw_user_meta_data"`

	Factors    []Factor   `json:"factors,omitempty" has_many:"factors"`
	Identities []Identity `json:"identities" has_many:"identities"`

	CreatedAt   time.Time  `json:"created_at" db:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at" db:"updated_at"`
	BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"`
	DeletedAt   *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
	IsAnonymous bool       `json:"is_anonymous" db:"is_anonymous"`

	DONTUSEINSTANCEID uuid.UUID `json:"-" db:"instance_id"`
}

func NewUserWithPasswordHash(phone, email, passwordHash, aud string, userData map[string]interface{}) (*User, error) {
	if strings.HasPrefix(passwordHash, crypto.Argon2Prefix) {
		_, err := crypto.ParseArgon2Hash(passwordHash)
		if err != nil {
			return nil, err
		}
	} else if strings.HasPrefix(passwordHash, crypto.FirebaseScryptPrefix) {
		_, err := crypto.ParseFirebaseScryptHash(passwordHash)
		if err != nil {
			return nil, err
		}
	} else {
		// verify that the hash is a bcrypt hash
		_, err := bcrypt.Cost([]byte(passwordHash))
		if err != nil {
			return nil, err
		}
	}
	id := uuid.Must(uuid.NewV4())
	user := &User{
		ID:                id,
		Aud:               aud,
		Email:             storage.NullString(strings.ToLower(email)),
		Phone:             storage.NullString(phone),
		UserMetaData:      userData,
		EncryptedPassword: &passwordHash,
	}
	return user, nil
}

// NewUser initializes a new user from an email, password and user data.
func NewUser(phone, email, password, aud string, userData map[string]interface{}) (*User, error) {
	passwordHash := ""

	if password != "" {
		pw, err := crypto.GenerateFromPassword(context.Background(), password)
		if err != nil {
			return nil, err
		}

		passwordHash = pw
	}

	if userData == nil {
		userData = make(map[string]interface{})
	}

	id := uuid.Must(uuid.NewV4())
	user := &User{
		ID:                id,
		Aud:               aud,
		Email:             storage.NullString(strings.ToLower(email)),
		Phone:             storage.NullString(phone),
		UserMetaData:      userData,
		EncryptedPassword: &passwordHash,
	}
	return user, nil
}

// TableName overrides the table name used by pop
func (User) TableName() string {
	tableName := "users"
	return tableName
}

func (u *User) HasPassword() bool {
	var pwd string

	if u.EncryptedPassword != nil {
		pwd = *u.EncryptedPassword
	}

	return pwd != ""
}

// BeforeSave is invoked before the user is saved to the database
func (u *User) BeforeSave(tx *pop.Connection) error {
	if u.EmailConfirmedAt != nil && u.EmailConfirmedAt.IsZero() {
		u.EmailConfirmedAt = nil
	}
	if u.PhoneConfirmedAt != nil && u.PhoneConfirmedAt.IsZero() {
		u.PhoneConfirmedAt = nil
	}
	if u.InvitedAt != nil && u.InvitedAt.IsZero() {
		u.InvitedAt = nil
	}
	if u.ConfirmationSentAt != nil && u.ConfirmationSentAt.IsZero() {
		u.ConfirmationSentAt = nil
	}
	if u.RecoverySentAt != nil && u.RecoverySentAt.IsZero() {
		u.RecoverySentAt = nil
	}
	if u.EmailChangeSentAt != nil && u.EmailChangeSentAt.IsZero() {
		u.EmailChangeSentAt = nil
	}
	if u.PhoneChangeSentAt != nil && u.PhoneChangeSentAt.IsZero() {
		u.PhoneChangeSentAt = nil
	}
	if u.ReauthenticationSentAt != nil && u.ReauthenticationSentAt.IsZero() {
		u.ReauthenticationSentAt = nil
	}
	if u.LastSignInAt != nil && u.LastSignInAt.IsZero() {
		u.LastSignInAt = nil
	}
	if u.BannedUntil != nil && u.BannedUntil.IsZero() {
		u.BannedUntil = nil
	}
	return nil
}

// IsConfirmed checks if a user has already been
// registered and confirmed.
func (u *User) IsConfirmed() bool {
	return u.EmailConfirmedAt != nil
}

// HasBeenInvited checks if user has been invited
func (u *User) HasBeenInvited() bool {
	return u.InvitedAt != nil
}

// IsPhoneConfirmed checks if a user's phone has already been
// registered and confirmed.
func (u *User) IsPhoneConfirmed() bool {
	return u.PhoneConfirmedAt != nil
}

// SetRole sets the users Role to roleName
func (u *User) SetRole(tx *storage.Connection, roleName string) error {
	u.Role = strings.TrimSpace(roleName)
	return tx.UpdateOnly(u, "role")
}

// HasRole returns true when the users role is set to roleName
func (u *User) HasRole(roleName string) bool {
	return u.Role == roleName
}

// GetEmail returns the user's email as a string
func (u *User) GetEmail() string {
	return string(u.Email)
}

// GetPhone returns the user's phone number as a string
func (u *User) GetPhone() string {
	return string(u.Phone)
}

// UpdateUserMetaData sets all user data from a map of updates,
// ensuring that it doesn't override attributes that are not
// in the provided map.
func (u *User) UpdateUserMetaData(tx *storage.Connection, updates map[string]interface{}) error {
	if u.UserMetaData == nil {
		u.UserMetaData = updates
	} else {
		for key, value := range updates {
			if value != nil {
				u.UserMetaData[key] = value
			} else {
				delete(u.UserMetaData, key)
			}
		}
	}
	return tx.UpdateOnly(u, "raw_user_meta_data")
}

// UpdateAppMetaData updates all app data from a map of updates
func (u *User) UpdateAppMetaData(tx *storage.Connection, updates map[string]interface{}) error {
	if u.AppMetaData == nil {
		u.AppMetaData = updates
	} else {
		for key, value := range updates {
			if value != nil {
				u.AppMetaData[key] = value
			} else {
				delete(u.AppMetaData, key)
			}
		}
	}
	return tx.UpdateOnly(u, "raw_app_meta_data")
}

// UpdateAppMetaDataProviders updates the provider field in AppMetaData column
func (u *User) UpdateAppMetaDataProviders(tx *storage.Connection) error {
	providers, terr := FindProvidersByUser(tx, u)
	if terr != nil {
		return terr
	}
	payload := map[string]interface{}{
		"providers": providers,
	}
	if len(providers) > 0 {
		payload["provider"] = providers[0]
	}
	return u.UpdateAppMetaData(tx, payload)
}

// UpdateUserEmail updates the user's email to one of the identity's email
// if the current email used doesn't match any of the identities email
func (u *User) UpdateUserEmailFromIdentities(tx *storage.Connection) error {
	identities, terr := FindIdentitiesByUserID(tx, u.ID)
	if terr != nil {
		return terr
	}
	for _, i := range identities {
		if u.GetEmail() == i.GetEmail() {
			// there's an existing identity that uses the same email
			// so the user's email can be kept
			return nil
		}
	}

	var primaryIdentity *Identity
	for _, i := range identities {
		if _, terr := FindUserByEmailAndAudience(tx, i.GetEmail(), u.Aud); terr != nil {
			if IsNotFoundError(terr) {
				// the identity's email is not used by another user
				// so we can set it as the primary identity
				primaryIdentity = i
				break
			}
			return terr
		}
	}
	if primaryIdentity == nil {
		return UserEmailUniqueConflictError{}
	}
	// default to the first identity's email
	if terr := u.SetEmail(tx, primaryIdentity.GetEmail()); terr != nil {
		return terr
	}
	if primaryIdentity.GetEmail() == "" {
		u.EmailConfirmedAt = nil
		if terr := tx.UpdateOnly(u, "email_confirmed_at"); terr != nil {
			return terr
		}
	}
	return nil
}

// SetEmail sets the user's email
func (u *User) SetEmail(tx *storage.Connection, email string) error {
	u.Email = storage.NullString(email)
	return tx.UpdateOnly(u, "email")
}

// SetPhone sets the user's phone
func (u *User) SetPhone(tx *storage.Connection, phone string) error {
	u.Phone = storage.NullString(phone)
	return tx.UpdateOnly(u, "phone")
}

func (u *User) SetPassword(ctx context.Context, password string, encrypt bool, encryptionKeyID, encryptionKey string) error {
	if password == "" {
		u.EncryptedPassword = nil
		return nil
	}

	pw, err := crypto.GenerateFromPassword(ctx, password)
	if err != nil {
		return err
	}

	u.EncryptedPassword = &pw
	if encrypt {
		es, err := crypto.NewEncryptedString(u.ID.String(), []byte(pw), encryptionKeyID, encryptionKey)
		if err != nil {
			return err
		}

		encryptedPassword := es.String()
		u.EncryptedPassword = &encryptedPassword
	}

	return nil
}

// UpdatePassword updates the user's password. Use SetPassword outside of a transaction first!
func (u *User) UpdatePassword(tx *storage.Connection, sessionID *uuid.UUID) error {
	// These need to be reset because password change may mean the user no longer trusts the actions performed by the previous password.
	u.ConfirmationToken = ""
	u.ConfirmationSentAt = nil
	u.RecoveryToken = ""
	u.RecoverySentAt = nil
	u.EmailChangeTokenCurrent = ""
	u.EmailChangeTokenNew = ""
	u.EmailChangeSentAt = nil
	u.PhoneChangeToken = ""
	u.PhoneChangeSentAt = nil
	u.ReauthenticationToken = ""
	u.ReauthenticationSentAt = nil

	if err := tx.UpdateOnly(u, "encrypted_password", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token_current", "email_change_token_new", "email_change_sent_at", "phone_change_token", "phone_change_sent_at", "reauthentication_token", "reauthentication_sent_at"); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	if sessionID == nil {
		// log out user from all sessions to ensure reauthentication after password change
		return Logout(tx, u.ID)
	} else {
		// log out user from all other sessions to ensure reauthentication after password change
		return LogoutAllExceptMe(tx, *sessionID, u.ID)
	}
}

// Authenticate a user from a password
func (u *User) Authenticate(ctx context.Context, tx *storage.Connection, password string, decryptionKeys map[string]string, encrypt bool, encryptionKeyID string) (bool, bool, error) {
	if u.EncryptedPassword == nil {
		return false, false, nil
	}

	hash := *u.EncryptedPassword

	if hash == "" {
		return false, false, nil
	}

	es := crypto.ParseEncryptedString(hash)
	if es != nil {
		h, err := es.Decrypt(u.ID.String(), decryptionKeys)
		if err != nil {
			return false, false, err
		}

		hash = string(h)
	}

	compareErr := crypto.CompareHashAndPassword(ctx, hash, password)

	if !strings.HasPrefix(hash, crypto.Argon2Prefix) && !strings.HasPrefix(hash, crypto.FirebaseScryptPrefix) {
		// check if cost exceeds default cost or is too low
		cost, err := bcrypt.Cost([]byte(hash))
		if err != nil {
			return compareErr == nil, false, err
		}

		if cost > bcrypt.DefaultCost || cost == bcrypt.MinCost {
			// don't bother with encrypting the password in Authenticate
			// since it's handled separately
			if err := u.SetPassword(ctx, password, false, "", ""); err != nil {
				return compareErr == nil, false, err
			}
		}
	}

	return compareErr == nil, encrypt && (es == nil || es.ShouldReEncrypt(encryptionKeyID)), nil
}

// ConfirmReauthentication resets the reauthentication token
func (u *User) ConfirmReauthentication(tx *storage.Connection) error {
	u.ReauthenticationToken = ""
	if err := tx.UpdateOnly(u, "reauthentication_token"); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	return nil
}

// Confirm resets the confimation token and sets the confirm timestamp
func (u *User) Confirm(tx *storage.Connection) error {
	u.ConfirmationToken = ""
	now := time.Now()
	u.EmailConfirmedAt = &now

	if err := tx.UpdateOnly(u, "confirmation_token", "email_confirmed_at"); err != nil {
		return err
	}

	if err := u.UpdateUserMetaData(tx, map[string]interface{}{
		"email_verified": true,
	}); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	return nil
}

// ConfirmPhone resets the confimation token and sets the confirm timestamp
func (u *User) ConfirmPhone(tx *storage.Connection) error {
	u.ConfirmationToken = ""
	now := time.Now()
	u.PhoneConfirmedAt = &now
	if err := tx.UpdateOnly(u, "confirmation_token", "phone_confirmed_at"); err != nil {
		return nil
	}

	return ClearAllOneTimeTokensForUser(tx, u.ID)
}

// UpdateLastSignInAt update field last_sign_in_at for user according to specified field
func (u *User) UpdateLastSignInAt(tx *storage.Connection) error {
	return tx.UpdateOnly(u, "last_sign_in_at")
}

// ConfirmEmailChange confirm the change of email for a user
func (u *User) ConfirmEmailChange(tx *storage.Connection, status int) error {
	email := u.EmailChange

	u.Email = storage.NullString(email)
	u.EmailChange = ""
	u.EmailChangeTokenCurrent = ""
	u.EmailChangeTokenNew = ""
	u.EmailChangeConfirmStatus = status

	if err := tx.UpdateOnly(
		u,
		"email",
		"email_change",
		"email_change_token_current",
		"email_change_token_new",
		"email_change_confirm_status",
	); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	if !u.IsConfirmed() {
		if err := u.Confirm(tx); err != nil {
			return err
		}
	}

	identity, err := FindIdentityByIdAndProvider(tx, u.ID.String(), "email")
	if err != nil {
		if IsNotFoundError(err) {
			// no email identity, not an error
			return nil
		}
		return err
	}

	if _, ok := identity.IdentityData["email"]; ok {
		identity.IdentityData["email"] = email
		if err := tx.UpdateOnly(identity, "identity_data"); err != nil {
			return err
		}
	}

	return nil
}

// ConfirmPhoneChange confirms the change of phone for a user
func (u *User) ConfirmPhoneChange(tx *storage.Connection) error {
	now := time.Now()
	phone := u.PhoneChange

	u.Phone = storage.NullString(phone)
	u.PhoneChange = ""
	u.PhoneChangeToken = ""
	u.PhoneConfirmedAt = &now

	if err := tx.UpdateOnly(
		u,
		"phone",
		"phone_change",
		"phone_change_token",
		"phone_confirmed_at",
	); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	identity, err := FindIdentityByIdAndProvider(tx, u.ID.String(), "phone")
	if err != nil {
		if IsNotFoundError(err) {
			// no phone identity, not an error
			return nil
		}

		return err
	}

	if _, ok := identity.IdentityData["phone"]; ok {
		identity.IdentityData["phone"] = phone
	}

	if err := tx.UpdateOnly(identity, "identity_data"); err != nil {
		return err
	}

	return nil
}

// Recover resets the recovery token
func (u *User) Recover(tx *storage.Connection) error {
	u.RecoveryToken = ""
	if err := tx.UpdateOnly(u, "recovery_token"); err != nil {
		return err
	}

	return ClearAllOneTimeTokensForUser(tx, u.ID)
}

// CountOtherUsers counts how many other users exist besides the one provided
func CountOtherUsers(tx *storage.Connection, id uuid.UUID) (int, error) {
	userCount, err := tx.Q().Where("instance_id = ? and id != ?", uuid.Nil, id).Count(&User{})
	return userCount, errors.Wrap(err, "error finding registered users")
}

func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, error) {
	obj := &User{}
	if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil {
		if errors.Cause(err) == sql.ErrNoRows {
			return nil, UserNotFoundError{}
		}
		return nil, errors.Wrap(err, "error finding user")
	}

	return obj, nil
}

// FindUserByEmailAndAudience finds a user with the matching email and audience.
func FindUserByEmailAndAudience(tx *storage.Connection, email, aud string) (*User, error) {
	return findUser(tx, "instance_id = ? and LOWER(email) = ? and aud = ? and is_sso_user = false", uuid.Nil, strings.ToLower(email), aud)
}

// FindUserByPhoneAndAudience finds a user with the matching email and audience.
func FindUserByPhoneAndAudience(tx *storage.Connection, phone, aud string) (*User, error) {
	return findUser(tx, "instance_id = ? and phone = ? and aud = ? and is_sso_user = false", uuid.Nil, phone, aud)
}

// FindUserByID finds a user matching the provided ID.
func FindUserByID(tx *storage.Connection, id uuid.UUID) (*User, error) {
	return findUser(tx, "instance_id = ? and id = ?", uuid.Nil, id)
}

// FindUserWithRefreshToken finds a user from the provided refresh token. If
// forUpdate is set to true, then the SELECT statement used by the query has
// the form SELECT ... FOR UPDATE SKIP LOCKED. This means that a FOR UPDATE
// lock will only be acquired if there's no other lock. In case there is a
// lock, a IsNotFound(err) error will be returned.
func FindUserWithRefreshToken(tx *storage.Connection, token string, forUpdate bool) (*User, *RefreshToken, *Session, error) {
	refreshToken := &RefreshToken{}

	if forUpdate {
		// pop does not provide us with a way to execute FOR UPDATE
		// queries which lock the rows affected by the query from
		// being accessed by any other transaction that also uses FOR
		// UPDATE
		if err := tx.RawQuery(fmt.Sprintf("SELECT * FROM %q WHERE token = ? LIMIT 1 FOR UPDATE SKIP LOCKED;", refreshToken.TableName()), token).First(refreshToken); err != nil {
			if errors.Cause(err) == sql.ErrNoRows {
				return nil, nil, nil, RefreshTokenNotFoundError{}
			}

			return nil, nil, nil, errors.Wrap(err, "error finding refresh token for update")
		}
	}

	// once the rows are locked (if forUpdate was true), we can query again using pop
	if err := tx.Where("token = ?", token).First(refreshToken); err != nil {
		if errors.Cause(err) == sql.ErrNoRows {
			return nil, nil, nil, RefreshTokenNotFoundError{}
		}
		return nil, nil, nil, errors.Wrap(err, "error finding refresh token")
	}

	user, err := FindUserByID(tx, refreshToken.UserID)
	if err != nil {
		return nil, nil, nil, err
	}

	var session *Session

	if refreshToken.SessionId != nil {
		sessionId := *refreshToken.SessionId

		if sessionId != uuid.Nil {
			session, err = FindSessionByID(tx, sessionId, forUpdate)
			if err != nil {
				if forUpdate {
					return nil, nil, nil, err
				}

				if !IsNotFoundError(err) {
					return nil, nil, nil, errors.Wrap(err, "error finding session from refresh token")
				}

				// otherwise, there's no session for this refresh token
			}
		}
	}

	return user, refreshToken, session, nil
}

// FindUsersInAudience finds users with the matching audience.
func FindUsersInAudience(tx *storage.Connection, aud string, pageParams *Pagination, sortParams *SortParams, filter string) ([]*User, error) {
	users := []*User{}
	q := tx.Q().Where("instance_id = ? and aud = ?", uuid.Nil, aud)

	if filter != "" {
		lf := "%" + filter + "%"
		// we must specify the collation in order to get case insensitive search for the JSON column
		q = q.Where("(email LIKE ? OR raw_user_meta_data->>'full_name' ILIKE ?)", lf, lf)
	}

	if sortParams != nil && len(sortParams.Fields) > 0 {
		for _, field := range sortParams.Fields {
			q = q.Order(field.Name + " " + string(field.Dir))
		}
	}

	var err error
	if pageParams != nil {
		err = q.Paginate(int(pageParams.Page), int(pageParams.PerPage)).All(&users) // #nosec G115
		pageParams.Count = uint64(q.Paginator.TotalEntriesSize)                     // #nosec G115
	} else {
		err = q.All(&users)
	}

	return users, err
}

// IsDuplicatedEmail returns whether a user exists with a matching email and audience.
// If a currentUser is provided, we will need to filter out any identities that belong to the current user.
func IsDuplicatedEmail(tx *storage.Connection, email, aud string, currentUser *User) (*User, error) {
	var identities []Identity

	if err := tx.Eager().Q().Where("email = ?", strings.ToLower(email)).All(&identities); err != nil {
		if errors.Cause(err) == sql.ErrNoRows {
			return nil, nil
		}

		return nil, errors.Wrap(err, "unable to find identity by email for duplicates")
	}

	userIDs := make(map[string]uuid.UUID)
	for _, identity := range identities {
		if _, ok := userIDs[identity.UserID.String()]; !ok {
			if !identity.IsForSSOProvider() {
				userIDs[identity.UserID.String()] = identity.UserID
			}
		}
	}

	var currentUserId uuid.UUID
	if currentUser != nil {
		currentUserId = currentUser.ID
	}

	for _, userID := range userIDs {
		if userID != currentUserId {
			user, err := FindUserByID(tx, userID)
			if err != nil {
				return nil, errors.Wrap(err, "unable to find user from email identity for duplicates")
			}
			if user.Aud == aud {
				return user, nil
			}
		}
	}

	// out of an abundance of caution, if nothing was found via the
	// identities table we also do a final check on the users table
	user, err := FindUserByEmailAndAudience(tx, email, aud)
	if err != nil && !IsNotFoundError(err) {
		return nil, errors.Wrap(err, "unable to find user email address for duplicates")
	}

	return user, nil
}

// IsDuplicatedPhone checks if the phone number already exists in the users table
func IsDuplicatedPhone(tx *storage.Connection, phone, aud string) (bool, error) {
	_, err := FindUserByPhoneAndAudience(tx, phone, aud)
	if err != nil {
		if IsNotFoundError(err) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// Ban a user for a given duration.
func (u *User) Ban(tx *storage.Connection, duration time.Duration) error {
	if duration == time.Duration(0) {
		u.BannedUntil = nil
	} else {
		t := time.Now().Add(duration)
		u.BannedUntil = &t
	}
	return tx.UpdateOnly(u, "banned_until")
}

// IsBanned checks if a user is banned or not
func (u *User) IsBanned() bool {
	if u.BannedUntil == nil {
		return false
	}
	return time.Now().Before(*u.BannedUntil)
}

func (u *User) HasMFAEnabled() bool {
	for _, factor := range u.Factors {
		if factor.IsVerified() {
			return true
		}
	}

	return false
}

func (u *User) UpdateBannedUntil(tx *storage.Connection) error {
	return tx.UpdateOnly(u, "banned_until")
}

// RemoveUnconfirmedIdentities removes potentially malicious unconfirmed identities from a user (if any)
func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection, identity *Identity) error {
	if identity.Provider != "email" && identity.Provider != "phone" {
		// user is unconfirmed so the password should be reset
		u.EncryptedPassword = nil
		if terr := tx.UpdateOnly(u, "encrypted_password"); terr != nil {
			return terr
		}
	}

	// user is unconfirmed so existing user_metadata should be overwritten
	// to use the current identity metadata
	u.UserMetaData = identity.IdentityData
	if terr := u.UpdateUserMetaData(tx, u.UserMetaData); terr != nil {
		return terr
	}

	// finally, remove all identities except the current identity being authenticated
	for i := range u.Identities {
		if u.Identities[i].ID != identity.ID {
			if terr := tx.Destroy(&u.Identities[i]); terr != nil {
				return terr
			}
		}
	}

	// user is unconfirmed so none of the providers associated to it are verified yet
	// only the current provider should be kept
	if terr := u.UpdateAppMetaDataProviders(tx); terr != nil {
		return terr
	}
	return nil
}

// SoftDeleteUser performs a soft deletion on the user by obfuscating and clearing certain fields
func (u *User) SoftDeleteUser(tx *storage.Connection) error {
	u.Email = storage.NullString(obfuscateEmail(u, u.GetEmail()))
	u.Phone = storage.NullString(obfuscatePhone(u, u.GetPhone()))
	u.EmailChange = obfuscateEmail(u, u.EmailChange)
	u.PhoneChange = obfuscatePhone(u, u.PhoneChange)
	u.EncryptedPassword = nil
	u.ConfirmationToken = ""
	u.RecoveryToken = ""
	u.EmailChangeTokenCurrent = ""
	u.EmailChangeTokenNew = ""
	u.PhoneChangeToken = ""

	// set deleted_at time
	now := time.Now()
	u.DeletedAt = &now

	if err := tx.UpdateOnly(
		u,
		"email",
		"phone",
		"encrypted_password",
		"email_change",
		"phone_change",
		"confirmation_token",
		"recovery_token",
		"email_change_token_current",
		"email_change_token_new",
		"phone_change_token",
		"deleted_at",
	); err != nil {
		return err
	}

	if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
		return err
	}

	// set raw_user_meta_data to {}
	userMetaDataUpdates := map[string]interface{}{}
	for k := range u.UserMetaData {
		userMetaDataUpdates[k] = nil
	}

	if err := u.UpdateUserMetaData(tx, userMetaDataUpdates); err != nil {
		return err
	}

	// set raw_app_meta_data to {}
	appMetaDataUpdates := map[string]interface{}{}
	for k := range u.AppMetaData {
		appMetaDataUpdates[k] = nil
	}

	if err := u.UpdateAppMetaData(tx, appMetaDataUpdates); err != nil {
		return err
	}

	if err := Logout(tx, u.ID); err != nil {
		return err
	}

	return nil
}

// SoftDeleteUserIdentities performs a soft deletion on all identities associated to a user
func (u *User) SoftDeleteUserIdentities(tx *storage.Connection) error {
	identities, err := FindIdentitiesByUserID(tx, u.ID)
	if err != nil {
		return err
	}

	// set identity_data to {}
	for _, identity := range identities {
		identityDataUpdates := map[string]interface{}{}
		for k := range identity.IdentityData {
			identityDataUpdates[k] = nil
		}
		if err := identity.UpdateIdentityData(tx, identityDataUpdates); err != nil {
			return err
		}
		// updating the identity.ID has to happen last since the primary key is on (provider, id)
		// we use RawQuery here instead of UpdateOnly because UpdateOnly relies on the primary key of Identity
		if err := tx.RawQuery(
			"update "+
				(&pop.Model{Value: Identity{}}).TableName()+
				" set provider_id = ? where id = ?",
			obfuscateIdentityProviderId(identity),
			identity.ID,
		).Exec(); err != nil {
			return err
		}
	}
	return nil
}

func (u *User) FindOwnedFactorByID(tx *storage.Connection, factorID uuid.UUID) (*Factor, error) {
	var factor Factor
	err := tx.Where("user_id = ? AND id = ?", u.ID, factorID).First(&factor)
	if err != nil {
		if errors.Cause(err) == sql.ErrNoRows {
			return nil, &FactorNotFoundError{}
		}
		return nil, err
	}
	return &factor, nil
}

func (user *User) WebAuthnID() []byte {
	return []byte(user.ID.String())
}

func (user *User) WebAuthnName() string {
	return user.Email.String()
}

func (user *User) WebAuthnDisplayName() string {
	return user.Email.String()
}

func (user *User) WebAuthnCredentials() []webauthn.Credential {
	var credentials []webauthn.Credential

	for _, factor := range user.Factors {
		if factor.IsVerified() && factor.FactorType == WebAuthn {
			credential := factor.WebAuthnCredential.Credential
			credentials = append(credentials, credential)
		}
	}

	return credentials
}

func obfuscateValue(id uuid.UUID, value string) string {
	hash := sha256.Sum256([]byte(id.String() + value))
	return base64.RawURLEncoding.EncodeToString(hash[:])
}

func obfuscateEmail(u *User, email string) string {
	return obfuscateValue(u.ID, email)
}

func obfuscatePhone(u *User, phone string) string {
	// Field converted from VARCHAR(15) to text
	return obfuscateValue(u.ID, phone)[:15]
}

func obfuscateIdentityProviderId(identity *Identity) string {
	return obfuscateValue(identity.UserID, identity.Provider+":"+identity.ProviderID)
}

// FindUserByPhoneChangeAndAudience finds a user with the matching phone change and audience.
func FindUserByPhoneChangeAndAudience(tx *storage.Connection, phone, aud string) (*User, error) {
	return findUser(tx, "instance_id = ? and phone_change = ? and aud = ? and is_sso_user = false", uuid.Nil, phone, aud)
}
