RatingHistoryService.kt

package delta.codecharacter.server.user.rating_history

import delta.codecharacter.dtos.RatingHistoryDto
import delta.codecharacter.server.logic.rating.GlickoRating
import delta.codecharacter.server.logic.rating.RatingAlgorithm
import delta.codecharacter.server.match.MatchEntity
import delta.codecharacter.server.match.MatchVerdictEnum
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.Instant
import java.util.UUID

@Service
class RatingHistoryService(
    @Autowired private val ratingHistoryRepository: RatingHistoryRepository,
    @Autowired private val ratingAlgorithm: RatingAlgorithm
) {
    fun create(userId: UUID) {
        val ratingHistory =
            RatingHistoryEntity(
                userId = userId, rating = 1500.0, ratingDeviation = 350.0, validFrom = Instant.now()
            )
        ratingHistoryRepository.save(ratingHistory)
    }

    fun getRatingHistory(userId: UUID): List<RatingHistoryDto> {
        return ratingHistoryRepository.findAllByUserId(userId).map {
            RatingHistoryDto(
                rating = BigDecimal(it.rating),
                ratingDeviation = BigDecimal(it.ratingDeviation),
                validFrom = it.validFrom
            )
        }
    }

    private fun convertVerdictToMatchResult(verdict: MatchVerdictEnum): Double {
        return when (verdict) {
            MatchVerdictEnum.PLAYER1 -> 1.0
            MatchVerdictEnum.PLAYER2 -> 0.0
            MatchVerdictEnum.TIE -> 0.5
        }
    }

    fun updateRating(
        userId: UUID,
        opponentId: UUID,
        verdict: MatchVerdictEnum
    ): Pair<Double, Double> {
        val (_, userRating, userRatingDeviation, userRatingValidFrom) =
            ratingHistoryRepository.findFirstByUserIdOrderByValidFromDesc(userId)
        val (_, opponentRating, opponentRatingDeviation, opponentRatingValidFrom) =
            ratingHistoryRepository.findFirstByUserIdOrderByValidFromDesc(opponentId)

        val userWeightedRatingDeviation =
            ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition(
                userRatingDeviation, userRatingValidFrom
            )
        val opponentWeightedRatingDeviation =
            ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition(
                opponentRatingDeviation, opponentRatingValidFrom
            )

        val newUserRating =
            ratingAlgorithm.calculateNewRating(
                GlickoRating(userRating, userWeightedRatingDeviation),
                listOf(GlickoRating(opponentRating, opponentWeightedRatingDeviation)),
                listOf(convertVerdictToMatchResult(verdict))
            )
        val newOpponentRating =
            ratingAlgorithm.calculateNewRating(
                GlickoRating(opponentRating, opponentWeightedRatingDeviation),
                listOf(GlickoRating(userRating, userWeightedRatingDeviation)),
                listOf(1.0 - convertVerdictToMatchResult(verdict))
            )

        val currentInstant = Instant.now()
        ratingHistoryRepository.save(
            RatingHistoryEntity(
                userId = userId,
                rating = newUserRating.rating,
                ratingDeviation = newUserRating.ratingDeviation,
                validFrom = currentInstant
            )
        )
        ratingHistoryRepository.save(
            RatingHistoryEntity(
                userId = opponentId,
                rating = newOpponentRating.rating,
                ratingDeviation = newOpponentRating.ratingDeviation,
                validFrom = currentInstant
            )
        )

        return Pair(newUserRating.rating, newOpponentRating.rating)
    }

    private fun getNewRatingAfterAutoMatches(
        userId: UUID,
        userRatings: Map<UUID, RatingHistoryEntity>,
        autoMatches: List<MatchEntity>
    ): GlickoRating {
        val userAsInitiatorMatches = autoMatches.filter { it.player1.userId == userId }
        val userAsOpponentMatches = autoMatches.filter { it.player2.userId == userId }

        val usersWeightedRatingDeviations =
            userRatings
                .map {
                    it.key to
                        ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition(
                            it.value.ratingDeviation, it.value.validFrom
                        )
                }
                .toMap()

        val ratingsForUserAsPlayer1 =
            userAsInitiatorMatches.map { match ->
                GlickoRating(match.player2.rating, usersWeightedRatingDeviations[match.player2.userId]!!)
            }
        val verdictsForUserAsPlayer1 =
            userAsInitiatorMatches.map { match -> convertVerdictToMatchResult(match.verdict) }

        val ratingsForUserAsPlayer2 =
            userAsOpponentMatches.map { match ->
                GlickoRating(match.player1.rating, usersWeightedRatingDeviations[match.player1.userId]!!)
            }
        val verdictsForUserAsPlayer2 =
            userAsOpponentMatches.map { match -> 1.0 - convertVerdictToMatchResult(match.verdict) }

        val ratings = ratingsForUserAsPlayer1 + ratingsForUserAsPlayer2
        val verdicts = verdictsForUserAsPlayer1 + verdictsForUserAsPlayer2

        return ratingAlgorithm.calculateNewRating(
            GlickoRating(userRatings[userId]!!.rating, usersWeightedRatingDeviations[userId]!!),
            ratings,
            verdicts
        )
    }
    fun updateTotalWinsTiesLosses(
        userIds: List<UUID>,
        matches: List<MatchEntity>
    ): Triple<Map<UUID, Int>, Map<UUID, Int>, Map<UUID, Int>> {
        val userIdWinMap = userIds.associateWith { 0 }.toMutableMap()
        val userIdLossMap = userIds.associateWith { 0 }.toMutableMap()
        val userIdTieMap = userIds.associateWith { 0 }.toMutableMap()
        matches.forEach { match ->
            when (match.verdict) {
                MatchVerdictEnum.PLAYER1 -> {
                    userIdWinMap[match.player1.userId] = userIdWinMap[match.player1.userId]!! + 1
                    userIdLossMap[match.player2.userId] = userIdLossMap[match.player2.userId]!! + 1
                }
                MatchVerdictEnum.PLAYER2 -> {
                    userIdWinMap[match.player2.userId] = userIdWinMap[match.player2.userId]!! + 1
                    userIdLossMap[match.player1.userId] = userIdLossMap[match.player1.userId]!! + 1
                }
                MatchVerdictEnum.TIE -> {
                    userIdTieMap[match.player1.userId] = userIdTieMap[match.player1.userId]!! + 1
                    userIdTieMap[match.player2.userId] = userIdTieMap[match.player2.userId]!! + 1
                }
            }
        }
        return Triple(userIdWinMap.toMap(), userIdLossMap.toMap(), userIdTieMap.toMap())
    }
    fun updateAndGetAutoMatchRatings(
        userIds: List<UUID>,
        matches: List<MatchEntity>
    ): Map<UUID, GlickoRating> {
        val userRatings =
            userIds.associateWith { userId ->
                ratingHistoryRepository.findFirstByUserIdOrderByValidFromDesc(userId)
            }
        val newRatings =
            userIds.associateWith { userId ->
                getNewRatingAfterAutoMatches(userId, userRatings, matches)
            }
        val currentInstant = Instant.now()
        newRatings.forEach { (userId, rating) ->
            ratingHistoryRepository.save(
                RatingHistoryEntity(
                    userId = userId,
                    rating = rating.rating,
                    ratingDeviation = rating.ratingDeviation,
                    validFrom = currentInstant
                )
            )
        }

        return newRatings
    }
}