package kr.lunaticbum.back.lun.model

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.withContext
import kr.lunaticbum.back.lun.utils.ImageUtils.convertTransparentToWhite
import kr.lunaticbum.back.lun.utils.LogService
import kr.lunaticbum.back.lun.utils.SudokuGenerator
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.Aggregation
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import reactor.core.publisher.Flux // (★ 오류 수정: 누락된 Flux import 추가)
import reactor.core.publisher.Mono
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.time.LocalDateTime
import java.util.Base64
import javax.imageio.ImageIO
import kotlin.random.Random
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.repository.reactive.ReactiveSortingRepository
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import java.io.File
import java.time.Instant
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import org.springframework.security.core.userdetails.UsernameNotFoundException // 👈 [추가]
import reactor.core.scheduler.Schedulers // 👈 [추가]

/**
 * ======================================================
 * 1. NONOGRAM 모델 및 리포지토리
 * ======================================================
 */

@Document("nonogram") // "puzzles" 컬렉션 (노노그램용)
data class NonogramPuzzle(
    @Id
    val id: String? = null,
    val solutionGrid: List<List<Int>>,
    val rowClues: List<List<Int>>,
    val colClues: List<List<Int>>,
    val grayscaleImageFile: String, // 파일명 (예: "uuid-gray.png")
    val originalImageFile: String,  // 파일명 (예: "uuid-original.png")
    val createdAt: LocalDateTime = LocalDateTime.now()
)

@Repository
interface NonogramPuzzleRepository : ReactiveMongoRepository<NonogramPuzzle, String> {
    @Aggregation(pipeline = [
//        "{ \$match: { originalImage: { \$exists: true }, grayscaleImage: { \$exists: true } } }",
//        "{ \$sample: { size: 1 } }"
        "{ \$match: { originalImageFile: { \$exists: true }, grayscaleImageFile: { \$exists: true } } }",
        "{ \$sample: { size: 1 } }"
    ])
    fun findRandom(): Flux<NonogramPuzzle> // (★ Flux를 인식하기 위해 import 필요)
}

/**
 * ======================================================
 * [통합 게임 서비스]
 * 모든 게임(Nonogram, Sudoku, Spider)의 로직을 처리하는 단일 서비스.
 * ======================================================
 */
