package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	"github.com/user-management-system/internal/auth"
	"github.com/user-management-system/internal/cache"
	"github.com/user-management-system/internal/domain"
	"github.com/user-management-system/internal/repository"
	"github.com/user-management-system/internal/security"
)

const (
	userInfoCachePrefix   = "auth_user_info:"
	tokenBlacklistPrefix  = "auth_token_blacklist:"
	defaultUserCacheTTL   = 15 * time.Minute
	defaultBlacklistTTL   = time.Hour
	defaultPasswordMinLen = 8
)

type userRepositoryInterface interface {
	Create(ctx context.Context, user *domain.User) error
	Update(ctx context.Context, user *domain.User) error
	UpdateTOTP(ctx context.Context, user *domain.User) error
	Delete(ctx context.Context, id int64) error
	GetByID(ctx context.Context, id int64) (*domain.User, error)
	GetByUsername(ctx context.Context, username string) (*domain.User, error)
	GetByEmail(ctx context.Context, email string) (*domain.User, error)
	GetByPhone(ctx context.Context, phone string) (*domain.User, error)
	List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error)
	ListByStatus(ctx context.Context, status domain.UserStatus, offset, limit int) ([]*domain.User, int64, error)
	UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error
	UpdateLastLogin(ctx context.Context, id int64, ip string) error
	ExistsByUsername(ctx context.Context, username string) (bool, error)
	ExistsByEmail(ctx context.Context, email string) (bool, error)
	ExistsByPhone(ctx context.Context, phone string) (bool, error)
	Search(ctx context.Context, keyword string, offset, limit int) ([]*domain.User, int64, error)
}

type userRoleRepositoryInterface interface {
	BatchCreate(ctx context.Context, userRoles []*domain.UserRole) error
	GetUserIDByRoleID(ctx context.Context, roleID int64) ([]int64, error)
}

type roleRepositoryInterface interface {
	GetDefaultRoles(ctx context.Context) ([]*domain.Role, error)
	GetByCode(ctx context.Context, code string) (*domain.Role, error)
}

type loginLogRepositoryInterface interface {
	Create(ctx context.Context, loginRecord *domain.LoginLog) error
}

type anomalyRecorder interface {
	RecordLogin(ctx context.Context, userID int64, ip, location, deviceFingerprint string, success bool) []security.AnomalyEvent
}

type PasswordStrengthInfo struct {
	Score      int  `json:"score"`
	Length     int  `json:"length"`
	HasUpper   bool `json:"has_upper"`
	HasLower   bool `json:"has_lower"`
	HasDigit   bool `json:"has_digit"`
	HasSpecial bool `json:"has_special"`
}

type RegisterRequest struct {
	Username  string `json:"username" binding:"required"`
	Email     string `json:"email"`
	Phone     string `json:"phone"`
	PhoneCode string `json:"phone_code"`
	Password  string `json:"password" binding:"required"`
	Nickname  string `json:"nickname"`
}

type LoginRequest struct {
	Account       string `json:"account"`
	Username      string `json:"username"`
	Email         string `json:"email"`
	Phone         string `json:"phone"`
	Password      string `json:"password"`
	Remember      bool   `json:"remember"` // 记住登录
	DeviceID      string `json:"device_id,omitempty"`       // 设备唯一标识
	DeviceName    string `json:"device_name,omitempty"`    // 设备名称
	DeviceBrowser string `json:"device_browser,omitempty"` // 浏览器
	DeviceOS      string `json:"device_os,omitempty"`      // 操作系统
}

