// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package model

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"sort"
	"strings"
	"unicode/utf8"

	"golang.org/x/crypto/bcrypt"
	"golang.org/x/text/language"

	"github.com/mattermost/mattermost-server/v6/services/timezones"
	"github.com/mattermost/mattermost-server/v6/shared/mlog"
)

const (
	Me                             = "me"
	UserNotifyAll                  = "all"
	UserNotifyHere                 = "here"
	UserNotifyMention              = "mention"
	UserNotifyNone                 = "none"
	DesktopNotifyProp              = "desktop"
	DesktopSoundNotifyProp         = "desktop_sound"
	MarkUnreadNotifyProp           = "mark_unread"
	PushNotifyProp                 = "push"
	PushStatusNotifyProp           = "push_status"
	EmailNotifyProp                = "email"
	ChannelMentionsNotifyProp      = "channel"
	CommentsNotifyProp             = "comments"
	MentionKeysNotifyProp          = "mention_keys"
	CommentsNotifyNever            = "never"
	CommentsNotifyRoot             = "root"
	CommentsNotifyAny              = "any"
	CommentsNotifyCRT              = "crt"
	FirstNameNotifyProp            = "first_name"
	AutoResponderActiveNotifyProp  = "auto_responder_active"
	AutoResponderMessageNotifyProp = "auto_responder_message"
	DesktopThreadsNotifyProp       = "desktop_threads"
	PushThreadsNotifyProp          = "push_threads"
	EmailThreadsNotifyProp         = "email_threads"

	DefaultLocale        = "en"
	UserAuthServiceEmail = "email"

	UserEmailMaxLength    = 128
	UserNicknameMaxRunes  = 64
	UserPositionMaxRunes  = 128
	UserFirstNameMaxRunes = 64
	UserLastNameMaxRunes  = 64
	UserAuthDataMaxLength = 128
	UserNameMaxLength     = 64
	UserNameMinLength     = 1
	UserPasswordMaxLength = 72
	UserLocaleMaxLength   = 5
	UserTimezoneMaxRunes  = 256
)

//msgp:tuple User

// User contains the details about the user.
// This struct's serializer methods are auto-generated. If a new field is added/removed,
// please run make gen-serialized.
type User struct {
	Id                     string    `json:"id"`
	CreateAt               int64     `json:"create_at,omitempty"`
	UpdateAt               int64     `json:"update_at,omitempty"`
	DeleteAt               int64     `json:"delete_at"`
	Username               string    `json:"username"`
	Password               string    `json:"password,omitempty"`
	AuthData               *string   `json:"auth_data,omitempty"`
	AuthService            string    `json:"auth_service"`
	Email                  string    `json:"email"`
	EmailVerified          bool      `json:"email_verified,omitempty"`
	Nickname               string    `json:"nickname"`
	FirstName              string    `json:"first_name"`
	LastName               string    `json:"last_name"`
	Position               string    `json:"position"`
	Roles                  string    `json:"roles"`
	AllowMarketing         bool      `json:"allow_marketing,omitempty"`
	Props                  StringMap `json:"props,omitempty"`
	NotifyProps            StringMap `json:"notify_props,omitempty"`
	LastPasswordUpdate     int64     `json:"last_password_update,omitempty"`
	LastPictureUpdate      int64     `json:"last_picture_update,omitempty"`
	FailedAttempts         int       `json:"failed_attempts,omitempty"`
	Locale                 string    `json:"locale"`
	Timezone               StringMap `json:"timezone"`
	MfaActive              bool      `json:"mfa_active,omitempty"`
	MfaSecret              string    `json:"mfa_secret,omitempty"`
	RemoteId               *string   `json:"remote_id,omitempty"`
	LastActivityAt         int64     `db:"-" json:"last_activity_at,omitempty"`
	IsBot                  bool      `db:"-" json:"is_bot,omitempty"`
	BotDescription         string    `db:"-" json:"bot_description,omitempty"`
	BotLastIconUpdate      int64     `db:"-" json:"bot_last_icon_update,omitempty"`
	TermsOfServiceId       string    `db:"-" json:"terms_of_service_id,omitempty"`
	TermsOfServiceCreateAt int64     `db:"-" json:"terms_of_service_create_at,omitempty"`
	DisableWelcomeEmail    bool      `db:"-" json:"disable_welcome_email"`
}

//msgp UserMap

// UserMap is a map from a userId to a user object.
// It is used to generate methods which can be used for fast serialization/de-serialization.
type UserMap map[string]*User

