// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT

package admin

import (
	"errors"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"code.gitea.io/gitea/models/auth"
	"code.gitea.io/gitea/models/db"
	org_model "code.gitea.io/gitea/models/organization"
	packages_model "code.gitea.io/gitea/models/packages"
	repo_model "code.gitea.io/gitea/models/repo"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/auth/password"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/optional"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/templates"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/routers/web/explore"
	user_setting "code.gitea.io/gitea/routers/web/user/setting"
	"code.gitea.io/gitea/services/context"
	"code.gitea.io/gitea/services/forms"
	"code.gitea.io/gitea/services/mailer"
	user_service "code.gitea.io/gitea/services/user"
)

const (
	tplUsers    templates.TplName = "admin/user/list"
	tplUserNew  templates.TplName = "admin/user/new"
	tplUserView templates.TplName = "admin/user/view"
	tplUserEdit templates.TplName = "admin/user/edit"
)

type adminInviteCreatorInfo struct {
	Name  string
	Email string
}

type adminActionActorInfo struct {
	Name  string
	Email string
}

type adminProhibitLoginInfo struct {
	At timeutil.TimeStamp
	adminActionActorInfo
}

type adminRestrictedInfo struct {
	At timeutil.TimeStamp
	adminActionActorInfo
}

type superAdminInfo struct {
	At timeutil.TimeStamp
	adminActionActorInfo
}

// UserSearchDefaultAdminSort is the default sort type for admin view
const UserSearchDefaultAdminSort = "alphabetically"

// Users show all the users
func Users(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("admin.users")
	ctx.Data["PageIsAdminUsers"] = true
	if !ensureSuperAdminBootstrap(ctx) {
		return
	}

	statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"}
	statusFilterMap := map[string]string{}
	for _, filterKey := range statusFilterKeys {
		paramKey := "status_filter[" + filterKey + "]"
		paramVal := ctx.FormString(paramKey)
		statusFilterMap[filterKey] = paramVal
	}

	sortType := ctx.FormString("sort")
	if sortType == "" {
		sortType = UserSearchDefaultAdminSort
		ctx.SetFormString("sort", sortType)
	}
	ctx.PageData["adminUserListSearchForm"] = map[string]any{
		"StatusFilterMap": statusFilterMap,
		"SortType":        sortType,
	}

	explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
		Actor: ctx.Doer,
		Types: []user_model.UserType{user_model.UserTypeIndividual},
		ListOptions: db.ListOptions{
			PageSize: setting.UI.Admin.UserPagingNum,
		},
		SearchByEmail:      true,
		IsActive:           optional.ParseBool(statusFilterMap["is_active"]),
		IsAdmin:            optional.ParseBool(statusFilterMap["is_admin"]),
		IsRestricted:       optional.ParseBool(statusFilterMap["is_restricted"]),
		IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
		IsProhibitLogin:    optional.ParseBool(statusFilterMap["is_prohibit_login"]),
		IncludeReserved:    true, // administrator needs to list all accounts include reserved, bot, remote ones
	}, tplUsers, loadAdminUserListDeleteState, loadAdminUserListAdminBy, loadAdminUserListSuperAdminBy, loadAdminUserListCreatedBy, loadAdminUserListActivatedBy, loadAdminUserListProhibitLoginBy, loadAdminUserListRestrictedBy, loadAdminUserListActionState)
}

func loadAdminUserListDeleteState(ctx *context.Context, users user_model.UserList) {
	usersIsLastAdmin := make(map[int64]bool, len(users))
	if user_model.CountUsers(ctx, &user_model.CountUserFilter{IsAdmin: optional.Some(true), IsActive: optional.Some(true)}) <= 1 {
		for _, u := range users {
			usersIsLastAdmin[u.ID] = u.IsAdmin && u.IsActive
		}
	}
	ctx.Data["UsersIsLastAdmin"] = usersIsLastAdmin
}

func loadAdminUserListAdminBy(ctx *context.Context, users user_model.UserList) {
	usersAdminByAdmin := make(map[int64]*adminActionActorInfo, len(users))
	for _, u := range users {
		if !u.IsAdmin {
			continue
		}
		actor := loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminGrantedBy, user_model.SettingsKeyAdminGrantedByName, user_model.SettingsKeyAdminGrantedByEmail, "admin grant")
		if actor != nil {
			usersAdminByAdmin[u.ID] = actor
		}
	}
	ctx.Data["UsersAdminByAdmin"] = usersAdminByAdmin
}

func loadAdminUserListSuperAdminBy(ctx *context.Context, users user_model.UserList) {
	usersSuperAdminByAdmin := make(map[int64]*superAdminInfo, len(users))
	if !setting.Admin.SuperAdminEnabled {
		ctx.Data["UsersSuperAdminByAdmin"] = usersSuperAdminByAdmin
		return
	}
	for _, u := range users {
		if !isSuperAdmin(ctx, u.ID) {
			continue
		}
		grantedUnixValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeySuperAdminGrantedUnix)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeySuperAdminGrantedUnix, u.ID, err)
			}
			continue
		}
		grantedUnix, err := strconv.ParseInt(grantedUnixValue, 10, 64)
		if err != nil {
			log.Error("Parse super admin grant unix %q for user %d: %v", grantedUnixValue, u.ID, err)
			continue
		}
		actor := loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeySuperAdminGrantedBy, user_model.SettingsKeySuperAdminGrantedByName, user_model.SettingsKeySuperAdminGrantedByEmail, "super admin grant")
		if actor == nil {
			actor = &adminActionActorInfo{Name: "GOD"}
		}
		usersSuperAdminByAdmin[u.ID] = &superAdminInfo{
			At:                   timeutil.TimeStamp(grantedUnix),
			adminActionActorInfo: *actor,
		}
	}
	ctx.Data["UsersSuperAdminByAdmin"] = usersSuperAdminByAdmin
}

func loadAdminUserListCreatedBy(ctx *context.Context, users user_model.UserList) {
	usersCreatedByAdmin := make(map[int64]*adminInviteCreatorInfo, len(users))
	firstAdminID := firstAdminUserID(ctx, users)
	for _, u := range users {
		adminIDValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminInviteCreatedBy)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminInviteCreatedBy, u.ID, err)
			}
			continue
		}
		if adminIDValue == "" {
			if u.IsAdmin && u.ID == firstAdminID {
				usersCreatedByAdmin[u.ID] = &adminInviteCreatorInfo{Name: "GOD"}
			}
			continue
		}

		adminName, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminInviteCreatedByName)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminInviteCreatedByName, u.ID, err)
		}
		adminEmail, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminInviteCreatedByEmail)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminInviteCreatedByEmail, u.ID, err)
		}
		if adminName != "" || adminEmail != "" {
			if adminName == "" {
				adminName = adminEmail
			}
			usersCreatedByAdmin[u.ID] = &adminInviteCreatorInfo{Name: adminName, Email: adminEmail}
			continue
		}

		adminID, err := strconv.ParseInt(adminIDValue, 10, 64)
		if err != nil {
			log.Error("Parse admin invite creator ID %q for user %d: %v", adminIDValue, u.ID, err)
			continue
		}

		admin, err := user_model.GetUserByID(ctx, adminID)
		if err != nil {
			log.Error("Get admin invite creator %d for user %d: %v", adminID, u.ID, err)
			continue
		}
		usersCreatedByAdmin[u.ID] = &adminInviteCreatorInfo{Name: admin.Name, Email: admin.Email}
	}
	ctx.Data["UsersCreatedByAdmin"] = usersCreatedByAdmin
}