func (r *LoginRequest) GetAccount() string {
	if r == nil {
		return ""
	}
	for _, candidate := range []string{r.Account, r.Username, r.Email, r.Phone} {
		if trimmed := strings.TrimSpace(candidate); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

type UserInfo struct {
	ID       int64             `json:"id"`
	Username string            `json:"username"`
	Email    string            `json:"email,omitempty"`
	Phone    string            `json:"phone,omitempty"`
	Nickname string            `json:"nickname,omitempty"`
	Avatar   string            `json:"avatar,omitempty"`
	Status   domain.UserStatus `json:"status"`
}

type LoginResponse struct {
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token"`
	ExpiresIn    int64     `json:"expires_in"`
	User         *UserInfo `json:"user"`
}

type LogoutRequest struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

type AuthService struct {
	userRepo          userRepositoryInterface
	socialRepo        repository.SocialAccountRepository
	jwtManager        *auth.JWT
	cache             *cache.CacheManager
	passwordMinLength int
	maxLoginAttempts  int
	loginLockDuration time.Duration

	userRoleRepo userRoleRepositoryInterface
	roleRepo     roleRepositoryInterface
	loginLogRepo loginLogRepositoryInterface

	webhookSvc         *WebhookService
	passwordPolicy     security.PasswordPolicy
	passwordPolicySet  bool
	anomalyDetector    anomalyRecorder
	smsCodeSvc         *SMSCodeService
	emailActivationSvc *EmailActivationService
	emailCodeSvc       *EmailCodeService
	oauthManager       auth.OAuthManager
	deviceService      *DeviceService
}

func NewAuthService(
	userRepo userRepositoryInterface,
	socialRepo repository.SocialAccountRepository,
	jwtManager *auth.JWT,
	cacheManager *cache.CacheManager,
	passwordMinLength int,
	maxLoginAttempts int,
	loginLockDuration time.Duration,
) *AuthService {
	if passwordMinLength <= 0 {
		passwordMinLength = defaultPasswordMinLen
	}
	if maxLoginAttempts <= 0 {
		maxLoginAttempts = 5
	}
	if loginLockDuration <= 0 {
		loginLockDuration = 15 * time.Minute
	}

	return &AuthService{
		userRepo:          userRepo,
		socialRepo:        socialRepo,
		jwtManager:        jwtManager,
		cache:             cacheManager,
		passwordMinLength: passwordMinLength,
		maxLoginAttempts:  maxLoginAttempts,
		loginLockDuration: loginLockDuration,
		oauthManager:      auth.NewOAuthManager(),
	}
}

func (s *AuthService) SetWebhookService(webhookSvc *WebhookService) {
	s.webhookSvc = webhookSvc
}

func (s *AuthService) SetRoleRepositories(userRoleRepo userRoleRepositoryInterface, roleRepo roleRepositoryInterface) {
	s.userRoleRepo = userRoleRepo
	s.roleRepo = roleRepo
}

func (s *AuthService) SetLoginLogRepository(loginLogRepo loginLogRepositoryInterface) {
	s.loginLogRepo = loginLogRepo
}

func (s *AuthService) SetPasswordPolicy(policy security.PasswordPolicy) {
	s.passwordPolicy = policy.Normalize()
	s.passwordPolicySet = true
}

func (s *AuthService) SetAnomalyDetector(detector anomalyRecorder) {
	s.anomalyDetector = detector
}

func (s *AuthService) SetDeviceService(svc *DeviceService) {
	s.deviceService = svc
}

func (s *AuthService) SetSMSCodeService(svc *SMSCodeService) {
	s.smsCodeSvc = svc
}

func sanitizeUsername(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return "user"
	}

	var builder strings.Builder
	lastUnderscore := false
	for _, r := range trimmed {
		switch {
		case unicode.IsLetter(r) || unicode.IsDigit(r):
			builder.WriteRune(unicode.ToLower(r))
			lastUnderscore = false
		case r == '.' || r == '-' || r == '_':
			builder.WriteRune(r)
			lastUnderscore = false
		case unicode.IsSpace(r):
			if !lastUnderscore && builder.Len() > 0 {
				builder.WriteByte('_')
				lastUnderscore = true
			}
		}
	}

	result := strings.Trim(builder.String(), "._-")
	if result == "" {
		return "user"
	}

	runes := []rune(result)
	if len(runes) > 50 {
		result = string(runes[:50])
	}

	return result
}

func (s *AuthService) generateUniqueUsername(ctx context.Context, base string) (string, error) {
	username := sanitizeUsername(base)
	if s == nil || s.userRepo == nil {
		return username, nil
	}

	exists, err := s.userRepo.ExistsByUsername(ctx, username)
	if err != nil {
		return "", err
	}
	if !exists {
		return username, nil
	}

	baseRunes := []rune(username)
	if len(baseRunes) > 40 {
		username = string(baseRunes[:40])
	}

	for i := 1; i <= 1000; i++ {
		candidate := fmt.Sprintf("%s_%d", username, i)
		exists, err = s.userRepo.ExistsByUsername(ctx, candidate)
		if err != nil {
			return "", err
		}
		if !exists {
			return candidate, nil
		}
	}

	return "", errors.New("unable to generate unique username")
}

func validatePasswordStrength(password string, minLength int, strict bool) error {
	if minLength <= 0 {
		minLength = defaultPasswordMinLen
	}

	info := GetPasswordStrength(password)
	if info.Length < minLength {
		return fmt.Errorf("密码长度不能少于%d位", minLength)
	}

	if strict {
		if !info.HasUpper || !info.HasLower || !info.HasDigit {
			return errors.New("密码必须包含大小写字母和数字")
		}
		return nil
	}

	if info.Score < 2 {
		return errors.New("密码强度不足")
	}

	return nil
}

func GetPasswordStrength(password string) PasswordStrengthInfo {
	info := PasswordStrengthInfo{
		Length: utf8.RuneCountInString(password),
	}

	for _, r := range password {
		switch {
		case unicode.IsUpper(r):
			info.HasUpper = true
		case unicode.IsLower(r):
			info.HasLower = true
		case unicode.IsDigit(r):
			info.HasDigit = true
		case unicode.IsPunct(r) || unicode.IsSymbol(r):
			info.HasSpecial = true
		}
	}

	if info.HasUpper {
		info.Score++
	}
	if info.HasLower {
		info.Score++
	}
	if info.HasDigit {
		info.Score++
	}
	if info.HasSpecial {
		info.Score++
	}

	return info
}

func (s *AuthService) validatePassword(password string) error {
	if s != nil && s.passwordPolicySet {
		return s.passwordPolicy.Validate(password)
	}
	minLength := defaultPasswordMinLen
	if s != nil && s.passwordMinLength > 0 {
		minLength = s.passwordMinLength
	}
	return validatePasswordStrength(password, minLength, false)
}

func (s *AuthService) accessTokenTTLSeconds() int64 {
	if s == nil || s.jwtManager == nil {
		return 0
	}
	return int64(s.jwtManager.GetAccessTokenExpire().Seconds())
}

func (s *AuthService) RefreshTokenTTLSeconds() int64 {
	if s == nil || s.jwtManager == nil {
		return 0
	}
	return int64(s.jwtManager.GetRefreshTokenExpire().Seconds())
}

func (s *AuthService) buildUserInfo(user *domain.User) *UserInfo {
	if user == nil {
		return nil
	}

	return &UserInfo{
		ID:       user.ID,
		Username: user.Username,
		Email:    domain.DerefStr(user.Email),
		Phone:    domain.DerefStr(user.Phone),
		Nickname: user.Nickname,
		Avatar:   user.Avatar,
		Status:   user.Status,
	}
}

func (s *AuthService) ensureUserActive(user *domain.User) error {
	if user == nil {
		return errors.New("用户不存在")
	}

	switch user.Status {
	case domain.UserStatusActive:
		return nil
	case domain.UserStatusInactive:
		return errors.New("账号未激活")
	case domain.UserStatusLocked:
		return errors.New("账号已锁定")
	case domain.UserStatusDisabled:
		return errors.New("账号已禁用")
	default:
		return errors.New("账号状态异常")
	}
}

func (s *AuthService) blacklistTokenClaims(ctx context.Context, token string, validate func(string) (*auth.Claims, error)) error {
	if s == nil || s.cache == nil {
		return nil
	}

	token = strings.TrimSpace(token)
	if token == "" || validate == nil {
		return nil
	}

	claims, err := validate(token)
	if err != nil || claims == nil || strings.TrimSpace(claims.JTI) == "" {
		return nil
	}

	ttl := defaultBlacklistTTL
	if claims.ExpiresAt != nil {
		if until := time.Until(claims.ExpiresAt.Time); until > 0 {
			ttl = until
		}
	}

	return s.cache.Set(ctx, tokenBlacklistPrefix+claims.JTI, true, ttl, ttl)
}

func (s *AuthService) recordLoginAnomaly(ctx context.Context, userID *int64, ip, location, deviceFingerprint string, success bool) {
	if s == nil || s.anomalyDetector == nil || userID == nil {
		return
	}

	events := s.anomalyDetector.RecordLogin(ctx, *userID, ip, location, deviceFingerprint, success)
	if len(events) == 0 {
		return
	}

	s.publishEvent(ctx, domain.EventAnomalyDetected, map[string]interface{}{
		"user_id":   *userID,
		"ip":        ip,
		"location":  location,
		"device":    deviceFingerprint,
		"events":    events,
		"success":   success,
	})
}

func (s *AuthService) publishEvent(ctx context.Context, eventType domain.WebhookEventType, data interface{}) {
	if s == nil || s.webhookSvc == nil {
		return
	}

	go s.webhookSvc.Publish(ctx, eventType, data)
}

func (s *AuthService) writeLoginLog(
	ctx context.Context,
	userID *int64,
	loginType domain.LoginType,
	ip string,
	success bool,
	failReason string,
) {
	if s == nil || s.loginLogRepo == nil {
		return
	}

	status := 0
	if success {
		status = 1
	}

	loginRecord := &domain.LoginLog{
		UserID:     userID,
		LoginType:  int(loginType),
		IP:         ip,
		Status:     status,
		FailReason: failReason,
	}

	// #nosec G118 - 使用带超时的独立 context，防止日志写入无限等待
	go func() { // #nosec G118
		bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := s.loginLogRepo.Create(bgCtx, loginRecord); err != nil {
			log.Printf("auth: write login log failed, user_id=%v login_type=%d err=%v", userID, loginType, err)
		}
	}()
}

func (s *AuthService) incrementFailAttempts(ctx context.Context, key string) int {
	if s == nil || s.cache == nil || key == "" {
		return 0
	}

	current := 0
	if value, ok := s.cache.Get(ctx, key); ok {
		current = attemptCount(value)
	}
	current++

	if err := s.cache.Set(ctx, key, current, s.loginLockDuration, s.loginLockDuration); err != nil {
		log.Printf("auth: store login attempts failed, key=%s err=%v", key, err)
	}

	return current
}

func isValidPhoneSimple(phone string) bool {
	return isValidPhone(phone)
}

// buildDeviceFingerprint 构建设备指纹字符串
func buildDeviceFingerprint(req *LoginRequest) string {
	if req == nil {
		return ""
	}
	var parts []string
	if req.DeviceID != "" {
		parts = append(parts, req.DeviceID)
	}
	if req.DeviceName != "" {
		parts = append(parts, req.DeviceName)
	}
	if req.DeviceBrowser != "" {
		parts = append(parts, req.DeviceBrowser)
	}
	if req.DeviceOS != "" {
		parts = append(parts, req.DeviceOS)
	}
	result := strings.Join(parts, "|")
	if result == "" {
		return ""
	}
	return result
}

// bestEffortRegisterDevice 尝试自动注册/更新设备记录
func (s *AuthService) bestEffortRegisterDevice(ctx context.Context, userID int64, req *LoginRequest) {
	if s == nil || s.deviceService == nil || req == nil || req.DeviceID == "" {
		return
	}

	createReq := &CreateDeviceRequest{
		DeviceID:      req.DeviceID,
		DeviceName:    req.DeviceName,
		DeviceBrowser: req.DeviceBrowser,
		DeviceOS:      req.DeviceOS,
	}
	_, _ = s.deviceService.CreateDevice(ctx, userID, createReq)
}

// BestEffortRegisterDevicePublic 供外部 handler（如 SMS 登录）调用，安静地注册设备
func (s *AuthService) BestEffortRegisterDevicePublic(ctx context.Context, userID int64, req *LoginRequest) {
	s.bestEffortRegisterDevice(ctx, userID, req)
}

func (s *AuthService) cacheUserInfo(ctx context.Context, user *domain.User) {
	if s == nil || s.cache == nil || user == nil {
		return
	}
	info := s.buildUserInfo(user)
	if info == nil {
		return
	}
	_ = s.cache.Set(ctx, userInfoCachePrefix+fmt.Sprintf("%d", user.ID), info, defaultUserCacheTTL, defaultUserCacheTTL)
}

func userInfoFromCacheValue(value interface{}) (*UserInfo, bool) {
	switch typed := value.(type) {
	case *UserInfo:
		return typed, true
	case UserInfo:
		userInfo := typed
		return &userInfo, true
	case map[string]interface{}:
		payload, err := json.Marshal(typed)
		if err != nil {
			return nil, false
		}
		var userInfo UserInfo
		if err := json.Unmarshal(payload, &userInfo); err != nil {
			return nil, false
		}
		return &userInfo, true
	default:
		return nil, false
	}
}

func (s *AuthService) Register(ctx context.Context, req *RegisterRequest) (*UserInfo, error) {
	if req == nil {
		return nil, errors.New("注册请求不能为空")
	}
	if s == nil || s.userRepo == nil {
		return nil, errors.New("user repository is not configured")
	}

	req.Username = strings.TrimSpace(req.Username)
	req.Email = strings.TrimSpace(req.Email)
	req.Phone = strings.TrimSpace(req.Phone)

	if req.Username == "" {
		return nil, errors.New("用户名不能为空")
	}
	if req.Password == "" {
		return nil, errors.New("密码不能为空")
	}
	if req.Phone != "" && !isValidPhoneSimple(req.Phone) {
		return nil, errors.New("手机号格式不正确")
	}
	if err := s.validatePassword(req.Password); err != nil {
		return nil, err
	}
	if err := s.verifyPhoneRegistration(ctx, req); err != nil {
		return nil, err
	}

	exists, err := s.userRepo.ExistsByUsername(ctx, req.Username)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, errors.New("用户名已存在")
	}

	if req.Email != "" {
		exists, err = s.userRepo.ExistsByEmail(ctx, req.Email)
		if err != nil {
			return nil, err
		}
		if exists {
			return nil, errors.New("邮箱已存在")
		}
	}

	if req.Phone != "" {
		exists, err = s.userRepo.ExistsByPhone(ctx, req.Phone)
		if err != nil {
			return nil, err
		}
		if exists {
			return nil, errors.New("手机号已存在")
		}
	}

	hashedPassword, err := auth.HashPassword(req.Password)
	if err != nil {
		return nil, err
	}

	nickname := strings.TrimSpace(req.Nickname)
	if nickname == "" {
		nickname = req.Username
	}

	user := &domain.User{
		Username: req.Username,
		Email:    domain.StrPtr(req.Email),
		Phone:    domain.StrPtr(req.Phone),
		Password: hashedPassword,
		Nickname: nickname,
		Status:   domain.UserStatusActive,
	}
	if err := s.userRepo.Create(ctx, user); err != nil {
		return nil, err
	}

	s.bestEffortAssignDefaultRoles(ctx, user.ID, "register")
	s.cacheUserInfo(ctx, user)

	userInfo := s.buildUserInfo(user)
	s.publishEvent(ctx, domain.EventUserRegistered, userInfo)
	return userInfo, nil
}

