package services

import (
	configs "astrology-api/configs"
	dto "astrology-api/dto"

	"errors"
	"log"
	"os"
	"strings"
	"time"

	"astrology-api/helpers"
	usermodel "astrology-api/models/usermodel"
	"astrology-api/repositories"

	"golang.org/x/crypto/bcrypt"
)

func CheckUserLoginAllowed(user *usermodel.User) error {

	if user.IsDelete {
		return errors.New("Your account has been deleted. Please contact support.")
	}

	if user.IsSuspended {
		return errors.New("Your account has been suspended. Please contact support.")
	}

	return nil
}

func RegisterUser(user usermodel.User, device dto.DeviceContext) (map[string]interface{}, error) {
	// Check mobile
	oldUser, _ := repositories.FindUserByMobile(user.ContactNo)

	if oldUser != nil {
		return nil, errors.New("mobile already exists")
	}

	// Check email
	if user.Email != "" {

		emailUser, _ := repositories.FindUserByEmail(user.Email)
		if emailUser != nil {
			return nil, errors.New("email already exists")
		}

	}

	// Encrypt password
	hashPassword, err := bcrypt.GenerateFromPassword(
		[]byte(user.Password),
		bcrypt.DefaultCost,
	)

	if err != nil {
		return nil, err
	}

	user.Password = string(hashPassword)

	// Default values
	user.IsActive = true
	user.IsDelete = false

	// Mirror the registration token across all three columns the schema keeps
	// for it, so the row is notifiable the moment it is created.
	if token := strings.TrimSpace(device.DeviceToken); token != "" {
		user.DeviceToken = token
		user.FcmToken = token
		user.Token = token
	}

	// device_type falls back to platform, because the apps send
	// "platform": "Android" and usually no device_type of their own.
	if deviceType := device.ResolvedDeviceType(); deviceType != "" {
		user.DeviceType = deviceType
	}

	// Referral Code
	user.ReferralCode = helpers.GenerateReferralCode(user.Name)

	// Create User
	err = repositories.CreateUser(&user)

	if err != nil {
		return nil, err
	}

	// Create Wallet
	amount := float64(0)
	wallet := usermodel.UserWallet{
		UserID:   user.ID,
		Amount:   &amount,
		IsActive: true,
		IsDelete: false,
	}

	err = configs.DB.Create(&wallet).Error

	if err != nil {
		return nil, err
	}

	// Create Role
	role := usermodel.UserRole{UserID: user.ID, RoleID: 3}
	err = repositories.CreateUserRole(&role)
	if err != nil {
		return nil, err
	}

	//------------------------------------------------
	// Session History
	//------------------------------------------------
	//
	// Registering is the account's first session, so it is recorded the same
	// way /verify-login-otp records a login. Not fatal: an account that
	// exists with no history row is fine, whereas failing the registration
	// over one would not be.

	if device.HasSession() {

		now := time.Now()

		history := usermodel.LoginHistory{
			UserID:     user.ID,
			IPAddress:  strings.TrimSpace(device.IPAddress),
			DeviceName: strings.TrimSpace(device.DeviceName),
			Platform:   strings.TrimSpace(device.Platform),
			LoginAt:    &now,
		}

		if err := repositories.SaveLoginHistory(&history); err != nil {
			log.Printf("register: saving the login history for user %d failed: %v", user.ID, err)
		}
	}

	// Static outside production, a fresh random code within it. NewOTP is the
	// single place either value comes from, so the environment is decided once
	// rather than re-tested at every call site.
	mobileOTP := MustNewOTP()
	emailOTP := MustNewOTP()

	expireTime := time.Now().Add(OTPExpiry())

	otp := usermodel.UserOTP{
		UserID:           &user.ID,
		Mobile:           user.ContactNo,
		Email:            user.Email,
		MobileOTP:        mobileOTP,
		EmailOTP:         emailOTP,
		MaxAttempts:      5,
		ResendCount:      0,
		IsMobileVerified: false,
		IsEmailVerified:  false,
		ExpiresAt:        &expireTime,
	}

	err = repositories.CreateOTP(&otp)

	if err != nil {

		return nil, err

	}

	// Delivery is best effort and runs once the OTP row is stored. It used to
	// return the gateway error, which failed the whole registration after the
	// account, wallet and role had already been created - the caller saw
	// "registration failed" and could not retry, because the mobile was taken.
	DeliverMobileOTP(user.ContactNo, mobileOTP)
	DeliverEmailOTP(user.Email, emailOTP)

	// Generate JWT Token
	accessToken, err := GenerateJWT(user.ID, user.Name)

	if err != nil {
		return nil, err
	}

	// Generate Refresh Token
	refreshToken, err := GenerateRefreshToken(
		user.ID,
	)

	if err != nil {
		return nil, err
	}

	// Save tokens
	user.JwtToken = accessToken
	user.RefreshToken = refreshToken
	user.RoleID = 3
	err = repositories.UpdateUser(&user)

	if err != nil {
		return nil, err
	}

	response := map[string]interface{}{
		"id":             user.ID,
		"name":           user.Name,
		"contactNo":      user.ContactNo,
		"email":          user.Email,
		"mobileVerified": user.MobileVerified,
		"emailVerified":  user.EmailVerified,
		"token":          accessToken,
		"refresh_token":  refreshToken,
	}

	return response, nil
}

