package identity

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"

	"qycw/go-server/internal/auth"
	"qycw/go-server/internal/config"
	qmiddleware "qycw/go-server/internal/middleware"
	"qycw/go-server/internal/plugins"
	redisstore "qycw/go-server/internal/redis"
)

type Service struct {
	db          *gorm.DB
	redis       *redisstore.Client
	cfg         config.Bootstrap
	tokens      *auth.TokenMaker
	smsProvider SMSProvider
	plugins     *plugins.Service
	logger      *slog.Logger
}

func (s *Service) EncryptSensitiveOptional(value string) (*string, error) {
	return EncryptOptional(value, s.cfg.JWTSecretKey)
}

func (s *Service) DecryptSensitiveOptional(value *string) *string {
	return DecryptOptional(value, s.cfg.JWTSecretKey)
}

func (s *Service) SensitiveHash(value string) *string {
	return DeterministicHash(value, s.cfg.JWTSecretKey)
}

func NewService(db *gorm.DB, redis *redisstore.Client, cfg config.Bootstrap, logger *slog.Logger, pluginSvc ...*plugins.Service) *Service {
	var pluginsService *plugins.Service
	if len(pluginSvc) > 0 {
		pluginsService = pluginSvc[0]
	}
	smsProvider := SMSProvider(NewLoggingSMSProvider(logger))
	if db != nil {
		smsProvider = NewDBSMSProvider(db, logger, pluginsService)
	}
	return &Service{
		db:          db,
		redis:       redis,
		cfg:         cfg,
		tokens:      auth.NewTokenMaker(cfg.JWTSecretKey, cfg.AccessTokenExpireMinutes, cfg.RefreshTokenExpireDays),
		smsProvider: smsProvider,
		plugins:     pluginsService,
		logger:      logger,
	}
}

func (s *Service) Authenticate(ctx context.Context, rawAccessToken string) (qmiddleware.Principal, error) {
	user, claims, err := s.currentUserWithClaims(ctx, rawAccessToken)
	if err != nil {
		return qmiddleware.Principal{}, err
	}
	return qmiddleware.Principal{ID: user.ID, Role: user.Role, User: user, ImpersonatorID: claims.ImpersonatorID}, nil
}

func (s *Service) SendRegisterCode(ctx context.Context, req SendCodeRequest) (int, error) {
	if err := s.requireDB(); err != nil {
		return 0, err
	}
	email := normalizeEmail(req.Email)
	if !validateEmail(email) {
		return 0, BadRequest("邮箱格式不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return 0, err
	}
	if enabled, err := s.registrationEnabled(ctx); err != nil {
		return 0, err
	} else if !enabled {
		return 0, Forbidden("当前暂停新用户注册")
	}

	var count int64
	if err := s.db.WithContext(ctx).Model(&User{}).Where("email = ?", email).Count(&count).Error; err != nil {
		return 0, err
	}
	if count > 0 {
		return 0, BadRequest("该邮箱已被注册")
	}
	return s.saveAndSendEmailCode(ctx, "email_register", email, "【清云互联】注册验证码", "您正在注册清云互联账号")
}

func (s *Service) SendLoginCode(ctx context.Context, req SendCodeRequest) (int, error) {
	if err := s.requireDB(); err != nil {
		return 0, err
	}
	email := normalizeEmail(req.Email)
	if !validateEmail(email) {
		return 0, BadRequest("邮箱格式不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return 0, err
	}

	user, err := s.findUserByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, BadRequest("该邮箱尚未注册")
		}
		return 0, err
	}
	if user.Status == "banned" {
		return 0, Forbidden("账户已被封禁")
	}
	return s.saveAndSendEmailCode(ctx, "email_login", email, "【清云互联】登录验证码", "您正在登录清云互联账号")
}