func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (*LoginResponse, error) {
	if req == nil {
		return nil, errors.New("登录请求不能为空")
	}
	if s == nil || s.userRepo == nil || s.jwtManager == nil {
		return nil, errors.New("auth service is not fully configured")
	}

	account := req.GetAccount()
	if account == "" {
		return nil, errors.New("账号不能为空")
	}
	if strings.TrimSpace(req.Password) == "" {
		return nil, errors.New("密码不能为空")
	}

	// 构建设备指纹
	deviceFingerprint := buildDeviceFingerprint(req)

	user, err := s.findUserForLogin(ctx, account)
	if err != nil && !isUserNotFoundError(err) {
		s.writeLoginLog(ctx, nil, domain.LoginTypePassword, ip, false, err.Error())
		return nil, err
	}

	attemptKey := loginAttemptKey(account, user)
	if s.cache != nil {
		if value, ok := s.cache.Get(ctx, attemptKey); ok && attemptCount(value) >= s.maxLoginAttempts {
			lockErr := errors.New("账号已锁定，请稍后再试")
			s.writeLoginLog(ctx, nil, domain.LoginTypePassword, ip, false, lockErr.Error())
			return nil, lockErr
		}
	}

	if user == nil {
		s.incrementFailAttempts(ctx, attemptKey)
		s.writeLoginLog(ctx, nil, domain.LoginTypePassword, ip, false, "用户不存在")
		return nil, errors.New("账号或密码错误")
	}

	if err := s.ensureUserActive(user); err != nil {
		s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, false, err.Error())
		s.recordLoginAnomaly(ctx, &user.ID, ip, "", deviceFingerprint, false)
		return nil, err
	}

	if !auth.VerifyPassword(user.Password, req.Password) {
		failCount := s.incrementFailAttempts(ctx, attemptKey)
		failErr := errors.New("账号或密码错误")
		if failCount >= s.maxLoginAttempts {
			s.publishEvent(ctx, domain.EventUserLocked, map[string]interface{}{
				"user_id":  user.ID,
				"username": user.Username,
				"ip":       ip,
			})
		}
		s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, false, failErr.Error())
		s.recordLoginAnomaly(ctx, &user.ID, ip, "", deviceFingerprint, false)
		s.publishEvent(ctx, domain.EventLoginFailed, map[string]interface{}{
			"user_id":  user.ID,
			"username": user.Username,
			"ip":       ip,
		})
		return nil, failErr
	}

	if s.cache != nil {
		_ = s.cache.Delete(ctx, attemptKey)
	}

	s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "password")
	s.cacheUserInfo(ctx, user)
	s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, true, "")
	s.recordLoginAnomaly(ctx, &user.ID, ip, "", deviceFingerprint, true)
	s.bestEffortRegisterDevice(ctx, user.ID, req)
	s.publishEvent(ctx, domain.EventUserLogin, map[string]interface{}{
		"user_id":  user.ID,
		"username": user.Username,
		"ip":       ip,
		"method":   "password",
	})

	return s.generateLoginResponse(ctx, user, req.Remember)
}

