MatchService.kt

package delta.codecharacter.server.match

import com.fasterxml.jackson.databind.ObjectMapper
import delta.codecharacter.dtos.ChallengeTypeDto
import delta.codecharacter.dtos.CreateMatchRequestDto
import delta.codecharacter.dtos.DailyChallengeMatchRequestDto
import delta.codecharacter.dtos.GameDto
import delta.codecharacter.dtos.GameStatusDto
import delta.codecharacter.dtos.MatchDto
import delta.codecharacter.dtos.MatchModeDto
import delta.codecharacter.dtos.PublicUserDto
import delta.codecharacter.dtos.TierTypeDto
import delta.codecharacter.dtos.VerdictDto
import delta.codecharacter.server.code.LanguageEnum
import delta.codecharacter.server.code.code_revision.CodeRevisionService
import delta.codecharacter.server.code.latest_code.LatestCodeService
import delta.codecharacter.server.code.locked_code.LockedCodeService
import delta.codecharacter.server.daily_challenge.DailyChallengeService
import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchEntity
import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchRepository
import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchVerdictEnum
import delta.codecharacter.server.exception.CustomException
import delta.codecharacter.server.game.GameService
import delta.codecharacter.server.game.GameStatusEnum
import delta.codecharacter.server.game_map.latest_map.LatestMapService
import delta.codecharacter.server.game_map.locked_map.LockedMapService
import delta.codecharacter.server.game_map.map_revision.MapRevisionService
import delta.codecharacter.server.logic.validation.MapValidator
import delta.codecharacter.server.logic.verdict.VerdictAlgorithm
import delta.codecharacter.server.notifications.NotificationService
import delta.codecharacter.server.user.public_user.PublicUserService
import delta.codecharacter.server.user.rating_history.RatingHistoryService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.Duration
import java.time.Instant
import java.util.UUID