@Service
class PuzzleService(
    // 1. Nonogram 의존성
    private val puzzleRepository: NonogramPuzzleRepository,

    // 2. Sudoku 의존성
    private val sudokuPuzzleRepository: SudokuPuzzleRepository,

    // 3. Spider 의존성
    private val spiderGameRepository: SpiderGameRepository,
    @Value("\${puzzle.image.path}") private val puzzleImagePath: String,
    private val logService: LogService
) {

    companion object {
        private const val MIN_PUZZLE_SIZE = 10
        private const val MAX_PUZZLE_SIZE = 30
    }

    // ======================================================
    // 1. NONOGRAM 서비스 로직 (기존 함수)
    // ======================================================

    suspend fun findRandomPuzzle(): NonogramPuzzle? {
        return puzzleRepository.findRandom().awaitFirstOrNull()
    }

    fun findById(id: String): Mono<NonogramPuzzle> = puzzleRepository.findById(id) // Nonogram 용

    suspend fun deletePuzzle(id : String): Mono<Void> {
        val puzzle = puzzleRepository.findById(id).awaitSingleOrNull()
        if (puzzle != null) {
            // 파일 시스템에서 이미지 파일 삭제
            try {
                File(puzzleImagePath, puzzle.grayscaleImageFile).delete()
                File(puzzleImagePath, puzzle.originalImageFile).delete()
            } catch (e: Exception) {
                // 파일 삭제 실패 시 로그를 남길 수 있습니다 (선택사항).
                e.printStackTrace()
            }
        }
        return puzzleRepository.deleteById(id) // DB에서 퍼즐 정보 삭제
    }

    suspend fun generateAndSavePuzzle(file: MultipartFile): NonogramPuzzle {
        val puzzleData = withContext(Dispatchers.IO) {
            val originalImage = ImageIO.read(file.inputStream)
            val imageWithBackground = convertTransparentToWhite(originalImage)
            val puzzleSize = determinePuzzleSize(imageWithBackground)
            val resizedOriginal = resizeImage(imageWithBackground, 300)
            val grayImage = BufferedImage(puzzleSize, puzzleSize, BufferedImage.TYPE_BYTE_GRAY).apply {
                createGraphics().run {
                    drawImage(imageWithBackground, 0, 0, puzzleSize, puzzleSize, null)
                    dispose()
                }
            }
            val averageBrightness = calculateAverageBrightness(grayImage)
            val adaptiveThreshold = determineAdaptiveThreshold(averageBrightness)
            val solutionGrid = List(puzzleSize) { y ->
                List(puzzleSize) { x ->
                    if (grayImage.raster.getSample(x, y, 0) < adaptiveThreshold) 1 else 0
                }
            }
            val rowClues = solutionGrid.map { getCluesForLine(it) }
            val colClues = transpose(solutionGrid).map { getCluesForLine(it) }

            // --- Base64 인코딩 대신 파일로 저장 ---
            // 1. 고유한 파일명 생성
            val uniqueId = UUID.randomUUID().toString()
            val grayFilename = "$uniqueId-gray.png"
            val originalFilename = "$uniqueId-original.png"

            // 2. 저장 경로에 파일 객체 생성
            val grayFile = File(puzzleImagePath, grayFilename)
            val originalFile = File(puzzleImagePath, originalFilename)

            // 3. 부모 디렉토리가 없으면 생성
            grayFile.parentFile.mkdirs()

            // 4. BufferedImage를 파일로 저장
            ImageIO.write(resizeImage(grayImage, 300), "png", grayFile)
            ImageIO.write(resizedOriginal, "png", originalFile)
            // --- 파일 저장 로직 끝 ---



            NonogramPuzzle(
                solutionGrid = solutionGrid,
                rowClues = rowClues,
                colClues = colClues,
                grayscaleImageFile = grayFilename,
                originalImageFile = originalFilename
            )
        }
        return puzzleRepository.save(puzzleData).awaitSingle()
    }

    // --- (★ 오류 수정: 축약되었던 Nonogram 헬퍼 함수 본문 전체 복원) ---

    private fun determinePuzzleSize(image: BufferedImage): Int {
        val width = image.width.toDouble()
        val height = image.height.toDouble()
        val maxDimension = maxOf(width, height)
        val minDimension = minOf(width, height)
        val aspectRatio = if (minDimension > 0) maxDimension / minDimension else 1.0
        val baseSize = 15.0
        val factor = 5.0
        val calculatedSize = baseSize + ((aspectRatio - 1.0) * factor)
        return calculatedSize.toInt().coerceIn(MIN_PUZZLE_SIZE, MAX_PUZZLE_SIZE)
    }

    private fun resizeImage(sourceImage: BufferedImage, size: Int): BufferedImage {
        return BufferedImage(size, size, BufferedImage.TYPE_INT_RGB).apply {
            createGraphics().run {
                drawImage(sourceImage, 0, 0, size, size, null)
                dispose()
            }
        }
    }

    private fun imageToBase64(image: BufferedImage): String {
        val os = ByteArrayOutputStream()
        ImageIO.write(image, "png", os)
        return "data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray())
    }

    private fun convertTransparentToWhite(sourceImage: BufferedImage): BufferedImage {
        if (!sourceImage.colorModel.hasAlpha()) {
            return sourceImage
        }
        return BufferedImage(sourceImage.width, sourceImage.height, BufferedImage.TYPE_INT_RGB).apply {
            createGraphics().also { g2d ->
                g2d.color = Color.WHITE
                g2d.fillRect(0, 0, width, height)
                g2d.drawImage(sourceImage, 0, 0, null)
                g2d.dispose()
            }
        }
    }

    private fun calculateAverageBrightness(image: BufferedImage): Int {
        var totalBrightness: Long = 0
        val width = image.width
        val height = image.height
        for (y in 0 until height) {
            for (x in 0 until width) {
                totalBrightness += image.raster.getSample(x, y, 0)
            }
        }
        return (totalBrightness / (width * height)).toInt()
    }

    private fun determineAdaptiveThreshold(averageBrightness: Int): Int {
        return when {
            averageBrightness > 180 -> (averageBrightness * 0.9).toInt()
            averageBrightness < 80 -> (averageBrightness * 1.1).toInt()
            else -> averageBrightness
        }
    }

    private fun getCluesForLine(line: List<Int>): List<Int> {
        val clues = mutableListOf<Int>()
        var count = 0
        for (cell in line) {
            if (cell == 1) {
                count++
            } else if (count > 0) {
                clues.add(count)
                count = 0
            }
        }
        if (count > 0) clues.add(count)
        return if (clues.isEmpty()) listOf(0) else clues
    }

    private fun transpose(grid: List<List<Int>>): List<List<Int>> {
        return List(grid[0].size) { j -> List(grid.size) { i -> grid[i][j] } }
    }
    // --- (헬퍼 함수 복원 끝) ---


    // ======================================================
    // 2. SUDOKU 서비스 로직 (통합됨)
    // ======================================================

    // 🔽 [수정] DTO가 puzzleId를 다시 포함하도록 변경
    data class SudokuGameDto(val puzzleId: Long, val question: String, val solution: String, val blockSize: Int)
    data class SudokuValidateDto(val puzzleId: Long, val answer: String)

    // [신규] 생성 작업 중복 실행 방지용 잠금
    private val generationLocks = ConcurrentHashMap<String, Boolean>()

    /**
     * [핵심 수정]
     * - 파라미터를 'difficulty' 1개만 받습니다.
     * - 'difficulty' (1~8)를 (blockSize, generatorLevel)로 변환합니다.
     */
    suspend fun sudoku_startGame(difficulty: String): SudokuGameDto {

        // 🔽 [신규] 9단계 난이도를 (blockSize, generatorLevel)로 매핑
        val (blockSize, generatorLevel) = when (difficulty) {
            // 4x4 (3 levels)
            "1" -> Pair(2, 1) // L1: 4x4, Easy (Gen L1)
            "2" -> Pair(2, 3) // L2: 4x4, Medium (Gen L3)
            "3" -> Pair(2, 5) // L3: 4x4, Hard (Gen L5)

            // 9x9 (5 levels)
            "4" -> Pair(3, 1) // L4: 9x9, Easy (Gen L1)
            "5" -> Pair(3, 2) // L5: 9x9, Medium (Gen L2)
            "6" -> Pair(3, 3) // L6: 9x9, Hard (Gen L3)
            "7" -> Pair(3, 4) // L7: 9x9, Expert (Gen L4)
            "8" -> Pair(3, 5) // L8: 9x9, Master (Gen L5)

            // 16x16 (3 levels)
            "9" -> Pair(4, 1) // L9: 16x16, Easy (Gen L1)
            "10" -> Pair(4, 3) // L10: 16x16, Medium (Gen L3)
            "11" -> Pair(4, 5) // L11: 16x16, Hard (Gen L5)

            else -> Pair(3, 3) // 기본값 (Level 6)
        }

        // 1. DB에서 조건에 맞는 '미사용 문제' 검색
        val solutionPuzzle = sudokuPuzzleRepository
            .findFirstByBlockSizeAndLevelAndPlayCountOrderByPuzzleKey(blockSize, generatorLevel, 0L)
        // 2. 없으면 오류 반환
            ?: throw IllegalStateException("현재 $blockSize x $blockSize (Level $generatorLevel) 사용 가능한 새 퍼즐이 없습니다.")

        // 3. playCount 1 증가 및 저장
        sudokuPuzzleRepository.save(
            solutionPuzzle.copy(playCount = solutionPuzzle.playCount + 1)
        )

        // 4. DTO로 반환 (DB에서 꺼낸 값 그대로)
        return SudokuGameDto(
            puzzleId = solutionPuzzle.puzzleKey ?: 0L,
            question = solutionPuzzle.question!!,
            solution = solutionPuzzle.solution!!,
            blockSize = solutionPuzzle.blockSize
        )
    }

    @Scheduled(fixedRate = 600000, initialDelay = 1000)
    suspend fun fillPuzzleCache() {
        // 🔽 [수정] 2..4 (4x4, 9x9, 16x16)
        for (blockSize in 2..4) {
            // 🔽 [수정] 1, 3, 5 (Easy, Medium, Hard)
            for (level in 1..5) {

                if ((blockSize == 2 || blockSize == 4) && (level == 2 || level == 4)) {
                    continue
                }

                val lockKey = "$blockSize-$level"
                if (generationLocks.putIfAbsent(lockKey, true) != null) {
                    continue
                }
                try {
                    val unplayedCount = sudokuPuzzleRepository
                        .countByBlockSizeAndLevelAndPlayCount(blockSize, level, 0L)

                    val minCacheSize = 5
                    if (unplayedCount < minCacheSize) {
                        logService.log("[$blockSize x $blockSize Level $level] 미사용 퍼즐 ${unplayedCount}개 감지. ${minCacheSize}개까지 생성 시작...")
                        repeat((minCacheSize - unplayedCount).toInt()) {
                            sudoku_generateAndSavePuzzle(blockSize, level)
                        }
                        logService.log("[$blockSize x $blockSize Level $level] 퍼즐 생성 완료.")
                    }
                } catch (e: Exception) {
                    logService.log("[$blockSize x $blockSize Level $level] 퍼즐 생성 중 오류: ${e.message}")
                } finally {
                    generationLocks.remove(lockKey)
                }
            }
        }
    }

    /**
     * [수정] Generator의 로컬 검증 대신, DB의 정답과 직접 비교 (가장 확실한 방식)
     */
    suspend fun sudoku_validateSolution(validateDto: SudokuValidateDto): Boolean {
        // 1. [수정] DB에서 'puzzleId'로 정답을 찾음
        val originalPuzzle = sudokuPuzzleRepository.findByPuzzleKey(validateDto.puzzleId)
            ?: throw IllegalStateException("퍼즐을 찾을 수 없습니다.")

        // 2. [수정] DB의 정답과 문자열 비교
        return originalPuzzle.solution == validateDto.answer
    }

    /**
     * [수정] 이 함수는 이제 '새 정답 퍼즐'을 1개 생성하여
     * 'playCount = 0' 상태로 DB에 저장하고, '저장된 객체'를 반환합니다.
     */
    suspend fun sudoku_generateAndSavePuzzle(blockSize: Int = 3, level: Int = 3) {
        val generator = SudokuGenerator(blockSize)
        val newPuzzle = generator.generatePuzzle(level)

        val lastPuzzle = sudokuPuzzleRepository.findTopByOrderByPuzzleKeyDesc()
        val nextKey = (lastPuzzle?.puzzleKey ?: 0L) + 1L

        val puzzleToSave = SudokuPuzzle(
            puzzleKey = nextKey,
            solution = newPuzzle.solution,
            question = newPuzzle.question,
            blockSize = blockSize,
            level = level,
            playCount = 0L
        )
        sudokuPuzzleRepository.save(puzzleToSave)
    }

    private fun sudoku_createQuestion(puzzle: String, holes: Int): String {
        val chars = puzzle.toMutableList()
        var remainingHoles = holes
        while (remainingHoles > 0) {
            val randomIndex = Random.nextInt(chars.size)
            if (chars[randomIndex] != '0') {
                chars[randomIndex] = '0'
                remainingHoles--
            }
        }
        return chars.joinToString("")
    }


    // ======================================================
    // 3. SPIDER 서비스 로직 (통합 및 Coroutine 변환됨)
    // ======================================================

    suspend fun spider_newGame(numSuits: Int, numCards: String): SpiderGame {
        val allCards = spider_createDeck(numSuits)
        val shuffledCards = allCards.shuffled(Random)
        val (tableau, stock) = spider_dealCards(shuffledCards, numCards)

        val initialGame = SpiderGame(
            id = null,
            tableau = tableau,
            stock = stock,
            foundation = emptyList(),
            moves = 0,
            isCompleted = false,
            undoCount = 0,
            undoHistory = emptyList()
        )
        return spiderGameRepository.save(initialGame).awaitSingle()
    }

    suspend fun spider_getGame(id: String): SpiderGame? {
        return spiderGameRepository.findById(id).awaitSingleOrNull()
    }

    suspend fun spider_updateGame(game: SpiderGame): SpiderGame {
        val historyToSave = SpiderGameHistory(
            tableau = game.tableau,
            stock = game.stock,
            foundation = game.foundation,
            moves = game.moves
        )
        val updatedHistory = (game.undoHistory + historyToSave).takeLast(5)
        val updatedGame = game.copy(undoHistory = updatedHistory)
        return spiderGameRepository.save(updatedGame).awaitSingle()
    }

    suspend fun spider_dealCardsFromStock(gameId: String): SpiderGame {
        val game = spiderGameRepository.findById(gameId).awaitSingleOrNull()
            ?: throw IllegalArgumentException("Game not found: $gameId")

        val stockCards = game.stock.toMutableList()
        if (stockCards.size < 10) {
            throw IllegalArgumentException("No more cards in stock.")
        }

        val historyToSave = SpiderGameHistory(
            tableau = game.tableau,
            stock = game.stock,
            foundation = game.foundation,
            moves = game.moves
        )
        val updatedHistory = (game.undoHistory + historyToSave).takeLast(5)

        val updatedTableau = game.tableau.toMutableList()
        val remainingStock = stockCards.drop(10)

        updatedTableau.forEachIndexed { index, stack ->
            val cardToDeal = stockCards[index]
            cardToDeal.isFaceUp = true
            (stack as MutableList).add(cardToDeal)
        }

        val updatedGame = game.copy(
            tableau = updatedTableau,
            stock = remainingStock,
            moves = game.moves + 1,
            undoHistory = updatedHistory
        )
        return spiderGameRepository.save(updatedGame).awaitSingle()
    }

    suspend fun spider_undoGame(gameId: String): SpiderGame {
        val game = spiderGameRepository.findById(gameId).awaitSingleOrNull()
            ?: throw IllegalArgumentException("Game not found: $gameId")

        if (game.undoHistory.isEmpty() || game.undoCount >= 5) {
            throw IllegalArgumentException("Cannot undo. No more history or undo limit reached.")
        }

        val lastHistory = game.undoHistory.last()
        val remainingHistory = game.undoHistory.dropLast(1)

        val updatedGame = game.copy(
            tableau = lastHistory.tableau,
            stock = lastHistory.stock,
            foundation = lastHistory.foundation,
            moves = lastHistory.moves,
            undoCount = game.undoCount + 1,
            undoHistory = remainingHistory
        )
        return spiderGameRepository.save(updatedGame).awaitSingle()
    }

    // --- (스파이더 헬퍼 함수들) ---
    private fun spider_createDeck(numSuits: Int): List<SpiderCard> {
        val allSuits = listOf("spade", "heart", "club", "diamond")
        val suits = allSuits.take(numSuits)
        val setsPerSuit = when (numSuits) {
            1 -> 8
            2 -> 4
            4 -> 2
            else -> throw IllegalArgumentException("Invalid number of suits: $numSuits")
        }
        val deck = mutableListOf<SpiderCard>()
        repeat(setsPerSuit) {
            for (suit in suits) {
                for (rank in 1..13) {
                    deck.add(SpiderCard(suit, rank, isFaceUp = false))
                }
            }
        }
        return deck
    }

    private fun spider_dealCards(shuffledCards: List<SpiderCard>, numCards: String): Pair<List<List<SpiderCard>>, List<SpiderCard>> {
        val initialCards = numCards.split(",").map { it.trim().toInt() }
        val cardsPerStack = List(10) { index ->
            if (index < 4) initialCards[0] else initialCards[1]
        }
        val cardsToDeal = shuffledCards.toMutableList()
        val tableau = MutableList(10) { mutableListOf<SpiderCard>() }
        cardsPerStack.forEachIndexed { stackIndex, count ->
            repeat(count) {
                if (cardsToDeal.isNotEmpty()) {
                    val card = cardsToDeal.removeFirst()
                    tableau[stackIndex].add(card)
                }
            }
        }
        tableau.forEach { stack ->
            if (stack.isNotEmpty()) {
                stack.last().isFaceUp = true
            }
        }
        return Pair(tableau, cardsToDeal)
    }
}