func firstAdminUserID(ctx *context.Context, users user_model.UserList) int64 {
	hasAdmin := false
	for _, u := range users {
		if u.IsAdmin {
			hasAdmin = true
			break
		}
	}
	if !hasAdmin {
		return 0
	}

	firstAdmin := &user_model.User{}
	has, err := db.GetEngine(ctx).Where("is_admin=?", true).Asc("id").Get(firstAdmin)
	if err != nil {
		log.Error("Get first admin user: %v", err)
		return 0
	}
	if !has {
		return 0
	}
	return firstAdmin.ID
}

func loadAdminUserListActivatedBy(ctx *context.Context, users user_model.UserList) {
	usersActivatedByAdmin := make(map[int64]*adminActionActorInfo, len(users))
	for _, u := range users {
		if !u.IsActive {
			continue
		}
		adminName, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminActivatedByName)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminActivatedByName, u.ID, err)
		}
		adminEmail, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminActivatedByEmail)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminActivatedByEmail, u.ID, err)
		}
		if adminName != "" || adminEmail != "" {
			if adminName == "" {
				adminName = adminEmail
			}
			usersActivatedByAdmin[u.ID] = &adminActionActorInfo{Name: adminName, Email: adminEmail}
			continue
		}

		adminIDValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminActivatedBy)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminActivatedBy, u.ID, err)
			}
			continue
		}
		if adminIDValue == "" {
			continue
		}
		adminID, err := strconv.ParseInt(adminIDValue, 10, 64)
		if err != nil {
			log.Error("Parse admin activation ID %q for user %d: %v", adminIDValue, u.ID, err)
			continue
		}
		admin, err := user_model.GetUserByID(ctx, adminID)
		if err != nil {
			log.Error("Get admin activator %d for user %d: %v", adminID, u.ID, err)
			continue
		}
		usersActivatedByAdmin[u.ID] = &adminActionActorInfo{Name: admin.Name, Email: admin.Email}
	}
	ctx.Data["UsersActivatedByAdmin"] = usersActivatedByAdmin
}

func loadAdminUserListProhibitLoginBy(ctx *context.Context, users user_model.UserList) {
	usersProhibitLoginByAdmin := make(map[int64]*adminProhibitLoginInfo, len(users))
	for _, u := range users {
		if !u.ProhibitLogin {
			continue
		}

		prohibitedUnixValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginUnix)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminProhibitLoginUnix, u.ID, err)
			}
			continue
		}
		prohibitedUnix, err := strconv.ParseInt(prohibitedUnixValue, 10, 64)
		if err != nil {
			log.Error("Parse admin prohibit login unix %q for user %d: %v", prohibitedUnixValue, u.ID, err)
			continue
		}
		if prohibitedUnix <= 0 {
			continue
		}

		adminName, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginByName)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminProhibitLoginByName, u.ID, err)
		}
		adminEmail, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginByEmail)
		if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminProhibitLoginByEmail, u.ID, err)
		}
		if adminName != "" || adminEmail != "" {
			if adminName == "" {
				adminName = adminEmail
			}
			usersProhibitLoginByAdmin[u.ID] = &adminProhibitLoginInfo{
				At:                   timeutil.TimeStamp(prohibitedUnix),
				adminActionActorInfo: adminActionActorInfo{Name: adminName, Email: adminEmail},
			}
			continue
		}

		adminIDValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginBy)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminProhibitLoginBy, u.ID, err)
			}
			continue
		}
		if adminIDValue == "" {
			continue
		}
		adminID, err := strconv.ParseInt(adminIDValue, 10, 64)
		if err != nil {
			log.Error("Parse admin prohibit login ID %q for user %d: %v", adminIDValue, u.ID, err)
			continue
		}
		admin, err := user_model.GetUserByID(ctx, adminID)
		if err != nil {
			log.Error("Get admin prohibit login actor %d for user %d: %v", adminID, u.ID, err)
			continue
		}
		usersProhibitLoginByAdmin[u.ID] = &adminProhibitLoginInfo{
			At:                   timeutil.TimeStamp(prohibitedUnix),
			adminActionActorInfo: adminActionActorInfo{Name: admin.Name, Email: admin.Email},
		}
	}
	ctx.Data["UsersProhibitLoginByAdmin"] = usersProhibitLoginByAdmin
}

func loadAdminUserListRestrictedBy(ctx *context.Context, users user_model.UserList) {
	usersRestrictedByAdmin := make(map[int64]*adminRestrictedInfo, len(users))
	for _, u := range users {
		if !u.IsRestricted {
			continue
		}

		restrictedUnixValue, err := user_model.GetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminRestrictedUnix)
		if err != nil {
			if !user_model.IsErrUserSettingIsNotExist(err) {
				log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyAdminRestrictedUnix, u.ID, err)
			}
			continue
		}
		restrictedUnix, err := strconv.ParseInt(restrictedUnixValue, 10, 64)
		if err != nil {
			log.Error("Parse admin restriction unix %q for user %d: %v", restrictedUnixValue, u.ID, err)
			continue
		}
		if restrictedUnix <= 0 {
			continue
		}

		actor := loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminRestrictedBy, user_model.SettingsKeyAdminRestrictedByName, user_model.SettingsKeyAdminRestrictedByEmail, "admin restriction")
		if actor == nil {
			continue
		}
		usersRestrictedByAdmin[u.ID] = &adminRestrictedInfo{
			At:                   timeutil.TimeStamp(restrictedUnix),
			adminActionActorInfo: *actor,
		}
	}
	ctx.Data["UsersRestrictedByAdmin"] = usersRestrictedByAdmin
}

