package service

import (
	"context"
	"database/sql"
	"encoding/base64"
	"errors"
	"html/template"
	"net/http"
	"time"

	"github.com/fleetdm/fleet/v4/server"
	"github.com/fleetdm/fleet/v4/server/authz"
	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
	"github.com/fleetdm/fleet/v4/server/contexts/viewer"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/mail"
	"github.com/fleetdm/fleet/v4/server/ptr"
)

////////////////////////////////////////////////////////////////////////////////
// Create User
////////////////////////////////////////////////////////////////////////////////

type createUserRequest struct {
	fleet.UserPayload
}

type createUserResponse struct {
	User *fleet.User `json:"user,omitempty"`
	Err  error       `json:"error,omitempty"`
}

func (r createUserResponse) error() error { return r.Err }

func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*createUserRequest)
	user, err := svc.CreateUser(ctx, req.UserPayload)
	if err != nil {
		return createUserResponse{Err: err}, nil
	}
	return createUserResponse{User: user}, nil
}

func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
	var teams []fleet.UserTeam
	if p.Teams != nil {
		teams = *p.Teams
	}
	if err := svc.authz.Authorize(ctx, &fleet.User{Teams: teams}, fleet.ActionWrite); err != nil {
		return nil, err
	}

	if err := p.VerifyAdminCreate(); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "verify user payload")
	}

	if invite, err := svc.ds.InviteByEmail(ctx, *p.Email); err == nil && invite != nil {
		return nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
	}

	if p.AdminForcedPasswordReset == nil {
		// By default, force password reset for users created this way.
		p.AdminForcedPasswordReset = ptr.Bool(true)
	}

	return svc.newUser(ctx, p)
}

////////////////////////////////////////////////////////////////////////////////
// Create User From Invite
////////////////////////////////////////////////////////////////////////////////

func createUserFromInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*createUserRequest)
	user, err := svc.CreateUserFromInvite(ctx, req.UserPayload)
	if err != nil {
		return createUserResponse{Err: err}, nil
	}
	return createUserResponse{User: user}, nil
}

func (svc *Service) CreateUserFromInvite(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
	// skipauth: There is no viewer context at this point. We rely on verifying
	// the invite for authNZ.
	svc.authz.SkipAuthorization(ctx)

	if err := p.VerifyInviteCreate(); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "verify user payload")
	}

	invite, err := svc.VerifyInvite(ctx, *p.InviteToken)
	if err != nil {
		return nil, err
	}

	// set the payload role property based on an existing invite.
	p.GlobalRole = invite.GlobalRole.Ptr()
	p.Teams = &invite.Teams

	user, err := svc.newUser(ctx, p)
	if err != nil {
		return nil, err
	}

	err = svc.ds.DeleteInvite(ctx, invite.ID)
	if err != nil {
		return nil, err
	}
	return user, nil
}

////////////////////////////////////////////////////////////////////////////////
// List Users
////////////////////////////////////////////////////////////////////////////////

type listUsersRequest struct {
	ListOptions fleet.UserListOptions `url:"user_options"`
}

type listUsersResponse struct {
	Users []fleet.User `json:"users"`
	Err   error        `json:"error,omitempty"`
}

func (r listUsersResponse) error() error { return r.Err }

func listUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*listUsersRequest)
	users, err := svc.ListUsers(ctx, req.ListOptions)
	if err != nil {
		return listUsersResponse{Err: err}, nil
	}

	resp := listUsersResponse{Users: []fleet.User{}}
	for _, user := range users {
		resp.Users = append(resp.Users, *user)
	}
	return resp, nil
}

func (svc *Service) ListUsers(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
		return nil, err
	}

	return svc.ds.ListUsers(ctx, opt)
}

////////////////////////////////////////////////////////////////////////////////
// Me (get own current user)
////////////////////////////////////////////////////////////////////////////////

func meEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	user, err := svc.AuthenticatedUser(ctx)
	if err != nil {
		return getUserResponse{Err: err}, nil
	}
	availableTeams, err := svc.ListAvailableTeamsForUser(ctx, user)
	if err != nil {
		if errors.Is(err, fleet.ErrMissingLicense) {
			availableTeams = []*fleet.TeamSummary{}
		} else {
			return getUserResponse{Err: err}, nil
		}
	}
	return getUserResponse{User: user, AvailableTeams: availableTeams}, nil
}