/**
 * 스파이더 게임 상태 저장 모델
 */
@Document(collection = "spider_games")
data class SpiderGame(
    @Id
    val id: String? = null,
    val tableau: List<List<SpiderCard>>,
    val stock: List<SpiderCard>,
    val foundation: List<List<SpiderCard>>,
    val moves: Int,
    val isCompleted: Boolean,
    val undoCount: Int = 0, // 실행 취소 횟수
    val undoHistory: List<SpiderGameHistory> = emptyList(), // 게임 상태 히스토리
    val timestamp: Long = System.currentTimeMillis()
)

data class SpiderCard(
    val suit: String,
    val rank: Int,
    var isFaceUp: Boolean,
)

// 게임 상태 히스토리 모델
data class SpiderGameHistory(
    val tableau: List<List<SpiderCard>>,
    val stock: List<SpiderCard>,
    val foundation: List<List<SpiderCard>>,
    val moves: Int
)

/**
 * 스파이더 게임 상태 리포지토리 (PuzzleService에서 사용됨)
 * (참고: 이 리포지토리는 Reactive 타입으로 유지하고,
 * 서비스단에서 Coroutine으로 브리징하여 사용합니다.)
 */
interface SpiderGameRepository : ReactiveMongoRepository<SpiderGame, String> {
    override fun findById(id: String): Mono<SpiderGame>
}


