PublicUserService.kt

package delta.codecharacter.server.user.public_user

import delta.codecharacter.dtos.CurrentUserProfileDto
import delta.codecharacter.dtos.DailyChallengeLeaderBoardResponseDto
import delta.codecharacter.dtos.LeaderboardEntryDto
import delta.codecharacter.dtos.PublicUserDto
import delta.codecharacter.dtos.TierTypeDto
import delta.codecharacter.dtos.TutorialUpdateTypeDto
import delta.codecharacter.dtos.UpdateCurrentUserProfileDto
import delta.codecharacter.dtos.UserStatsDto
import delta.codecharacter.server.daily_challenge.DailyChallengeEntity
import delta.codecharacter.server.exception.CustomException
import delta.codecharacter.server.match.MatchVerdictEnum
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.util.UUID

@Service
class PublicUserService(@Autowired private val publicUserRepository: PublicUserRepository) {

    @Value("\${environment.no-of-tutorial-level}") private lateinit var totalTutorialLevels: Number
    @Value("\${environment.no-of-tier-1-players}") private var tier1Players: Number = 1
    @Value("\${environment.no-of-players-for-promotion}") private var topPlayer: Number = 1
    private val logger: Logger = LoggerFactory.getLogger(PublicUserService::class.java)

    fun create(
        userId: UUID,
        username: String,
        name: String,
        country: String,
        college: String,
        avatarId: Int
    ) {
        val publicUser =
            PublicUserEntity(
                userId = userId,
                username = username,
                name = name,
                country = country,
                college = college,
                avatarId = avatarId,
                rating = 1500.0,
                wins = 0,
                losses = 0,
                ties = 0,
                score = 0.0,
                // tier = TierTypeDto.TIER_PRACTICE, TODO: Automatically assign tier2 to players
                // registering after practice phase
                tier = TierTypeDto.TIER2,
                tutorialLevel = 1,
                dailyChallengeHistory = HashMap()
            )
        publicUserRepository.save(publicUser)
    }

    fun updateLeaderboardAfterPracticePhase() {
        val publicUsers = publicUserRepository.findAll()
        publicUsers.forEachIndexed { index, user ->
            if (index < tier1Players.toInt()) {
                publicUserRepository.save(user.copy(tier = TierTypeDto.TIER1))
            } else {
                publicUserRepository.save(user.copy(tier = TierTypeDto.TIER2))
            }
        }
        logger.info("Leaderboard tier set during the start of game phase")
    }

    fun resetRatingsAfterPracticePhase() {
        val users = publicUserRepository.findAll()
        users.forEach { user ->
            publicUserRepository.save(user.copy(rating = 1500.0, wins = 0, ties = 0, losses = 0))
        }
        logger.info("Ratings reset after practice phase done")
    }