// VerifyMobileOTP confirms the number and, when the app sends one, stores its
// FCM registration token — verification is the first point at which a freshly
// registered account is real enough to notify.
func VerifyMobileOTP(
	userID uint,
	mobile string,
	otpValue string,
	deviceToken string,
	deviceType string,
) (map[string]interface{}, error) {

	var user *usermodel.User
	var err error

	if userID != 0 {
		user, err = repositories.FindUserByID(userID)
	} else {
		user, err = repositories.FindUserByMobile(mobile)
	}

	if err != nil {
		return nil, errors.New("user not found")
	}

	userID = user.ID

	otp, err := repositories.GetOTPByUserID(userID)

	if err != nil {
		return nil, errors.New("otp record not found")
	}

	if otp.MobileOTP != otpValue {
		return map[string]interface{}{
			"success": false,
			"message": "invalid otp",
			"id":      user.ID,
			"mobile":  user.ContactNo,
		}, nil
	}

	otp.IsMobileVerified = true

	err = repositories.UpdateOTP(otp)

	if err != nil {
		return nil, err
	}

	user.MobileVerified = true

	err = repositories.UpdateMobileVerified(userID)

	if err != nil {
		return nil, err
	}

	// Not fatal: the number is verified either way, and an account without a
	// push token is usable — it simply misses notifications until the next
	// login or /device-token call.
	if err := repositories.SaveDeviceToken(userID, deviceToken, deviceType); err != nil {
		log.Printf("verify-mobile-otp: storing the device token for user %d failed: %v", userID, err)
	}

	return map[string]interface{}{
		"success": true,
		"message": "Mobile verified successfully",
		"id":      user.ID,
		"mobile":  user.ContactNo,
	}, nil

}

func VerifyEmailOTP(userID uint, email string, otpValue string) (map[string]interface{}, error) {

	var user *usermodel.User
	var err error

	if userID != 0 {
		user, err = repositories.FindUserByID(userID)
	} else {
		user, err = repositories.FindUserByEmail(email)
	}

	if err != nil {
		return nil, errors.New("user not found")
	}

	userID = user.ID

	otp, err := repositories.GetOTPByUserID(userID)

	if err != nil {
		return nil, errors.New("otp record not found")
	}

	if otp.EmailOTP != otpValue {
		return map[string]interface{}{
			"success": false,
			"message": "invalid otp",
			"id":      user.ID,
			"email":   user.Email,
		}, nil
	}

	otp.IsEmailVerified = true

	err = repositories.UpdateOTP(otp)

	if err != nil {
		return nil, err
	}

	user.EmailVerified = true

	err = repositories.UpdateEmailVerified(userID)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "Email Verified Successfully",
		"id":      user.ID,
		"email":   user.Email,
	}, nil

}