func (svc *Service) AuthenticatedUser(ctx context.Context) (*fleet.User, error) {
	vc, ok := viewer.FromContext(ctx)
	if !ok {
		return nil, fleet.ErrNoContext
	}

	if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionRead); err != nil {
		return nil, err
	}

	if !vc.IsLoggedIn() {
		return nil, fleet.NewPermissionError("not logged in")
	}
	return vc.User, nil
}

////////////////////////////////////////////////////////////////////////////////
// Get User
////////////////////////////////////////////////////////////////////////////////

type getUserRequest struct {
	ID uint `url:"id"`
}

type getUserResponse struct {
	User           *fleet.User          `json:"user,omitempty"`
	AvailableTeams []*fleet.TeamSummary `json:"available_teams"`
	Err            error                `json:"error,omitempty"`
}

func (r getUserResponse) error() error { return r.Err }

func getUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*getUserRequest)
	user, err := svc.User(ctx, req.ID)
	if err != nil {
		return getUserResponse{Err: err}, nil
	}
	availableTeams, err := svc.ListAvailableTeamsForUser(ctx, user)
	if err != nil {
		if errors.Is(err, fleet.ErrMissingLicense) {
			availableTeams = []*fleet.TeamSummary{}
		} else {
			return getUserResponse{Err: err}, nil
		}
	}
	return getUserResponse{User: user, AvailableTeams: availableTeams}, nil
}

func (svc *Service) User(ctx context.Context, id uint) (*fleet.User, error) {
	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionRead); err != nil {
		return nil, err
	}

	return svc.ds.UserByID(ctx, id)
}

////////////////////////////////////////////////////////////////////////////////
// Modify User
////////////////////////////////////////////////////////////////////////////////

type modifyUserRequest struct {
	ID uint `json:"-" url:"id"`
	fleet.UserPayload
}

type modifyUserResponse struct {
	User *fleet.User `json:"user,omitempty"`
	Err  error       `json:"error,omitempty"`
}

func (r modifyUserResponse) error() error { return r.Err }

func modifyUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*modifyUserRequest)
	user, err := svc.ModifyUser(ctx, req.ID, req.UserPayload)
	if err != nil {
		return modifyUserResponse{Err: err}, nil
	}

	return modifyUserResponse{User: user}, nil
}

func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPayload) (*fleet.User, error) {
	if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
		return nil, err
	}

	user, err := svc.User(ctx, userID)
	if err != nil {
		return nil, err
	}

	if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
		return nil, err
	}

	vc, ok := viewer.FromContext(ctx)
	if !ok {
		return nil, ctxerr.New(ctx, "viewer not present") // should never happen, authorize would've failed
	}
	if err := p.VerifyModify(vc.UserID() == userID); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "verify user payload")
	}

	if p.GlobalRole != nil || p.Teams != nil {
		if err := svc.authz.Authorize(ctx, user, fleet.ActionWriteRole); err != nil {
			return nil, err
		}
	}
	if p.Name != nil {
		user.Name = *p.Name
	}

	if p.Email != nil && *p.Email != user.Email {
		err = svc.modifyEmailAddress(ctx, user, *p.Email, p.Password)
		if err != nil {
			return nil, err
		}
	}

	if p.Position != nil {
		user.Position = *p.Position
	}

	if p.GravatarURL != nil {
		user.GravatarURL = *p.GravatarURL
	}

	if p.SSOEnabled != nil {
		user.SSOEnabled = *p.SSOEnabled
	}

	currentUser := authz.UserFromContext(ctx)

	if p.GlobalRole != nil && *p.GlobalRole != "" {
		if currentUser.GlobalRole == nil {
			return nil, ctxerr.New(ctx, "Cannot edit global role as a team member")
		}

		if p.Teams != nil && len(*p.Teams) > 0 {
			return nil, fleet.NewInvalidArgumentError("teams", "may not be specified with global_role")
		}
		user.GlobalRole = p.GlobalRole
		user.Teams = []fleet.UserTeam{}
	} else if p.Teams != nil {
		if !isAdminOfTheModifiedTeams(currentUser, user.Teams, *p.Teams) {
			return nil, ctxerr.New(ctx, "Cannot modify teams in that way")
		}
		user.Teams = *p.Teams
		user.GlobalRole = nil
	}

	err = svc.saveUser(ctx, user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

////////////////////////////////////////////////////////////////////////////////
// Delete User
////////////////////////////////////////////////////////////////////////////////

type deleteUserRequest struct {
	ID uint `url:"id"`
}

type deleteUserResponse struct {
	Err error `json:"error,omitempty"`
}

func (r deleteUserResponse) error() error { return r.Err }

func deleteUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*deleteUserRequest)
	err := svc.DeleteUser(ctx, req.ID)
	if err != nil {
		return deleteUserResponse{Err: err}, nil
	}
	return deleteUserResponse{}, nil
}