func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
	if s == nil || s.jwtManager == nil || s.userRepo == nil {
		return nil, errors.New("auth service is not fully configured")
	}

	refreshToken = strings.TrimSpace(refreshToken)
	claims, err := s.jwtManager.ValidateRefreshToken(refreshToken)
	if err != nil {
		return nil, err
	}
	if s.IsTokenBlacklisted(ctx, claims.JTI) {
		return nil, errors.New("refresh token has been revoked")
	}

	user, err := s.userRepo.GetByID(ctx, claims.UserID)
	if err != nil {
		return nil, err
	}
	if err := s.ensureUserActive(user); err != nil {
		return nil, err
	}

	// Token Rotation: 使旧的 refresh token 失效，防止无限刷新
	if s.cache != nil {
		blacklistKey := tokenBlacklistPrefix + claims.JTI
		// TTL 设置为 refresh token 的剩余有效期
		if claims.ExpiresAt != nil {
			remaining := claims.ExpiresAt.Time.Sub(time.Now())
			if remaining > 0 {
				_ = s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining)
			}
		}
	}

	return s.generateLoginResponse(ctx, user, claims.Remember)
}

func (s *AuthService) GetUserInfo(ctx context.Context, userID int64) (*UserInfo, error) {
	if s == nil || s.userRepo == nil {
		return nil, errors.New("user repository is not configured")
	}

	if s.cache != nil {
		cacheKey := userInfoCachePrefix + fmt.Sprintf("%d", userID)
		if value, ok := s.cache.Get(ctx, cacheKey); ok {
			if info, ok := userInfoFromCacheValue(value); ok {
				return info, nil
			}
		}
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}

	s.cacheUserInfo(ctx, user)
	return s.buildUserInfo(user), nil
}