func (s *Service) SendRegisterSMSCode(ctx context.Context, req SendSMSRequest) (int, error) {
	if err := s.requireDB(); err != nil {
		return 0, err
	}
	if !validatePhone(req.Phone) {
		return 0, BadRequest("手机号格式不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return 0, err
	}
	if enabled, err := s.registrationEnabled(ctx); err != nil {
		return 0, err
	} else if !enabled {
		return 0, Forbidden("当前暂停新用户注册")
	}

	var count int64
	if err := s.db.WithContext(ctx).Model(&User{}).Where("phone = ?", req.Phone).Count(&count).Error; err != nil {
		return 0, err
	}
	if count > 0 {
		return 0, BadRequest("该手机号已被注册")
	}
	return s.saveAndSendSMSCode(ctx, "sms_register", req.Phone, "register")
}

func (s *Service) SendLoginSMSCode(ctx context.Context, req SendSMSRequest) (int, error) {
	if err := s.requireDB(); err != nil {
		return 0, err
	}
	if !validatePhone(req.Phone) {
		return 0, BadRequest("手机号格式不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return 0, err
	}

	user, err := s.findUserByPhone(ctx, req.Phone)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, BadRequest("该手机号尚未注册")
		}
		return 0, err
	}
	if user.Status == "banned" {
		return 0, Forbidden("账户已被封禁")
	}
	return s.saveAndSendSMSCode(ctx, "sms_login", req.Phone, "login")
}

func (s *Service) Register(ctx context.Context, req RegisterRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	email := normalizeEmail(req.Email)
	if !validateEmail(email) || !validateUsername(req.Username) || !validateCode(req.Code) {
		return nil, BadRequest("请求参数不正确")
	}
	if req.Password != nil && !validatePassword(*req.Password) {
		return nil, BadRequest("密码至少 8 位，最大 128 位")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return nil, err
	}
	if enabled, err := s.registrationEnabled(ctx); err != nil {
		return nil, err
	} else if !enabled {
		return nil, Forbidden("当前暂停新用户注册")
	}

	ok, err := s.verifyCode(ctx, "email_register", email, req.Code)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, BadRequest("验证码错误或已失效")
	}
	return s.createUserWithTokens(ctx, req.Username, &email, nil, req.Password, true, false, req.InviteCode)
}

func (s *Service) RegisterBySMS(ctx context.Context, req RegisterBySMSRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if !validatePhone(req.Phone) || !validateUsername(req.Username) || !validateCode(req.Code) {
		return nil, BadRequest("请求参数不正确")
	}
	if req.Password != nil && !validatePassword(*req.Password) {
		return nil, BadRequest("密码至少 8 位，最大 128 位")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return nil, err
	}
	if enabled, err := s.registrationEnabled(ctx); err != nil {
		return nil, err
	} else if !enabled {
		return nil, Forbidden("当前暂停新用户注册")
	}

	ok, err := s.verifyCode(ctx, "sms_register", req.Phone, req.Code)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, BadRequest("验证码错误或已失效")
	}
	return s.createUserWithTokens(ctx, req.Username, nil, &req.Phone, req.Password, false, true, req.InviteCode)
}

func (s *Service) Login(ctx context.Context, req LoginRequest, adminOnly bool) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return nil, err
	}

	user, err := s.findUserByIdentifier(ctx, req.Email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, Unauthorized("账号或密码错误")
		}
		return nil, err
	}
	if user.HashedPassword == nil || !auth.VerifyPassword(req.Password, *user.HashedPassword) {
		return nil, Unauthorized("账号或密码错误")
	}
	if user.Status == "banned" {
		return nil, Forbidden("账户已被封禁")
	}
	if adminOnly && user.Role != "admin" && user.Role != "super_admin" {
		return nil, Forbidden("该账号无后台访问权限")
	}
	return s.loginResponse(ctx, user)
}

func (s *Service) LoginByCode(ctx context.Context, req LoginByCodeRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	email := normalizeEmail(req.Email)
	if !validateEmail(email) || !validateCode(req.Code) {
		return nil, BadRequest("请求参数不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return nil, err
	}

	user, err := s.findUserByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, BadRequest("该邮箱尚未注册")
		}
		return nil, err
	}
	if user.Status == "banned" {
		return nil, Forbidden("账户已被封禁")
	}

	ok, err := s.verifyCode(ctx, "email_login", email, req.Code)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, BadRequest("验证码错误或已失效")
	}
	return s.loginResponse(ctx, user)
}

