package authService

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	"git.ma-al.com/goc_daniel/b2b/app/config"
	"git.ma-al.com/goc_daniel/b2b/app/db"
	"git.ma-al.com/goc_daniel/b2b/app/model"
	"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
	constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
	"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"

	"github.com/dlclark/regexp2"
	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

// JWTClaims represents the JWT claims
type JWTClaims struct {
	UserID    uint               `json:"user_id"`
	Email     string             `json:"email"`
	Username  string             `json:"username"`
	Role      model.CustomerRole `json:"customer_role"`
	CartsIDs  []uint             `json:"carts_ids"`
	LangID    uint               `json:"lang_id"`
	CountryID uint               `json:"country_id"`
	jwt.RegisteredClaims
}

// AuthService handles authentication operations
type AuthService struct {
	db     *gorm.DB
	config *config.AuthConfig
	email  *emailService.EmailService
}

// NewAuthService creates a new AuthService instance
func NewAuthService() *AuthService {
	svc := &AuthService{
		db:     db.Get(),
		config: &config.Get().Auth,
		email:  emailService.NewEmailService(),
	}
	// Auto-migrate the refresh_tokens table
	if svc.db != nil {
		_ = svc.db.AutoMigrate(&model.RefreshToken{})
	}
	return svc
}

// Login authenticates a user with email and password
func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, string, error) {
	var user model.Customer

	// Find user by email
	if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", responseErrors.ErrInvalidCredentials
		}
		return nil, "", fmt.Errorf("database error: %w", err)
	}
	// Check if user is active
	if !user.IsActive {
		return nil, "", responseErrors.ErrUserInactive
	}

	// Check if email is verified
	if !user.EmailVerified {
		return nil, "", responseErrors.ErrEmailNotVerified
	}

	// Verify password
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
		return nil, "", responseErrors.ErrInvalidCredentials
	}

	// Update last login time
	now := time.Now()
	user.LastLoginAt = &now
	s.db.Save(&user)

	// Generate access token (JWT)
	accessToken, err := s.generateAccessToken(&user)
	if err != nil {
		return nil, "", fmt.Errorf("failed to generate access token: %w", err)
	}

	// Generate opaque refresh token and store in DB
	rawRefreshToken, err := s.createRefreshToken(user.ID)
	if err != nil {
		return nil, "", fmt.Errorf("failed to create refresh token: %w", err)
	}

	return &model.AuthResponse{
		AccessToken: accessToken,
		TokenType:   "Bearer",
		ExpiresIn:   s.config.JWTExpiration,
		User:        user.ToSession(),
	}, rawRefreshToken, nil
}

// Register initiates user registration
func (s *AuthService) Register(req *model.RegisterRequest) error {
	// Check if email already exists
	var existingUser model.Customer
	if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
		return responseErrors.ErrEmailExists
	}

	// Validate passwords match
	if req.Password != req.ConfirmPassword {
		return responseErrors.ErrPasswordsDoNotMatch
	}

	// Validate password strength
	if err := validatePassword(req.Password); err != nil {
		return responseErrors.ErrInvalidPassword
	}

	// Hash password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Generate verification token
	token, err := s.generateVerificationToken()
	if err != nil {
		return fmt.Errorf("failed to generate verification token: %w", err)
	}

	// Set expiration (24 hours from now)
	expiresAt := time.Now().Add(24 * time.Hour)

	// Create user with verification token
	user := model.Customer{
		Email:                    req.Email,
		Password:                 string(hashedPassword),
		FirstName:                req.FirstName,
		LastName:                 req.LastName,
		Role:                     model.RoleUser,
		Provider:                 model.ProviderLocal,
		IsActive:                 false,
		EmailVerified:            false,
		EmailVerificationToken:   token,
		EmailVerificationExpires: &expiresAt,
		LangID:                   req.LangID,
		CountryID:                req.CountryID,
	}

	if err := s.db.Create(&user).Error; err != nil {
		return fmt.Errorf("failed to create user: %w", err)
	}

	// Send verification email
	baseURL := config.Get().App.BaseURL
	lang, err := s.GetLangISOCode(req.LangID)
	if err != nil {
		return err
	}

	if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
		// Log error but don't fail registration - user can request resend
		_ = err
	}

	return nil
}