func loadStoredAdminActor(ctx *context.Context, userID int64, idKey, nameKey, emailKey, logLabel string) *adminActionActorInfo {
	adminName, err := user_model.GetUserSetting(ctx, userID, nameKey)
	if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
		log.Error("GetUserSetting[%s] for user %d: %v", nameKey, userID, err)
	}
	adminEmail, err := user_model.GetUserSetting(ctx, userID, emailKey)
	if err != nil && !user_model.IsErrUserSettingIsNotExist(err) {
		log.Error("GetUserSetting[%s] for user %d: %v", emailKey, userID, err)
	}
	if adminName != "" || adminEmail != "" {
		if adminName == "" {
			adminName = adminEmail
		}
		return &adminActionActorInfo{Name: adminName, Email: adminEmail}
	}

	adminIDValue, err := user_model.GetUserSetting(ctx, userID, idKey)
	if err != nil {
		if !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", idKey, userID, err)
		}
		return nil
	}
	if adminIDValue == "" {
		return nil
	}
	adminID, err := strconv.ParseInt(adminIDValue, 10, 64)
	if err != nil {
		log.Error("Parse %s ID %q for user %d: %v", logLabel, adminIDValue, userID, err)
		return nil
	}
	admin, err := user_model.GetUserByID(ctx, adminID)
	if err != nil {
		log.Error("Get %s actor %d for user %d: %v", logLabel, adminID, userID, err)
		return nil
	}
	return &adminActionActorInfo{Name: admin.Name, Email: admin.Email}
}

func storeAdminActivationInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminActivatedBy, strconv.FormatInt(admin.ID, 10)); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminActivatedByName, admin.Name); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminActivatedByEmail, admin.Email); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return true
}

func storeAdminCreatedByInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminInviteCreatedBy, strconv.FormatInt(admin.ID, 10)); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminInviteCreatedByName, admin.Name); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeyAdminInviteCreatedByEmail, admin.Email); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return true
}

func storeAdminGrantedInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeyAdminGrantedUnix, user_model.SettingsKeyAdminGrantedBy, user_model.SettingsKeyAdminGrantedByName, user_model.SettingsKeyAdminGrantedByEmail)
}

func storeAdminProhibitLoginInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeyAdminProhibitLoginUnix, user_model.SettingsKeyAdminProhibitLoginBy, user_model.SettingsKeyAdminProhibitLoginByName, user_model.SettingsKeyAdminProhibitLoginByEmail)
}

func storeAdminRestrictedInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeyAdminRestrictedUnix, user_model.SettingsKeyAdminRestrictedBy, user_model.SettingsKeyAdminRestrictedByName, user_model.SettingsKeyAdminRestrictedByEmail)
}

func storeAdminDeactivationInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeyAdminDeactivatedUnix, user_model.SettingsKeyAdminDeactivatedBy, user_model.SettingsKeyAdminDeactivatedByName, user_model.SettingsKeyAdminDeactivatedByEmail)
}

func storeSuperAdminGrantedInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminEnabled, "true"); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeySuperAdminGrantedUnix, user_model.SettingsKeySuperAdminGrantedBy, user_model.SettingsKeySuperAdminGrantedByName, user_model.SettingsKeySuperAdminGrantedByEmail)
}

func storeGodSuperAdminGrantedInfo(ctx *context.Context, userID int64) bool {
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminEnabled, "true"); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminGrantedUnix, strconv.FormatInt(int64(timeutil.TimeStampNow()), 10)); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminGrantedByName, "GOD"); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return true
}

func storeSuperAdminRevokedInfo(ctx *context.Context, userID int64, admin *user_model.User) bool {
	if err := user_model.SetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminEnabled, "false"); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return storeAdminTimedActionInfo(ctx, userID, admin, user_model.SettingsKeySuperAdminRevokedUnix, user_model.SettingsKeySuperAdminRevokedBy, user_model.SettingsKeySuperAdminRevokedByName, user_model.SettingsKeySuperAdminRevokedByEmail)
}

func storeAdminTimedActionInfo(ctx *context.Context, userID int64, admin *user_model.User, unixKey, byKey, byNameKey, byEmailKey string) bool {
	if err := user_model.SetUserSetting(ctx, userID, unixKey, strconv.FormatInt(int64(timeutil.TimeStampNow()), 10)); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, byKey, strconv.FormatInt(admin.ID, 10)); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, byNameKey, admin.Name); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	if err := user_model.SetUserSetting(ctx, userID, byEmailKey, admin.Email); err != nil {
		ctx.ServerError("SetUserSetting", err)
		return false
	}
	return true
}

func ensureSuperAdminBootstrap(ctx *context.Context) bool {
	if !setting.Admin.SuperAdminEnabled {
		return true
	}
	if countSuperAdmins(ctx) > 0 {
		return true
	}
	firstAdmin := &user_model.User{}
	has, err := db.GetEngine(ctx).Where("is_admin=?", true).And("is_active=?", true).Asc("id").Get(firstAdmin)
	if err != nil {
		ctx.ServerError("GetFirstAdmin", err)
		return false
	}
	if !has {
		return true
	}
	return storeGodSuperAdminGrantedInfo(ctx, firstAdmin.ID)
}

func isSuperAdmin(ctx *context.Context, userID int64) bool {
	if !setting.Admin.SuperAdminEnabled {
		return false
	}
	value, err := user_model.GetUserSetting(ctx, userID, user_model.SettingsKeySuperAdminEnabled)
	if err != nil {
		if !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeySuperAdminEnabled, userID, err)
		}
		return false
	}
	return value == "true"
}

func countSuperAdmins(ctx *context.Context) int {
	settings := make([]*user_model.Setting, 0, 5)
	if err := db.GetEngine(ctx).Where("setting_key=? AND setting_value=?", user_model.SettingsKeySuperAdminEnabled, "true").Find(&settings); err != nil {
		log.Error("Find super admin settings: %v", err)
		return 0
	}
	count := 0
	for _, superAdminSetting := range settings {
		u, err := user_model.GetUserByID(ctx, superAdminSetting.UserID)
		if err != nil {
			if !user_model.IsErrUserNotExist(err) {
				log.Error("Get super admin user %d: %v", superAdminSetting.UserID, err)
			}
			continue
		}
		if u.IsAdmin && u.IsActive {
			count++
		}
	}
	return count
}

func canManageAdminStatus(ctx *context.Context, target *user_model.User, targetRequestedAdmin, targetIsSuperAdmin bool) bool {
	targetWasAdmin := target.IsAdmin
	if !setting.Admin.SuperAdminEnabled || targetWasAdmin == targetRequestedAdmin {
		return true
	}
	if isSuperAdmin(ctx, ctx.Doer.ID) {
		return true
	}
	if !ctx.Doer.IsAdmin || targetIsSuperAdmin {
		return false
	}
	switch setting.Admin.AdminManagementPolicy {
	case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
		if !targetWasAdmin && targetRequestedAdmin {
			return true
		}
		canManage, _ := canManageAdminUser(ctx, ctx.Doer, target)
		return canManage
	default:
		return false
	}
}

func canManageSuperAdminStatus(ctx *context.Context, targetWasSuperAdmin, targetRequestedSuperAdmin bool) bool {
	if !setting.Admin.SuperAdminEnabled || targetWasSuperAdmin == targetRequestedSuperAdmin {
		return true
	}
	return isSuperAdmin(ctx, ctx.Doer.ID)
}

