// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package user

import (
	"context"
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"
	"time"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/optional"

	"xorm.io/builder"
)

const (
	SettingsKeyAccountRequestStatus               = "account_request.status"
	SettingsKeyAccountRequestExpiresUnix          = "account_request.expires_unix"
	SettingsKeyAccountRequestSignupAttempts       = "account_request.signup_attempts"
	SettingsKeyAccountRequestEmailValidatedUnix   = "account_request.email_validated_unix"
	SettingsKeyAccountRequestReviewedUnix         = "account_request.reviewed_unix"
	SettingsKeyAccountRequestReviewedBy           = "account_request.reviewed_by"
	SettingsKeyAccountRequestValidationMailStatus = "account_request.validation_mail_status"
	SettingsKeyAccountRequestValidationMailUnix   = "account_request.validation_mail_unix"
	SettingsKeyAccountRequestValidationResendHits = "account_request.validation_resend_hits"
	SettingsKeyAccountRequestValidationCodeHash   = "account_request.validation_code_hash"
	SettingsKeyActivationMailStatus               = "activation.mail_status"
	SettingsKeyActivationMailUnix                 = "activation.mail_unix"
	SettingsKeyActivationResendHits               = "activation.resend_hits"
	SettingsKeyAdminInvitePending                 = "admin_invite.pending"
	SettingsKeyAdminInviteMailStatus              = "admin_invite.mail_status"
	SettingsKeyAdminInviteMailUnix                = "admin_invite.mail_unix"
	SettingsKeyAdminInviteResendHits              = "admin_invite.resend_hits"

	AccountRequestStatusPendingEmailValidation = "pending_email_validation"
	AccountRequestStatusPendingAdminReview     = "pending_admin_review"
	AccountRequestStatusRejected               = "rejected"
	AccountRequestStatusBlocked                = "blocked"

	AccountRequestValidationHours = 48
	AccountRequestMaxAttempts     = 5
	AccountRequestMaxResendHits   = 5
	ActivationMaxResendHits       = 5
	AdminInviteMaxResendHits      = 5

	AccountRequestValidationMailStatusSucceeded = "succeeded"
	AccountRequestValidationMailStatusFailed    = "failed"
)

func GetAccountRequestStatus(ctx context.Context, user *User) (string, error) {
	status, err := GetUserSetting(ctx, user.ID, SettingsKeyAccountRequestStatus)
	if err != nil || status != "" {
		return status, err
	}
	hasValidationState, err := HasAccountRequestValidationState(ctx, user)
	if err != nil || !hasValidationState {
		return "", err
	}
	return AccountRequestStatusPendingEmailValidation, nil
}

func SetAccountRequestStatus(ctx context.Context, userID int64, status string) error {
	if status == "" {
		return DeleteUserSetting(ctx, userID, SettingsKeyAccountRequestStatus)
	}
	return SetUserSetting(ctx, userID, SettingsKeyAccountRequestStatus, status)
}

func HasAccountRequestValidationState(ctx context.Context, user *User) (bool, error) {
	if user == nil || user.IsActive {
		return false, nil
	}
	for _, key := range []string{
		SettingsKeyAccountRequestStatus,
		SettingsKeyAccountRequestExpiresUnix,
		SettingsKeyAccountRequestValidationMailStatus,
		SettingsKeyAccountRequestValidationCodeHash,
	} {
		value, err := GetUserSetting(ctx, user.ID, key)
		if err != nil || value != "" {
			return value != "", err
		}
	}
	return false, nil
}

func getAccountRequestInt64(ctx context.Context, userID int64, key string) (int64, error) {
	value, err := GetUserSetting(ctx, userID, key)
	if err != nil || value == "" {
		return 0, err
	}
	return strconv.ParseInt(value, 10, 64)
}

func setAccountRequestInt64(ctx context.Context, userID int64, key string, value int64) error {
	if value == 0 {
		return DeleteUserSetting(ctx, userID, key)
	}
	return SetUserSetting(ctx, userID, key, strconv.FormatInt(value, 10))
}

func GetAccountRequestExpiry(ctx context.Context, user *User) (time.Time, error) {
	unix, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
	if err != nil || unix == 0 {
		return time.Time{}, err
	}
	return time.Unix(unix, 0), nil
}

func RefreshAccountRequestValidationWindow(ctx context.Context, user *User) error {
	return setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix, time.Now().Add(AccountRequestValidationHours*time.Hour).Unix())
}

func GetAccountRequestSignupAttempts(ctx context.Context, user *User) (int, error) {
	value, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestSignupAttempts)
	return int(value), err
}

func SetAccountRequestSignupAttempts(ctx context.Context, userID int64, attempts int) error {
	return setAccountRequestInt64(ctx, userID, SettingsKeyAccountRequestSignupAttempts, int64(attempts))
}