//msgp:ignore UserUpdate
type UserUpdate struct {
	Old *User
	New *User
}

//msgp:ignore UserPatch
type UserPatch struct {
	Username    *string   `json:"username"`
	Password    *string   `json:"password,omitempty"`
	Nickname    *string   `json:"nickname"`
	FirstName   *string   `json:"first_name"`
	LastName    *string   `json:"last_name"`
	Position    *string   `json:"position"`
	Email       *string   `json:"email"`
	Props       StringMap `json:"props,omitempty"`
	NotifyProps StringMap `json:"notify_props,omitempty"`
	Locale      *string   `json:"locale"`
	Timezone    StringMap `json:"timezone"`
	RemoteId    *string   `json:"remote_id"`
}

//msgp:ignore UserAuth
type UserAuth struct {
	Password    string  `json:"password,omitempty"` // DEPRECATED: It is not used.
	AuthData    *string `json:"auth_data,omitempty"`
	AuthService string  `json:"auth_service,omitempty"`
}

//msgp:ignore UserForIndexing
type UserForIndexing struct {
	Id          string   `json:"id"`
	Username    string   `json:"username"`
	Nickname    string   `json:"nickname"`
	FirstName   string   `json:"first_name"`
	LastName    string   `json:"last_name"`
	Roles       string   `json:"roles"`
	CreateAt    int64    `json:"create_at"`
	DeleteAt    int64    `json:"delete_at"`
	TeamsIds    []string `json:"team_id"`
	ChannelsIds []string `json:"channel_id"`
}

//msgp:ignore ViewUsersRestrictions
type ViewUsersRestrictions struct {
	Teams    []string
	Channels []string
}

func (r *ViewUsersRestrictions) Hash() string {
	if r == nil {
		return ""
	}
	ids := append(r.Teams, r.Channels...)
	sort.Strings(ids)
	hash := sha256.New()
	hash.Write([]byte(strings.Join(ids, "")))
	return fmt.Sprintf("%x", hash.Sum(nil))
}

//msgp:ignore UserSlice
type UserSlice []*User

func (u UserSlice) Usernames() []string {
	usernames := []string{}
	for _, user := range u {
		usernames = append(usernames, user.Username)
	}
	sort.Strings(usernames)
	return usernames
}

func (u UserSlice) IDs() []string {
	ids := []string{}
	for _, user := range u {
		ids = append(ids, user.Id)
	}
	return ids
}

func (u UserSlice) FilterWithoutBots() UserSlice {
	var matches []*User

	for _, user := range u {
		if !user.IsBot {
			matches = append(matches, user)
		}
	}
	return UserSlice(matches)
}

func (u UserSlice) FilterByActive(active bool) UserSlice {
	var matches []*User

	for _, user := range u {
		if user.DeleteAt == 0 && active {
			matches = append(matches, user)
		} else if user.DeleteAt != 0 && !active {
			matches = append(matches, user)
		}
	}
	return UserSlice(matches)
}

func (u UserSlice) FilterByID(ids []string) UserSlice {
	var matches []*User
	for _, user := range u {
		for _, id := range ids {
			if id == user.Id {
				matches = append(matches, user)
			}
		}
	}
	return UserSlice(matches)
}

func (u UserSlice) FilterWithoutID(ids []string) UserSlice {
	var keep []*User
	for _, user := range u {
		present := false
		for _, id := range ids {
			if id == user.Id {
				present = true
			}
		}
		if !present {
			keep = append(keep, user)
		}
	}
	return UserSlice(keep)
}

func (u *User) DeepCopy() *User {
	copyUser := *u
	if u.AuthData != nil {
		copyUser.AuthData = NewString(*u.AuthData)
	}
	if u.Props != nil {
		copyUser.Props = CopyStringMap(u.Props)
	}
	if u.NotifyProps != nil {
		copyUser.NotifyProps = CopyStringMap(u.NotifyProps)
	}
	if u.Timezone != nil {
		copyUser.Timezone = CopyStringMap(u.Timezone)
	}
	return &copyUser
}