/**
 * 스도쿠 퍼즐 원본 데이터를 저장하는 모델
 */
@Document(collection = "Sudoku")
data class SudokuPuzzle(
    @Id
    val id: String? = null,
    val puzzleKey: Long? = null,
    val solution: String?,
    val question: String?, // 👈 [추가]
    val blockSize: Int = 3,
    val level: Int = 3,     // 👈 [추가] 생성기 난이도 (1, 3, 5)
    val playCount: Long = 0L
)

/**
 * 스도쿠 퍼즐 리포지토리 (PuzzleService에서 사용됨)
 */
@Repository
interface SudokuPuzzleRepository : CoroutineCrudRepository<SudokuPuzzle, String> {
    // 🔽 [수정] override suspend fun count(): Long -> countByBlockSize
    suspend fun countByBlockSize(blockSize: Int): Long // 특정 크기의 퍼즐만 카운트
    override suspend fun count(): Long
    suspend fun findByPuzzleKey(puzzleKey: Long): SudokuPuzzle?
    suspend fun findTopByOrderByPuzzleKeyDesc(): SudokuPuzzle?
    // 🔽 [신규] 특정 blockSize의 퍼즐 중 랜덤 1개 조회
    @Aggregation(pipeline = [
        "{ \$match: { blockSize: ?0 } }", // ?0은 첫번째 파라미터(blockSize)
        "{ \$sample: { size: 1 } }"
    ])
    suspend fun findRandomByBlockSize(blockSize: Int): SudokuPuzzle?