func Login(req dto.LoginRequest) (map[string]interface{}, error) {

	var user *usermodel.User
	var err error

	if req.Mobile != "" {

		user, err = repositories.FindUserByMobile(req.Mobile)

	} else {

		user, err = repositories.FindUserByEmail(req.Email)

	}

	if err != nil {

		return nil, errors.New("User Not Found....")

	}

	if err := CheckUserLoginAllowed(user); err != nil {
		return nil, err
	}

	if !user.MobileVerified || !user.EmailVerified {

		message := "mobile not verified....!"

		if user.MobileVerified {
			message = "email not verified....!"
		} else if !user.EmailVerified {
			message = "mobile and email not verified....!"
		}

		return map[string]interface{}{
			"success":       false,
			"message":       message,
			"id":            user.ID,
			"mobile":        user.ContactNo,
			"email":         user.Email,
			"mobile_verify": user.MobileVerified,
			"email_verify":  user.EmailVerified,
		}, nil

	}

	// Static outside production, a fresh random code within it. NewOTP is the
	// single place either value comes from, so the environment is decided once
	// rather than re-tested at every call site.
	mobileOTP := MustNewOTP()
	emailOTP := MustNewOTP()

	expireTime := time.Now().Add(OTPExpiry())

	otp, err := repositories.GetOTPByUserID(user.ID)

	if err != nil {

		newOTP := usermodel.UserOTP{
			UserID:      &user.ID,
			Mobile:      user.ContactNo,
			Email:       user.Email,
			MobileOTP:   mobileOTP,
			EmailOTP:    emailOTP,
			MaxAttempts: MaxOTPAttempts,
			ExpiresAt:   &expireTime,
		}

		if err := repositories.CreateOTP(&newOTP); err != nil {
			return nil, err
		}

	} else {

		otp.MobileOTP = mobileOTP
		otp.EmailOTP = emailOTP

		otp.ExpiresAt = &expireTime
		otp.ResendCount = 0

		if otp.MaxAttempts == 0 {
			otp.MaxAttempts = MaxOTPAttempts
		}

		if err := repositories.UpdateOTP(otp); err != nil {
			return nil, err
		}
	}

	DeliverMobileOTP(user.ContactNo, mobileOTP)

	if req.Email != "" {
		DeliverEmailOTP(user.Email, emailOTP)
	}

	return map[string]interface{}{
		"success": true,
		"userId":  user.ID,
		"message": "otp sent successfully",
	}, nil
}

