package mysql

import (
	"context"
	"errors"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"wucher/internal/domain/auth"
)

type AuthRepository struct {
	db *gorm.DB
}

func NewAuthRepository(db *gorm.DB) *AuthRepository {
	return &AuthRepository{db: db}
}

// Users

func (r *AuthRepository) CreateUser(ctx context.Context, user *auth.User) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
			user.CreatedBy = actor
			user.UpdatedBy = actor
		}
		if err := tx.Create(user).Error; err != nil {
			return err
		}
		return replaceUserRolesTx(ctx, tx, user.ID, user.RoleID, user.RoleIDs)
	})
}

func (r *AuthRepository) GetUserByID(ctx context.Context, id []byte) (*auth.User, error) {
	var user auth.User
	err := r.db.WithContext(ctx).
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		First(&user, "id = ?", id).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachAuthUserRoles(ctx, r.db, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *AuthRepository) GetUserByEmail(ctx context.Context, email string) (*auth.User, error) {
	var user auth.User
	err := r.db.WithContext(ctx).
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("email = ?", email).
		First(&user).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachAuthUserRoles(ctx, r.db, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *AuthRepository) GetUserBySSOEmail(ctx context.Context, ssoEmail string) (*auth.User, error) {
	var user auth.User
	err := r.db.WithContext(ctx).
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("sso_email = ?", ssoEmail).
		First(&user).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachAuthUserRoles(ctx, r.db, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *AuthRepository) GetUserByUsername(ctx context.Context, username string) (*auth.User, error) {
	var user auth.User
	err := r.db.WithContext(ctx).
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("username = ?", username).
		First(&user).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachAuthUserRoles(ctx, r.db, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *AuthRepository) UpdateUserTimezone(ctx context.Context, id []byte, timezone string) error {
	return r.db.WithContext(ctx).
		Model(&auth.User{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"timezone":   timezone,
			"updated_at": time.Now().UTC(),
		}).Error
}

func (r *AuthRepository) SetUserPassword(ctx context.Context, id []byte, passwordHash, salt []byte) error {
	return r.db.WithContext(ctx).
		Model(&auth.User{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"password_hash": passwordHash,
			"salt_value":    salt,
			"updated_at":    time.Now().UTC(),
		}).Error
}

func (r *AuthRepository) SetUserSecurityPIN(ctx context.Context, id []byte, pinHash []byte, setAt time.Time) error {
	return r.db.WithContext(ctx).
		Model(&auth.User{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"security_pin_hash":   pinHash,
			"security_pin_set_at": setAt,
			"updated_at":          setAt,
		}).Error
}

func (r *AuthRepository) MarkEmailVerified(ctx context.Context, id []byte) error {
	now := time.Now().UTC()
	return r.db.WithContext(ctx).
		Model(&auth.User{}).
		Where("id = ?", id).
		Update("email_verified_at", &now).Error
}

func (r *AuthRepository) CreatePasswordResetToken(ctx context.Context, token *auth.PasswordResetToken) error {
	return r.db.WithContext(ctx).Create(token).Error
}

func (r *AuthRepository) InvalidateActivePasswordResetTokensByUserID(ctx context.Context, userID []byte, usedAt time.Time) error {
	return r.db.WithContext(ctx).
		Model(&auth.PasswordResetToken{}).
		Where("user_id = ? AND used_at IS NULL AND expires_at > ?", userID, usedAt).
		Updates(map[string]any{
			"used_at":    usedAt,
			"updated_at": usedAt,
		}).Error
}

func (r *AuthRepository) ConsumePasswordResetTokenAndSetPassword(
	ctx context.Context,
	tokenHash, passwordHash, salt []byte,
	usedAt time.Time,
) ([]byte, bool, error) {
	var token auth.PasswordResetToken
	consumed := false

	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		err := tx.
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("token_hash = ? AND used_at IS NULL AND expires_at > ?", tokenHash, usedAt).
			First(&token).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		if err != nil {
			return err
		}
		consumed = true

		res := tx.Model(&auth.User{}).
			Where("id = ?", token.UserID).
			Updates(map[string]any{
				"password_hash": passwordHash,
				"salt_value":    salt,
				"updated_at":    usedAt,
			})
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected == 0 {
			return gorm.ErrRecordNotFound
		}

		if err := tx.Model(&auth.PasswordResetToken{}).
			Where("id = ?", token.ID).
			Updates(map[string]any{
				"used_at":    usedAt,
				"updated_at": usedAt,
			}).Error; err != nil {
			return err
		}

		return tx.Model(&auth.PasswordResetToken{}).
			Where("user_id = ? AND id <> ? AND used_at IS NULL AND expires_at > ?", token.UserID, token.ID, usedAt).
			Updates(map[string]any{
				"used_at":    usedAt,
				"updated_at": usedAt,
			}).Error
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, false, nil
		}
		return nil, false, err
	}
	if !consumed {
		return nil, false, nil
	}
	return token.UserID, true, nil
}

func (r *AuthRepository) CreateSecurityPINResetToken(ctx context.Context, token *auth.SecurityPINResetToken) error {
	return r.db.WithContext(ctx).Create(token).Error
}

func (r *AuthRepository) InvalidateActiveSecurityPINResetTokensByUserID(ctx context.Context, userID []byte, usedAt time.Time) error {
	return r.db.WithContext(ctx).
		Model(&auth.SecurityPINResetToken{}).
		Where("user_id = ? AND used_at IS NULL AND expires_at > ?", userID, usedAt).
		Updates(map[string]any{
			"used_at":    usedAt,
			"updated_at": usedAt,
		}).Error
}

func (r *AuthRepository) GetActiveSecurityPINResetTokenUser(ctx context.Context, tokenHash []byte, now time.Time) (*auth.User, error) {
	var user auth.User
	err := r.db.WithContext(ctx).
		Table("security_pin_reset_tokens AS token").
		Select("users.*").
		Joins("JOIN users ON users.id = token.user_id").
		Where("token.token_hash = ? AND token.used_at IS NULL AND token.expires_at > ?", tokenHash, now).
		First(&user).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &user, err
}

func (r *AuthRepository) ConsumeSecurityPINResetTokenAndSetPIN(
	ctx context.Context,
	tokenHash, pinHash []byte,
	usedAt time.Time,
) ([]byte, bool, error) {
	var token auth.SecurityPINResetToken
	consumed := false

	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		err := tx.
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("token_hash = ? AND used_at IS NULL AND expires_at > ?", tokenHash, usedAt).
			First(&token).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		if err != nil {
			return err
		}
		consumed = true

		res := tx.Model(&auth.User{}).
			Where("id = ?", token.UserID).
			Updates(map[string]any{
				"security_pin_hash":   pinHash,
				"security_pin_set_at": usedAt,
				"updated_at":          usedAt,
			})
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected == 0 {
			return gorm.ErrRecordNotFound
		}

		if err := tx.Model(&auth.SecurityPINResetToken{}).
			Where("id = ?", token.ID).
			Updates(map[string]any{
				"used_at":    usedAt,
				"updated_at": usedAt,
			}).Error; err != nil {
			return err
		}

		return tx.Model(&auth.SecurityPINResetToken{}).
			Where("user_id = ? AND id <> ? AND used_at IS NULL AND expires_at > ?", token.UserID, token.ID, usedAt).
			Updates(map[string]any{
				"used_at":    usedAt,
				"updated_at": usedAt,
			}).Error
	})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, false, nil
		}
		return nil, false, err
	}
	if !consumed {
		return nil, false, nil
	}
	return token.UserID, true, nil
}

