package repositories_astrologer

import (
	config "astrology-api/configs"
	"astrology-api/constants"
	dto_astrologer "astrology-api/dto_astrologer"
	models "astrology-api/models/astrologermodel"
	"errors"
	"time"

	"gorm.io/gorm"
)

type AuthRepository interface {

	// Transaction
	BeginTx() *gorm.DB
	Commit(tx *gorm.DB) error
	Rollback(tx *gorm.DB)

	// User
	FindLoginUserByMobile(tx *gorm.DB, mobile string) (*models.User, error)
	FindUserByMobile(tx *gorm.DB, mobile string) (*models.User, error)
	FindUserByEmail(tx *gorm.DB, email string) (*models.User, error)
	CreateUser(tx *gorm.DB, user *models.User) error
	// User Role
	CreateUserRole(tx *gorm.DB, role *models.UserRole) error
	// Wallet
	CreateWallet(tx *gorm.DB, wallet *models.UserWallet) error
	// OTP
	CreateUserOTP(tx *gorm.DB, otp *models.UserOTP) error

	FindOTPByMobile(tx *gorm.DB, mobile string) (*models.UserOTP, error)

	UpdateOTP(tx *gorm.DB, otp *models.UserOTP) error

	IncrementOTPAttempt(otpID uint, channel string) error

	UpdateUser(tx *gorm.DB, user *models.User) error

	UpdateUserToken(tx *gorm.DB, user *models.User) error

	FindUserByID(tx *gorm.DB, id uint) (*models.User, error)

	FindOTPByEmail(tx *gorm.DB, email string) (*models.UserOTP, error)

	FindOTPByUserID(tx *gorm.DB, userID uint) (*models.UserOTP, error)

	FindAstrologerByUserID(tx *gorm.DB, userID uint) (*models.Astrologer, error)
	UpdateDeviceInfo(tx *gorm.DB, userID uint, deviceType string, deviceToken string) error

	UpdateLastLogin(tx *gorm.DB, userID uint) error
	SaveLoginHistory(tx *gorm.DB, history *models.LoginHistory) error
	GetAstrologerProfileByUserID(tx *gorm.DB, userID uint) (*models.Astrologer, error)
	LogoutUser(tx *gorm.DB, userID uint) error
	GetLoggedInProfile(tx *gorm.DB, userID uint) (*models.Astrologer, error)

	// GetProfileMeta supplies what the astrologers row cannot: the skill and
	// language names behind its denormalised id columns, the pincode from the
	// users row, and the rating/follower/session counters.
	GetProfileMeta(tx *gorm.DB, astrologer *models.Astrologer) (*dto_astrologer.ProfileMeta, error)

	UpdateDeviceToken(tx *gorm.DB, userID uint, deviceType string, deviceToken string) error
}

type authRepository struct {
	db *gorm.DB
}

func NewAuthRepository() AuthRepository {

	return &authRepository{
		db: config.DB,
	}

}

func (r *authRepository) BeginTx() *gorm.DB {

	return r.db.Begin()

}

func (r *astrologerRepository) GetUserByID(userID uint) (*models.User, error) {

	var user models.User

	err := r.db.First(&user, userID).Error

	if err != nil {
		return nil, err
	}

	return &user, nil
}

func (r *authRepository) FindLoginUserByMobile(tx *gorm.DB, mobile string) (*models.User, error) {

	var user models.User

	err := tx.
		Where("contactNo = ? AND role_id = ?", mobile, constants.RoleAstrologer).
		First(&user).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &user, nil

}

func (r *authRepository) FindUserByMobile(tx *gorm.DB, mobile string) (*models.User, error) {

	var user models.User

	err := tx.
		Where("contactNo = ? ", mobile).
		First(&user).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &user, nil

}

func (r *authRepository) FindUserByEmail(tx *gorm.DB, email string) (*models.User, error) {

	var user models.User

	err := tx.
		Where("email = ? ", email).
		First(&user).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &user, nil

}