func GetAccountRequestValidationMailAttemptStatus(ctx context.Context, user *User) (string, error) {
	return GetUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationMailStatus)
}

func GetAccountRequestValidationMailAttemptUnix(ctx context.Context, user *User) (int64, error) {
	return getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestValidationMailUnix)
}

func GetAccountRequestValidationResendHits(ctx context.Context, user *User) (int, error) {
	value, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestValidationResendHits)
	return int(value), err
}

func SetAccountRequestValidationResendHits(ctx context.Context, userID int64, hits int) error {
	return setAccountRequestInt64(ctx, userID, SettingsKeyAccountRequestValidationResendHits, int64(hits))
}

func SetAccountRequestValidationCode(ctx context.Context, userID int64, code string) error {
	if code == "" {
		return DeleteUserSetting(ctx, userID, SettingsKeyAccountRequestValidationCodeHash)
	}
	return SetUserSetting(ctx, userID, SettingsKeyAccountRequestValidationCodeHash, base.EncodeSha256(code))
}

func IncrementAccountRequestValidationResendHits(ctx context.Context, user *User) (int, error) {
	hits, err := GetAccountRequestValidationResendHits(ctx, user)
	if err != nil {
		return 0, err
	}
	hits++
	if err := SetAccountRequestValidationResendHits(ctx, user.ID, hits); err != nil {
		return 0, err
	}
	return hits, nil
}

func RecordAccountRequestValidationMailAttempt(ctx context.Context, userID int64, success bool) error {
	status := AccountRequestValidationMailStatusFailed
	if success {
		status = AccountRequestValidationMailStatusSucceeded
	}
	if err := SetUserSetting(ctx, userID, SettingsKeyAccountRequestValidationMailStatus, status); err != nil {
		return err
	}
	return setAccountRequestInt64(ctx, userID, SettingsKeyAccountRequestValidationMailUnix, time.Now().Unix())
}

func GetActivationMailAttemptStatus(ctx context.Context, user *User) (string, error) {
	return GetUserSetting(ctx, user.ID, SettingsKeyActivationMailStatus)
}

func GetActivationMailAttemptUnix(ctx context.Context, user *User) (int64, error) {
	return getAccountRequestInt64(ctx, user.ID, SettingsKeyActivationMailUnix)
}

func GetActivationResendHits(ctx context.Context, user *User) (int, error) {
	value, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyActivationResendHits)
	return int(value), err
}

func SetActivationResendHits(ctx context.Context, userID int64, hits int) error {
	return setAccountRequestInt64(ctx, userID, SettingsKeyActivationResendHits, int64(hits))
}

func IncrementActivationResendHits(ctx context.Context, user *User) (int, error) {
	hits, err := GetActivationResendHits(ctx, user)
	if err != nil {
		return 0, err
	}
	hits++
	if err := SetActivationResendHits(ctx, user.ID, hits); err != nil {
		return 0, err
	}
	return hits, nil
}

func RecordActivationMailAttempt(ctx context.Context, userID int64, success bool) error {
	status := AccountRequestValidationMailStatusFailed
	if success {
		status = AccountRequestValidationMailStatusSucceeded
	}
	if err := SetUserSetting(ctx, userID, SettingsKeyActivationMailStatus, status); err != nil {
		return err
	}
	return setAccountRequestInt64(ctx, userID, SettingsKeyActivationMailUnix, time.Now().Unix())
}

func SetAdminInvitePending(ctx context.Context, userID int64, pending bool) error {
	if !pending {
		return DeleteUserSetting(ctx, userID, SettingsKeyAdminInvitePending)
	}
	return SetUserSetting(ctx, userID, SettingsKeyAdminInvitePending, "true")
}

func hasStoredAdminInviteCreator(ctx context.Context, userID int64) (bool, error) {
	for _, key := range []string{
		SettingsKeyAdminInviteCreatedBy,
		SettingsKeyAdminInviteCreatedByName,
		SettingsKeyAdminInviteCreatedByEmail,
	} {
		value, err := GetUserSetting(ctx, userID, key)
		if err == nil && value != "" {
			return true, nil
		}
		if err != nil && !IsErrUserSettingIsNotExist(err) {
			return false, err
		}
	}
	return false, nil
}

func IsPendingAdminInvite(ctx context.Context, user *User) (bool, error) {
	if user == nil || user.IsActive {
		return false, nil
	}

	pending, err := GetUserSetting(ctx, user.ID, SettingsKeyAdminInvitePending)
	if err == nil && pending == "true" {
		return true, nil
	}
	if err != nil && !IsErrUserSettingIsNotExist(err) {
		return false, err
	}
	if !user.ProhibitLogin {
		return false, nil
	}
	return hasStoredAdminInviteCreator(ctx, user.ID)
}