// IsValid validates the user and returns an error if it isn't configured
// correctly.
func (u *User) IsValid() *AppError {

	if !IsValidId(u.Id) {
		return InvalidUserError("id", "")
	}

	if u.CreateAt == 0 {
		return InvalidUserError("create_at", u.Id)
	}

	if u.UpdateAt == 0 {
		return InvalidUserError("update_at", u.Id)
	}

	if u.IsRemote() {
		if !IsValidUsernameAllowRemote(u.Username) {
			return InvalidUserError("username", u.Id)
		}
	} else {
		if !IsValidUsername(u.Username) {
			return InvalidUserError("username", u.Id)
		}
	}

	if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) {
		return InvalidUserError("email", u.Id)
	}

	if utf8.RuneCountInString(u.Nickname) > UserNicknameMaxRunes {
		return InvalidUserError("nickname", u.Id)
	}

	if utf8.RuneCountInString(u.Position) > UserPositionMaxRunes {
		return InvalidUserError("position", u.Id)
	}

	if utf8.RuneCountInString(u.FirstName) > UserFirstNameMaxRunes {
		return InvalidUserError("first_name", u.Id)
	}

	if utf8.RuneCountInString(u.LastName) > UserLastNameMaxRunes {
		return InvalidUserError("last_name", u.Id)
	}

	if u.AuthData != nil && len(*u.AuthData) > UserAuthDataMaxLength {
		return InvalidUserError("auth_data", u.Id)
	}

	if u.AuthData != nil && *u.AuthData != "" && u.AuthService == "" {
		return InvalidUserError("auth_data_type", u.Id)
	}

	if u.Password != "" && u.AuthData != nil && *u.AuthData != "" {
		return InvalidUserError("auth_data_pwd", u.Id)
	}

	if len(u.Password) > UserPasswordMaxLength {
		return InvalidUserError("password_limit", u.Id)
	}

	if !IsValidLocale(u.Locale) {
		return InvalidUserError("locale", u.Id)
	}

	if len(u.Timezone) > 0 {
		if tzJSON, err := json.Marshal(u.Timezone); err != nil {
			return NewAppError("User.IsValid", "model.user.is_valid.marshal.app_error", nil, err.Error(), http.StatusInternalServerError)
		} else if utf8.RuneCount(tzJSON) > UserTimezoneMaxRunes {
			return InvalidUserError("timezone_limit", u.Id)
		}
	}

	return nil
}

func InvalidUserError(fieldName string, userId string) *AppError {
	id := fmt.Sprintf("model.user.is_valid.%s.app_error", fieldName)
	details := ""
	if userId != "" {
		details = "user_id=" + userId
	}
	return NewAppError("User.IsValid", id, nil, details, http.StatusBadRequest)
}

func NormalizeUsername(username string) string {
	return strings.ToLower(username)
}

func NormalizeEmail(email string) string {
	return strings.ToLower(email)
}

// PreSave will set the Id and Username if missing.  It will also fill
// in the CreateAt, UpdateAt times.  It will also hash the password.  It should
// be run before saving the user to the db.
func (u *User) PreSave() {
	if u.Id == "" {
		u.Id = NewId()
	}

	if u.Username == "" {
		u.Username = NewId()
	}

	if u.AuthData != nil && *u.AuthData == "" {
		u.AuthData = nil
	}

	u.Username = SanitizeUnicode(u.Username)
	u.FirstName = SanitizeUnicode(u.FirstName)
	u.LastName = SanitizeUnicode(u.LastName)
	u.Nickname = SanitizeUnicode(u.Nickname)

	u.Username = NormalizeUsername(u.Username)
	u.Email = NormalizeEmail(u.Email)

	u.CreateAt = GetMillis()
	u.UpdateAt = u.CreateAt

	u.LastPasswordUpdate = u.CreateAt

	u.MfaActive = false

	if u.Locale == "" {
		u.Locale = DefaultLocale
	}

	if u.Props == nil {
		u.Props = make(map[string]string)
	}

	if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
		u.SetDefaultNotifications()
	}

	if u.Timezone == nil {
		u.Timezone = timezones.DefaultUserTimezone()
	}

	if u.Password != "" {
		u.Password = HashPassword(u.Password)
	}
}