func VerifyLoginOTP(req dto.VerifyLoginOTPRequest) (*dto.LoginResponse, error) {

	var user *usermodel.User
	var err error

	if req.Mobile != "" {
		user, err = repositories.FindUserByMobile(req.Mobile)
	} else {
		user, err = repositories.FindUserByEmail(req.Email)
	}

	if err != nil {
		return nil, errors.New("user not found")
	}

	if err := CheckUserLoginAllowed(user); err != nil {
		return nil, err
	}

	otp, err := repositories.GetOTPByUserID(user.ID)

	if err != nil {
		return nil, errors.New("otp not found")
	}

	if req.OTP != otp.MobileOTP && req.OTP != otp.EmailOTP {
		return nil, errors.New("invalid otp")
	}

	accessToken, _ := GenerateJWT(user.ID, user.Name)

	refreshToken, _ := GenerateRefreshToken(user.ID)

	user.JwtToken = accessToken
	user.RefreshToken = refreshToken

	now := time.Now()
	user.LastLogin = &now

	// Refresh the push token on every login. FCM rotates tokens and a
	// reinstall issues a new one, so whatever was captured at registration
	// goes stale — login is the reliable moment to catch up. Written onto the
	// row about to be saved, so it costs no extra query.
	device := dto.DeviceContext{
		DeviceToken: req.DeviceToken,
		DeviceType:  req.DeviceType,
		Platform:    req.Platform,
	}

	if token := strings.TrimSpace(device.DeviceToken); token != "" {
		user.DeviceToken = token
		user.FcmToken = token
		user.Token = token
	}

	// Platform is already required on this request, so device_type gets
	// filled in even when the app does not send one of its own.
	if deviceType := device.ResolvedDeviceType(); deviceType != "" {
		user.DeviceType = deviceType
	}

	// Must not be ignored: the middleware authenticates by looking this token up
	// in users.jwt_token, so a failed write yields a token that can never be used.
	if err := repositories.UpdateUser(user); err != nil {
		return nil, errors.New("could not complete login, please try again")
	}

	// Save Login History
	loginHistory := usermodel.LoginHistory{
		UserID:     user.ID,
		IPAddress:  req.IPAddress,
		DeviceName: req.DeviceName,
		Platform:   req.Platform,
		LoginAt:    &now,
	}

	err = repositories.SaveLoginHistory(&loginHistory)
	if err != nil {
		return nil, err
	}

	wallet, _ := repositories.FindWalletByUserID(user.ID)
	walletResponse := &dto.UserWalletResponse{
		ID:       wallet.ID,
		UserID:   wallet.UserID,
		IsActive: wallet.IsActive,
		IsDelete: wallet.IsDelete,
	}

	response := dto.LoginResponse{
		Success:      true,
		Token:        accessToken,
		RefreshToken: refreshToken,
		User: dto.UserResponse{
			Id:             user.ID,
			Name:           user.Name,
			Email:          user.Email,
			Password:       user.Password,
			ContactNo:      user.ContactNo,
			CountryCode:    user.CountryCode,
			BirthDate:      user.BirthDate,
			BirthTime:      user.BirthTime,
			BirthPlace:     user.BirthPlace,
			AddressLine1:   user.AddressLine1,
			Location:       user.Location,
			Pincode:        user.Pincode,
			Gender:         user.Gender,
			MobileVerified: user.MobileVerified,
			EmailVerified:  user.EmailVerified,
			JwtToken:       user.JwtToken,
			RefreshToken:   user.RefreshToken,
			ReferralCode:   user.ReferralCode,
			DeviceToken:    user.DeviceToken,
			LastLogin:      user.LastLogin,
			IsActive:       user.IsActive,
			IsDelete:       user.IsDelete,
			RoleID:         user.RoleID,
			UserWallet:     walletResponse,
		},
	}

	return &response, nil
}
func UpdateDeviceToken(userID uint, req dto.UpdateDeviceTokenRequest) (*dto.UpdateDeviceTokenResponse, error) {

	if userID == 0 {
		return nil, errors.New("unauthorized")
	}

	token := strings.TrimSpace(req.DeviceToken)

	if token == "" {
		return nil, errors.New("device_token is required")
	}

	if err := repositories.SaveDeviceToken(userID, token, req.DeviceType); err != nil {
		return nil, errors.New("could not save the device token, please try again")
	}

	return &dto.UpdateDeviceTokenResponse{
		UserID:      userID,
		DeviceToken: token,
		DeviceType:  strings.TrimSpace(req.DeviceType),
		Updated:     true,
	}, nil
}

func ResendOTP(userID uint) (map[string]interface{}, error) {
	otp, err := repositories.GetOTPByUserID(userID)
	if err != nil {
		return nil, errors.New("otp record not found")
	}

	// A row written by the login path carries no MaxAttempts, and comparing
	// against that zero rejected the very first resend with "maximum resend
	// limit reached".
	maxResends := otp.MaxAttempts

	if maxResends == 0 {
		maxResends = MaxOTPAttempts
	}

	if otp.ResendCount >= maxResends {
		return nil, errors.New("maximum resend limit reached")
	}

	// Static outside production, a fresh random code within it. NewOTP is the
	// single place either value comes from, so the environment is decided once
	// rather than re-tested at every call site.
	mobileOTP := MustNewOTP()
	emailOTP := MustNewOTP()

	otp.MobileOTP = mobileOTP
	otp.EmailOTP = emailOTP
	otp.ResendCount++
	otp.MaxAttempts = maxResends

	expireTime := time.Now().Add(OTPExpiry())
	otp.ExpiresAt = &expireTime

	if err := repositories.UpdateOTP(otp); err != nil {
		return nil, err
	}

	// The email used to be sent the static code rather than the one just
	// stored, so in production the mail carried 123456 while the row held a
	// random value and the verification could never match.
	DeliverMobileOTP(otp.Mobile, mobileOTP)
	DeliverEmailOTP(otp.Email, emailOTP)

	return map[string]interface{}{
		"success": true,
		"message": "otp resent successfully",
	}, nil
}