func (svc *Service) DeleteUser(ctx context.Context, id uint) error {
	if err := svc.authz.Authorize(ctx, &fleet.User{ID: id}, fleet.ActionWrite); err != nil {
		return err
	}

	return svc.ds.DeleteUser(ctx, id)
}

////////////////////////////////////////////////////////////////////////////////
// Require Password Reset
////////////////////////////////////////////////////////////////////////////////

type requirePasswordResetRequest struct {
	Require bool `json:"require"`
	ID      uint `json:"-" url:"id"`
}

type requirePasswordResetResponse struct {
	User *fleet.User `json:"user,omitempty"`
	Err  error       `json:"error,omitempty"`
}

func (r requirePasswordResetResponse) error() error { return r.Err }

func requirePasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*requirePasswordResetRequest)
	user, err := svc.RequirePasswordReset(ctx, req.ID, req.Require)
	if err != nil {
		return requirePasswordResetResponse{Err: err}, nil
	}
	return requirePasswordResetResponse{User: user}, nil
}

func (svc *Service) RequirePasswordReset(ctx context.Context, uid uint, require bool) (*fleet.User, error) {
	if err := svc.authz.Authorize(ctx, &fleet.User{ID: uid}, fleet.ActionWrite); err != nil {
		return nil, err
	}

	user, err := svc.ds.UserByID(ctx, uid)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "loading user by ID")
	}
	if user.SSOEnabled {
		return nil, ctxerr.New(ctx, "password reset for single sign on user not allowed")
	}
	// Require reset on next login
	user.AdminForcedPasswordReset = require
	if err := svc.saveUser(ctx, user); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "saving user")
	}

	if require {
		// Clear all of the existing sessions
		if err := svc.DeleteSessionsForUser(ctx, user.ID); err != nil {
			return nil, ctxerr.Wrap(ctx, err, "deleting user sessions")
		}
	}

	return user, nil
}

////////////////////////////////////////////////////////////////////////////////
// Change Password
////////////////////////////////////////////////////////////////////////////////

type changePasswordRequest struct {
	OldPassword string `json:"old_password"`
	NewPassword string `json:"new_password"`
}

type changePasswordResponse struct {
	Err error `json:"error,omitempty"`
}

func (r changePasswordResponse) error() error { return r.Err }

func changePasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*changePasswordRequest)
	err := svc.ChangePassword(ctx, req.OldPassword, req.NewPassword)
	return changePasswordResponse{Err: err}, nil
}

func (svc *Service) ChangePassword(ctx context.Context, oldPass, newPass string) error {
	vc, ok := viewer.FromContext(ctx)
	if !ok {
		return fleet.ErrNoContext
	}

	if err := svc.authz.Authorize(ctx, vc.User, fleet.ActionWrite); err != nil {
		return err
	}

	if oldPass == "" {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("old_password", "Old password cannot be empty"))
	}
	if newPass == "" {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "New password cannot be empty"))
	}
	if err := fleet.ValidatePasswordRequirements(newPass); err != nil {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", err.Error()))
	}
	if vc.User.SSOEnabled {
		return ctxerr.New(ctx, "change password for single sign on user not allowed")
	}
	if err := vc.User.ValidatePassword(newPass); err == nil {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "cannot reuse old password"))
	}
	if err := vc.User.ValidatePassword(oldPass); err != nil {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("old_password", "old password does not match"))
	}

	if err := svc.setNewPassword(ctx, vc.User, newPass); err != nil {
		return ctxerr.Wrap(ctx, err, "setting new password")
	}
	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Get Info About Sessions For User
////////////////////////////////////////////////////////////////////////////////

type getInfoAboutSessionsForUserRequest struct {
	ID uint `url:"id"`
}

type getInfoAboutSessionsForUserResponse struct {
	Sessions []getInfoAboutSessionResponse `json:"sessions"`
	Err      error                         `json:"error,omitempty"`
}

func (r getInfoAboutSessionsForUserResponse) error() error { return r.Err }

func getInfoAboutSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*getInfoAboutSessionsForUserRequest)
	sessions, err := svc.GetInfoAboutSessionsForUser(ctx, req.ID)
	if err != nil {
		return getInfoAboutSessionsForUserResponse{Err: err}, nil
	}
	var resp getInfoAboutSessionsForUserResponse
	for _, session := range sessions {
		resp.Sessions = append(resp.Sessions, getInfoAboutSessionResponse{
			SessionID: session.ID,
			UserID:    session.UserID,
			CreatedAt: session.CreatedAt,
		})
	}
	return resp, nil
}

func (svc *Service) GetInfoAboutSessionsForUser(ctx context.Context, id uint) ([]*fleet.Session, error) {
	if err := svc.authz.Authorize(ctx, &fleet.Session{UserID: id}, fleet.ActionRead); err != nil {
		return nil, err
	}

	var validatedSessions []*fleet.Session

	sessions, err := svc.ds.ListSessionsForUser(ctx, id)
	if err != nil {
		return validatedSessions, err
	}

	for _, session := range sessions {
		if svc.validateSession(ctx, session) == nil {
			validatedSessions = append(validatedSessions, session)
		}
	}

	return validatedSessions, nil
}

////////////////////////////////////////////////////////////////////////////////
// Delete Sessions For User
////////////////////////////////////////////////////////////////////////////////

type deleteSessionsForUserRequest struct {
	ID uint `url:"id"`
}

type deleteSessionsForUserResponse struct {
	Err error `json:"error,omitempty"`
}

func (r deleteSessionsForUserResponse) error() error { return r.Err }

func deleteSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*deleteSessionsForUserRequest)
	err := svc.DeleteSessionsForUser(ctx, req.ID)
	if err != nil {
		return deleteSessionsForUserResponse{Err: err}, nil
	}
	return deleteSessionsForUserResponse{}, nil
}

func (svc *Service) DeleteSessionsForUser(ctx context.Context, id uint) error {
	if err := svc.authz.Authorize(ctx, &fleet.Session{UserID: id}, fleet.ActionWrite); err != nil {
		return err
	}

	return svc.ds.DestroyAllSessionsForUser(ctx, id)
}

////////////////////////////////////////////////////////////////////////////////
// Change user email
////////////////////////////////////////////////////////////////////////////////

type changeEmailRequest struct {
	Token string `url:"token"`
}

type changeEmailResponse struct {
	NewEmail string `json:"new_email"`
	Err      error  `json:"error,omitempty"`
}

func (r changeEmailResponse) error() error { return r.Err }

func changeEmailEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*changeEmailRequest)
	newEmailAddress, err := svc.ChangeUserEmail(ctx, req.Token)
	if err != nil {
		return changeEmailResponse{Err: err}, nil
	}
	return changeEmailResponse{NewEmail: newEmailAddress}, nil
}

func (svc *Service) ChangeUserEmail(ctx context.Context, token string) (string, error) {
	vc, ok := viewer.FromContext(ctx)
	if !ok {
		return "", fleet.ErrNoContext
	}

	if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionWrite); err != nil {
		return "", err
	}

	return svc.ds.ConfirmPendingEmailChange(ctx, vc.UserID(), token)
}

func isAdminOfTheModifiedTeams(currentUser *fleet.User, originalUserTeams, newUserTeams []fleet.UserTeam) bool {
	// If the user is of the right global role, then they can modify the teams
	if currentUser.GlobalRole != nil && (*currentUser.GlobalRole == fleet.RoleAdmin || *currentUser.GlobalRole == fleet.RoleMaintainer) {
		return true
	}

	// otherwise, gather the resulting teams
	resultingTeams := make(map[uint]string)
	for _, team := range newUserTeams {
		resultingTeams[team.ID] = team.Role
	}

	// and see which ones were removed or changed from the original
	teamsAffected := make(map[uint]struct{})
	for _, team := range originalUserTeams {
		if resultingTeams[team.ID] != team.Role {
			teamsAffected[team.ID] = struct{}{}
		}
	}

	// then gather the teams the current user is admin for
	currentUserTeamAdmin := make(map[uint]struct{})
	for _, team := range currentUser.Teams {
		if team.Role == fleet.RoleAdmin {
			currentUserTeamAdmin[team.ID] = struct{}{}
		}
	}

	// and let's check that the teams that were either removed or changed are also teams this user is an admin of
	for teamID := range teamsAffected {
		if _, ok := currentUserTeamAdmin[teamID]; !ok {
			return false
		}
	}

	return true
}