// PreUpdate should be run before updating the user in the db.
func (u *User) PreUpdate() {
	u.Username = SanitizeUnicode(u.Username)
	u.FirstName = SanitizeUnicode(u.FirstName)
	u.LastName = SanitizeUnicode(u.LastName)
	u.Nickname = SanitizeUnicode(u.Nickname)
	u.BotDescription = SanitizeUnicode(u.BotDescription)

	u.Username = NormalizeUsername(u.Username)
	u.Email = NormalizeEmail(u.Email)
	u.UpdateAt = GetMillis()

	u.FirstName = SanitizeUnicode(u.FirstName)
	u.LastName = SanitizeUnicode(u.LastName)
	u.Nickname = SanitizeUnicode(u.Nickname)
	u.BotDescription = SanitizeUnicode(u.BotDescription)

	if u.AuthData != nil && *u.AuthData == "" {
		u.AuthData = nil
	}

	if u.NotifyProps == nil || len(u.NotifyProps) == 0 {
		u.SetDefaultNotifications()
	} else if _, ok := u.NotifyProps[MentionKeysNotifyProp]; ok {
		// Remove any blank mention keys
		splitKeys := strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",")
		goodKeys := []string{}
		for _, key := range splitKeys {
			if key != "" {
				goodKeys = append(goodKeys, strings.ToLower(key))
			}
		}
		u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",")
	}
}

func (u *User) SetDefaultNotifications() {
	u.NotifyProps = make(map[string]string)
	u.NotifyProps[EmailNotifyProp] = "true"
	u.NotifyProps[PushNotifyProp] = UserNotifyMention
	u.NotifyProps[DesktopNotifyProp] = UserNotifyMention
	u.NotifyProps[DesktopSoundNotifyProp] = "true"
	u.NotifyProps[MentionKeysNotifyProp] = ""
	u.NotifyProps[ChannelMentionsNotifyProp] = "true"
	u.NotifyProps[PushStatusNotifyProp] = StatusAway
	u.NotifyProps[CommentsNotifyProp] = CommentsNotifyNever
	u.NotifyProps[FirstNameNotifyProp] = "false"
	u.NotifyProps[DesktopThreadsNotifyProp] = UserNotifyAll
	u.NotifyProps[EmailThreadsNotifyProp] = UserNotifyAll
	u.NotifyProps[PushThreadsNotifyProp] = UserNotifyAll
}

func (u *User) UpdateMentionKeysFromUsername(oldUsername string) {
	nonUsernameKeys := []string{}
	for _, key := range u.GetMentionKeys() {
		if key != oldUsername && key != "@"+oldUsername {
			nonUsernameKeys = append(nonUsernameKeys, key)
		}
	}

	u.NotifyProps[MentionKeysNotifyProp] = ""
	if len(nonUsernameKeys) > 0 {
		u.NotifyProps[MentionKeysNotifyProp] += "," + strings.Join(nonUsernameKeys, ",")
	}
}

func (u *User) GetMentionKeys() []string {
	var keys []string

	for _, key := range strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",") {
		trimmedKey := strings.TrimSpace(key)

		if trimmedKey == "" {
			continue
		}

		keys = append(keys, trimmedKey)
	}

	return keys
}

func (u *User) Patch(patch *UserPatch) {
	if patch.Username != nil {
		u.Username = *patch.Username
	}

	if patch.Nickname != nil {
		u.Nickname = *patch.Nickname
	}

	if patch.FirstName != nil {
		u.FirstName = *patch.FirstName
	}

	if patch.LastName != nil {
		u.LastName = *patch.LastName
	}

	if patch.Position != nil {
		u.Position = *patch.Position
	}

	if patch.Email != nil {
		u.Email = *patch.Email
	}

	if patch.Props != nil {
		u.Props = patch.Props
	}

	if patch.NotifyProps != nil {
		u.NotifyProps = patch.NotifyProps
	}

	if patch.Locale != nil {
		u.Locale = *patch.Locale
	}

	if patch.Timezone != nil {
		u.Timezone = patch.Timezone
	}

	if patch.RemoteId != nil {
		u.RemoteId = patch.RemoteId
	}
}

// Generate a valid strong etag so the browser can cache the results
func (u *User) Etag(showFullName, showEmail bool) string {
	return Etag(u.Id, u.UpdateAt, u.TermsOfServiceId, u.TermsOfServiceCreateAt, showFullName, showEmail, u.BotLastIconUpdate)
}

// Remove any private data from the user object
func (u *User) Sanitize(options map[string]bool) {
	u.Password = ""
	u.AuthData = NewString("")
	u.MfaSecret = ""

	if len(options) != 0 && !options["email"] {
		u.Email = ""
	}
	if len(options) != 0 && !options["fullname"] {
		u.FirstName = ""
		u.LastName = ""
	}
	if len(options) != 0 && !options["passwordupdate"] {
		u.LastPasswordUpdate = 0
	}
	if len(options) != 0 && !options["authservice"] {
		u.AuthService = ""
	}
}