@Service
class MatchService(
    @Autowired private val matchRepository: MatchRepository,
    @Autowired private val gameService: GameService,
    @Autowired private val latestCodeService: LatestCodeService,
    @Autowired private val codeRevisionService: CodeRevisionService,
    @Autowired private val lockedCodeService: LockedCodeService,
    @Autowired private val latestMapService: LatestMapService,
    @Autowired private val mapRevisionService: MapRevisionService,
    @Autowired private val lockedMapService: LockedMapService,
    @Autowired private val publicUserService: PublicUserService,
    @Autowired private val verdictAlgorithm: VerdictAlgorithm,
    @Autowired private val ratingHistoryService: RatingHistoryService,
    @Autowired private val notificationService: NotificationService,
    @Autowired private val dailyChallengeService: DailyChallengeService,
    @Autowired private val dailyChallengeMatchRepository: DailyChallengeMatchRepository,
    @Autowired private val jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder,
    @Autowired private val simpMessagingTemplate: SimpMessagingTemplate,
    @Autowired private val mapValidator: MapValidator,
    @Autowired private val autoMatchRepository: AutoMatchRepository
) {
    private var mapper: ObjectMapper = jackson2ObjectMapperBuilder.build()
    private val logger: Logger = LoggerFactory.getLogger(MatchService::class.java)

    private fun createSelfMatch(userId: UUID, codeRevisionId: UUID?, mapRevisionId: UUID?) {
        val code: String
        val language: LanguageEnum
        if (codeRevisionId == null) {
            val latestCode = latestCodeService.getLatestCode(userId)
            code = latestCode.code
            language = LanguageEnum.valueOf(latestCode.language.name)
        } else {
            val codeRevision =
                codeRevisionService.getCodeRevisions(userId).find { it.id == codeRevisionId }
                    ?: throw CustomException(HttpStatus.BAD_REQUEST, "Invalid revision ID")
            code = codeRevision.code
            language = LanguageEnum.valueOf(codeRevision.language.name)
        }

        val map: String =
            if (mapRevisionId == null) {
                val latestMap = latestMapService.getLatestMap(userId)
                latestMap.map
            } else {
                val mapRevision =
                    mapRevisionService.getMapRevisions(userId).find { it.id == mapRevisionId }
                        ?: throw CustomException(HttpStatus.BAD_REQUEST, "Invalid revision ID")
                mapRevision.map
            }

        val matchId = UUID.randomUUID()
        val game = gameService.createGame(matchId)
        val publicUser = publicUserService.getPublicUser(userId)
        val match =
            MatchEntity(
                id = matchId,
                games = listOf(game),
                mode = MatchModeEnum.SELF,
                verdict = MatchVerdictEnum.TIE,
                createdAt = Instant.now(),
                totalPoints = 0,
                player1 = publicUser,
                player2 = publicUser,
            )
        matchRepository.save(match)
        gameService.sendGameRequest(game, code, LanguageEnum.valueOf(language.name), map)
    }

    fun createDualMatch(userId: UUID, opponentUsername: String, mode: MatchModeEnum): UUID {
        val publicUser = publicUserService.getPublicUser(userId)
        val publicOpponent = publicUserService.getPublicUserByUsername(opponentUsername)
        val opponentId = publicOpponent.userId
        if (userId == opponentId) {
            throw CustomException(HttpStatus.BAD_REQUEST, "You cannot play against yourself")
        }
        val (userLanguage, userCode) = lockedCodeService.getLockedCode(userId)
        val userMap = lockedMapService.getLockedMap(userId)

        val (opponentLanguage, opponentCode) = lockedCodeService.getLockedCode(opponentId)
        val opponentMap = lockedMapService.getLockedMap(opponentId)

        val matchId = UUID.randomUUID()

        val game1 = gameService.createGame(matchId)
        val game2 = gameService.createGame(matchId)

        val match =
            MatchEntity(
                id = matchId,
                games = listOf(game1, game2),
                mode = mode,
                verdict = MatchVerdictEnum.TIE,
                createdAt = Instant.now(),
                totalPoints = 0,
                player1 = publicUser,
                player2 = publicOpponent,
            )
        matchRepository.save(match)

        gameService.sendGameRequest(game1, userCode, userLanguage, opponentMap)
        gameService.sendGameRequest(game2, opponentCode, opponentLanguage, userMap)
        if (mode == MatchModeEnum.AUTO) {
            logger.info(
                "Auto match started between ${match.player1.username} and ${match.player2.username}"
            )
        }
        return matchId
    }

    fun createDCMatch(userId: UUID, dailyChallengeMatchRequestDto: DailyChallengeMatchRequestDto) {
        val (_, chall, challType, _, completionStatus) =
            dailyChallengeService.getDailyChallengeByDateForUser(userId)
        if (completionStatus != null && completionStatus) {
            throw CustomException(
                HttpStatus.BAD_REQUEST, "You have already completed your daily challenge"
            )
        }
        val dc = dailyChallengeService.getDailyChallengeByDate()
        val (value, _) = dailyChallengeMatchRequestDto
        val language: LanguageEnum
        val map: String
        val code: String
        when (challType) {
            ChallengeTypeDto.CODE -> { // code as question and map as answer
                mapValidator.validateMap(value)
                code = chall.cpp.toString()
                language = LanguageEnum.CPP
                map = value
            }
            ChallengeTypeDto.MAP -> {
                map = dc.map
                language = LanguageEnum.valueOf(dailyChallengeMatchRequestDto.language.toString())
                code = value
            }
        }
        val matchId = UUID.randomUUID()
        val game = gameService.createGame(matchId)
        val user = publicUserService.getPublicUser(userId)
        val match =
            DailyChallengeMatchEntity(
                id = matchId,
                verdict = DailyChallengeMatchVerdictEnum.STARTED,
                createdAt = Instant.now(),
                user = user,
                game = game
            )
        dailyChallengeMatchRepository.save(match)
        gameService.sendGameRequest(game, code, language, map)
    }
    fun createMatch(userId: UUID, createMatchRequestDto: CreateMatchRequestDto) {
        when (createMatchRequestDto.mode) {
            MatchModeDto.SELF -> {
                val (_, _, mapRevisionId, codeRevisionId) = createMatchRequestDto
                createSelfMatch(userId, codeRevisionId, mapRevisionId)
            }
            MatchModeDto.MANUAL, MatchModeDto.AUTO -> {
                if (createMatchRequestDto.opponentUsername == null) {
                    throw CustomException(HttpStatus.BAD_REQUEST, "Opponent ID is required")
                }
                createDualMatch(userId, createMatchRequestDto.opponentUsername!!, MatchModeEnum.MANUAL)
            }
            else -> {
                throw CustomException(HttpStatus.BAD_REQUEST, "MatchMode Is Not Correct")
            }
        }
    }

    fun createAutoMatch() {
        val topNUsers = publicUserService.getTopNUsers()
        val userIds = topNUsers.map { it.userId }
        val usernames = topNUsers.map { it.username }
        logger.info("Auto matches started for users: $usernames")
        autoMatchRepository.deleteAll()
        userIds.forEachIndexed { i, userId ->
            run {
                for (j in i + 1 until userIds.size) {
                    val opponentUsername = usernames[j]
                    val matchId = createDualMatch(userId, opponentUsername, MatchModeEnum.AUTO)
                    autoMatchRepository.save(AutoMatchEntity(matchId, 0))
                }
            }
        }
    }

    private fun mapMatchEntitiesToDtos(matchEntities: List<MatchEntity>): List<MatchDto> {
        return matchEntities.map { matchEntity ->
            MatchDto(
                id = matchEntity.id,
                matchMode = MatchModeDto.valueOf(matchEntity.mode.name),
                matchVerdict = VerdictDto.valueOf(matchEntity.verdict.name),
                createdAt = matchEntity.createdAt,
                games =
                matchEntity
                    .games
                    .map { gameEntity ->
                        GameDto(
                            id = gameEntity.id,
                            destruction = BigDecimal(gameEntity.destruction),
                            coinsUsed = gameEntity.coinsUsed,
                            status = GameStatusDto.valueOf(gameEntity.status.name),
                        )
                    }
                    .toSet(),
                user1 =
                PublicUserDto(
                    username = matchEntity.player1.username,
                    name = matchEntity.player1.name,
                    tier = TierTypeDto.valueOf(matchEntity.player1.tier.name),
                    country = matchEntity.player1.country,
                    college = matchEntity.player1.college,
                    avatarId = matchEntity.player1.avatarId,
                ),
                user2 =
                PublicUserDto(
                    username = matchEntity.player2.username,
                    name = matchEntity.player2.name,
                    tier = TierTypeDto.valueOf(matchEntity.player2.tier.name),
                    country = matchEntity.player2.country,
                    college = matchEntity.player2.college,
                    avatarId = matchEntity.player2.avatarId,
                ),
            )
        }
    }

    private fun mapDailyChallengeMatchEntitiesToDtos(
        dailyChallengeMatchEntities: List<DailyChallengeMatchEntity>
    ): List<MatchDto> {
        return dailyChallengeMatchEntities.map { entity ->
            MatchDto(
                id = entity.id,
                matchMode = MatchModeDto.valueOf("DAILYCHALLENGE"),
                matchVerdict = VerdictDto.valueOf(entity.verdict.name),
                createdAt = entity.createdAt,
                games =
                setOf(
                    GameDto(
                        id = entity.game.id,
                        destruction = BigDecimal(entity.game.destruction),
                        coinsUsed = entity.game.coinsUsed,
                        status = GameStatusDto.valueOf(entity.game.status.name)
                    )
                ),
                user1 =
                PublicUserDto(
                    username = entity.user.username,
                    name = entity.user.name,
                    tier = TierTypeDto.valueOf(entity.user.tier.name),
                    country = entity.user.country,
                    college = entity.user.college,
                    avatarId = entity.user.avatarId,
                ),
            )
        }
    }

    fun getTopMatches(): List<MatchDto> {
        val matches = matchRepository.findTop10ByOrderByTotalPointsDesc()
        return mapMatchEntitiesToDtos(matches)
    }

    fun getUserMatches(userId: UUID): List<MatchDto> {
        val publicUser = publicUserService.getPublicUser(userId)
        val matches = matchRepository.findByPlayer1OrderByCreatedAtDesc(publicUser)
        val dcMatches =
            dailyChallengeMatchRepository.findByUserOrderByCreatedAtDesc(publicUser).takeWhile {
                Duration.between(it.createdAt, Instant.now()).toHours() < 24 &&
                    it.verdict != DailyChallengeMatchVerdictEnum.STARTED
            }
        return mapDailyChallengeMatchEntitiesToDtos(dcMatches) + mapMatchEntitiesToDtos(matches)
    }

    @RabbitListener(queues = ["gameStatusUpdateQueue"], ackMode = "AUTO")
    fun receiveGameResult(gameStatusUpdateJson: String) {
        val updatedGame = gameService.updateGameStatus(gameStatusUpdateJson)
        val matchId = updatedGame.matchId
        if (matchRepository.findById(matchId).isPresent) {
            val match = matchRepository.findById(updatedGame.matchId).get()
            if (match.mode != MatchModeEnum.AUTO && match.games.first().id == updatedGame.id) {
                simpMessagingTemplate.convertAndSend(
                    "/updates/${match.player1.userId}",
                    mapper.writeValueAsString(
                        GameDto(
                            id = updatedGame.id,
                            destruction = BigDecimal(updatedGame.destruction),
                            coinsUsed = updatedGame.coinsUsed,
                            status = GameStatusDto.valueOf(updatedGame.status.name),
                        )
                    )
                )
            }
            if (match.mode != MatchModeEnum.SELF &&
                match.games.all { game ->
                    game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR
                }
            ) {

                if (match.mode == MatchModeEnum.AUTO) {
                    if (match.games.any { game -> game.status == GameStatusEnum.EXECUTE_ERROR }) {
                        val autoMatch = autoMatchRepository.findById(match.id).get()
                        if (autoMatch.tries < 2) {
                            autoMatchRepository.delete(autoMatch)
                            val newMatchId =
                                createDualMatch(match.player1.userId, match.player2.username, MatchModeEnum.AUTO)
                            autoMatchRepository.save(AutoMatchEntity(newMatchId, autoMatch.tries + 1))
                            return
                        }
                    }
                }

                val player1Game = match.games.first()
                val player2Game = match.games.last()
                val verdict =
                    verdictAlgorithm.getVerdict(
                        player1Game.status == GameStatusEnum.EXECUTE_ERROR,
                        player1Game.coinsUsed,
                        player1Game.destruction,
                        player2Game.status == GameStatusEnum.EXECUTE_ERROR,
                        player2Game.coinsUsed,
                        player2Game.destruction
                    )
                val finishedMatch = match.copy(verdict = verdict)
                val (newUserRating, newOpponentRating) =
                    ratingHistoryService.updateRating(match.player1.userId, match.player2.userId, verdict)
                if (match.mode == MatchModeEnum.MANUAL) {
                    if ((
                        match.player1.tier == TierTypeDto.TIER2 &&
                            match.player2.tier == TierTypeDto.TIER2
                        ) ||
                        (
                            match.player1.tier == TierTypeDto.TIER_PRACTICE &&
                                match.player2.tier == TierTypeDto.TIER_PRACTICE
                            )
                    ) {
                        publicUserService.updatePublicRating(
                            userId = match.player1.userId,
                            isInitiator = true,
                            verdict = verdict,
                            newRating = newUserRating
                        )
                        publicUserService.updatePublicRating(
                            userId = match.player2.userId,
                            isInitiator = false,
                            verdict = verdict,
                            newRating = newOpponentRating
                        )
                    }
                    notificationService.sendNotification(
                        match.player1.userId,
                        "Match Result",
                        "${
                        when (verdict) {
                            MatchVerdictEnum.PLAYER1 -> "Won"
                            MatchVerdictEnum.PLAYER2 -> "Lost"
                            MatchVerdictEnum.TIE -> "Tied"
                        }
                        } against ${match.player2.username}",
                    )
                }
                matchRepository.save(finishedMatch)

                if (match.mode == MatchModeEnum.AUTO) {
                    if (autoMatchRepository.findAll().all { autoMatch ->
                        matchRepository.findById(autoMatch.matchId).get().games.all { game ->
                            game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR
                        }
                    }
                    ) {
                        val matches =
                            matchRepository.findByIdIn(autoMatchRepository.findAll().map { it.matchId })
                        val userIds =
                            matches.map { it.player1.userId }.toSet() +
                                matches.map { it.player2.userId }.toSet()
                        val (userIdWinMap, userIdLossMap, userIdTieMap) =
                            ratingHistoryService.updateTotalWinsTiesLosses(
                                userIds = userIds.toList(), matches = matches
                            )
                        publicUserService.updateAutoMatchWinsLosses(
                            userIds.toList(), userIdWinMap, userIdLossMap, userIdTieMap
                        )
                        val newRatings =
                            ratingHistoryService.updateAndGetAutoMatchRatings(userIds.toList(), matches)
                        newRatings.forEach { (userId, newRating) ->
                            publicUserService.updateAutoMatchRating(userId = userId, newRating = newRating.rating)
                        }
                        logger.info("LeaderBoard Tier Promotion and Demotion started")
                        publicUserService.promoteTiers()
                    }
                    notificationService.sendNotification(
                        match.player1.userId,
                        "Auto Match Result",
                        "${
                        when (verdict) {
                            MatchVerdictEnum.PLAYER1 -> "Won"
                            MatchVerdictEnum.PLAYER2 -> "Lost"
                            MatchVerdictEnum.TIE -> "Tied"
                        }
                        } against ${match.player2.username}",
                    )
                    logger.info(
                        "Match between ${match.player1.username} and ${match.player2.username} completed with verdict $verdict"
                    )
                }
            }
        } else if (dailyChallengeMatchRepository.findById(matchId).isPresent) {
            val match = dailyChallengeMatchRepository.findById(matchId).get()
            simpMessagingTemplate.convertAndSend(
                "/updates/${match.user.userId}",
                mapper.writeValueAsString(
                    GameDto(
                        id = updatedGame.id,
                        destruction = BigDecimal(updatedGame.destruction),
                        coinsUsed = updatedGame.coinsUsed,
                        status = GameStatusDto.valueOf(updatedGame.status.name),
                    )
                )
            )
            if (updatedGame.status != GameStatusEnum.EXECUTING) {
                val updatedMatch =
                    match.copy(
                        verdict =
                        dailyChallengeService.completeDailyChallenge(updatedGame, match.user.userId)
                    )
                notificationService.sendNotification(
                    match.user.userId,
                    title = "Daily Challenge Results",
                    content =
                    when (updatedMatch.verdict) {
                        DailyChallengeMatchVerdictEnum.SUCCESS -> "Successfully completed challenge"
                        DailyChallengeMatchVerdictEnum.FAILURE -> "Failed to complete challenge"
                        else -> {
                            "Some error occurred. Try again!"
                        }
                    }
                )
                dailyChallengeMatchRepository.save(updatedMatch)
            }
        }
    }
}