// CompleteRegistration completes the registration with password verification after email verification
func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationRequest) (*model.AuthResponse, string, error) {
	// Find user by verification token
	var user model.Customer
	if err := s.db.Where("email_verification_token = ?", req.Token).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", responseErrors.ErrInvalidVerificationToken
		}
		return nil, "", fmt.Errorf("database error: %w", err)
	}

	// Check if token is expired
	if user.EmailVerificationExpires != nil && user.EmailVerificationExpires.Before(time.Now()) {
		return nil, "", responseErrors.ErrVerificationTokenExpired
	}

	// Update user - activate account and mark email as verified
	user.IsActive = true
	user.EmailVerified = true
	user.EmailVerificationToken = ""
	user.EmailVerificationExpires = nil

	if err := s.db.Save(&user).Error; err != nil {
		return nil, "", fmt.Errorf("failed to update user: %w", err)
	}

	// Send admin notification about new user registration
	baseURL := config.Get().App.BaseURL
	if err := s.email.SendNewUserAdminNotification(user.Email, user.FullName(), baseURL); err != nil {
		_ = err
	}

	// Generate access token
	accessToken, err := s.generateAccessToken(&user)
	if err != nil {
		return nil, "", fmt.Errorf("failed to generate access token: %w", err)
	}

	// Generate opaque refresh token and store in DB
	rawRefreshToken, err := s.createRefreshToken(user.ID)
	if err != nil {
		return nil, "", fmt.Errorf("failed to create refresh token: %w", err)
	}

	return &model.AuthResponse{
		AccessToken: accessToken,
		TokenType:   "Bearer",
		ExpiresIn:   s.config.JWTExpiration,
		User:        user.ToSession(),
	}, rawRefreshToken, nil
}

// RequestPasswordReset initiates a password reset request
func (s *AuthService) RequestPasswordReset(emailAddr string) error {
	// Find user by email
	var user model.Customer
	if err := s.db.Where("email = ?", emailAddr).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			// Don't reveal if email exists or not for security
			return nil
		}
		return fmt.Errorf("database error: %w", err)
	}

	// Check if user is active, email verified, and is a local user
	if !user.IsActive || !user.EmailVerified || user.Provider != model.ProviderLocal {
		// Don't reveal account status for security
		return nil
	}

	// Check rate limit: don't allow password reset requests more than once per hour
	if user.LastPasswordResetRequest != nil && time.Since(*user.LastPasswordResetRequest) < time.Hour {
		// Rate limit hit, silently fail for security
		return nil
	}

	// Generate reset token
	token, err := s.generateVerificationToken()
	if err != nil {
		return fmt.Errorf("failed to generate reset token: %w", err)
	}

	// Set expiration (1 hour from now)
	expiresAt := time.Now().Add(1 * time.Hour)

	// Update user with reset token and last request time
	now := time.Now()
	user.PasswordResetToken = token
	user.PasswordResetExpires = &expiresAt
	user.LastPasswordResetRequest = &now
	if err := s.db.Save(&user).Error; err != nil {
		return fmt.Errorf("failed to save reset token: %w", err)
	}

	// Send password reset email
	baseURL := config.Get().App.BaseURL
	lang, err := s.GetLangISOCode(user.LangID)
	if err != nil {
		return err
	}

	if err := s.email.SendPasswordResetEmail(user.Email, user.PasswordResetToken, baseURL, lang); err != nil {
		_ = err
	}

	return nil
}

// ResetPassword completes the password reset with a new password
func (s *AuthService) ResetPassword(token, newPassword string) error {
	// Find user by reset token
	var user model.Customer
	if err := s.db.Where("password_reset_token = ?", token).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return responseErrors.ErrInvalidResetToken
		}
		return fmt.Errorf("database error: %w", err)
	}

	// Check if token is expired
	if user.PasswordResetExpires == nil || user.PasswordResetExpires.Before(time.Now()) {
		return responseErrors.ErrResetTokenExpired
	}

	// Validate new password
	if err := validatePassword(newPassword); err != nil {
		return responseErrors.ErrInvalidPassword
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update user password and clear reset token
	user.Password = string(hashedPassword)
	user.PasswordResetToken = ""
	user.PasswordResetExpires = nil

	if err := s.db.Save(&user).Error; err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	// Revoke all existing refresh tokens for this user (security: password changed)
	s.db.Where("customer_id = ?", user.ID).Delete(&model.RefreshToken{})

	return nil
}

// ValidateToken validates a JWT access token and returns the claims
func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(s.config.JWTSecret), nil
	})

	if err != nil {
		if errors.Is(err, jwt.ErrTokenExpired) {
			return nil, responseErrors.ErrTokenExpired
		}
		return nil, responseErrors.ErrInvalidToken
	}

	claims, ok := token.Claims.(*JWTClaims)
	if !ok || !token.Valid {
		return nil, responseErrors.ErrInvalidToken
	}

	return claims, nil
}

// RefreshToken validates an opaque refresh token, rotates it, and returns a new access token.
// Returns: AuthResponse, new raw refresh token, error
func (s *AuthService) RefreshToken(rawToken string) (*model.AuthResponse, string, error) {
	tokenHash := hashToken(rawToken)

	// Find the refresh token record
	var rt model.RefreshToken
	if err := s.db.Where("token_hash = ?", tokenHash).First(&rt).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", responseErrors.ErrInvalidToken
		}
		return nil, "", fmt.Errorf("database error: %w", err)
	}

	// Check expiry
	if rt.ExpiresAt.Before(time.Now()) {
		// Clean up expired token
		s.db.Delete(&rt)
		return nil, "", responseErrors.ErrTokenExpired
	}

	// Get user from database
	var user model.Customer
	if err := s.db.First(&user, rt.CustomerID).Error; err != nil {
		return nil, "", responseErrors.ErrUserNotFound
	}

	if !user.IsActive {
		return nil, "", responseErrors.ErrUserInactive
	}

	if !user.EmailVerified {
		return nil, "", responseErrors.ErrEmailNotVerified
	}

	// Delete the old refresh token (rotation: one-time use)
	s.db.Delete(&rt)

	// Generate new access token
	accessToken, err := s.generateAccessToken(&user)
	if err != nil {
		return nil, "", fmt.Errorf("failed to generate access token: %w", err)
	}

	// Issue a new opaque refresh token
	newRawRefreshToken, err := s.createRefreshToken(user.ID)
	if err != nil {
		return nil, "", fmt.Errorf("failed to create refresh token: %w", err)
	}

	return &model.AuthResponse{
		AccessToken: accessToken,
		TokenType:   "Bearer",
		ExpiresIn:   s.config.JWTExpiration,
		User:        user.ToSession(),
	}, newRawRefreshToken, nil
}