    // [수정] blockSize, level, playCount 모두 일치하는 퍼즐 검색
    suspend fun findFirstByBlockSizeAndLevelAndPlayCountOrderByPuzzleKey(
        blockSize: Int,
        level: Int,
        playCount: Long
    ): SudokuPuzzle?

    // [추가] 스케줄러가 사용할, 특정 조건의 퍼즐 개수 카운트
    suspend fun countByBlockSizeAndLevelAndPlayCount(
        blockSize: Int,
        level: Int,
        playCount: Long
    ): Long


}

// In PuzzleData.kt
@Document(collection = "game_ranks")
data class GameRank(
    @Id
    val id: String? = null,

    @Indexed // 👈 [추가]
    val userId: String?, // 👈 [추가] 앱-고유 ID 또는 인증된 사용자 ID

    val gameType: GameType,
    val contextId: String?,
    val playerName: String,
    val primaryScore: Long,
    val secondaryScore: Long? = null,
    val timestamp: Instant = Instant.now()
)

/**
 * 지원하는 게임 타입을 정의하는 Enum
 */
enum class GameType {
    GAME_2048,
    SUDOKU,
    SPIDER,
    NONOGRAM
}

/**
 * 랭킹 등록 시 모든 프론트엔드에서 공통으로 사용할 DTO
 */