func (r *authRepository) CreateUser(tx *gorm.DB, user *models.User) error {

	return tx.Create(user).Error

}

func (r *authRepository) CreateUserRole(tx *gorm.DB, role *models.UserRole) error {

	return tx.Create(role).Error

}

func (r *authRepository) CreateWallet(tx *gorm.DB, wallet *models.UserWallet) error {

	return tx.Create(wallet).Error

}

func (r *authRepository) CreateUserOTP(tx *gorm.DB, otp *models.UserOTP) error {

	return tx.Create(otp).Error

}

func (r *authRepository) Commit(tx *gorm.DB) error {
	return tx.Commit().Error
}

func (r *authRepository) Rollback(tx *gorm.DB) {
	if tx != nil {
		tx.Rollback()
	}
}

// FindOTPByMobile returns the OTP row for a mobile, or nil when there is none.
//
// It used to return gorm.ErrRecordNotFound, which the mobile verify turned into
// "otp not found" but the resend path could not tell apart from a real database
// error. The email lookup already returns nil, nil; this now matches it.
//
// The astrologer's own row is preferred, because the users table is shared with
// the user app and the same mobile can carry an OTP row on either side. The
// unscoped lookup remains as a fallback so a row written without a user_id still
// resolves the way it always did.
func (r *authRepository) FindOTPByMobile(tx *gorm.DB, mobile string) (*models.UserOTP, error) {

	return findOTP(tx, "mobile = ?", mobile)
}

func (r *authRepository) UpdateOTP(tx *gorm.DB, otp *models.UserOTP) error {

	return tx.Save(otp).Error

}

// IncrementOTPAttempt records one failed OTP guess.
//
// It deliberately runs on the base connection rather than a caller's
// transaction. Every verify path rejects a wrong OTP by rolling back, and a
// counter written inside that transaction was rolled back with it — so the
// attempts columns never moved and the lockout never engaged, no matter how
// many wrong guesses arrived.
//
// The increment is expressed in SQL rather than read-modify-write so two
// concurrent guesses cannot overwrite each other's count.
func (r *authRepository) IncrementOTPAttempt(otpID uint, channel string) error {

	column := "mobile_attempts"

	if channel == "email" {
		column = "email_attempts"
	}

	return r.db.
		Model(&models.UserOTP{}).
		Where("id = ?", otpID).
		UpdateColumn(column, gorm.Expr(column+" + 1")).Error
}

func (r *authRepository) FindUserByID(tx *gorm.DB, id uint) (*models.User, error) {

	var user models.User

	err := tx.First(&user, id).Error

	if err != nil {

		return nil, err

	}

	return &user, nil

}

func (r *authRepository) UpdateUser(tx *gorm.DB, user *models.User) error {

	return tx.Save(user).Error

}

func (r *authRepository) FindOTPByEmail(tx *gorm.DB, email string) (*models.UserOTP, error) {

	return findOTP(tx, "email = ?", email)
}

// findOTP resolves one OTP row, preferring the newest that belongs to an
// astrologer account and falling back to the newest row overall.
//
// Both lookups return nil, nil when there is nothing, so callers can tell an
// absent row from a database failure.
func findOTP(tx *gorm.DB, condition string, value string) (*models.UserOTP, error) {

	astrologerIDs := tx.
		Model(&models.User{}).
		Select("id").
		Where("role_id = ?", constants.RoleAstrologer)

	var otp models.UserOTP

	err := tx.
		Where(condition, value).
		Where("user_id IN (?)", astrologerIDs).
		Order("id DESC").
		First(&otp).Error

	if err == nil {
		return &otp, nil
	}

	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	err = tx.
		Where(condition, value).
		Order("id DESC").
		First(&otp).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &otp, nil
}

func (r *authRepository) FindOTPByUserID(tx *gorm.DB, userID uint) (*models.UserOTP, error) {

	var otp models.UserOTP

	err := tx.
		Where("user_id=?", userID).
		First(&otp).Error

	if err != nil {
		return nil, err
	}

	return &otp, nil
}