// Remove any input data from the user object that is not user controlled
func (u *User) SanitizeInput(isAdmin bool) {
	if !isAdmin {
		u.AuthData = NewString("")
		u.AuthService = ""
		u.EmailVerified = false
	}
	u.LastPasswordUpdate = 0
	u.LastPictureUpdate = 0
	u.FailedAttempts = 0
	u.MfaActive = false
	u.MfaSecret = ""
	u.Email = strings.TrimSpace(u.Email)
}

func (u *User) ClearNonProfileFields() {
	u.Password = ""
	u.AuthData = NewString("")
	u.MfaSecret = ""
	u.EmailVerified = false
	u.AllowMarketing = false
	u.NotifyProps = StringMap{}
	u.LastPasswordUpdate = 0
	u.FailedAttempts = 0
}

func (u *User) SanitizeProfile(options map[string]bool) {
	u.ClearNonProfileFields()

	u.Sanitize(options)
}

func (u *User) MakeNonNil() {
	if u.Props == nil {
		u.Props = make(map[string]string)
	}

	if u.NotifyProps == nil {
		u.NotifyProps = make(map[string]string)
	}
}

func (u *User) AddNotifyProp(key string, value string) {
	u.MakeNonNil()

	u.NotifyProps[key] = value
}

func (u *User) SetCustomStatus(cs *CustomStatus) error {
	u.MakeNonNil()
	statusJSON, jsonErr := json.Marshal(cs)
	if jsonErr != nil {
		return jsonErr
	}
	u.Props[UserPropsKeyCustomStatus] = string(statusJSON)
	return nil
}

func (u *User) ClearCustomStatus() {
	u.MakeNonNil()
	u.Props[UserPropsKeyCustomStatus] = ""
}

func (u *User) GetFullName() string {
	if u.FirstName != "" && u.LastName != "" {
		return u.FirstName + " " + u.LastName
	} else if u.FirstName != "" {
		return u.FirstName
	} else if u.LastName != "" {
		return u.LastName
	} else {
		return ""
	}
}

func (u *User) getDisplayName(baseName, nameFormat string) string {
	displayName := baseName

	if nameFormat == ShowNicknameFullName {
		if u.Nickname != "" {
			displayName = u.Nickname
		} else if fullName := u.GetFullName(); fullName != "" {
			displayName = fullName
		}
	} else if nameFormat == ShowFullName {
		if fullName := u.GetFullName(); fullName != "" {
			displayName = fullName
		}
	}

	return displayName
}

func (u *User) GetDisplayName(nameFormat string) string {
	displayName := u.Username

	return u.getDisplayName(displayName, nameFormat)
}

func (u *User) GetDisplayNameWithPrefix(nameFormat, prefix string) string {
	displayName := prefix + u.Username

	return u.getDisplayName(displayName, nameFormat)
}

func (u *User) GetRoles() []string {
	return strings.Fields(u.Roles)
}

func (u *User) GetRawRoles() string {
	return u.Roles
}

func IsValidUserRoles(userRoles string) bool {

	roles := strings.Fields(userRoles)

	for _, r := range roles {
		if !IsValidRoleName(r) {
			return false
		}
	}

	// Exclude just the system_admin role explicitly to prevent mistakes
	if len(roles) == 1 && roles[0] == "system_admin" {
		return false
	}

	return true
}

// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsGuest() bool {
	return IsInRole(u.Roles, SystemGuestRoleId)
}

func (u *User) IsSystemAdmin() bool {
	return IsInRole(u.Roles, SystemAdminRoleId)
}

// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func (u *User) IsInRole(inRole string) bool {
	return IsInRole(u.Roles, inRole)
}

// Make sure you acually want to use this function. In context.go there are functions to check permissions
// This function should not be used to check permissions.
func IsInRole(userRoles string, inRole string) bool {
	roles := strings.Split(userRoles, " ")

	for _, r := range roles {
		if r == inRole {
			return true
		}
	}

	return false
}

func (u *User) IsSSOUser() bool {
	return u.AuthService != "" && u.AuthService != UserAuthServiceEmail
}

func (u *User) IsOAuthUser() bool {
	return u.AuthService == ServiceGitlab ||
		u.AuthService == ServiceGoogle ||
		u.AuthService == ServiceOffice365 ||
		u.AuthService == ServiceOpenid
}

func (u *User) IsLDAPUser() bool {
	return u.AuthService == UserAuthServiceLdap
}