func canEditSuperAdminUser(ctx *context.Context, targetIsSuperAdmin bool) bool {
	if !setting.Admin.SuperAdminEnabled || !targetIsSuperAdmin {
		return true
	}
	return isSuperAdmin(ctx, ctx.Doer.ID)
}

func canManageAdminUser(ctx *context.Context, doer, target *user_model.User) (bool, string) {
	if !target.IsAdmin || doer.ID == target.ID {
		return true, ""
	}
	if isSuperAdmin(ctx, doer.ID) {
		return true, ""
	}
	if !doer.IsAdmin {
		return false, ctx.Locale.TrString("admin.users.super_admin.required")
	}

	switch setting.Admin.AdminManagementPolicy {
	case setting.AdminManagementPolicyGrantorOnly:
		grantorID, has, err := user_model.GetAdminGrantorID(ctx, target.ID)
		if err != nil {
			log.Error("GetAdminGrantorID for user %d: %v", target.ID, err)
			return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
		}
		if has && grantorID == doer.ID {
			return true, ""
		}
		return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
	case setting.AdminManagementPolicyGrantorInheritance:
		grantorID, has, err := user_model.GetEffectiveAdminGrantorID(ctx, target.ID)
		if err != nil {
			log.Error("GetEffectiveAdminGrantorID for user %d: %v", target.ID, err)
			return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
		}
		if has && grantorID == doer.ID {
			return true, ""
		}
		return false, ctx.Locale.TrString("admin.users.admin_grantor.required")
	default:
		return false, ctx.Locale.TrString("admin.users.super_admin.required")
	}
}

func canEditAdminStatus(ctx *context.Context, target *user_model.User) bool {
	if !setting.Admin.SuperAdminEnabled {
		return true
	}
	if isSuperAdmin(ctx, ctx.Doer.ID) {
		return true
	}
	if !ctx.Doer.IsAdmin || isSuperAdmin(ctx, target.ID) {
		return false
	}

	switch setting.Admin.AdminManagementPolicy {
	case setting.AdminManagementPolicyGrantorOnly, setting.AdminManagementPolicyGrantorInheritance:
		if !target.IsAdmin {
			return true
		}
		canManage, _ := canManageAdminUser(ctx, ctx.Doer, target)
		return canManage
	default:
		return false
	}
}

func isBootstrapProtectedUser(ctx *context.Context, userID int64) bool {
	protected, err := user_model.HasBootstrapAdminGrant(ctx, userID)
	if err != nil {
		log.Error("HasBootstrapAdminGrant for user %d: %v", userID, err)
		return false
	}
	return protected
}

func canEditUserFromAdminPanel(ctx *context.Context, doer, target *user_model.User) (bool, string) {
	if isBootstrapProtectedUser(ctx, target.ID) && doer.ID != target.ID {
		return false, ctx.Locale.TrString("admin.users.god_immutable")
	}
	if !canEditSuperAdminUser(ctx, isSuperAdmin(ctx, target.ID)) {
		return false, ctx.Locale.TrString("admin.users.super_admin.required")
	}
	if target.IsAdmin {
		return canManageAdminUser(ctx, doer, target)
	}
	return true, ""
}

func denyBootstrapProtectedUserAccess(ctx *context.Context, u *user_model.User) bool {
	if !isBootstrapProtectedUser(ctx, u.ID) || ctx.Doer.ID == u.ID {
		return false
	}
	ctx.Flash.Error(ctx.Tr("admin.users.god_immutable"))
	ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
	return true
}

func denySuperAdminUserEditAccess(ctx *context.Context, u *user_model.User) bool {
	if canEditSuperAdminUser(ctx, isSuperAdmin(ctx, u.ID)) {
		return false
	}
	ctx.Flash.Error(ctx.Tr("admin.users.super_admin.required"))
	ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
	return true
}

func canDeleteUserFromAdminPanel(ctx *context.Context, doer, target *user_model.User) (bool, string) {
	if target.ID == doer.ID {
		return false, ctx.Locale.TrString("admin.users.cannot_delete_self")
	}

	if isBootstrapProtectedUser(ctx, target.ID) {
		return false, ctx.Locale.TrString("admin.users.god_immutable")
	}

	if target.IsAdmin {
		canManage, reason := canManageAdminUser(ctx, doer, target)
		if !canManage {
			return false, reason
		}
	}

	isCreator, err := user_model.IsGrantorOf(ctx, doer.ID, target.ID)
	if err != nil {
		log.Error("IsGrantorOf doer %d target %d: %v", doer.ID, target.ID, err)
		return false, ctx.Locale.TrString("admin.users.delete_account")
	}
	if isCreator {
		return false, ctx.Locale.TrString("admin.users.cannot_delete_grantor")
	}

	targetIsSuperAdmin := isSuperAdmin(ctx, target.ID)
	if targetIsSuperAdmin && countSuperAdmins(ctx) <= 1 {
		return false, ctx.Locale.TrString("admin.users.super_admin.last")
	}
	if targetIsSuperAdmin && !isSuperAdmin(ctx, doer.ID) {
		return false, ctx.Locale.TrString("admin.users.super_admin.required")
	}
	return true, ""
}

func loadAdminUserListActionState(ctx *context.Context, users user_model.UserList) {
	usersCanEdit := make(map[int64]bool, len(users))
	usersEditDisabledReason := make(map[int64]string, len(users))
	usersCanDelete := make(map[int64]bool, len(users))
	usersDeleteDisabledReason := make(map[int64]string, len(users))

	for _, u := range users {
		canEdit, editReason := canEditUserFromAdminPanel(ctx, ctx.Doer, u)
		usersCanEdit[u.ID] = canEdit
		if !canEdit {
			usersEditDisabledReason[u.ID] = editReason
		}

		canDelete, deleteReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
		usersCanDelete[u.ID] = canDelete
		if !canDelete {
			usersDeleteDisabledReason[u.ID] = deleteReason
		}
	}

	ctx.Data["UsersCanEdit"] = usersCanEdit
	ctx.Data["UsersEditDisabledReason"] = usersEditDisabledReason
	ctx.Data["UsersCanDelete"] = usersCanDelete
	ctx.Data["UsersDeleteDisabledReason"] = usersDeleteDisabledReason
}

// NewUser render adding a new user page
func NewUser(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
	ctx.Data["PageIsAdminUsers"] = true
	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()

	ctx.Data["login_type"] = "0-0"

	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
		IsActive: optional.Some(true),
	})
	if err != nil {
		ctx.ServerError("auth.Sources", err)
		return
	}
	ctx.Data["Sources"] = sources

	ctx.Data["CanSendEmail"] = setting.MailService != nil
	ctx.Data["send_notify"] = false
	ctx.Data["send_invite"] = setting.MailService != nil
	ctx.HTML(http.StatusOK, tplUserNew)
}