// Identities (SSO)

func (r *AuthRepository) UpsertIdentity(ctx context.Context, identity *auth.UserIdentity) error {
	// Ensure we keep a single row per provider+subject
	return r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns:   []clause.Column{{Name: "provider"}, {Name: "provider_subject"}},
			DoUpdates: clause.AssignmentColumns([]string{"user_id", "email", "metadata", "updated_at", "updated_by"}),
		}).
		Create(identity).Error
}

func (r *AuthRepository) CreateIdentity(ctx context.Context, identity *auth.UserIdentity) error {
	return r.db.WithContext(ctx).Create(identity).Error
}

func (r *AuthRepository) GetIdentityByProviderSubject(ctx context.Context, provider, subject string) (*auth.UserIdentity, error) {
	var identity auth.UserIdentity
	tx := r.db.WithContext(ctx).
		Where("provider = ? AND provider_subject = ?", provider, subject).
		Limit(1).
		Find(&identity)
	if tx.Error != nil {
		return nil, tx.Error
	}
	if tx.RowsAffected == 0 {
		return nil, nil
	}
	return &identity, nil
}

func (r *AuthRepository) GetIdentityByUserID(ctx context.Context, userID []byte) (*auth.UserIdentity, error) {
	var identity auth.UserIdentity
	tx := r.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Limit(1).
		Find(&identity)
	if tx.Error != nil {
		return nil, tx.Error
	}
	if tx.RowsAffected == 0 {
		return nil, nil
	}
	return &identity, nil
}