func (svc *Service) modifyEmailAddress(ctx context.Context, user *fleet.User, email string, password *string) error {
	// password requirement handled in validation middleware
	if password != nil {
		err := user.ValidatePassword(*password)
		if err != nil {
			return fleet.NewPermissionError("incorrect password")
		}
	}
	random, err := server.GenerateRandomText(svc.config.App.TokenKeySize)
	if err != nil {
		return err
	}
	token := base64.URLEncoding.EncodeToString([]byte(random))

	switch _, err = svc.ds.UserByEmail(ctx, email); {
	case err == nil:
		return ctxerr.Wrap(ctx, alreadyExistsError{})
	case errors.Is(err, sql.ErrNoRows):
		// OK
	default:
		return ctxerr.Wrap(ctx, err)
	}

	switch _, err = svc.ds.InviteByEmail(ctx, email); {
	case err == nil:
		return ctxerr.Wrap(ctx, alreadyExistsError{})
	case errors.Is(err, sql.ErrNoRows):
		// OK
	default:
		return ctxerr.Wrap(ctx, err)
	}

	err = svc.ds.PendingEmailChange(ctx, user.ID, email, token)
	if err != nil {
		return err
	}
	config, err := svc.AppConfig(ctx)
	if err != nil {
		return err
	}

	changeEmail := fleet.Email{
		Subject: "Confirm Fleet Email Change",
		To:      []string{email},
		Config:  config,
		Mailer: &mail.ChangeEmailMailer{
			Token:    token,
			BaseURL:  template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix),
			AssetURL: getAssetURL(),
		},
	}
	return svc.mailService.SendEmail(changeEmail)
}

// saves user in datastore.
// doesn't need to be exposed to the transport
// the service should expose actions for modifying a user instead
func (svc *Service) saveUser(ctx context.Context, user *fleet.User) error {
	return svc.ds.SaveUser(ctx, user)
}

////////////////////////////////////////////////////////////////////////////////
// Perform Required Password Reset
////////////////////////////////////////////////////////////////////////////////

type performRequiredPasswordResetRequest struct {
	Password string `json:"new_password"`
	ID       uint   `json:"id"`
}

type performRequiredPasswordResetResponse struct {
	User *fleet.User `json:"user,omitempty"`
	Err  error       `json:"error,omitempty"`
}

func (r performRequiredPasswordResetResponse) error() error { return r.Err }

func performRequiredPasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*performRequiredPasswordResetRequest)
	user, err := svc.PerformRequiredPasswordReset(ctx, req.Password)
	if err != nil {
		return performRequiredPasswordResetResponse{Err: err}, nil
	}
	return performRequiredPasswordResetResponse{User: user}, nil
}

func (svc *Service) PerformRequiredPasswordReset(ctx context.Context, password string) (*fleet.User, error) {
	vc, ok := viewer.FromContext(ctx)
	if !ok {
		return nil, fleet.ErrNoContext
	}
	if !vc.CanPerformPasswordReset() {
		return nil, fleet.NewPermissionError("cannot reset password")
	}
	user := vc.User

	if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
		return nil, err
	}

	if user.SSOEnabled {
		return nil, ctxerr.New(ctx, "password reset for single sign on user not allowed")
	}
	if !user.IsAdminForcedPasswordReset() {
		return nil, ctxerr.New(ctx, "user does not require password reset")
	}

	// prevent setting the same password
	if err := user.ValidatePassword(password); err == nil {
		return nil, fleet.NewInvalidArgumentError("new_password", "cannot reuse old password")
	}

	user.AdminForcedPasswordReset = false
	err := svc.setNewPassword(ctx, user, password)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "setting new password")
	}

	// Sessions should already have been cleared when the reset was
	// required

	return user, nil
}

// setNewPassword is a helper for changing a user's password. It should be
// called to set the new password after proper authorization has been
// performed.
func (svc *Service) setNewPassword(ctx context.Context, user *fleet.User, password string) error {
	err := user.SetPassword(password, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "setting new password")
	}
	if user.SSOEnabled {
		return ctxerr.New(ctx, "set password for single sign on user not allowed")
	}
	err = svc.saveUser(ctx, user)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "saving changed password")
	}

	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Reset Password
////////////////////////////////////////////////////////////////////////////////

type resetPasswordRequest struct {
	PasswordResetToken string `json:"password_reset_token"`
	NewPassword        string `json:"new_password"`
}

type resetPasswordResponse struct {
	Err error `json:"error,omitempty"`
}