func (s *Service) LoginBySMS(ctx context.Context, req LoginBySMSRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if !validatePhone(req.Phone) || !validateCode(req.Code) {
		return nil, BadRequest("请求参数不正确")
	}
	if err := verifyCaptcha(ctx, s.cfg, s.logger, req.Captcha); err != nil {
		return nil, err
	}

	user, err := s.findUserByPhone(ctx, req.Phone)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, BadRequest("该手机号尚未注册")
		}
		return nil, err
	}
	if user.Status == "banned" {
		return nil, Forbidden("账户已被封禁")
	}

	ok, err := s.verifyCode(ctx, "sms_login", req.Phone, req.Code)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, BadRequest("验证码错误或已失效")
	}
	return s.loginResponse(ctx, user)
}

func (s *Service) Refresh(ctx context.Context, raw string) (auth.TokenPair, error) {
	claims, err := s.tokens.Parse(raw)
	if err != nil {
		return auth.TokenPair{}, Unauthorized("Invalid refresh token")
	}
	if claims.Type != "refresh" {
		return auth.TokenPair{}, Unauthorized("Invalid token type")
	}
	userID, err := strconv.ParseInt(claims.Sub, 10, 64)
	if err != nil || claims.JTI == "" {
		return auth.TokenPair{}, Unauthorized("Invalid token payload")
	}

	exists, err := s.existsRefresh(ctx, userID, claims.JTI)
	if err != nil {
		return auth.TokenPair{}, err
	}
	if !exists {
		return auth.TokenPair{}, Unauthorized("Refresh token revoked")
	}

	user, err := s.findUserByID(ctx, userID)
	if err != nil {
		return auth.TokenPair{}, Unauthorized("User not found")
	}
	if user.Status != "active" {
		return auth.TokenPair{}, Forbidden("账户当前不可登录")
	}
	if err := s.revokeRefresh(ctx, userID, claims.JTI); err != nil {
		return auth.TokenPair{}, err
	}
	return s.createTokens(ctx, user)
}

func (s *Service) Logout(ctx context.Context, raw string) error {
	claims, err := s.tokens.Parse(raw)
	if err != nil {
		return nil
	}
	userID, err := strconv.ParseInt(claims.Sub, 10, 64)
	if err != nil || claims.JTI == "" {
		return nil
	}
	return s.revokeRefresh(ctx, userID, claims.JTI)
}

func (s *Service) CurrentUser(ctx context.Context, rawAccessToken string) (User, error) {
	user, _, err := s.currentUserWithClaims(ctx, rawAccessToken)
	return user, err
}

func (s *Service) currentUserWithClaims(ctx context.Context, rawAccessToken string) (User, *auth.Claims, error) {
	claims, err := s.tokens.Parse(rawAccessToken)
	if err != nil || claims.Type != "access" {
		return User{}, nil, Unauthorized("Could not validate credentials")
	}
	userID, err := strconv.ParseInt(claims.Sub, 10, 64)
	if err != nil {
		return User{}, nil, Unauthorized("Could not validate credentials")
	}
	user, err := s.findUserByID(ctx, userID)
	if err != nil {
		return User{}, nil, Unauthorized("Could not validate credentials")
	}
	if user.Status != "active" {
		return User{}, nil, Forbidden("账户当前不可访问")
	}
	return user, claims, nil
}

func (s *Service) UserResponse(user User) UserResponse {
	return NewUserResponse(user, s.cfg.JWTSecretKey)
}

func (s *Service) UpdateMe(ctx context.Context, user User, req UserUpdateRequest) (User, error) {
	updates := map[string]any{}
	if req.Nickname != nil {
		updates["nickname"] = *req.Nickname
	}
	if req.Avatar != nil {
		updates["avatar"] = *req.Avatar
	}
	if len(updates) == 0 {
		return user, nil
	}
	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil {
		return User{}, err
	}
	return s.findUserByID(ctx, user.ID)
}