func (r *AuthRepository) DeleteIdentityByUserIDProvider(ctx context.Context, userID []byte, provider string) (bool, error) {
	tx := r.db.WithContext(ctx).
		Where("user_id = ? AND provider = ?", userID, provider).
		Delete(&auth.UserIdentity{})
	if tx.Error != nil {
		return false, tx.Error
	}
	return tx.RowsAffected > 0, nil
}

func (r *AuthRepository) UpsertMicrosoftOAuthToken(ctx context.Context, token *auth.UserMicrosoftOAuthToken) error {
	return r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "user_id"}, {Name: "provider"}},
			DoUpdates: clause.AssignmentColumns([]string{
				"tenant_id",
				"microsoft_object_id",
				"microsoft_subject",
				"refresh_token_encrypted",
				"access_token_encrypted",
				"id_token_encrypted",
				"scope",
				"token_type",
				"access_token_expires_at",
				"refresh_token_expires_at",
				"refresh_token_rotated_at",
				"updated_at",
				"updated_by",
			}),
		}).
		Create(token).Error
}

func (r *AuthRepository) GetMicrosoftOAuthTokenByUserIDProvider(ctx context.Context, userID []byte, provider string) (*auth.UserMicrosoftOAuthToken, error) {
	var token auth.UserMicrosoftOAuthToken
	tx := r.db.WithContext(ctx).
		Where("user_id = ? AND provider = ?", userID, provider).
		Limit(1).
		Find(&token)
	if tx.Error != nil {
		return nil, tx.Error
	}
	if tx.RowsAffected == 0 {
		return nil, nil
	}
	return &token, nil
}

func (r *AuthRepository) DeleteMicrosoftOAuthTokenByUserIDProvider(ctx context.Context, userID []byte, provider string) (bool, error) {
	tx := r.db.WithContext(ctx).
		Where("user_id = ? AND provider = ?", userID, provider).
		Delete(&auth.UserMicrosoftOAuthToken{})
	if tx.Error != nil {
		return false, tx.Error
	}
	return tx.RowsAffected > 0, nil
}

// TOTP

func (r *AuthRepository) GetUserTOTP(ctx context.Context, userID []byte) (*auth.UserTOTP, error) {
	var totp auth.UserTOTP
	tx := r.db.WithContext(ctx).Where("user_id = ?", userID).Limit(1).Find(&totp)
	if tx.Error != nil {
		return nil, tx.Error
	}
	if tx.RowsAffected == 0 {
		return nil, nil
	}
	return &totp, nil
}

func (r *AuthRepository) UpsertUserTOTP(ctx context.Context, totp *auth.UserTOTP) error {
	return r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns:   []clause.Column{{Name: "user_id"}},
			DoUpdates: clause.AssignmentColumns([]string{"secret_encrypted", "enabled", "updated_at", "updated_by"}),
		}).
		Create(totp).Error
}

func (r *AuthRepository) DisableUserTOTP(ctx context.Context, userID []byte) error {
	return r.db.WithContext(ctx).
		Model(&auth.UserTOTP{}).
		Where("user_id = ?", userID).
		Updates(map[string]any{
			"enabled":    false,
			"updated_at": time.Now().UTC(),
		}).Error
}

func (r *AuthRepository) DeleteUserTOTPSetup(ctx context.Context, userID []byte) error {
	return r.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Delete(&auth.UserTOTP{}).Error
}

func (r *AuthRepository) UpdateUserTOTPLastUsed(ctx context.Context, userID []byte) error {
	return r.db.WithContext(ctx).
		Model(&auth.UserTOTP{}).
		Where("user_id = ? AND enabled = 1", userID).
		Update("last_used_at", time.Now().UTC()).Error
}

// Email login OTP