func (s *AuthService) Logout(ctx context.Context, username string, req *LogoutRequest) error {
	if s == nil {
		return nil
	}
	if req == nil {
		return nil
	}

	_ = s.blacklistTokenClaims(ctx, req.AccessToken, func(token string) (*auth.Claims, error) {
		if s.jwtManager == nil {
			return nil, nil
		}
		return s.jwtManager.ValidateAccessToken(token)
	})
	_ = s.blacklistTokenClaims(ctx, req.RefreshToken, func(token string) (*auth.Claims, error) {
		if s.jwtManager == nil {
			return nil, nil
		}
		return s.jwtManager.ValidateRefreshToken(token)
	})

	if strings.TrimSpace(username) != "" {
		s.publishEvent(ctx, domain.EventUserLogout, map[string]interface{}{
			"username": strings.TrimSpace(username),
		})
	}

	return nil
}

func (s *AuthService) IsTokenBlacklisted(ctx context.Context, jti string) bool {
	if s == nil || s.cache == nil {
		return false
	}
	jti = strings.TrimSpace(jti)
	if jti == "" {
		return false
	}
	_, ok := s.cache.Get(ctx, tokenBlacklistPrefix+jti)
	return ok
}

func (s *AuthService) OAuthLogin(ctx context.Context, provider, state string) (string, error) {
	if s == nil || s.oauthManager == nil {
		return "", errors.New("oauth manager is not configured")
	}
	return s.oauthManager.GetAuthURL(auth.OAuthProvider(strings.ToLower(strings.TrimSpace(provider))), state)
}