// NewUserPost response for adding a new user
func NewUserPost(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.AdminCreateUserForm)
	ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
	ctx.Data["PageIsAdminUsers"] = true
	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()

	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
		IsActive: optional.Some(true),
	})
	if err != nil {
		ctx.ServerError("auth.Sources", err)
		return
	}
	ctx.Data["Sources"] = sources
	canSendEmail := setting.MailService != nil
	ctx.Data["CanSendEmail"] = canSendEmail
	ctx.Data["send_notify"] = form.SendNotify
	ctx.Data["send_invite"] = form.SendInvite

	if ctx.HasError() {
		ctx.HTML(http.StatusOK, tplUserNew)
		return
	}

	u := &user_model.User{
		Name:      form.UserName,
		Email:     form.Email,
		Passwd:    form.Password,
		LoginType: auth.Plain,
	}

	if form.SendInvite {
		form.SendNotify = false
	}
	ctx.Data["send_notify"] = form.SendNotify
	ctx.Data["send_invite"] = form.SendInvite
	isAdminInvite := canSendEmail && form.SendInvite

	overwriteDefault := &user_model.CreateUserOverwriteOptions{
		IsActive:   optional.Some(!isAdminInvite),
		Visibility: &form.Visibility,
	}

	if len(form.LoginType) > 0 {
		fields := strings.Split(form.LoginType, "-")
		if len(fields) == 2 {
			lType, _ := strconv.ParseInt(fields[0], 10, 0)
			u.LoginType = auth.Type(lType)
			u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
			u.LoginName = form.LoginName
		}
	}
	if u.LoginType == auth.NoType || u.LoginType == auth.Plain {
		if len(form.Password) < setting.MinPasswordLength {
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
			return
		}
		if !password.IsComplexEnough(form.Password) {
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
			return
		}
		if err := password.IsPwned(ctx, form.Password); err != nil {
			ctx.Data["Err_Password"] = true
			errMsg := ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords")
			if password.IsErrIsPwnedRequest(err) {
				log.Error(err.Error())
				errMsg = ctx.Tr("auth.password_pwned_err")
			}
			ctx.RenderWithErrDeprecated(errMsg, tplUserNew, &form)
			return
		}
		u.MustChangePassword = form.MustChangePassword
	}

	if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
		switch {
		case user_model.IsErrUserAlreadyExist(err):
			ctx.Data["Err_UserName"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
		case user_model.IsErrEmailAlreadyUsed(err):
			ctx.Data["Err_Email"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserNew, &form)
		case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
			ctx.Data["Err_Email"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserNew, &form)
		case db.IsErrNameReserved(err):
			ctx.Data["Err_UserName"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form)
		case db.IsErrNamePatternNotAllowed(err):
			ctx.Data["Err_UserName"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
		case db.IsErrNameCharsNotAllowed(err):
			ctx.Data["Err_UserName"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
		default:
			ctx.ServerError("CreateUser", err)
		}
		return
	}

	if !user_model.IsEmailDomainAllowed(u.Email) {
		ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
	}

	log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)

	if ok := storeAdminCreatedByInfo(ctx, u.ID, ctx.Doer); !ok {
		return
	}
	if isAdminInvite {
		if err := user_model.SetAdminInvitePending(ctx, u.ID, true); err != nil {
			ctx.ServerError("SetAdminInvitePending", err)
			return
		}
	}

	// Send the selected account creation email.
	if isAdminInvite {
		mailer.SendAdminInviteMailWithLocale(ctx.Locale, ctx.Doer, u)
	} else if form.SendNotify {
		mailer.SendRegisterNotifyMailWithLocale(ctx.Locale, ctx.Doer, u)
	}

	ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
	ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}

func prepareUserInfo(ctx *context.Context) *user_model.User {
	u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid"))
	if err != nil {
		if user_model.IsErrUserNotExist(err) {
			ctx.Redirect(setting.AppSubURL + "/-/admin/users")
		} else {
			ctx.ServerError("GetUserByID", err)
		}
		return nil
	}
	ctx.Data["User"] = u

	if u.LoginSource > 0 {
		ctx.Data["LoginSource"], err = auth.GetSourceByID(ctx, u.LoginSource)
		if err != nil {
			ctx.ServerError("auth.GetSourceByID", err)
			return nil
		}
	} else {
		ctx.Data["LoginSource"] = &auth.Source{}
	}

	sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{})
	if err != nil {
		ctx.ServerError("auth.Sources", err)
		return nil
	}
	ctx.Data["Sources"] = sources

	hasTOTP, err := auth.HasTwoFactorByUID(ctx, u.ID)
	if err != nil {
		ctx.ServerError("auth.HasTwoFactorByUID", err)
		return nil
	}
	hasWebAuthn, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
	if err != nil {
		ctx.ServerError("auth.HasWebAuthnRegistrationsByUID", err)
		return nil
	}
	ctx.Data["TwoFactorEnabled"] = hasTOTP || hasWebAuthn

	return u
}

func ViewUser(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("admin.users.details")
	ctx.Data["PageIsAdminUsers"] = true
	ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
	ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()

	u := prepareUserInfo(ctx)
	if ctx.Written() {
		return
	}
	canEditUser, editDisabledReason := canEditUserFromAdminPanel(ctx, ctx.Doer, u)
	ctx.Data["CanEditUser"] = canEditUser
	ctx.Data["EditUserDisabledReason"] = editDisabledReason

	repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
		ListOptions: db.ListOptionsAll,
		OwnerID:     u.ID,
		OrderBy:     db.SearchOrderByAlphabetically,
		Private:     true,
		Collaborate: optional.Some(false),
	})
	if err != nil {
		ctx.ServerError("SearchRepository", err)
		return
	}

	ctx.Data["Repos"] = repos
	ctx.Data["ReposTotal"] = int(count)

	emails, err := user_model.GetEmailAddresses(ctx, u.ID)
	if err != nil {
		ctx.ServerError("GetEmailAddresses", err)
		return
	}
	ctx.Data["Emails"] = emails
	ctx.Data["EmailsTotal"] = len(emails)

	orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
		ListOptions:       db.ListOptionsAll,
		UserID:            u.ID,
		IncludeVisibility: structs.VisibleTypePrivate,
	})
	if err != nil {
		ctx.ServerError("FindOrgs", err)
		return
	}

	ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template
	ctx.Data["OrgsTotal"] = len(orgs)

	ctx.HTML(http.StatusOK, tplUserView)
}

func editUserCommon(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
	ctx.Data["PageIsAdminUsers"] = true
	ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
	ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
	ctx.Data["DisableGitHooks"] = setting.DisableGitHooks
	ctx.Data["DisableImportLocal"] = !setting.ImportLocalPaths
	ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
	ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
}