func ForgotPassword(req dto.ForgotPasswordRequest) (map[string]interface{}, error) {

	var user *usermodel.User
	var err error

	if req.Mobile != "" {
		user, err = repositories.FindUserByMobile(req.Mobile)
	} else {
		user, err = repositories.FindUserByEmail(req.Email)
	}

	if err != nil {
		return nil, errors.New("user not found")
	}

	mobileOTP := MustNewOTP()
	emailOTP := MustNewOTP()

	expireTime := time.Now().Add(OTPExpiry())

	otp, err := repositories.GetOTPByUserID(user.ID)

	if err != nil {

		// An account with no OTP row used to be sent a code that was never
		// stored, and /reset-password then answered "otp not found" - the
		// reset was impossible to complete. The row is written instead.
		newOTP := usermodel.UserOTP{
			UserID:      &user.ID,
			Mobile:      user.ContactNo,
			Email:       user.Email,
			MobileOTP:   mobileOTP,
			EmailOTP:    emailOTP,
			MaxAttempts: MaxOTPAttempts,
			ExpiresAt:   &expireTime,
		}

		if err := repositories.CreateOTP(&newOTP); err != nil {
			return nil, err
		}

	} else {

		otp.MobileOTP = mobileOTP
		otp.EmailOTP = emailOTP
		otp.ExpiresAt = &expireTime
		otp.ResendCount = 0

		if otp.MaxAttempts == 0 {
			otp.MaxAttempts = MaxOTPAttempts
		}

		if err := repositories.UpdateOTP(otp); err != nil {
			return nil, err
		}
	}

	// The code that was actually stored. The email leg used to be handed the
	// static value regardless of environment, and only goes out when the
	// request was made with an email.
	DeliverMobileOTP(user.ContactNo, mobileOTP)

	if req.Email != "" {
		DeliverEmailOTP(user.Email, emailOTP)
	}

	return map[string]interface{}{
		"success": true,
		"message": "otp sent successfully",
		"userId":  user.ID,
	}, nil
}

func ResetPassword(req dto.ResetPasswordRequest) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(req.UserID)

	if err != nil {
		return nil, errors.New("user not found")
	}

	otp, err := repositories.GetOTPByUserID(req.UserID)

	if err != nil {
		return nil, errors.New("otp not found")
	}

	if req.OTP != otp.MobileOTP && req.OTP != otp.EmailOTP {
		return nil, errors.New("invalid otp")
	}

	hashPassword, err := helpers.HashPassword(req.NewPassword)

	if err != nil {
		return nil, err
	}

	user.Password = hashPassword

	err = repositories.UpdateUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "password reset successfully",
	}, nil
}

func Profile(userID uint) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(userID)

	if err != nil {
		return nil, errors.New("user not found")
	}

	wallet, _ := repositories.FindWalletByUserID(user.ID)

	response := dto.ProfileResponse{
		Id:             user.ID,
		Name:           user.Name,
		Email:          user.Email,
		ContactNo:      user.ContactNo,
		CountryCode:    user.CountryCode,
		BirthDate:      user.BirthDate,
		BirthTime:      user.BirthTime,
		BirthPlace:     user.BirthPlace,
		AddressLine1:   user.AddressLine1,
		Location:       user.Location,
		Pincode:        user.Pincode,
		Gender:         user.Gender,
		MobileVerified: user.MobileVerified,
		EmailVerified:  user.EmailVerified,
		ReferralCode:   user.ReferralCode,
		DeviceToken:    user.DeviceToken,
		LastLogin:      user.LastLogin,
		IsActive:       user.IsActive,
		RoleID:         user.RoleID,
		ProfileImage:   user.Profile,
	}

	if wallet != nil {

		amount := float64(0)

		if wallet.Amount != nil {
			amount = *wallet.Amount
		}

		response.UserWallet = &dto.WalletResponse{
			ID:     wallet.ID,
			UserID: wallet.UserID,
			Amount: amount,
		}
	}

	return map[string]interface{}{
		"success": true,
		"user":    response,
	}, nil
}