func (s *AuthService) OAuthCallback(ctx context.Context, provider, code string) (*LoginResponse, error) {
	if s == nil || s.oauthManager == nil || s.socialRepo == nil || s.userRepo == nil {
		return nil, errors.New("oauth login is not fully configured")
	}

	oauthProvider := auth.OAuthProvider(strings.ToLower(strings.TrimSpace(provider)))
	token, err := s.oauthManager.ExchangeCode(oauthProvider, strings.TrimSpace(code))
	if err != nil {
		return nil, err
	}

	oauthUser, err := s.oauthManager.GetUserInfo(oauthProvider, token)
	if err != nil {
		return nil, err
	}
	if oauthUser == nil {
		return nil, errors.New("oauth user info is empty")
	}

	socialAccount, err := s.socialRepo.GetByProviderAndOpenID(ctx, string(oauthProvider), oauthUser.OpenID)
	if err != nil {
		return nil, err
	}

	var user *domain.User
	if socialAccount != nil {
		user, err = s.userRepo.GetByID(ctx, socialAccount.UserID)
		if err != nil {
			return nil, err
		}

		socialAccount.UnionID = oauthUser.UnionID
		socialAccount.Nickname = oauthUser.Nickname
		socialAccount.Avatar = oauthUser.Avatar
		socialAccount.Gender = oauthUser.Gender
		socialAccount.Email = oauthUser.Email
		socialAccount.Phone = oauthUser.Phone
		socialAccount.Status = domain.SocialAccountStatusActive
		if oauthUser.Extra != nil {
			socialAccount.Extra = oauthUser.Extra
		}
		if err := s.socialRepo.Update(ctx, socialAccount); err != nil {
			log.Printf("auth: update social account failed, provider=%s open_id=%s err=%v", oauthProvider, oauthUser.OpenID, err)
		}
	} else {
		if strings.TrimSpace(oauthUser.Email) != "" {
			user, err = s.userRepo.GetByEmail(ctx, strings.TrimSpace(oauthUser.Email))
			if err != nil {
				if !isUserNotFoundError(err) {
					return nil, err
				}
				user = nil
			}
		}

		if user == nil {
			baseUsername := oauthUser.Nickname
			if baseUsername == "" && oauthUser.Email != "" {
				baseUsername = strings.Split(strings.TrimSpace(oauthUser.Email), "@")[0]
			}
			if baseUsername == "" {
				baseUsername = string(oauthProvider) + "_" + oauthUser.OpenID
			}

			username, err := s.generateUniqueUsername(ctx, baseUsername)
			if err != nil {
				return nil, err
			}

			user = &domain.User{
				Username: username,
				Email:    domain.StrPtr(strings.TrimSpace(oauthUser.Email)),
				Phone:    domain.StrPtr(strings.TrimSpace(oauthUser.Phone)),
				Nickname: strings.TrimSpace(oauthUser.Nickname),
				Avatar:   strings.TrimSpace(oauthUser.Avatar),
				Status:   domain.UserStatusActive,
			}
			if user.Nickname == "" {
				user.Nickname = user.Username
			}
			if err := s.userRepo.Create(ctx, user); err != nil {
				return nil, err
			}
			s.bestEffortAssignDefaultRoles(ctx, user.ID, "oauth")
			s.publishEvent(ctx, domain.EventUserRegistered, s.buildUserInfo(user))
		}

		socialAccount = &domain.SocialAccount{
			UserID:   user.ID,
			Provider: string(oauthProvider),
			OpenID:   oauthUser.OpenID,
			UnionID:  oauthUser.UnionID,
			Nickname: oauthUser.Nickname,
			Avatar:   oauthUser.Avatar,
			Gender:   oauthUser.Gender,
			Email:    oauthUser.Email,
			Phone:    oauthUser.Phone,
			Status:   domain.SocialAccountStatusActive,
		}
		if oauthUser.Extra != nil {
			socialAccount.Extra = oauthUser.Extra
		}
		if err := s.socialRepo.Create(ctx, socialAccount); err != nil {
			return nil, err
		}
	}

	if err := s.ensureUserActive(user); err != nil {
		return nil, err
	}

	s.bestEffortUpdateLastLogin(ctx, user.ID, "", "oauth")
	s.cacheUserInfo(ctx, user)
	s.writeLoginLog(ctx, &user.ID, domain.LoginTypeOAuth, "", true, "")
	s.recordLoginAnomaly(ctx, &user.ID, "", "", "", true)
	s.publishEvent(ctx, domain.EventUserLogin, map[string]interface{}{
		"user_id":  user.ID,
		"username": user.Username,
		"method":   "oauth",
		"provider": string(oauthProvider),
	})

	return s.generateLoginResponseWithoutRemember(ctx, user)
}

func (s *AuthService) StartSocialAccountBinding(
	ctx context.Context,
	userID int64,
	provider string,
	returnTo string,
	currentPassword string,
	totpCode string,
) (string, string, error) {
	if s == nil || s.oauthManager == nil || s.socialRepo == nil || s.userRepo == nil {
		return "", "", errors.New("social account binding is not fully configured")
	}

	normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return "", "", err
	}
	if err := s.ensureUserActive(user); err != nil {
		return "", "", err
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return "", "", err
	}

	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return "", "", err
	}
	if existing := findSocialAccountByProvider(accounts, normalizedProvider); existing != nil {
		return "", "", auth.ErrOAuthAlreadyBound
	}

	state, err := s.CreateOAuthBindState(ctx, userID, returnTo)
	if err != nil {
		return "", "", err
	}

	authURL, err := s.OAuthLogin(ctx, normalizedProvider, state)
	if err != nil {
		return "", "", err
	}

	return authURL, state, nil
}

func (s *AuthService) OAuthBindCallback(ctx context.Context, userID int64, provider, code string) (*domain.SocialAccountInfo, error) {
	if s == nil || s.oauthManager == nil || s.socialRepo == nil || s.userRepo == nil {
		return nil, errors.New("social account binding is not fully configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if err := s.ensureUserActive(user); err != nil {
		return nil, err
	}

	oauthProvider := auth.OAuthProvider(strings.ToLower(strings.TrimSpace(provider)))
	token, err := s.oauthManager.ExchangeCode(oauthProvider, strings.TrimSpace(code))
	if err != nil {
		return nil, err
	}

	oauthUser, err := s.oauthManager.GetUserInfo(oauthProvider, token)
	if err != nil {
		return nil, err
	}
	if oauthUser == nil {
		return nil, errors.New("oauth user info is empty")
	}

	account, err := s.upsertOAuthSocialAccount(ctx, userID, oauthProvider, oauthUser)
	if err != nil {
		return nil, err
	}

	return account.ToInfo(), nil
}

func (s *AuthService) upsertOAuthSocialAccount(
	ctx context.Context,
	userID int64,
	provider auth.OAuthProvider,
	oauthUser *auth.OAuthUser,
) (*domain.SocialAccount, error) {
	if s == nil || s.socialRepo == nil || s.userRepo == nil {
		return nil, errors.New("social account binding is not configured")
	}
	if oauthUser == nil {
		return nil, errors.New("oauth user info is empty")
	}

	normalizedProvider := strings.ToLower(strings.TrimSpace(string(provider)))
	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return nil, err
	}
	if currentProviderBinding := findSocialAccountByProvider(accounts, normalizedProvider); currentProviderBinding != nil &&
		!strings.EqualFold(strings.TrimSpace(currentProviderBinding.OpenID), strings.TrimSpace(oauthUser.OpenID)) {
		return nil, errors.New("provider already bound to current account")
	}

	existing, err := s.socialRepo.GetByProviderAndOpenID(ctx, normalizedProvider, strings.TrimSpace(oauthUser.OpenID))
	if err != nil {
		return nil, err
	}
	if existing != nil {
		if existing.UserID != userID {
			return nil, auth.ErrOAuthAlreadyBound
		}
		existing.UnionID = oauthUser.UnionID
		existing.Nickname = oauthUser.Nickname
		existing.Avatar = oauthUser.Avatar
		existing.Gender = oauthUser.Gender
		existing.Email = oauthUser.Email
		existing.Phone = oauthUser.Phone
		existing.Status = domain.SocialAccountStatusActive
		if oauthUser.Extra != nil {
			existing.Extra = oauthUser.Extra
		}
		if err := s.socialRepo.Update(ctx, existing); err != nil {
			return nil, err
		}
		return existing, nil
	}

	account := &domain.SocialAccount{
		UserID:   userID,
		Provider: normalizedProvider,
		OpenID:   strings.TrimSpace(oauthUser.OpenID),
		UnionID:  oauthUser.UnionID,
		Nickname: oauthUser.Nickname,
		Avatar:   oauthUser.Avatar,
		Gender:   oauthUser.Gender,
		Email:    oauthUser.Email,
		Phone:    oauthUser.Phone,
		Status:   domain.SocialAccountStatusActive,
	}
	if oauthUser.Extra != nil {
		account.Extra = oauthUser.Extra
	}
	if err := s.socialRepo.Create(ctx, account); err != nil {
		return nil, err
	}
	return account, nil
}