// EditUser show editing user page
func EditUser(ctx *context.Context) {
	editUserCommon(ctx)
	if !ensureSuperAdminBootstrap(ctx) {
		return
	}
	u := prepareUserInfo(ctx)
	if ctx.Written() {
		return
	}
	if denyBootstrapProtectedUserAccess(ctx, u) {
		return
	}
	if denySuperAdminUserEditAccess(ctx, u) {
		return
	}
	if !loadAccountRequestData(ctx, u) {
		return
	}
	loadAdminStatusReasonData(ctx, u)
	ctx.Data["IsLastAdminUser"] = user_model.IsLastAdminUser(ctx, u)
	canDeleteUser, deleteDisabledReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
	ctx.Data["CanDeleteUser"] = canDeleteUser
	ctx.Data["DeleteUserDisabledReason"] = deleteDisabledReason
	targetIsSuperAdmin := isSuperAdmin(ctx, u.ID)
	ctx.Data["SuperAdminEnabled"] = setting.Admin.SuperAdminEnabled
	ctx.Data["IsUserSuperAdmin"] = targetIsSuperAdmin
	ctx.Data["CanEditSuperAdmin"] = isSuperAdmin(ctx, ctx.Doer.ID) && ctx.Doer.ID != u.ID
	ctx.Data["CanEditAdminStatus"] = canEditAdminStatus(ctx, u)
	ctx.Data["IsLastSuperAdminUser"] = targetIsSuperAdmin && countSuperAdmins(ctx) <= 1

	ctx.HTML(http.StatusOK, tplUserEdit)
}