// RevokeRefreshToken deletes a specific refresh token from the DB (used on logout)
func (s *AuthService) RevokeRefreshToken(rawToken string) {
	if rawToken == "" {
		return
	}
	tokenHash := hashToken(rawToken)
	s.db.Where("token_hash = ?", tokenHash).Delete(&model.RefreshToken{})
}

// RevokeAllRefreshTokens deletes all refresh tokens for a user (used on logout-all-devices)
func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
	s.db.Where("customer_id = ?", userID).Delete(&model.RefreshToken{})
}

// GetUserByID retrieves a user by ID
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
	var user model.Customer
	if err := s.db.First(&user, userID).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, responseErrors.ErrUserNotFound
		}
		return nil, fmt.Errorf("database error: %w", err)
	}
	return &user, nil
}

// GetUserByEmail retrieves a user by email
func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) {
	var user model.Customer
	if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, responseErrors.ErrUserNotFound
		}
		return nil, fmt.Errorf("database error: %w", err)
	}
	return &user, nil
}

// createRefreshToken generates a random opaque token, stores its hash in the DB, and returns the raw token.
func (s *AuthService) createRefreshToken(userID uint) (string, error) {
	// Generate 32 random bytes → 64-char hex string
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	rawToken := hex.EncodeToString(b)
	tokenHash := hashToken(rawToken)

	expiresAt := time.Now().Add(time.Duration(s.config.RefreshExpiration) * time.Second)

	rt := model.RefreshToken{
		CustomerID: userID,
		TokenHash:  tokenHash,
		ExpiresAt:  expiresAt,
	}
	if err := s.db.Create(&rt).Error; err != nil {
		return "", fmt.Errorf("failed to store refresh token: %w", err)
	}

	return rawToken, nil
}

// hashToken returns the SHA-256 hex digest of a raw token string.
func hashToken(raw string) string {
	h := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(h[:])
}

// generateAccessToken generates a short-lived JWT access token
func (s *AuthService) generateAccessToken(user *model.Customer) (string, error) {
	_, err := s.GetLangISOCode(user.LangID)
	if err != nil {
		return "", err
	}

	err = s.CheckIfCountryExists(user.CountryID)
	if err != nil {
		return "", responseErrors.ErrBadCountryID
	}

	claims := JWTClaims{
		UserID:    user.ID,
		Email:     user.Email,
		Username:  user.Email,
		Role:      user.Role,
		CartsIDs:  []uint{},
		LangID:    user.LangID,
		CountryID: user.CountryID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.JWTExpiration) * time.Second)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(s.config.JWTSecret))
}

func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
	// Update choice and get new access token using AuthService
	new_access_token, err := s.generateAccessToken(user)
	if err != nil {
		return "", err
	}

	// Save the updated user
	if err := s.db.Save(user).Error; err != nil {
		return "", fmt.Errorf("database error: %w", err)
	}

	return new_access_token, nil
}

// generateVerificationToken generates a random verification token
func (s *AuthService) generateVerificationToken() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}

// validatePassword validates password strength using RE2-compatible regexes.
func validatePassword(password string) error {
	var passregex2 = regexp2.MustCompile(constdata.PASSWORD_VALIDATION_REGEX, regexp2.None)

	if ok, _ := passregex2.MatchString(password); !ok {
		return errors.New("password must be at least 10 characters long and contain at least one lowercase letter, one uppercase letter, and one digit")
	}

	return nil
}

func (s *AuthService) GetLangISOCode(langID uint) (string, error) {
	var lang string
	var err error

	if langID == 0 { // retrieve the default lang
		err = db.DB.Table("b2b_language").Where("is_default = ?", 1).Select("iso_code").Scan(&lang).Error
	} else {
		err = db.DB.Table("b2b_language").Where("id = ?", langID).Where("active = ?", 1).Select("iso_code").Scan(&lang).Error
	}

	if err != nil {
		return lang, err
	} else if lang == "" {
		return lang, responseErrors.ErrBadLangID
	}
	return lang, nil
}

func (s *AuthService) CheckIfCountryExists(countryID uint) error {
	var count int64

	err := db.DB.Table("b2b_countries").Where("id = ?", countryID).Count(&count).Error

	if err != nil {
		return err
	}
	if count == 0 {
		return responseErrors.ErrBadCountryID
	}
	return nil
}