func (s *AuthService) verifySensitiveAction(
	ctx context.Context,
	user *domain.User,
	currentPassword string,
	totpCode string,
) error {
	if user == nil {
		return errors.New("user is required")
	}

	password := strings.TrimSpace(currentPassword)
	code := strings.TrimSpace(totpCode)
	hasPassword := strings.TrimSpace(user.Password) != ""
	hasTOTP := user.TOTPEnabled && strings.TrimSpace(user.TOTPSecret) != ""

	// 如果用户既没有密码也没有启用TOTP，禁止执行敏感操作
	if !hasPassword && !hasTOTP {
		return errors.New("请先设置密码或启用两步验证")
	}

	if password != "" {
		if !hasPassword || !auth.VerifyPassword(user.Password, password) {
			return errors.New("当前密码不正确")
		}
		return nil
	}

	if code != "" {
		if !hasTOTP {
			return errors.New("TOTP verification is not available")
		}
		return s.verifyTOTPCodeOrRecoveryCode(ctx, user, code)
	}

	return errors.New("password or TOTP verification is required")
}

func (s *AuthService) verifyTOTPCodeOrRecoveryCode(ctx context.Context, user *domain.User, code string) error {
	if user == nil {
		return errors.New("user is required")
	}
	if !user.TOTPEnabled || strings.TrimSpace(user.TOTPSecret) == "" {
		return errors.New("TOTP verification is not available")
	}

	manager := auth.NewTOTPManager()
	if manager.ValidateCode(user.TOTPSecret, code) {
		return nil
	}

	var hashedCodes []string
	if strings.TrimSpace(user.TOTPRecoveryCodes) != "" {
		_ = json.Unmarshal([]byte(user.TOTPRecoveryCodes), &hashedCodes)
	}
	index, matched := auth.VerifyRecoveryCode(code, hashedCodes)
	if !matched {
		return errors.New("TOTP code or recovery code is invalid")
	}

	hashedCodes = append(hashedCodes[:index], hashedCodes[index+1:]...)
	payload, err := json.Marshal(hashedCodes)
	if err != nil {
		return err
	}
	user.TOTPRecoveryCodes = string(payload)
	return s.userRepo.UpdateTOTP(ctx, user)
}

// VerifyTOTP 验证 TOTP（支持设备信任跳过）
// 如果设备已信任且未过期，跳过 TOTP 验证
func (s *AuthService) VerifyTOTP(ctx context.Context, userID int64, code, deviceID string) error {
	if s == nil || s.userRepo == nil {
		return errors.New("auth service is not fully configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return errors.New("用户不存在")
	}

	// 检查设备信任状态
	if deviceID != "" && s.deviceService != nil {
		device, err := s.deviceService.GetDeviceByDeviceID(ctx, userID, deviceID)
		if err == nil && device.IsTrusted {
			// 检查信任是否过期
			if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) {
				return nil // 设备已信任，跳过 TOTP 验证
			}
		}
	}

	// 执行 TOTP 验证
	return s.verifyTOTPCodeOrRecoveryCode(ctx, user, code)
}

func findSocialAccountByProvider(accounts []*domain.SocialAccount, provider string) *domain.SocialAccount {
	normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
	for _, account := range accounts {
		if account == nil {
			continue
		}
		if strings.EqualFold(strings.TrimSpace(account.Provider), normalizedProvider) {
			return account
		}
	}
	return nil
}

func (s *AuthService) availableLoginMethodCount(
	user *domain.User,
	accounts []*domain.SocialAccount,
	excludeProvider string,
) int {
	if user == nil {
		return 0
	}

	count := 0
	if strings.TrimSpace(user.Password) != "" {
		count++
	}
	if s.emailCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Email)) != "" {
		count++
	}
	if s.smsCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Phone)) != "" {
		count++
	}

	normalizedExcludeProvider := strings.ToLower(strings.TrimSpace(excludeProvider))
	for _, account := range accounts {
		if account == nil || account.Status != domain.SocialAccountStatusActive {
			continue
		}
		if strings.EqualFold(strings.TrimSpace(account.Provider), normalizedExcludeProvider) {
			continue
		}
		count++
	}

	return count
}