func loadAdminStatusReasonData(ctx *context.Context, u *user_model.User) {
	ctx.Data["admin_reason"] = getAdminUserSetting(ctx, u.ID, user_model.SettingsKeyAdminGrantedReason)
	ctx.Data["super_admin_reason"] = getAdminUserSetting(ctx, u.ID, user_model.SettingsKeySuperAdminRevokedReason)
	ctx.Data["active_reason"] = getAdminUserSetting(ctx, u.ID, user_model.SettingsKeyAdminDeactivatedReason)
	ctx.Data["prohibit_login_reason"] = getAdminUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginReason)
	ctx.Data["restricted_reason"] = getAdminUserSetting(ctx, u.ID, user_model.SettingsKeyAdminRestrictedReason)
	ctx.Data["AdminReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminGrantedBy, user_model.SettingsKeyAdminGrantedByName, user_model.SettingsKeyAdminGrantedByEmail, "admin grant")
	ctx.Data["SuperAdminGrantByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeySuperAdminGrantedBy, user_model.SettingsKeySuperAdminGrantedByName, user_model.SettingsKeySuperAdminGrantedByEmail, "super admin grant")
	ctx.Data["SuperAdminReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeySuperAdminRevokedBy, user_model.SettingsKeySuperAdminRevokedByName, user_model.SettingsKeySuperAdminRevokedByEmail, "super admin revoke")
	ctx.Data["ActiveReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminDeactivatedBy, user_model.SettingsKeyAdminDeactivatedByName, user_model.SettingsKeyAdminDeactivatedByEmail, "admin deactivation")
	ctx.Data["ProhibitLoginReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginBy, user_model.SettingsKeyAdminProhibitLoginByName, user_model.SettingsKeyAdminProhibitLoginByEmail, "admin prohibit login")
	ctx.Data["RestrictedReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminRestrictedBy, user_model.SettingsKeyAdminRestrictedByName, user_model.SettingsKeyAdminRestrictedByEmail, "admin restriction")
}

func getAdminUserSetting(ctx *context.Context, userID int64, key string) string {
	value, err := user_model.GetUserSetting(ctx, userID, key)
	if err != nil {
		if !user_model.IsErrUserSettingIsNotExist(err) {
			log.Error("GetUserSetting[%s] for user %d: %v", key, userID, err)
		}
		return ""
	}
	return value
}

func renderAdminUserEditReasonErr(ctx *context.Context, u *user_model.User, form *forms.AdminEditUserForm) {
	if !loadAccountRequestData(ctx, u) {
		return
	}
	targetIsSuperAdmin := isSuperAdmin(ctx, u.ID)
	ctx.Data["SuperAdminEnabled"] = setting.Admin.SuperAdminEnabled
	ctx.Data["IsUserSuperAdmin"] = targetIsSuperAdmin
	ctx.Data["CanEditSuperAdmin"] = isSuperAdmin(ctx, ctx.Doer.ID) && ctx.Doer.ID != u.ID
	ctx.Data["CanEditAdminStatus"] = canEditAdminStatus(ctx, u)
	ctx.Data["IsLastSuperAdminUser"] = targetIsSuperAdmin && countSuperAdmins(ctx) <= 1
	ctx.Data["IsLastAdminUser"] = user_model.IsLastAdminUser(ctx, u)
	canDeleteUser, deleteDisabledReason := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u)
	ctx.Data["CanDeleteUser"] = canDeleteUser
	ctx.Data["DeleteUserDisabledReason"] = deleteDisabledReason
	ctx.Data["admin_reason"] = form.AdminReason
	ctx.Data["super_admin_reason"] = form.SuperAdminReason
	ctx.Data["active_reason"] = form.ActiveReason
	ctx.Data["prohibit_login_reason"] = form.ProhibitLoginReason
	ctx.Data["restricted_reason"] = form.RestrictedReason
	ctx.Data["AdminReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminGrantedBy, user_model.SettingsKeyAdminGrantedByName, user_model.SettingsKeyAdminGrantedByEmail, "admin grant")
	ctx.Data["SuperAdminGrantByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeySuperAdminGrantedBy, user_model.SettingsKeySuperAdminGrantedByName, user_model.SettingsKeySuperAdminGrantedByEmail, "super admin grant")
	ctx.Data["SuperAdminReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeySuperAdminRevokedBy, user_model.SettingsKeySuperAdminRevokedByName, user_model.SettingsKeySuperAdminRevokedByEmail, "super admin revoke")
	ctx.Data["ActiveReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminDeactivatedBy, user_model.SettingsKeyAdminDeactivatedByName, user_model.SettingsKeyAdminDeactivatedByEmail, "admin deactivation")
	ctx.Data["ProhibitLoginReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginBy, user_model.SettingsKeyAdminProhibitLoginByName, user_model.SettingsKeyAdminProhibitLoginByEmail, "admin prohibit login")
	ctx.Data["RestrictedReasonByAdmin"] = loadStoredAdminActor(ctx, u.ID, user_model.SettingsKeyAdminRestrictedBy, user_model.SettingsKeyAdminRestrictedByName, user_model.SettingsKeyAdminRestrictedByEmail, "admin restriction")
	ctx.RenderWithErrDeprecated(ctx.Tr("admin.users.status_change.reason_required"), tplUserEdit, form)
}

// EditUserPost response for editing user
func EditUserPost(ctx *context.Context) {
	editUserCommon(ctx)
	if !ensureSuperAdminBootstrap(ctx) {
		return
	}
	u := prepareUserInfo(ctx)
	if ctx.Written() {
		return
	}
	if denyBootstrapProtectedUserAccess(ctx, u) {
		return
	}
	if denySuperAdminUserEditAccess(ctx, u) {
		return
	}

	form := web.GetForm(ctx).(*forms.AdminEditUserForm)
	if ctx.HasError() {
		ctx.HTML(http.StatusOK, tplUserEdit)
		return
	}

	requestStatus, err := user_model.GetAccountRequestStatus(ctx, u)
	if err != nil {
		ctx.ServerError("GetAccountRequestStatus", err)
		return
	}
	wasActive := u.IsActive
	wasAdmin := u.IsAdmin
	wasSuperAdmin := isSuperAdmin(ctx, u.ID)
	wasRestricted := u.IsRestricted
	wasProhibitLogin := u.ProhibitLogin
	requestedActive := form.Active
	requestedAdmin := form.Admin
	requestedSuperAdmin := form.SuperAdmin
	requestedRestricted := form.Restricted
	requestedProhibitLogin := form.ProhibitLogin
	if ctx.Doer.ID == u.ID {
		requestedAdmin = u.IsAdmin
		requestedSuperAdmin = wasSuperAdmin
		requestedActive = u.IsActive
		requestedRestricted = u.IsRestricted
		requestedProhibitLogin = u.ProhibitLogin
	}
	if requestStatus != "" {
		requestedActive = u.IsActive
	}
	if !setting.Admin.SuperAdminEnabled {
		requestedSuperAdmin = false
	} else if !canManageSuperAdminStatus(ctx, wasSuperAdmin, requestedSuperAdmin) {
		requestedSuperAdmin = wasSuperAdmin
	}
	if requestedSuperAdmin {
		requestedAdmin = true
	}
	if !requestedAdmin {
		requestedSuperAdmin = false
	}
	if !canManageAdminStatus(ctx, u, requestedAdmin, wasSuperAdmin) {
		requestedAdmin = wasAdmin
		requestedSuperAdmin = wasSuperAdmin
	}
	adminReason := strings.TrimSpace(form.AdminReason)
	superAdminReason := strings.TrimSpace(form.SuperAdminReason)
	activeReason := strings.TrimSpace(form.ActiveReason)
	prohibitLoginReason := strings.TrimSpace(form.ProhibitLoginReason)
	restrictedReason := strings.TrimSpace(form.RestrictedReason)
	if wasActive && !requestedActive && activeReason == "" {
		ctx.Data["Err_ActiveReason"] = true
		renderAdminUserEditReasonErr(ctx, u, form)
		return
	}
	if wasAdmin && !requestedAdmin && adminReason == "" {
		ctx.Data["Err_AdminReason"] = true
		renderAdminUserEditReasonErr(ctx, u, form)
		return
	}
	if wasSuperAdmin && !requestedSuperAdmin && superAdminReason == "" {
		ctx.Data["Err_SuperAdminReason"] = true
		renderAdminUserEditReasonErr(ctx, u, form)
		return
	}
	if wasSuperAdmin && !requestedSuperAdmin && countSuperAdmins(ctx) <= 1 {
		ctx.RenderWithErrDeprecated(ctx.Tr("admin.users.super_admin.last"), tplUserEdit, form)
		return
	}
	if !wasProhibitLogin && requestedProhibitLogin && prohibitLoginReason == "" {
		ctx.Data["Err_ProhibitLoginReason"] = true
		renderAdminUserEditReasonErr(ctx, u, form)
		return
	}
	if !wasRestricted && requestedRestricted && restrictedReason == "" {
		ctx.Data["Err_RestrictedReason"] = true
		renderAdminUserEditReasonErr(ctx, u, form)
		return
	}

	if form.UserName != "" {
		if err := user_service.RenameUser(ctx, u, form.UserName, ctx.Doer); err != nil {
			switch {
			case user_model.IsErrUserIsNotLocal(err):
				ctx.Data["Err_UserName"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
			case user_model.IsErrUserAlreadyExist(err):
				ctx.Data["Err_UserName"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
			case db.IsErrNameReserved(err):
				ctx.Data["Err_UserName"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
			case db.IsErrNamePatternNotAllowed(err):
				ctx.Data["Err_UserName"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
			case db.IsErrNameCharsNotAllowed(err):
				ctx.Data["Err_UserName"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
			default:
				ctx.ServerError("RenameUser", err)
			}
			return
		}
	}

	authOpts := &user_service.UpdateAuthOptions{
		Password:  optional.FromNonDefault(form.Password),
		LoginName: optional.Some(form.LoginName),
	}

	// skip self Prohibit Login
	if ctx.Doer.ID == u.ID {
		authOpts.ProhibitLogin = optional.Some(false)
	} else {
		authOpts.ProhibitLogin = optional.Some(requestedProhibitLogin)
	}

	fields := strings.Split(form.LoginType, "-")
	if len(fields) == 2 {
		authSource, _ := strconv.ParseInt(fields[1], 10, 64)

		authOpts.LoginSource = optional.Some(authSource)
	}

	if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
		switch {
		case errors.Is(err, password.ErrMinLength):
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
		case errors.Is(err, password.ErrComplexity):
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
		case errors.Is(err, password.ErrIsPwned):
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form)
		case password.IsErrIsPwnedRequest(err):
			ctx.Data["Err_Password"] = true
			ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
		default:
			ctx.ServerError("UpdateUser", err)
		}
		return
	}

	if form.Email != "" {
		if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil {
			switch {
			case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
				ctx.Data["Err_Email"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
			case user_model.IsErrEmailAlreadyUsed(err):
				ctx.Data["Err_Email"] = true
				ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
			default:
				ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
			}
			return
		}
		if !user_model.IsEmailDomainAllowed(form.Email) {
			ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
		}
	}

	opts := &user_service.UpdateOptions{
		FullName:                optional.Some(form.FullName),
		Website:                 optional.Some(form.Website),
		Location:                optional.Some(form.Location),
		IsActive:                optional.Some(requestedActive),
		IsAdmin:                 user_service.UpdateOptionFieldFromValue(requestedAdmin),
		AllowGitHook:            optional.Some(form.AllowGitHook),
		AllowImportLocal:        optional.Some(form.AllowImportLocal),
		MaxRepoCreation:         optional.Some(form.MaxRepoCreation),
		AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
		IsRestricted:            optional.Some(requestedRestricted),
		Visibility:              optional.Some(form.Visibility),
		Language:                optional.Some(form.Language),
	}

	if err := user_service.UpdateUser(ctx, u, opts); err != nil {
		if user_model.IsErrDeleteLastAdminUser(err) {
			ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
		} else {
			ctx.ServerError("UpdateUser", err)
		}
		return
	}
	log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)

	if !wasProhibitLogin && u.ProhibitLogin {
		if ok := storeAdminProhibitLoginInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
		if err := user_model.SetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminProhibitLoginReason, prohibitLoginReason); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	if !wasActive && requestedActive {
		if ok := storeAdminActivationInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
	}

	if !wasAdmin && requestedAdmin {
		if ok := storeAdminGrantedInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
	}

	if wasAdmin && !requestedAdmin {
		if err := user_model.SetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminGrantedReason, adminReason); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	if !wasSuperAdmin && requestedSuperAdmin {
		if ok := storeSuperAdminGrantedInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
	}

	if wasSuperAdmin && !requestedSuperAdmin {
		if ok := storeSuperAdminRevokedInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
		if err := user_model.SetUserSetting(ctx, u.ID, user_model.SettingsKeySuperAdminRevokedReason, superAdminReason); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	if wasActive && !requestedActive {
		if ok := storeAdminDeactivationInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
		if err := user_model.SetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminDeactivatedReason, activeReason); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	if !wasRestricted && requestedRestricted {
		if ok := storeAdminRestrictedInfo(ctx, u.ID, ctx.Doer); !ok {
			return
		}
		if err := user_model.SetUserSetting(ctx, u.ID, user_model.SettingsKeyAdminRestrictedReason, restrictedReason); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	statusChanges := make([]mailer.AdminUserStatusChange, 0, 3)
	if wasActive && !requestedActive {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.deactivated"),
			Reason: activeReason,
		})
	} else if !wasActive && requestedActive {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.reactivated"),
		})
	}
	if !wasAdmin && requestedAdmin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.admin_granted"),
		})
	} else if wasAdmin && !requestedAdmin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.admin_revoked"),
			Reason: adminReason,
		})
	}
	if !wasSuperAdmin && requestedSuperAdmin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.super_admin_granted"),
		})
	} else if wasSuperAdmin && !requestedSuperAdmin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.super_admin_revoked"),
			Reason: superAdminReason,
		})
	}
	if !wasProhibitLogin && u.ProhibitLogin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.prohibit_login"),
			Reason: prohibitLoginReason,
		})
	} else if wasProhibitLogin && !u.ProhibitLogin {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.allow_login"),
		})
	}
	if !wasRestricted && requestedRestricted {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.restricted"),
			Reason: restrictedReason,
		})
	} else if wasRestricted && !requestedRestricted {
		statusChanges = append(statusChanges, mailer.AdminUserStatusChange{
			Action: ctx.Locale.TrString("admin.users.status_change.unrestricted"),
		})
	}
	mailer.SendAdminUserStatusChangedMail(ctx.Doer, u, statusChanges)

	if form.Reset2FA {
		tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
		if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
			ctx.ServerError("auth.GetTwoFactorByUID", err)
			return
		} else if tf != nil {
			if err := auth.DeleteTwoFactorByID(ctx, tf.ID, u.ID); err != nil {
				ctx.ServerError("auth.DeleteTwoFactorByID", err)
				return
			}
		}

		wn, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
		if err != nil {
			ctx.ServerError("auth.GetTwoFactorByUID", err)
			return
		}
		for _, cred := range wn {
			if _, err := auth.DeleteCredential(ctx, cred.ID, u.ID); err != nil {
				ctx.ServerError("auth.DeleteCredential", err)
				return
			}
		}
	}

	ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
	ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
}