data class UnifiedRankDto(
    val userId: String, // 👈 [추가] (널 허용 안 함)
    val gameType: GameType,
    val contextId: String?,
    val playerName: String,
    val primaryScore: Long,
    val secondaryScore: Long? = null
)

// 🔽 [신규 추가 DTO 1]
// 서버가 클라이언트에 최종적으로 반환할 랭킹 결과 DTO
data class RankSubmissionResult(
    val topRanks: List<GameRank>,
    val myRank: GameRankWithRankNumber? // 👈 내 랭킹 (순위 포함)
)

// 🔽 [신규 추가 DTO 2]
// 내 랭킹 객체와 순위(숫자)를 함께 담는 DTO
data class GameRankWithRankNumber(
    val rankData: GameRank,
    val rankNumber: Long //
)

@Repository
interface GameRankRepository : ReactiveSortingRepository<GameRank, String> {

    fun save(gameRank: GameRank): Mono<GameRank>
    // 점수가 높은 순 (DESC) 랭킹 조회 (예: 2048)
    fun findTop10ByGameTypeAndContextIdOrderByPrimaryScoreDesc(
        gameType: GameType,
        contextId: String?
    ): Flux<GameRank>

    // 점수가 낮은 순 (ASC) 랭킹 조회 (예: Sudoku-시간, Spider-이동횟수)
    fun findTop10ByGameTypeAndContextIdOrderByPrimaryScoreAscSecondaryScoreAsc(
        gameType: GameType,
        contextId: String?
    ): Flux<GameRank>