    fun promoteTiers() {
        val topPlayersInTier2 =
            publicUserRepository.findAllByTier(
                TierTypeDto.TIER2,
                PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.desc("rating")))
            )
        val bottomPlayersInTier1 =
            publicUserRepository.findAllByTier(
                TierTypeDto.TIER1,
                PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.asc("rating")))
            )
        topPlayersInTier2.forEach { users ->
            val updatedToTier1User = publicUserRepository.save(users.copy(tier = TierTypeDto.TIER1))
            if (updatedToTier1User.tier == TierTypeDto.TIER1) {
                logger.info("UserName ${updatedToTier1User.username} got promoted to TIER1")
            } else {
                logger.error(
                    "Error occurred while updating ${updatedToTier1User.username} (UserName) to TIER1"
                )
            }
        }
        bottomPlayersInTier1.forEach { users ->
            val updateToTier2User = publicUserRepository.save(users.copy(tier = TierTypeDto.TIER2))
            if (updateToTier2User.tier == TierTypeDto.TIER2) {
                logger.info("UserName ${updateToTier2User.username} got demoted to TIER2")
            } else {
                logger.error(
                    "Error occurred while updating ${updateToTier2User.username} (UserName) to TIER2"
                )
            }
        }
    }

    fun getLeaderboard(page: Int?, size: Int?, tier: TierTypeDto?): List<LeaderboardEntryDto> {
        val pageRequest =
            PageRequest.of(
                page ?: 0,
                size ?: 10,
                Sort.by(Sort.Order.desc("rating"), Sort.Order.desc("wins"), Sort.Order.asc("username"))
            )
        val publicUsers =
            if (tier == null) {
                publicUserRepository.findAll(pageRequest).content
            } else {
                publicUserRepository.findAllByTier(tier, pageRequest)
            }
        return publicUsers.map {
            LeaderboardEntryDto(
                user =
                PublicUserDto(
                    username = it.username,
                    name = it.name,
                    tier = TierTypeDto.valueOf(it.tier.name),
                    country = it.country,
                    college = it.college,
                    avatarId = it.avatarId,
                ),
                stats =
                UserStatsDto(
                    rating = BigDecimal(it.rating),
                    wins = it.wins,
                    losses = it.losses,
                    ties = it.ties
                ),
            )
        }
    }

    fun getDailyChallengeLeaderboard(
        page: Int?,
        size: Int?
    ): List<DailyChallengeLeaderBoardResponseDto> {
        val pageRequest = PageRequest.of(page ?: 0, size ?: 10, Sort.by(Sort.Direction.DESC, "score"))
        return publicUserRepository.findAll(pageRequest).content.map {
            DailyChallengeLeaderBoardResponseDto(
                userName = it.username, score = BigDecimal(it.score), avatarId = it.avatarId
            )
        }
    }

    fun getUserProfile(userId: UUID, email: String): CurrentUserProfileDto {
        val user = publicUserRepository.findById(userId).get()
        return CurrentUserProfileDto(
            id = userId,
            username = user.username,
            email = email,
            name = user.name,
            country = user.country,
            college = user.college,
            tutorialLevel = user.tutorialLevel,
            avatarId = user.avatarId,
            isTutorialComplete = user.tutorialLevel == totalTutorialLevels.toInt()
        )
    }

    fun updateUserProfile(userId: UUID, updateCurrentUserProfileDto: UpdateCurrentUserProfileDto) {
        val user = publicUserRepository.findById(userId).get()

        if (updateCurrentUserProfileDto.name != null &&
            updateCurrentUserProfileDto.name!!.trim().length < 5
        ) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Name must be minimum 5 characters")
        }

        if (updateCurrentUserProfileDto.country != null &&
            updateCurrentUserProfileDto.country!!.trim().isEmpty()
        ) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Country can not be an empty")
        }

        if (updateCurrentUserProfileDto.college != null &&
            updateCurrentUserProfileDto.college!!.trim().isEmpty()
        ) {
            throw CustomException(HttpStatus.BAD_REQUEST, "College can not be an empty")
        }
        if (updateCurrentUserProfileDto.avatarId != null &&
            updateCurrentUserProfileDto.avatarId!! !in 0..19
        ) {
            throw CustomException(HttpStatus.BAD_REQUEST, "Selected Avatar is invalid")
        }

        val updatedUser =
            user.copy(
                name = updateCurrentUserProfileDto.name ?: user.name,
                country = updateCurrentUserProfileDto.country ?: user.country,
                college = updateCurrentUserProfileDto.college ?: user.college,
                avatarId = updateCurrentUserProfileDto.avatarId ?: user.avatarId,
                tutorialLevel =
                updateTutorialLevel(
                    updateCurrentUserProfileDto.updateTutorialLevel, user.tutorialLevel
                )
            )
        publicUserRepository.save(updatedUser)
    }

    fun updateTutorialLevel(updateTutorialType: TutorialUpdateTypeDto?, tutorialLevel: Int): Int {
        var updatedTutorialLevel = tutorialLevel
        when (updateTutorialType) {
            TutorialUpdateTypeDto.NEXT -> {
                if (tutorialLevel >= totalTutorialLevels.toInt()) {
                    return tutorialLevel
                }
                updatedTutorialLevel += 1
            }
            TutorialUpdateTypeDto.PREVIOUS -> {
                if (tutorialLevel <= 1) {
                    return 1
                }
                updatedTutorialLevel -= 1
            }
            TutorialUpdateTypeDto.SKIP -> {
                updatedTutorialLevel = totalTutorialLevels.toInt()
            }
            TutorialUpdateTypeDto.RESET -> {
                updatedTutorialLevel = 1
            }
            else -> {
                return tutorialLevel
            }
        }
        return updatedTutorialLevel
    }

    fun updatePublicRating(
        userId: UUID,
        isInitiator: Boolean,
        verdict: MatchVerdictEnum,
        newRating: Double
    ) {
        val user = publicUserRepository.findById(userId).get()
        val updatedUser =
            user.copy(
                rating = newRating,
                wins =
                if ((isInitiator && verdict == MatchVerdictEnum.PLAYER1) ||
                    (!isInitiator && verdict == MatchVerdictEnum.PLAYER2)
                )
                    user.wins + 1
                else user.wins,
                losses =
                if ((isInitiator && verdict == MatchVerdictEnum.PLAYER2) ||
                    (!isInitiator && verdict == MatchVerdictEnum.PLAYER1)
                )
                    user.losses + 1
                else user.losses,
                ties = if (verdict == MatchVerdictEnum.TIE) user.ties + 1 else user.ties
            )
        publicUserRepository.save(updatedUser)
    }

    fun updateAutoMatchRating(userId: UUID, newRating: Double) {
        val user = publicUserRepository.findById(userId).get()
        val updatedUser = user.copy(rating = newRating)
        publicUserRepository.save(updatedUser)
        logger.info("Ratings updated for ${user.username}")
    }
    fun updateAutoMatchWinsLosses(
        userIds: List<UUID>,
        userIdWinsMap: Map<UUID, Int>,
        userIdLoss: Map<UUID, Int>,
        userIdTies: Map<UUID, Int>
    ) {
        logger.info("Updating wins and losses for $userIds")
        userIds.forEach {
            val user = publicUserRepository.findById(it).get()
            val updatedUser =
                user.copy(
                    wins = user.wins + userIdWinsMap[it]!!,
                    losses = user.losses + userIdLoss[it]!!,
                    ties = user.ties + userIdTies[it]!!
                )
            publicUserRepository.save(updatedUser)
        }
    }
    fun getPublicUser(userId: UUID): PublicUserEntity {
        return publicUserRepository.findById(userId).get()
    }

    fun getPublicUserByUsername(username: String): PublicUserEntity {
        return publicUserRepository.findByUsername(username).orElseThrow {
            CustomException(HttpStatus.BAD_REQUEST, "Invalid username")
        }
    }

    fun isUsernameUnique(username: String): Boolean {
        return publicUserRepository.findByUsername(username).isEmpty
    }

    fun updateDailyChallengeScore(userId: UUID, score: Double, dailyChallenge: DailyChallengeEntity) {
        val user = publicUserRepository.findById(userId).get()
        val current = user.dailyChallengeHistory
        current[dailyChallenge.day] = DailyChallengeHistory(score, dailyChallenge)
        val updatedUser = user.copy(score = user.score + score, dailyChallengeHistory = current)
        publicUserRepository.save(updatedUser)
    }

    fun getTopNUsers(): List<PublicUserEntity> {
        return publicUserRepository.findAllByTier(TierTypeDto.TIER1).sortedByDescending { it.rating }
    }
}