func (s *Service) ChangePassword(ctx context.Context, user User, req PasswordChangeRequest) (User, error) {
	if !validatePassword(req.NewPassword) {
		return User{}, BadRequest("密码至少 8 位，最大 128 位")
	}
	if user.HashedPassword != nil {
		if req.OldPassword == nil || *req.OldPassword == "" {
			return User{}, BadRequest("请输入当前密码")
		}
		if !auth.VerifyPassword(*req.OldPassword, *user.HashedPassword) {
			return User{}, BadRequest("当前密码不正确")
		}
	}

	hashed, err := auth.HashPassword(req.NewPassword)
	if err != nil {
		return User{}, err
	}
	if err := s.RevokeAllRefresh(ctx, user.ID); err != nil {
		return User{}, err
	}
	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", user.ID).Update("hashed_password", hashed).Error; err != nil {
		return User{}, err
	}
	return s.findUserByID(ctx, user.ID)
}

func (s *Service) SendBindEmailCode(ctx context.Context, user User, req SendCodeRequest) (int, error) {
	email := normalizeEmail(req.Email)
	if !validateEmail(email) {
		return 0, BadRequest("邮箱格式不正确")
	}
	if user.Email != nil && strings.EqualFold(*user.Email, email) {
		return 0, BadRequest("新邮箱与当前邮箱相同")
	}

	var count int64
	if err := s.db.WithContext(ctx).Model(&User{}).Where("email = ? AND id <> ?", email, user.ID).Count(&count).Error; err != nil {
		return 0, err
	}
	if count > 0 {
		return 0, BadRequest("该邮箱已被其他账号占用")
	}
	return s.saveAndSendEmailCode(ctx, "email_bind", email, "【清云互联】邮箱绑定验证码", "您正在将该邮箱绑定到清云互联账号")
}

func (s *Service) SendBindSMSCode(ctx context.Context, user User, req SendSMSRequest) (int, error) {
	if !validatePhone(req.Phone) {
		return 0, BadRequest("手机号格式不正确")
	}
	if user.Phone != nil && *user.Phone == req.Phone {
		return 0, BadRequest("新手机号与当前手机号相同")
	}

	var count int64
	if err := s.db.WithContext(ctx).Model(&User{}).Where("phone = ? AND id <> ?", req.Phone, user.ID).Count(&count).Error; err != nil {
		return 0, err
	}
	if count > 0 {
		return 0, BadRequest("该手机号已被其他账号占用")
	}
	return s.saveAndSendSMSCode(ctx, "sms_bind", req.Phone, "bind")
}

func (s *Service) BindEmail(ctx context.Context, user User, req BindEmailRequest) (User, error) {
	email := normalizeEmail(req.Email)
	if !validateEmail(email) || !validateCode(req.Code) {
		return User{}, BadRequest("请求参数不正确")
	}

	ok, err := s.verifyCode(ctx, "email_bind", email, req.Code)
	if err != nil {
		return User{}, err
	}
	if !ok {
		return User{}, BadRequest("验证码错误或已失效")
	}

	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", user.ID).Updates(map[string]any{
		"email":             email,
		"is_email_verified": true,
	}).Error; err != nil {
		return User{}, err
	}
	return s.findUserByID(ctx, user.ID)
}

func (s *Service) BindPhone(ctx context.Context, user User, req BindPhoneRequest) (User, error) {
	if !validatePhone(req.Phone) || !validateCode(req.Code) {
		return User{}, BadRequest("请求参数不正确")
	}

	ok, err := s.verifyCode(ctx, "sms_bind", req.Phone, req.Code)
	if err != nil {
		return User{}, err
	}
	if !ok {
		return User{}, BadRequest("验证码错误或已失效")
	}

	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", user.ID).Updates(map[string]any{
		"phone":             req.Phone,
		"is_phone_verified": true,
	}).Error; err != nil {
		return User{}, err
	}
	return s.findUserByID(ctx, user.ID)
}