func (u *User) IsSAMLUser() bool {
	return u.AuthService == UserAuthServiceSaml
}

func (u *User) GetPreferredTimezone() string {
	return GetPreferredTimezone(u.Timezone)
}

// IsRemote returns true if the user belongs to a remote cluster (has RemoteId).
func (u *User) IsRemote() bool {
	return u.RemoteId != nil && *u.RemoteId != ""
}

// GetRemoteID returns the remote id for this user or "" if not a remote user.
func (u *User) GetRemoteID() string {
	if u.RemoteId != nil {
		return *u.RemoteId
	}
	return ""
}

// GetProp fetches a prop value by name.
func (u *User) GetProp(name string) (string, bool) {
	val, ok := u.Props[name]
	return val, ok
}

// SetProp sets a prop value by name, creating the map if nil.
// Not thread safe.
func (u *User) SetProp(name string, value string) {
	if u.Props == nil {
		u.Props = make(map[string]string)
	}
	u.Props[name] = value
}

func (u *User) ToPatch() *UserPatch {
	return &UserPatch{
		Username: &u.Username, Password: &u.Password,
		Nickname: &u.Nickname, FirstName: &u.FirstName, LastName: &u.LastName,
		Position: &u.Position, Email: &u.Email,
		Props: u.Props, NotifyProps: u.NotifyProps,
		Locale: &u.Locale, Timezone: u.Timezone,
	}
}

func (u *UserPatch) SetField(fieldName string, fieldValue string) {
	switch fieldName {
	case "FirstName":
		u.FirstName = &fieldValue
	case "LastName":
		u.LastName = &fieldValue
	case "Nickname":
		u.Nickname = &fieldValue
	case "Email":
		u.Email = &fieldValue
	case "Position":
		u.Position = &fieldValue
	case "Username":
		u.Username = &fieldValue
	}
}

// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
	if err != nil {
		panic(err)
	}

	return string(hash)
}

var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
var validUsernameCharsForRemote = regexp.MustCompile(`^[a-z0-9\.\-_:]+$`)

var restrictedUsernames = map[string]struct{}{
	"all":       {},
	"channel":   {},
	"matterbot": {},
	"system":    {},
}

func IsValidUsername(s string) bool {
	if len(s) < UserNameMinLength || len(s) > UserNameMaxLength {
		return false
	}

	if !validUsernameChars.MatchString(s) {
		return false
	}

	_, found := restrictedUsernames[s]
	return !found
}

func IsValidUsernameAllowRemote(s string) bool {
	if len(s) < UserNameMinLength || len(s) > UserNameMaxLength {
		return false
	}

	if !validUsernameCharsForRemote.MatchString(s) {
		return false
	}

	_, found := restrictedUsernames[s]
	return !found
}

func CleanUsername(username string) string {
	s := NormalizeUsername(strings.Replace(username, " ", "-", -1))

	for _, value := range reservedName {
		if s == value {
			s = strings.Replace(s, value, "", -1)
		}
	}

	s = strings.TrimSpace(s)

	for _, c := range s {
		char := fmt.Sprintf("%c", c)
		if !validUsernameChars.MatchString(char) {
			s = strings.Replace(s, char, "-", -1)
		}
	}

	s = strings.Trim(s, "-")

	if !IsValidUsername(s) {
		s = "a" + NewId()
		mlog.Warn("Generating new username since provided username was invalid",
			mlog.String("provided_username", username), mlog.String("new_username", s))
	}

	return s
}

func IsValidLocale(locale string) bool {
	if locale != "" {
		if len(locale) > UserLocaleMaxLength {
			return false
		} else if _, err := language.Parse(locale); err != nil {
			return false
		}
	}

	return true
}

//msgp:ignore UserWithGroups
type UserWithGroups struct {
	User
	GroupIDs    *string  `json:"-"`
	Groups      []*Group `json:"groups"`
	SchemeGuest bool     `json:"scheme_guest"`
	SchemeUser  bool     `json:"scheme_user"`
	SchemeAdmin bool     `json:"scheme_admin"`
}

func (u *UserWithGroups) GetGroupIDs() []string {
	if u.GroupIDs == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*u.GroupIDs)
	if trimmed == "" {
		return nil
	}
	return strings.Split(trimmed, ",")
}

//msgp:ignore UsersWithGroupsAndCount
type UsersWithGroupsAndCount struct {
	Users []*UserWithGroups `json:"users"`
	Count int64             `json:"total_count"`
}