func GetAdminInviteMailAttemptStatus(ctx context.Context, user *User) (string, error) {
	return GetUserSetting(ctx, user.ID, SettingsKeyAdminInviteMailStatus)
}

func GetAdminInviteMailAttemptUnix(ctx context.Context, user *User) (int64, error) {
	return getAccountRequestInt64(ctx, user.ID, SettingsKeyAdminInviteMailUnix)
}

func GetAdminInviteResendHits(ctx context.Context, user *User) (int, error) {
	value, err := getAccountRequestInt64(ctx, user.ID, SettingsKeyAdminInviteResendHits)
	return int(value), err
}

func SetAdminInviteResendHits(ctx context.Context, userID int64, hits int) error {
	return setAccountRequestInt64(ctx, userID, SettingsKeyAdminInviteResendHits, int64(hits))
}

func IncrementAdminInviteResendHits(ctx context.Context, user *User) (int, error) {
	hits, err := GetAdminInviteResendHits(ctx, user)
	if err != nil {
		return 0, err
	}
	hits++
	if err := SetAdminInviteResendHits(ctx, user.ID, hits); err != nil {
		return 0, err
	}
	return hits, nil
}

func RecordAdminInviteMailAttempt(ctx context.Context, userID int64, success bool) error {
	status := AccountRequestValidationMailStatusFailed
	if success {
		status = AccountRequestValidationMailStatusSucceeded
	}
	if err := SetUserSetting(ctx, userID, SettingsKeyAdminInviteMailStatus, status); err != nil {
		return err
	}
	return setAccountRequestInt64(ctx, userID, SettingsKeyAdminInviteMailUnix, time.Now().Unix())
}

func ClearAdminInviteState(ctx context.Context, userID int64) error {
	for _, key := range []string{
		SettingsKeyAdminInvitePending,
		SettingsKeyAdminInviteMailStatus,
		SettingsKeyAdminInviteMailUnix,
		SettingsKeyAdminInviteResendHits,
	} {
		if err := DeleteUserSetting(ctx, userID, key); err != nil {
			return err
		}
	}
	return nil
}

func ClearActivationResendState(ctx context.Context, userID int64) error {
	for _, key := range []string{
		SettingsKeyActivationMailStatus,
		SettingsKeyActivationMailUnix,
		SettingsKeyActivationResendHits,
	} {
		if err := DeleteUserSetting(ctx, userID, key); err != nil {
			return err
		}
	}
	return nil
}

func IsAccountRequestExpired(ctx context.Context, user *User) (bool, error) {
	status, err := GetAccountRequestStatus(ctx, user)
	if err != nil || status != AccountRequestStatusPendingEmailValidation {
		return false, err
	}
	expiresAt, err := GetAccountRequestExpiry(ctx, user)
	if err != nil || expiresAt.IsZero() {
		return false, err
	}
	return time.Now().After(expiresAt), nil
}

func ActivateAccountRequestPrimaryEmail(ctx context.Context, user *User) error {
	return db.WithTx(ctx, func(ctx context.Context) error {
		addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "lower_email": strings.ToLower(user.Email)})
		if err != nil {
			return err
		}
		if !exist {
			return fmt.Errorf("no such email: %d (%s)", user.ID, user.Email)
		}
		if addr.IsActivated {
			return nil
		}
		return updateActivation(ctx, addr, true)
	})
}

func ResetAccountRequestToPendingEmailValidation(ctx context.Context, user *User) error {
	if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusPendingEmailValidation); err != nil {
		return err
	}
	if err := RefreshAccountRequestValidationWindow(ctx, user); err != nil {
		return err
	}
	if err := SetAccountRequestSignupAttempts(ctx, user.ID, 1); err != nil {
		return err
	}
	if err := SetAccountRequestValidationResendHits(ctx, user.ID, 0); err != nil {
		return err
	}
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestEmailValidatedUnix)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestReviewedBy)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationCodeHash)
	user.ProhibitLogin = false
	_, err := db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
	return err
}

func IncrementAccountRequestSignupAttempts(ctx context.Context, user *User) (attempts int, blocked bool, _ error) {
	attempts, err := GetAccountRequestSignupAttempts(ctx, user)
	if err != nil {
		return 0, false, err
	}
	attempts++
	if err := SetAccountRequestSignupAttempts(ctx, user.ID, attempts); err != nil {
		return 0, false, err
	}
	if attempts < AccountRequestMaxAttempts {
		return attempts, false, nil
	}
	if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusBlocked); err != nil {
		return 0, false, err
	}
	user.ProhibitLogin = true
	_, err = db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
	return attempts, true, err
}