func (r resetPasswordResponse) error() error { return r.Err }

func resetPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*resetPasswordRequest)
	err := svc.ResetPassword(ctx, req.PasswordResetToken, req.NewPassword)
	return resetPasswordResponse{Err: err}, nil
}

func (svc *Service) ResetPassword(ctx context.Context, token, password string) error {
	// skipauth: No viewer context available. The user is locked out of their
	// account and authNZ is performed entirely by providing a valid password
	// reset token.
	svc.authz.SkipAuthorization(ctx)

	if token == "" {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Token cannot be empty field"))
	}
	if password == "" {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "New password cannot be empty field"))
	}
	if err := fleet.ValidatePasswordRequirements(password); err != nil {
		return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", err.Error()))
	}

	reset, err := svc.ds.FindPasswordResetByToken(ctx, token)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "looking up reset by token")
	}
	user, err := svc.ds.UserByID(ctx, reset.UserID)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "retrieving user")
	}

	if user.SSOEnabled {
		return ctxerr.New(ctx, "password reset for single sign on user not allowed")
	}

	// prevent setting the same password
	if err := user.ValidatePassword(password); err == nil {
		return fleet.NewInvalidArgumentError("new_password", "cannot reuse old password")
	}

	err = svc.setNewPassword(ctx, user, password)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "setting new password")
	}

	// delete password reset tokens for user
	if err := svc.ds.DeletePasswordResetRequestsForUser(ctx, user.ID); err != nil {
		return ctxerr.Wrap(ctx, err, "delete password reset requests")
	}

	// Clear sessions so that any other browsers will have to log in with
	// the new password
	if err := svc.ds.DestroyAllSessionsForUser(ctx, user.ID); err != nil {
		return ctxerr.Wrap(ctx, err, "delete user sessions")
	}

	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Forgot Password
////////////////////////////////////////////////////////////////////////////////

type forgotPasswordRequest struct {
	Email string `json:"email"`
}

type forgotPasswordResponse struct {
	Err error `json:"error,omitempty"`
}

func (r forgotPasswordResponse) error() error { return r.Err }
func (r forgotPasswordResponse) status() int  { return http.StatusAccepted }

func forgotPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
	req := request.(*forgotPasswordRequest)
	// Any error returned by the service should not be returned to the
	// client to prevent information disclosure (it will be logged in the
	// server logs).
	_ = svc.RequestPasswordReset(ctx, req.Email)
	return forgotPasswordResponse{}, nil
}

func (svc *Service) RequestPasswordReset(ctx context.Context, email string) error {
	// skipauth: No viewer context available. The user is locked out of their
	// account and trying to reset their password.
	svc.authz.SkipAuthorization(ctx)

	// Regardless of error, sleep until the request has taken at least 1 second.
	// This means that any request to this method will take ~1s and frustrate a timing attack.
	defer func(start time.Time) {
		time.Sleep(time.Until(start.Add(1 * time.Second)))
	}(time.Now())

	user, err := svc.ds.UserByEmail(ctx, email)
	if err != nil {
		return err
	}
	if user.SSOEnabled {
		return ctxerr.New(ctx, "password reset for single sign on user not allowed")
	}

	random, err := server.GenerateRandomText(svc.config.App.TokenKeySize)
	if err != nil {
		return err
	}
	token := base64.URLEncoding.EncodeToString([]byte(random))

	request := &fleet.PasswordResetRequest{
		ExpiresAt: time.Now().Add(time.Hour * 24),
		UserID:    user.ID,
		Token:     token,
	}
	_, err = svc.ds.NewPasswordResetRequest(ctx, request)
	if err != nil {
		return err
	}

	config, err := svc.ds.AppConfig(ctx)
	if err != nil {
		return err
	}

	resetEmail := fleet.Email{
		Subject: "Reset Your Fleet Password",
		To:      []string{user.Email},
		Config:  config,
		Mailer: &mail.PasswordResetMailer{
			BaseURL:  template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix),
			AssetURL: getAssetURL(),
			Token:    token,
		},
	}

	return svc.mailService.SendEmail(resetEmail)
}

func (svc *Service) ListAvailableTeamsForUser(ctx context.Context, user *fleet.User) ([]*fleet.TeamSummary, error) {
	// skipauth: No authorization check needed due to implementation returning
	// only license error.
	svc.authz.SkipAuthorization(ctx)

	return nil, fleet.ErrMissingLicense
}