func (s *AuthService) generateLoginResponse(ctx context.Context, user *domain.User, remember bool) (*LoginResponse, error) {
	if s == nil || s.jwtManager == nil {
		return nil, errors.New("jwt manager is not configured")
	}
	if user == nil {
		return nil, errors.New("user is required")
	}

	var accessToken, refreshToken string
	var err error

	if remember {
		accessToken, refreshToken, err = s.jwtManager.GenerateTokenPairWithRemember(user.ID, user.Username, remember)
	} else {
		accessToken, refreshToken, err = s.jwtManager.GenerateTokenPair(user.ID, user.Username)
	}
	if err != nil {
		return nil, err
	}

	s.cacheUserInfo(ctx, user)

	return &LoginResponse{
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    s.accessTokenTTLSeconds(),
		User:         s.buildUserInfo(user),
	}, nil
}

// generateLoginResponseWithoutRemember 生成登录响应（不支持记住登录）
func (s *AuthService) generateLoginResponseWithoutRemember(ctx context.Context, user *domain.User) (*LoginResponse, error) {
	return s.generateLoginResponse(ctx, user, false)
}

func (s *AuthService) BindSocialAccount(ctx context.Context, userID int64, provider, openID string) error {
	if s == nil || s.socialRepo == nil || s.userRepo == nil {
		return errors.New("social account binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}

	normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
	normalizedOpenID := strings.TrimSpace(openID)
	if normalizedProvider == "" || normalizedOpenID == "" {
		return errors.New("provider and open_id are required")
	}

	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return err
	}
	if existingProvider := findSocialAccountByProvider(accounts, normalizedProvider); existingProvider != nil &&
		!strings.EqualFold(strings.TrimSpace(existingProvider.OpenID), normalizedOpenID) {
		return errors.New("provider already bound to current account")
	}

	existing, err := s.socialRepo.GetByProviderAndOpenID(ctx, normalizedProvider, normalizedOpenID)
	if err != nil {
		return err
	}
	if existing != nil {
		if existing.UserID == userID {
			return nil
		}
		return auth.ErrOAuthAlreadyBound
	}

	return s.socialRepo.Create(ctx, &domain.SocialAccount{
		UserID:   userID,
		Provider: normalizedProvider,
		OpenID:   normalizedOpenID,
		Status:   domain.SocialAccountStatusActive,
	})
}

func (s *AuthService) UnbindSocialAccount(ctx context.Context, userID int64, provider, currentPassword, totpCode string) error {
	if s == nil || s.socialRepo == nil || s.userRepo == nil {
		return errors.New("social account binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}

	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return err
	}

	normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
	if findSocialAccountByProvider(accounts, normalizedProvider) == nil {
		return auth.ErrOAuthNotFound
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return err
	}
	if s.availableLoginMethodCount(user, accounts, normalizedProvider) == 0 {
		return errors.New("at least one login method must remain after unbinding")
	}

	return s.socialRepo.DeleteByProviderAndUserID(ctx, normalizedProvider, userID)
}

func (s *AuthService) GetSocialAccounts(ctx context.Context, userID int64) ([]*domain.SocialAccount, error) {
	if s == nil || s.socialRepo == nil {
		return []*domain.SocialAccount{}, nil
	}

	accounts, err := s.socialRepo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if accounts == nil {
		return []*domain.SocialAccount{}, nil
	}
	return accounts, nil
}

func (s *AuthService) GetEnabledOAuthProviders() []auth.OAuthProviderInfo {
	if s == nil || s.oauthManager == nil {
		return []auth.OAuthProviderInfo{}
	}

	providers := s.oauthManager.GetEnabledProviders()
	if providers == nil {
		return []auth.OAuthProviderInfo{}
	}
	return providers
}

func (s *AuthService) LoginByCode(ctx context.Context, phone, code, ip string) (*LoginResponse, error) {
	if s == nil || s.smsCodeSvc == nil || s.userRepo == nil {
		return nil, errors.New("sms code login is not configured")
	}

	phone = strings.TrimSpace(phone)
	if phone == "" {
		return nil, errors.New("手机号不能为空")
	}

	if err := s.smsCodeSvc.VerifyCode(ctx, phone, "login", strings.TrimSpace(code)); err != nil {
		s.writeLoginLog(ctx, nil, domain.LoginTypeSMSCode, ip, false, err.Error())
		return nil, err
	}

	user, err := s.userRepo.GetByPhone(ctx, phone)
	if err != nil {
		if isUserNotFoundError(err) {
			s.writeLoginLog(ctx, nil, domain.LoginTypeSMSCode, ip, false, "手机号未注册")
			return nil, errors.New("手机号未注册")
		}
		s.writeLoginLog(ctx, nil, domain.LoginTypeSMSCode, ip, false, err.Error())
		return nil, err
	}

	if err := s.ensureUserActive(user); err != nil {
		s.writeLoginLog(ctx, &user.ID, domain.LoginTypeSMSCode, ip, false, err.Error())
		s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", false)
		return nil, err
	}

	s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "sms_code")
	s.cacheUserInfo(ctx, user)
	s.writeLoginLog(ctx, &user.ID, domain.LoginTypeSMSCode, ip, true, "")
	s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", true)
	s.publishEvent(ctx, domain.EventUserLogin, map[string]interface{}{
		"user_id":  user.ID,
		"username": user.Username,
		"ip":       ip,
		"method":   "sms_code",
	})

	return s.generateLoginResponseWithoutRemember(ctx, user)
}