func Logout(userID uint) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(userID)

	if err != nil {
		return nil, err
	}

	user.JwtToken = ""
	user.RefreshToken = ""

	err = repositories.UpdateUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "logout successful",
	}, nil
}

func RefreshUserToken(userID uint) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(userID)

	if err != nil {
		return nil, err
	}

	accessToken, err := GenerateJWT(
		user.ID,
		user.Name,
	)

	if err != nil {
		return nil, err
	}

	refreshToken, err := GenerateRefreshToken(
		user.ID,
	)

	if err != nil {
		return nil, err
	}

	user.JwtToken = accessToken
	user.RefreshToken = refreshToken

	err = repositories.UpdateUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success":       true,
		"token":         accessToken,
		"refresh_token": refreshToken,
	}, nil
}

func UpdateProfile(req dto.UpdateProfileRequest) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(req.ID)

	if err != nil {
		return nil, errors.New("user not found")
	}

	user.Name = req.Name
	user.BirthDate = req.BirthDate
	user.BirthTime = req.BirthTime
	user.BirthPlace = req.BirthPlace
	user.AddressLine1 = req.AddressLine1
	user.Location = req.Location
	user.Pincode = req.Pincode
	user.Gender = req.Gender

	err = repositories.UpdateUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "User updated successfully",
	}, nil
}

func UpdateUserStatus(req dto.UpdateUserStatusRequest) (map[string]interface{}, error) {

	user, err := repositories.FindUserByID(req.ID)

	if err != nil {
		return nil, errors.New("user not found")
	}

	user.IsActive = req.IsActive

	err = repositories.UpdateUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "User status changed successfully",
	}, nil
}

func DeleteUser(req dto.DeleteUserRequest, loginUserID uint) (map[string]interface{}, error) {

	var userID uint

	if req.ID != 0 {
		userID = req.ID
	} else {
		userID = loginUserID
	}

	user, err := repositories.FindUserByID(userID)

	if err != nil {
		return nil, errors.New("no user found")
	}

	err = repositories.DeleteUser(user)

	if err != nil {
		return nil, err
	}

	return map[string]interface{}{
		"success": true,
		"message": "User deleted successfully",
	}, nil
}

func UploadProfileImage(userID uint, imageURL string) (*dto.UploadProfileResponse, error) {
	err := repositories.UpdateProfileImage(userID, imageURL)
	if err != nil {
		return nil, err
	}
	return &dto.UploadProfileResponse{
		Success: true, Message: "Profile image uploaded successfully", ProfileImage: imageURL,
	}, nil
}

func RemoveProfileImage(userID uint) (*dto.RemoveProfileImageResponse, error) {

	user, err := repositories.FindUserByID(userID)

	if err != nil {
		return nil, errors.New("user not found")
	}

	if user.Profile == "" {
		return nil, errors.New("profile image not found")
	}

	// The column holds a public URL, so map it back to the local file before
	// clearing the row. A missing file must not block the removal.
	if localPath := helpers.LocalPathFromPublicURL(user.Profile); localPath != "" {

		if _, statErr := os.Stat(localPath); statErr == nil {
			_ = os.Remove(localPath)
		}
	}

	if err := repositories.RemoveProfileImage(userID); err != nil {
		return nil, err
	}

	return &dto.RemoveProfileImageResponse{
		Success: true, Message: "Profile image removed successfully", ProfileImage: "",
	}, nil
}