// DeleteUser response for deleting a user
func DeleteUser(ctx *context.Context) {
	if !ensureSuperAdminBootstrap(ctx) {
		return
	}
	isInlineAction := ctx.FormBool("inline")
	redirectToUser := setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))
	redirectToUsers := setting.AppSubURL + "/-/admin/users"

	u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid"))
	if err != nil {
		ctx.ServerError("GetUserByID", err)
		return
	}

	if canDelete, msg := canDeleteUserFromAdminPanel(ctx, ctx.Doer, u); !canDelete {
		if isInlineAction {
			ctx.JSONError(msg)
		} else {
			ctx.Flash.Error(msg)
			ctx.Redirect(redirectToUser)
		}
		return
	}

	enableNewAccountRequestNotificationsFallback, err := user_model.ShouldEnableNewAccountRequestNotificationsFallback(ctx, ctx.Doer, u)
	if err != nil {
		ctx.ServerError("ShouldEnableNewAccountRequestNotificationsFallback", err)
		return
	}

	if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
		switch {
		case repo_model.IsErrUserOwnRepos(err):
			if isInlineAction {
				ctx.JSONError(ctx.Tr("admin.users.still_own_repo"))
			} else {
				ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
				ctx.Redirect(redirectToUser)
			}
		case org_model.IsErrUserHasOrgs(err):
			if isInlineAction {
				ctx.JSONError(ctx.Tr("admin.users.still_has_org"))
			} else {
				ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
				ctx.Redirect(redirectToUser)
			}
		case packages_model.IsErrUserOwnPackages(err):
			if isInlineAction {
				ctx.JSONError(ctx.Tr("admin.users.still_own_packages"))
			} else {
				ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
				ctx.Redirect(redirectToUser)
			}
		case user_model.IsErrDeleteLastAdminUser(err):
			if isInlineAction {
				ctx.JSONError(ctx.Tr("auth.last_admin"))
			} else {
				ctx.Flash.Error(ctx.Tr("auth.last_admin"))
				ctx.Redirect(redirectToUser)
			}
		default:
			ctx.ServerError("DeleteUser", err)
		}
		return
	}
	if enableNewAccountRequestNotificationsFallback {
		if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyEmailNotificationNewAccountRequests, user_model.SettingEmailNotificationNewAccountRequestsEnabled); err != nil {
			ctx.ServerError("SetUserSetting", err)
			return
		}
	}

	log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)

	ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
	if isInlineAction {
		ctx.JSONRedirect(redirectToUsers)
	} else {
		ctx.Redirect(redirectToUsers)
	}
}

// AvatarPost response for change user's avatar request
func AvatarPost(ctx *context.Context) {
	u := prepareUserInfo(ctx)
	if ctx.Written() {
		return
	}
	if denyBootstrapProtectedUserAccess(ctx, u) {
		return
	}
	if denySuperAdminUserEditAccess(ctx, u) {
		return
	}

	form := web.GetForm(ctx).(*forms.AvatarForm)
	if err := user_setting.UpdateAvatarSetting(ctx, form, u); err != nil {
		ctx.Flash.Error(err.Error())
	} else {
		ctx.Flash.Success(ctx.Tr("settings.update_user_avatar_success"))
	}

	ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}

// DeleteAvatar render delete avatar page
func DeleteAvatar(ctx *context.Context) {
	u := prepareUserInfo(ctx)
	if ctx.Written() {
		return
	}
	if denyBootstrapProtectedUserAccess(ctx, u) {
		return
	}
	if denySuperAdminUserEditAccess(ctx, u) {
		return
	}

	if err := user_service.DeleteAvatar(ctx, u); err != nil {
		ctx.Flash.Error(err.Error())
	}

	ctx.JSONRedirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}