    // [신규 추가] 특정 플레이어의 G킹을 최신순으로 조회
    fun findByPlayerNameOrderByTimestampDesc(playerName: String): Flux<GameRank>

    // 🔽 [추가]
    fun findFirstByUserId(userId: String): Mono<GameRank>
    fun findByPlayerName(playerName: String): Flux<GameRank> // 이름 중복 확인용

    // 🔽 [신규 추가] (ASC 정렬용: Sudoku 등)
    // 나의 primaryScore보다 '작은(더 좋은)' 점수를 가진 사람 수
    fun countByGameTypeAndContextIdAndPrimaryScoreLessThan(
        gameType: GameType,
        contextId: String?,
        primaryScore: Long
    ): Mono<Long>

    // 🔽 [신규 추가] (DESC 정렬용: 2048 등)
    // 나의 primaryScore보다 '큰(더 좋은)' 점수를 가진 사람 수
    fun countByGameTypeAndContextIdAndPrimaryScoreGreaterThan(
        gameType: GameType,
        contextId: String?,
        primaryScore: Long
    ): Mono<Long>

}


@Service
class GameRankService(
    private val rankRepository: GameRankRepository,
    private val userManager: UserManager, // 👈 이 컴포넌트가 Blocking IO를 유발
    private val logService: LogService
) {
    /**
     * 게임 타입에 따라 적절한 정렬 방식으로 랭킹을 조회합니다. (변경 없음)
     */
    fun getRanks(gameType: GameType, contextId: String?): Flux<GameRank> {
        return when (gameType) {
            // 점수가 높아야 하는 게임 (2048)
            GameType.GAME_2048 ->
                rankRepository.findTop10ByGameTypeAndContextIdOrderByPrimaryScoreDesc(gameType, contextId)

            // 점수가 낮아야 하는 게임 (스도쿠 시간, 스파이더 무브/시간, 노노그램 시간)
            GameType.SUDOKU, GameType.SPIDER, GameType.NONOGRAM ->
                rankRepository.findTop10ByGameTypeAndContextIdOrderByPrimaryScoreAscSecondaryScoreAsc(gameType, contextId)
        }
    }

    /**
     * [전체 수정]
     * 랭킹을 등록하고, '상위 10개'와 '내 순위'를 포함한 객체를 반환합니다.
     * 🔽 반환 타입이 Mono<RankSubmissionResult>로 변경되었습니다.
     */
    fun submitRank(rankDto: UnifiedRankDto): Mono<RankSubmissionResult> {
        val auth = SecurityContextHolder.getContext().authentication
        val isAuthenticated = auth != null && auth.isAuthenticated && auth !is AnonymousAuthenticationToken

        // 1. 랭크 저장 로직 (기존과 동일)
        val saveOperation: Mono<GameRank> = if (isAuthenticated) {
            // ... (기존 인증 사용자 저장 로직) ...
            val principal = auth.principal as UserDetails
            val authenticatedUserId = principal.username
            val gameRank = GameRank(
                userId = authenticatedUserId,
                gameType = rankDto.gameType,
                contextId = rankDto.contextId,
                playerName = authenticatedUserId,
                primaryScore = rankDto.primaryScore,
                secondaryScore = rankDto.secondaryScore
            )
            rankRepository.save(gameRank)
        } else {
            // ... (기존 익명 사용자 검증 및 저장 로직) ...
            val anonymousUserId = rankDto.userId
            val requestedName = rankDto.playerName
            val checkAuthUsers = Mono.fromCallable {
                userManager.loadUserByUsername(requestedName)
            }
                .subscribeOn(Schedulers.boundedElastic())
                .flatMap<GameRank> {
                    Mono.error(IllegalArgumentException("이미 등록된 회원의 이름입니다."))
                }
                .onErrorResume(UsernameNotFoundException::class.java) {
                    Mono.empty()
                }
                .onErrorResume { error ->
                    logService.log("!!! submitRank: checkAuthUsers 중 예상치 못한 크래시 발생 !!!", error)
                    Mono.error(IllegalArgumentException("이름 확인 중 서버 오류가 발생했습니다."))
                }
            val checkAnonymousUsers = rankRepository.findByPlayerName(requestedName)
                .next()
                .flatMap<GameRank> { rankWithSameName ->
                    if (rankWithSameName.userId == anonymousUserId) {
                        Mono.empty()
                    } else {
                        Mono.error(IllegalArgumentException("이미 사용 중인 이름입니다."))
                    }
                }
            val gameRankMono = checkAuthUsers.then(checkAnonymousUsers)
                .then(Mono.just(GameRank(
                    userId = anonymousUserId,
                    gameType = rankDto.gameType,
                    contextId = rankDto.contextId,
                    playerName = requestedName,
                    primaryScore = rankDto.primaryScore,
                    secondaryScore = rankDto.secondaryScore
                )))
            gameRankMono.flatMap { rankRepository.save(it) }
        }

        // 2. 🔽 [로직 변경] 저장이 성공하면(flatMap), 상위 랭킹과 내 순위를 '조합'합니다.
        return saveOperation.flatMap { mySavedRank ->

            // 2a. 상위 10개 랭킹 조회
            val topRanksMono: Mono<List<GameRank>> = getRanks(mySavedRank.gameType, mySavedRank.contextId)
                .collectList()

            // 2b. 내 순위(숫자) 계산: 나보다 점수 좋은 사람 수 + 1
            val myRankNumberMono: Mono<Long> = when (mySavedRank.gameType) {
                // DESC (점수 높은 순)
                GameType.GAME_2048 ->
                    rankRepository.countByGameTypeAndContextIdAndPrimaryScoreGreaterThan(
                        mySavedRank.gameType, mySavedRank.contextId, mySavedRank.primaryScore
                    )
                // ASC (점수 낮은 순)
                else ->
                    rankRepository.countByGameTypeAndContextIdAndPrimaryScoreLessThan(
                        mySavedRank.gameType, mySavedRank.contextId, mySavedRank.primaryScore
                    )
            }.map { count -> count + 1 } // 나보다 잘한 사람 수 + 1

            // 3. (2a)와 (2b)의 결과가 모두 오면, Mono.zip으로 합칩니다.
            Mono.zip(topRanksMono, myRankNumberMono)
                .map { tuple ->
                    val topRanksList = tuple.t1
                    val myRankNumber = tuple.t2

                    // 4. 최종 반환 객체(RankSubmissionResult)로 만듭니다.
                    RankSubmissionResult(
                        topRanks = topRanksList,
                        myRank = GameRankWithRankNumber(
                            rankData = mySavedRank,
                            rankNumber = myRankNumber
                        )
                    )
                }
        }
    }


    /**
     * 특정 플레이어의 모든 게임 랭킹을 조회합니다. (변경 없음)
     */
    fun getRanksByPlayer(playerName: String): Flux<GameRank> {
        return rankRepository.findByPlayerNameOrderByTimestampDesc(playerName)
    }
}