func (r *AuthRepository) GetActiveUserEmailLoginOTP(ctx context.Context, userID []byte, now time.Time) (*auth.UserEmailLoginOTP, error) {
	var row auth.UserEmailLoginOTP
	tx := r.db.WithContext(ctx).
		Where("user_id = ? AND used_at IS NULL AND expires_at > ?", userID, now.UTC()).
		Order("created_at DESC").
		Limit(1).
		Find(&row)
	if tx.Error != nil {
		return nil, tx.Error
	}
	if tx.RowsAffected == 0 {
		return nil, nil
	}
	return &row, nil
}

func (r *AuthRepository) CountUserEmailLoginOTPSince(ctx context.Context, userID []byte, since time.Time) (int64, error) {
	var count int64
	err := r.db.WithContext(ctx).
		Model(&auth.UserEmailLoginOTP{}).
		Where("user_id = ? AND created_at >= ?", userID, since.UTC()).
		Count(&count).Error
	return count, err
}

func (r *AuthRepository) CreateUserEmailLoginOTP(ctx context.Context, otp *auth.UserEmailLoginOTP) error {
	return r.db.WithContext(ctx).Create(otp).Error
}

func (r *AuthRepository) InvalidateActiveUserEmailLoginOTPs(ctx context.Context, userID []byte, usedAt time.Time) error {
	return r.db.WithContext(ctx).
		Model(&auth.UserEmailLoginOTP{}).
		Where("user_id = ? AND used_at IS NULL AND expires_at > ?", userID, usedAt.UTC()).
		Updates(map[string]any{
			"used_at":    usedAt.UTC(),
			"updated_at": usedAt.UTC(),
		}).Error
}

func (r *AuthRepository) ConsumeUserEmailLoginOTP(ctx context.Context, userID, otpHash []byte, now time.Time) (bool, int, error) {
	success := false
	attemptsLeft := 0
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var row auth.UserEmailLoginOTP
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("user_id = ? AND used_at IS NULL AND expires_at > ?", userID, now.UTC()).
			Order("created_at DESC").
			Limit(1).
			First(&row).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		if err != nil {
			return err
		}

		if string(row.OTPHash) == string(otpHash) {
			success = true
			attemptsLeft = max(0, row.MaxAttempts-row.AttemptCount)
			if err := tx.Model(&auth.UserEmailLoginOTP{}).
				Where("id = ?", row.ID).
				Updates(map[string]any{
					"used_at":    now.UTC(),
					"updated_at": now.UTC(),
				}).Error; err != nil {
				return err
			}
			return tx.Model(&auth.UserEmailLoginOTP{}).
				Where("user_id = ? AND id <> ? AND used_at IS NULL AND expires_at > ?", userID, row.ID, now.UTC()).
				Updates(map[string]any{
					"used_at":    now.UTC(),
					"updated_at": now.UTC(),
				}).Error
		}

		nextAttempts := row.AttemptCount + 1
		attemptsLeft = max(0, row.MaxAttempts-nextAttempts)
		updates := map[string]any{
			"attempt_count": nextAttempts,
			"updated_at":    now.UTC(),
		}
		if nextAttempts >= row.MaxAttempts {
			updates["used_at"] = now.UTC()
		}
		return tx.Model(&auth.UserEmailLoginOTP{}).
			Where("id = ?", row.ID).
			Updates(updates).Error
	})
	return success, attemptsLeft, err
}

// WebAuthn

func (r *AuthRepository) ListUserWebAuthnCredentials(ctx context.Context, userID []byte) ([]auth.UserWebAuthnCredential, error) {
	var credentials []auth.UserWebAuthnCredential
	if err := r.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Order("created_at ASC").
		Find(&credentials).Error; err != nil {
		return nil, err
	}
	return credentials, nil
}

func (r *AuthRepository) CreateUserWebAuthnCredential(ctx context.Context, credential *auth.UserWebAuthnCredential) error {
	return r.db.WithContext(ctx).Create(credential).Error
}

func (r *AuthRepository) UpdateUserWebAuthnCredential(ctx context.Context, userID, credentialID, credentialJSON []byte, usedAt time.Time) error {
	return r.db.WithContext(ctx).
		Model(&auth.UserWebAuthnCredential{}).
		Where("user_id = ? AND credential_id = ?", userID, credentialID).
		Updates(map[string]any{
			"credential_json": credentialJSON,
			"last_used_at":    usedAt,
			"updated_at":      usedAt,
		}).Error
}

func (r *AuthRepository) DeleteAllUserWebAuthnCredentials(ctx context.Context, userID []byte) error {
	return r.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Delete(&auth.UserWebAuthnCredential{}).Error
}