func (s *Service) requireDB() error {
	if s.db == nil {
		return ServiceUnavailable(msgDatabaseDisabled)
	}
	return nil
}

func (s *Service) createUserWithTokens(ctx context.Context, username string, email *string, phone *string, password *string, emailVerified bool, phoneVerified bool, inviteCode *string) (map[string]any, error) {
	hashed := (*string)(nil)
	if password != nil && *password != "" {
		value, err := auth.HashPassword(*password)
		if err != nil {
			return nil, err
		}
		hashed = &value
	}

	user := User{
		Email:           email,
		Username:        username,
		Phone:           phone,
		HashedPassword:  hashed,
		Role:            "user",
		Status:          "active",
		IDCardVerified:  "unverified",
		IsEmailVerified: emailVerified,
		IsPhoneVerified: phoneVerified,
	}
	if err := s.db.WithContext(ctx).Create(&user).Error; err != nil {
		if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
			return nil, BadRequest("邮箱、手机号或用户名已被注册")
		}
		return nil, err
	}
	if err := s.bindInviteCode(ctx, &user, inviteCode); err != nil {
		return nil, err
	}

	tokens, err := s.createTokens(ctx, user)
	if err != nil {
		return nil, err
	}
	if email != nil && strings.TrimSpace(*email) != "" {
		if err := s.sendWelcomeEmail(ctx, user, password != nil && strings.TrimSpace(*password) != ""); err != nil && s.logger != nil {
			s.logger.Warn("welcome email skipped", "user_id", user.ID, "email", *email, "error", err)
		}
	}
	return map[string]any{"user": s.UserResponse(user), "tokens": tokens}, nil
}

func (s *Service) loginResponse(ctx context.Context, user User) (map[string]any, error) {
	if user.Status != "active" {
		return nil, Forbidden("账户当前不可登录")
	}
	now := time.Now().UTC()
	if err := s.db.WithContext(ctx).Model(&User{}).Where("id = ?", user.ID).Update("last_login_at", now).Error; err != nil {
		return nil, err
	}
	user.LastLoginAt = &now

	tokens, err := s.createTokens(ctx, user)
	if err != nil {
		return nil, err
	}
	return map[string]any{"user": s.UserResponse(user), "tokens": tokens}, nil
}

func (s *Service) ImpersonateUser(ctx context.Context, operatorID int64, userID int64, reason string, ipAddress string, userAgent string) (map[string]any, error) {
	reason = strings.TrimSpace(reason)
	if reason == "" {
		return nil, BadRequest("impersonation reason is required")
	}
	if len(reason) > 500 {
		return nil, BadRequest("impersonation reason is too long")
	}
	var operator User
	if err := s.db.WithContext(ctx).Where("id = ?", operatorID).First(&operator).Error; err != nil {
		return nil, err
	}
	if operator.Role != "super_admin" || operator.Status != "active" {
		return nil, Forbidden("only active super administrators can impersonate users")
	}
	var user User
	if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
		return nil, err
	}
	if user.Status != "active" {
		return nil, BadRequest("only active users can be impersonated")
	}
	if user.Role != "user" {
		return nil, BadRequest("administrator accounts cannot be impersonated")
	}
	accessToken, tokenJTI, expiresAt, err := s.tokens.CreateImpersonation(fmt.Sprint(user.ID), user.Role, operator.ID, 10*time.Minute)
	if err != nil {
		return nil, err
	}
	audit := AdminImpersonationAudit{
		OperatorID:     operator.ID,
		TargetUserID:   user.ID,
		Reason:         reason,
		IPAddress:      optionalTrimmedString(ipAddress),
		UserAgent:      optionalTrimmedString(userAgent),
		TokenJTI:       tokenJTI,
		TokenExpiresAt: expiresAt,
	}
	if err := s.db.WithContext(ctx).Create(&audit).Error; err != nil {
		return nil, err
	}
	return map[string]any{
		"access_token": accessToken,
		"token_type":   "bearer",
		"expires_at":   expiresAt,
		"user_id":      user.ID,
		"username":     user.Username,
		"user":         s.UserResponse(user),
		"audit_id":     audit.ID,
		"read_only":    true,
	}, nil
}