func (r *authRepository) UpdateUserToken(tx *gorm.DB, user *models.User) error {

	return tx.Model(&models.User{}).
		Where("id = ?", user.ID).
		Updates(map[string]interface{}{
			"jwt_token":     user.JwtToken,
			"refresh_token": user.RefreshToken,
			"last_login":    time.Now(),
		}).Error
}

func (r *authRepository) FindAstrologerByUserID(tx *gorm.DB, userID uint) (*models.Astrologer, error) {

	var astrologer models.Astrologer
	err := tx.Where("userId = ?", userID).First(&astrologer).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return &astrologer, nil
}

// UpdateDeviceInfo records the device a registration or login came from.
//
// deviceType was accepted and then dropped on the floor here, so only
// /device-token ever stored it. Both columns are written by name rather than
// through models.User, which carries no DeviceType field.
func (r *authRepository) UpdateDeviceInfo(tx *gorm.DB, userID uint, deviceType string, deviceToken string) error {

	values := map[string]interface{}{
		"device_token": deviceToken,
		"fcm_token":    deviceToken,
		"token":        deviceToken,
	}

	// An empty device_type means the client did not send one; keep whatever is
	// already on the row rather than blanking it.
	if deviceType != "" {
		values["device_type"] = deviceType
	}

	return tx.Model(&models.User{}).Where("id=?", userID).Updates(values).Error
}

func (r *authRepository) UpdateLastLogin(tx *gorm.DB, userID uint) error {
	return tx.Model(&models.User{}).
		Where("id = ?", userID).
		Update("last_login", time.Now()).Error
}

func (r *authRepository) SaveLoginHistory(tx *gorm.DB, history *models.LoginHistory) error {
	return tx.Create(history).Error
}

func (r *authRepository) GetAstrologerProfileByUserID(
	tx *gorm.DB,
	userID uint,
) (*models.Astrologer, error) {

	var astrologer models.Astrologer

	err := tx.
		Preload("Languages").
		Preload("Skills").
		Preload("Educations").
		Preload("Professionals").
		Preload("Experiences").
		Preload("BankAccount").
		Preload("Documents").
		Preload("Progress").
		Where("userId = ?", userID).
		First(&astrologer).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.New("astrologer profile not found")
		}

		return nil, err
	}

	return &astrologer, nil
}

func (r *authRepository) LogoutUser(tx *gorm.DB, userID uint) error {

	return tx.Model(&models.User{}).
		Where("id = ?", userID).
		Updates(map[string]interface{}{
			"jwt_token":     "",
			"refresh_token": "",
			"device_token":  "",
			"fcm_token":     "",
			"token":         "",
		}).Error
}

func (r *authRepository) GetLoggedInProfile(
	tx *gorm.DB,
	userID uint,
) (*models.Astrologer, error) {

	var astrologer models.Astrologer

	err := tx.
		// Languages.Language, not Languages: the join rows came back with an
		// empty Language master object ("LanguageName": ""), because the
		// nested belongs-to was never preloaded.
		Preload("Languages.Language").
		Preload("Skills.Skill").
		Preload("SocialLinks").
		Preload("Educations").
		Preload("Professionals").
		Preload("Experiences").
		Preload("BankAccount").
		Preload("Documents").
		Preload("Availability").
		Preload("Progress").
		Preload("ProfileCompletion").
		Where("userId = ?", userID).
		First(&astrologer).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.New("astrologer profile not found")
		}

		return nil, err
	}

	return &astrologer, nil
}

func (r *authRepository) UpdateDeviceToken(tx *gorm.DB, userID uint, deviceType string, deviceToken string) error {

	return tx.Model(&models.User{}).
		Where("id = ?", userID).
		Updates(map[string]interface{}{
			"device_type":  deviceType,
			"device_token": deviceToken,
			"fcm_token":    deviceToken,
			"token":        deviceToken,
		}).Error
}