func MarkAccountRequestPendingAdminReview(ctx context.Context, user *User) error {
	if err := ActivateAccountRequestPrimaryEmail(ctx, user); err != nil {
		return err
	}
	if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusPendingAdminReview); err != nil {
		return err
	}
	if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestEmailValidatedUnix, time.Now().Unix()); err != nil {
		return err
	}
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationResendHits)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationCodeHash)
	return nil
}

func RejectAccountRequest(ctx context.Context, user *User, reviewerID int64) error {
	if err := SetAccountRequestStatus(ctx, user.ID, AccountRequestStatusRejected); err != nil {
		return err
	}
	if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix, time.Now().Unix()); err != nil {
		return err
	}
	if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedBy, reviewerID); err != nil {
		return err
	}
	user.ProhibitLogin = true
	_, err := db.GetEngine(ctx).ID(user.ID).Cols("prohibit_login").Update(user)
	return err
}

func ApproveAccountRequest(ctx context.Context, user *User, reviewerID int64) error {
	if err := ActivateAccountRequestPrimaryEmail(ctx, user); err != nil {
		return err
	}
	var err error
	user.IsActive = true
	user.ProhibitLogin = false
	if user.Rands, err = GetUserSalt(); err != nil {
		return err
	}
	if err := UpdateUserCols(ctx, user, "is_active", "prohibit_login", "rands"); err != nil {
		return err
	}
	if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedUnix, time.Now().Unix()); err != nil {
		return err
	}
	if err := setAccountRequestInt64(ctx, user.ID, SettingsKeyAccountRequestReviewedBy, reviewerID); err != nil {
		return err
	}
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestStatus)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestExpiresUnix)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestSignupAttempts)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationResendHits)
	_ = DeleteUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationCodeHash)
	return nil
}

func UnblockAccountRequest(ctx context.Context, user *User) error {
	return ResetAccountRequestToPendingEmailValidation(ctx, user)
}

func GenerateAccountRequestValidationCode(user *User) string {
	data := makeTimeLimitCodeHashData(&TimeLimitCodeOptions{Purpose: TimeLimitCodeAccountRequest}, user)
	code := base.CreateTimeLimitCode(data, AccountRequestValidationHours*60, time.Now(), nil)
	code += hex.EncodeToString([]byte(user.LowerName))
	return code
}

func VerifyAccountRequestValidationCode(ctx context.Context, code string) (user *User) {
	if user = GetVerifyUser(ctx, code); user != nil {
		prefix := code[:base.TimeLimitCodeLength]
		hasValidationState, err := HasAccountRequestValidationState(ctx, user)
		if err != nil || !hasValidationState {
			return nil
		}
		for _, purpose := range []TimeLimitCodePurpose{TimeLimitCodeAccountRequest, TimeLimitCodeActivateAccount} {
			data := makeTimeLimitCodeHashData(&TimeLimitCodeOptions{Purpose: purpose}, user)
			if base.VerifyTimeLimitCode(time.Now(), data, AccountRequestValidationHours*60, prefix) {
				return user
			}
		}
		storedHash, err := GetUserSetting(ctx, user.ID, SettingsKeyAccountRequestValidationCodeHash)
		if err == nil && storedHash != "" && storedHash == base.EncodeSha256(code) {
			return user
		}
	}
	return nil
}

func GetUserByAnyEmail(ctx context.Context, email string) (*User, error) {
	email = strings.TrimSpace(email)
	if err := validateEmailBasic(email); err != nil {
		return nil, err
	}
	addr := &EmailAddress{LowerEmail: strings.ToLower(email)}
	has, err := db.GetEngine(ctx).Get(addr)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, ErrUserNotExist{Name: email}
	}
	return GetUserByID(ctx, addr.UID)
}

func DeleteExpiredPendingAccountRequests(ctx context.Context) error {
	users, _, err := SearchUsers(ctx, SearchUserOptions{
		Types:       []UserType{UserTypeIndividual, UserTypeUserReserved},
		IsActive:    optional.Some(false),
		ListOptions: db.ListOptionsAll,
	})
	if err != nil {
		return err
	}
	for _, user := range users {
		status, err := GetAccountRequestStatus(ctx, user)
		if err != nil || status != AccountRequestStatusPendingEmailValidation {
			if err != nil {
				return err
			}
			continue
		}
		expired, err := IsAccountRequestExpired(ctx, user)
		if err != nil {
			return err
		}
		if !expired {
			continue
		}
		if _, err := db.GetEngine(ctx).Where("uid=?", user.ID).Delete(&EmailAddress{}); err != nil {
			return err
		}
		if _, err := db.GetEngine(ctx).Where("uid=?", user.ID).Delete(&Setting{}); err != nil {
			return err
		}
		if _, err := db.GetEngine(ctx).ID(user.ID).Delete(&User{}); err != nil {
			return err
		}
	}
	return nil
}