func optionalTrimmedString(value string) *string {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	return &value
}

func (s *Service) createTokens(ctx context.Context, user User) (auth.TokenPair, error) {
	access, _, err := s.tokens.Create(fmt.Sprint(user.ID), user.Role, "access")
	if err != nil {
		return auth.TokenPair{}, err
	}
	refresh, refreshJTI, err := s.tokens.Create(fmt.Sprint(user.ID), user.Role, "refresh")
	if err != nil {
		return auth.TokenPair{}, err
	}
	if err := s.addRefresh(ctx, user.ID, refreshJTI); err != nil {
		return auth.TokenPair{}, err
	}
	return auth.TokenPair{AccessToken: access, RefreshToken: refresh, TokenType: "bearer"}, nil
}

func (s *Service) findUserByIdentifier(ctx context.Context, identifier string) (User, error) {
	identifier = strings.TrimSpace(identifier)
	var user User
	err := s.db.WithContext(ctx).
		Where("email = ? OR username = ? OR phone = ?", normalizeEmail(identifier), identifier, identifier).
		First(&user).Error
	return user, err
}

func (s *Service) findUserByEmail(ctx context.Context, email string) (User, error) {
	var user User
	err := s.db.WithContext(ctx).Where("email = ?", normalizeEmail(email)).First(&user).Error
	return user, err
}

func (s *Service) findUserByPhone(ctx context.Context, phone string) (User, error) {
	var user User
	err := s.db.WithContext(ctx).Where("phone = ?", strings.TrimSpace(phone)).First(&user).Error
	return user, err
}

func (s *Service) findUserByID(ctx context.Context, userID int64) (User, error) {
	var user User
	err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
	return user, err
}

func (s *Service) registrationEnabled(ctx context.Context) (bool, error) {
	var item SystemSetting
	err := s.db.WithContext(ctx).Where("`key` = ?", "platform.registration_enabled").First(&item).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return true, nil
	}
	if err != nil {
		return false, err
	}
	return item.Value.Bool(true), nil
}

func (s *Service) saveAndSendEmailCode(ctx context.Context, purpose string, email string, subject string, actionText string) (int, error) {
	remaining, err := s.canSend(ctx, purpose, email)
	if err != nil {
		return 0, err
	}
	if remaining > 0 {
		return 0, TooManyRequests(fmt.Sprintf("请 %d 秒后重试", remaining))
	}

	code, err := generateCode()
	if err != nil {
		return 0, err
	}
	if err := s.saveCode(ctx, purpose, email, code); err != nil {
		return 0, err
	}
	if err := sendEmail(ctx, s.db, s.cfg, s.plugins, email, subject, verificationEmailBody(actionText, code)); err != nil {
		return 0, err
	}
	return int(sendCooldown.Seconds()), nil
}

func (s *Service) saveAndSendSMSCode(ctx context.Context, purpose string, phone string, smsPurpose string) (int, error) {
	remaining, err := s.canSend(ctx, purpose, phone)
	if err != nil {
		return 0, err
	}
	if remaining > 0 {
		return 0, TooManyRequests(fmt.Sprintf("请 %d 秒后重试", remaining))
	}

	code, err := generateCode()
	if err != nil {
		return 0, err
	}
	if err := s.saveCode(ctx, purpose, phone, code); err != nil {
		return 0, err
	}
	if err := s.smsProvider.SendVerificationCode(ctx, phone, code, smsPurpose); err != nil {
		return 0, err
	}
	return int(sendCooldown.Seconds()), nil
}

func verificationEmailBody(actionText string, code string) string {
	return fmt.Sprintf(
		"您好：\n\n%s。本次验证码为：%s\n验证码 10 分钟内有效，请勿向他人泄露。\n\n如非本人操作，请忽略此邮件。\n\n-- 清云互联团队",
		actionText,
		code,
	)
}
