package ru.durakscore.app

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.util.Date
import java.util.Locale
import kotlin.math.max

private val auditCorePlayers =
    listOf(
        "dmitry" to "Дмитрий",
        "denis" to "Денис",
        "rybka" to "Рыбка",
        "masha" to "Маша"
    )

private enum class AuditSeverity {
    OK,
    WARNING,
    ERROR
}

private data class AuditIssue(
    val severity: AuditSeverity,
    val title: String,
    val details: String? = null
)

private data class AuditGame(
    val id: String,
    val type: String,
    val isTest: Boolean,
    val affectsMainScore: Boolean,
    val mainScoreApplied: Boolean,
    val targetScore: Int,
    val playerIds: List<String>,
    val finalScores: Map<String, Int>,
    val roundHistory: List<String>,
    val loserId: String?,
    val finishedAt: Timestamp?,
    val hasFinalScores: Boolean
)

private data class AuditLogEvent(
    val id: String,
    val type: String,
    val playerId: String?,
    val oldValue: Int?,
    val newValue: Int?,
    val gameId: String?,
    val values: Map<String, Int>,
    val eventAt: Timestamp?
)

private data class AuditAchievement(
    val title: String,
    val unlocked: Boolean,
    val progress: String
)

private data class AuditPlayerStats(
    val id: String,
    val name: String,
    val gamesPlayed: Int,
    val appLosses: Int,
    val currentMainScore: Int?,
    val expectedMainScoreFromAudit: Int?,
    val totalPoints: Int,
    val bestNoPointStreak: Int,
    val maxLossStreak: Int,
    val cleanGames: Int,
    val nearEscapeGames: Int,
    val bestSurvivedScore: Int,
    val bestSurvivedTarget: Int,
    val achievements: List<AuditAchievement>
)

private data class DataAuditReport(
    val finishedGames: Int,
    val classicGamesCurrentMode: Int,
    val productionClassicGames: Int,
    val customGames: Int,
    val testGames: Int,
    val currentMainScores: Map<String, Int>,
    val expectedMainScores: Map<String, Int>,
    val players: List<AuditPlayerStats>,
    val issues: List<AuditIssue>,
    val checkedGames: Int,
    val leaderText: String,
    val worstText: String,
    val longestLossStreakText: String,
    val bestRateText: String
) {
    val errorCount: Int
        get() = issues.count { it.severity == AuditSeverity.ERROR }

    val warningCount: Int
        get() = issues.count { it.severity == AuditSeverity.WARNING }
}

@Composable
fun DataAuditScreen(
    access: UserAccess,
    onBack: () -> Unit
) {
    BackHandler(onBack = onBack)

    var reloadKey by remember { mutableIntStateOf(0) }
    var isLoading by remember { mutableStateOf(true) }
    var errorText by remember { mutableStateOf<String?>(null) }
    var report by remember { mutableStateOf<DataAuditReport?>(null) }

    LaunchedEffect(reloadKey) {
        isLoading = true
        errorText = null

        loadDataAudit(
            onSuccess = { loadedReport ->
                report = loadedReport
                isLoading = false
            },
            onError = { message ->
                errorText = message
                isLoading = false
            }
        )
    }

    PokerScreen {
        PokerBackTextButton(
            text = "← Назад",
            onClick = onBack
        )

        Spacer(modifier = Modifier.height(4.dp))

        PokerHeader(
            title = "Проверка данных",
            subtitle = "Независимый пересчёт PostgreSQL • только чтение"
        )

        Spacer(modifier = Modifier.height(14.dp))

        if (access.role != "admin") {
            PokerPanel {
                Text(
                    text = "Этот экран доступен только администратору.",
                    color = PokerPalette.Danger,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold
                )
            }

            Spacer(modifier = Modifier.height(14.dp))
            PokerBottomBackButton(onClick = onBack)
            return@PokerScreen
        }

        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = PokerPalette.Panel,
            shape = androidx.compose.foundation.shape.RoundedCornerShape(18.dp),
            border = BorderStroke(1.dp, PokerPalette.GoldDark)
        ) {
            Column(modifier = Modifier.padding(14.dp)) {
                Text(
                    text = if (TEST_MODE) "РЕЖИМ: TEST" else "РЕЖИМ: БОЕВОЙ",
                    color = PokerPalette.Gold,
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Black
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = "Экран ничего не исправляет и не записывает. Он только сверяет партии, общий счёт, журнал и условия достижений в PostgreSQL.",
                    color = PokerPalette.TextSecondary,
                    fontSize = 13.sp,
                    lineHeight = 17.sp
                )
            }
        }

        Spacer(modifier = Modifier.height(14.dp))

        when {
            isLoading -> {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center
                ) {
                    CircularProgressIndicator(color = PokerPalette.Gold)
                }

                Spacer(modifier = Modifier.height(12.dp))

                Text(
                    text = "Пересчитываю всю статистику...",
                    color = PokerPalette.TextSecondary,
                    fontSize = 14.sp,
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }

            errorText != null -> {
                PokerPanel {
                    Text(
                        text = "Не удалось выполнить проверку",
                        color = PokerPalette.Danger,
                        fontSize = 17.sp,
                        fontWeight = FontWeight.Black
                    )
                    Spacer(modifier = Modifier.height(6.dp))
                    Text(
                        text = errorText!!,
                        color = PokerPalette.TextSecondary,
                        fontSize = 14.sp
                    )
                }
            }

            report != null -> {
                val value = report!!

                AuditStatusCard(value)
                Spacer(modifier = Modifier.height(12.dp))

                AuditSectionTitle("ПАРТИИ")
                Spacer(modifier = Modifier.height(8.dp))
                AuditGamesSummary(value)

                Spacer(modifier = Modifier.height(14.dp))
                AuditSectionTitle("ОБЩИЙ СЧЁТ")
                Spacer(modifier = Modifier.height(8.dp))
                AuditMainScoreSummary(value)

                Spacer(modifier = Modifier.height(14.dp))
                AuditSectionTitle("СТАТИСТИКА ЭКРАНА «ОБЩИЙ СЧЁТ»")
                Spacer(modifier = Modifier.height(8.dp))
                PokerPanel {
                    AuditValueLine("Лидер", value.leaderText)
                    AuditValueLine("Больше поражений", value.worstText)
                    AuditValueLine("Самая длинная серия", value.longestLossStreakText)
                    AuditValueLine("Самая стабильная игра", value.bestRateText)
                }

                Spacer(modifier = Modifier.height(14.dp))
                AuditSectionTitle("ИГРОКИ И ДОСТИЖЕНИЯ")
                Spacer(modifier = Modifier.height(8.dp))

                value.players.forEach { player ->
                    AuditPlayerCard(player)
                    Spacer(modifier = Modifier.height(10.dp))
                }

                Spacer(modifier = Modifier.height(4.dp))
                AuditSectionTitle("НАЙДЕННЫЕ РАСХОЖДЕНИЯ")
                Spacer(modifier = Modifier.height(8.dp))

                val visibleIssues =
                    value.issues.filter {
                        it.severity != AuditSeverity.OK
                    }

                if (visibleIssues.isEmpty()) {
                    PokerPanel {
                        Text(
                            text = "✓ Расхождений не найдено",
                            color = PokerPalette.Good,
                            fontSize = 16.sp,
                            fontWeight = FontWeight.Black
                        )
                        Spacer(modifier = Modifier.height(5.dp))
                        Text(
                            text = "Проверено завершённых партий: ${value.checkedGames}",
                            color = PokerPalette.TextSecondary,
                            fontSize = 13.sp
                        )
                    }
                } else {
                    visibleIssues.forEach { issue ->
                        AuditIssueCard(issue)
                        Spacer(modifier = Modifier.height(8.dp))
                    }
                }
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        PokerSecondaryButton(
            text = "↻  Проверить заново",
            onClick = {
                reloadKey++
            }
        )

        Spacer(modifier = Modifier.height(10.dp))
        PokerBottomBackButton(onClick = onBack)
    }
}

@Composable
private fun AuditStatusCard(report: DataAuditReport) {
    val allGood = report.errorCount == 0 && report.warningCount == 0
    val title =
        when {
            report.errorCount > 0 -> "✕ ЕСТЬ РАСХОЖДЕНИЯ"
            report.warningCount > 0 -> "! ЕСТЬ ПРЕДУПРЕЖДЕНИЯ"
            else -> "✓ ВСЁ СХОДИТСЯ"
        }

    val accent =
        when {
            report.errorCount > 0 -> PokerPalette.Danger
            report.warningCount > 0 -> PokerPalette.Gold
            else -> PokerPalette.Good
        }

    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = PokerPalette.Panel,
        shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp),
        border = BorderStroke(if (allGood) 1.dp else 1.5.dp, accent)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = title,
                color = accent,
                fontSize = 18.sp,
                fontWeight = FontWeight.Black
            )
            Spacer(modifier = Modifier.height(7.dp))
            Text(
                text = "Ошибок: ${report.errorCount}  •  предупреждений: ${report.warningCount}",
                color = PokerPalette.TextPrimary,
                fontSize = 14.sp,
                fontWeight = FontWeight.SemiBold
            )
            Spacer(modifier = Modifier.height(3.dp))
            Text(
                text = "Проверено finished-партий: ${report.checkedGames}",
                color = PokerPalette.TextSecondary,
                fontSize = 13.sp
            )
        }
    }
}

@Composable
private fun AuditGamesSummary(report: DataAuditReport) {
    PokerPanel {
        AuditValueLine("Всего finished", report.finishedGames.toString())
        AuditValueLine(
            if (TEST_MODE) "Classic в текущем TEST_MODE" else "Classic в приложении",
            report.classicGamesCurrentMode.toString()
        )
        AuditValueLine("Боевых classic", report.productionClassicGames.toString())
        AuditValueLine("Custom", report.customGames.toString())
        AuditValueLine("Тестовых finished", report.testGames.toString())
    }
}

@Composable
private fun AuditMainScoreSummary(report: DataAuditReport) {
    PokerPanel {
        auditCorePlayers.forEach { (id, fallbackName) ->
            val current = report.currentMainScores[id]
            val expected = report.expectedMainScores[id]
            val matches = current != null && expected != null && current == expected

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(modifier = Modifier.weight(1f)) {
                    Text(
                        text = fallbackName,
                        color = PokerPalette.TextPrimary,
                        fontSize = 15.sp,
                        fontWeight = FontWeight.Bold
                    )
                    Text(
                        text = "auditLogs ожидание: ${expected?.toString() ?: "—"}",
                        color = PokerPalette.TextSecondary,
                        fontSize = 12.sp
                    )
                }

                Text(
                    text = "${current?.toString() ?: "—"}  ${if (matches) "✓" else "!"}",
                    color = if (matches) PokerPalette.Good else PokerPalette.Danger,
                    fontSize = 17.sp,
                    fontWeight = FontWeight.Black
                )
            }
            Spacer(modifier = Modifier.height(8.dp))
        }
    }
}

@Composable
private fun AuditPlayerCard(player: AuditPlayerStats) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = PokerPalette.Panel,
        shape = androidx.compose.foundation.shape.RoundedCornerShape(18.dp),
        border = BorderStroke(1.dp, PokerPalette.GoldDark)
    ) {
        Column(modifier = Modifier.padding(14.dp)) {
            Text(
                text = player.name,
                color = PokerPalette.Gold,
                fontSize = 19.sp,
                fontWeight = FontWeight.Black
            )

            Spacer(modifier = Modifier.height(8.dp))

            AuditValueLine("Classic-партий", player.gamesPlayed.toString())
            AuditValueLine("Поражений в приложении", player.appLosses.toString())
            AuditValueLine("Общий /scores", player.currentMainScore?.toString() ?: "—")
            AuditValueLine("Ожидание по auditLogs", player.expectedMainScoreFromAudit?.toString() ?: "—")
            AuditValueLine("Всего получено +1", player.totalPoints.toString())
            AuditValueLine("Макс. раздач без +1", player.bestNoPointStreak.toString())
            AuditValueLine("Макс. поражений подряд", player.maxLossStreak.toString())
            AuditValueLine("Сухих каток", player.cleanGames.toString())
            AuditValueLine("Финишей на target−1", player.nearEscapeGames.toString())

            Spacer(modifier = Modifier.height(8.dp))

            Text(
                text = "ДОСТИЖЕНИЯ",
                color = PokerPalette.CrimsonBright,
                fontSize = 12.sp,
                fontWeight = FontWeight.Black
            )

            Spacer(modifier = Modifier.height(5.dp))

            player.achievements.forEach { achievement ->
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
                    verticalAlignment = Alignment.Top
                ) {
                    Text(
                        text = if (achievement.unlocked) "✓" else "○",
                        color = if (achievement.unlocked) PokerPalette.Good else PokerPalette.TextSecondary,
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Black
                    )
                    Text(
                        text = "${achievement.title} — ${achievement.progress}",
                        color = if (achievement.unlocked) PokerPalette.TextPrimary else PokerPalette.TextSecondary,
                        fontSize = 13.sp,
                        lineHeight = 17.sp,
                        modifier = Modifier.weight(1f)
                    )
                }
                Spacer(modifier = Modifier.height(4.dp))
            }
        }
    }
}

@Composable
private fun AuditIssueCard(issue: AuditIssue) {
    val color =
        when (issue.severity) {
            AuditSeverity.ERROR -> PokerPalette.Danger
            AuditSeverity.WARNING -> PokerPalette.Gold
            AuditSeverity.OK -> PokerPalette.Good
        }

    val prefix =
        when (issue.severity) {
            AuditSeverity.ERROR -> "✕"
            AuditSeverity.WARNING -> "!"
            AuditSeverity.OK -> "✓"
        }

    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = PokerPalette.Panel,
        shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
        border = BorderStroke(1.dp, color)
    ) {
        Column(modifier = Modifier.padding(13.dp)) {
            Text(
                text = "$prefix ${issue.title}",
                color = color,
                fontSize = 14.sp,
                fontWeight = FontWeight.Bold,
                lineHeight = 18.sp
            )
            if (!issue.details.isNullOrBlank()) {
                Spacer(modifier = Modifier.height(5.dp))
                Text(
                    text = issue.details,
                    color = PokerPalette.TextSecondary,
                    fontSize = 12.sp,
                    lineHeight = 16.sp
                )
            }
        }
    }
}

@Composable
private fun AuditSectionTitle(text: String) {
    Text(
        text = text,
        color = PokerPalette.CrimsonBright,
        fontSize = 13.sp,
        fontWeight = FontWeight.Black
    )
}

@Composable
private fun AuditValueLine(
    label: String,
    value: String
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.Top
    ) {
        Text(
            text = label,
            color = PokerPalette.TextSecondary,
            fontSize = 13.sp,
            lineHeight = 17.sp,
            modifier = Modifier.weight(1f)
        )
        Spacer(modifier = Modifier.padding(horizontal = 4.dp))
        Text(
            text = value,
            color = PokerPalette.TextPrimary,
            fontSize = 13.sp,
            fontWeight = FontWeight.Bold,
            lineHeight = 17.sp
        )
    }
    Spacer(modifier = Modifier.height(5.dp))
}

private fun loadDataAudit(
    onSuccess: (DataAuditReport) -> Unit,
    onError: (String) -> Unit
) {

    val user =
        DurakAuthSession.current()

    if (
        user ==
        null
    ) {
        onError(
            "Пользователь не авторизован"
        )
        return
    }

    DurakServerApi
        .loadDataAuditSource(
            user =
                user,
            onSuccess = {
                    source ->

                runCatching {
                    buildDataAuditReport(
                        source
                    )
                }
                    .onSuccess(
                        onSuccess
                    )
                    .onFailure {
                            error ->

                        onError(
                            error.message
                                ?: "Ошибка пересчёта данных"
                        )
                    }
            },
            onError =
                onError
        )
}

private fun buildDataAuditReport(
    source: ServerDataAuditSource
): DataAuditReport {
    val issues = mutableListOf<AuditIssue>()

    val playerNames =
        source.players.associate {
            it.id to
                it.name.ifBlank {
                    it.id
                }
        }

    val currentScores =
        source.scores

    auditCorePlayers.forEach { (id, fallbackName) ->
        if (!playerNames.containsKey(id)) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "В /players отсутствует $fallbackName ($id)"
            )
        }
        if (!currentScores.containsKey(id)) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "В /scores отсутствует $fallbackName ($id)"
            )
        }
    }

    val games =
        source.games.map {
            parseAuditGame(
                it
            )
        }

    val auditEvents =
        source.auditEvents.map {
            parseAuditLogEvent(
                it
            )
        }

    games.forEach { game ->
        validateGame(game, auditEvents, issues)
    }

    val expectedScores = rebuildMainScoresFromAudit(auditEvents, issues)

    auditCorePlayers.forEach { (id, fallbackName) ->
        val current = currentScores[id]
        val expected = expectedScores[id]

        if (expected == null) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "Не удалось восстановить общий счёт $fallbackName по auditLogs",
                "Нет достаточной цепочки initial_score_import / корректировок / game_result."
            )
        } else if (current != expected) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "Общий счёт $fallbackName не сходится",
                "/scores = ${current ?: "—"}, по auditLogs должно быть $expected"
            )
        }
    }

    val gameResultEvents = auditEvents.filter { it.type == "game_result" }
    val productionGamesById =
        games.filter { it.affectsMainScore }.associateBy { it.id }

    gameResultEvents.forEach { event ->
        val gameId = event.gameId
        if (gameId.isNullOrBlank()) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "game_result ${event.id} без gameId"
            )
        } else if (!productionGamesById.containsKey(gameId)) {
            issues += AuditIssue(
                AuditSeverity.WARNING,
                "game_result ссылается на неизвестную боевую партию",
                "Лог ${event.id}, gameId=$gameId"
            )
        }
    }

    val modeClassicGames =
        games
            .filter {
                it.type == "classic" &&
                    it.isTest == TEST_MODE
            }
            .sortedBy { it.finishedAt?.seconds ?: 0L }

    val players =
        auditCorePlayers.map { (id, fallbackName) ->
            calculateAuditPlayerStats(
                games = modeClassicGames,
                playerId = id,
                playerName = playerNames[id] ?: fallbackName,
                currentMainScore = currentScores[id],
                expectedMainScore = expectedScores[id],
                issues = issues
            )
        }

    val sortedMainPlayers =
        auditCorePlayers
            .mapNotNull { (id, fallbackName) ->
                currentScores[id]?.let { score ->
                    Triple(id, playerNames[id] ?: fallbackName, score)
                }
            }

    val minLosses = sortedMainPlayers.minOfOrNull { it.third }
    val maxLosses = sortedMainPlayers.maxOfOrNull { it.third }

    val leaderText =
        if (minLosses == null) {
            "—"
        } else {
            val names = sortedMainPlayers.filter { it.third == minLosses }.joinToString(" и ") { it.second }
            "$names — $minLosses"
        }

    val worstText =
        if (maxLosses == null) {
            "—"
        } else {
            val names = sortedMainPlayers.filter { it.third == maxLosses }.joinToString(" и ") { it.second }
            "$names — $maxLosses"
        }

    val longest = players.maxByOrNull { it.maxLossStreak }
    val longestLossStreakText =
        if (longest == null || longest.maxLossStreak <= 0) {
            "—"
        } else {
            "${longest.name} — ${longest.maxLossStreak} подряд"
        }

    val bestRate =
        if (modeClassicGames.isEmpty()) {
            null
        } else {
            players.minByOrNull {
                it.appLosses.toDouble() / modeClassicGames.size.toDouble()
            }
        }

    val bestRateText =
        if (bestRate == null || modeClassicGames.isEmpty()) {
            "—"
        } else {
            val rate = bestRate.appLosses.toDouble() / modeClassicGames.size.toDouble()
            "${bestRate.name} — ${String.format(Locale.US, "%.2f", rate)}"
        }

    return DataAuditReport(
        finishedGames = games.size,
        classicGamesCurrentMode = modeClassicGames.size,
        productionClassicGames = games.count { it.type == "classic" && !it.isTest },
        customGames = games.count { it.type != "classic" },
        testGames = games.count { it.isTest },
        currentMainScores = currentScores,
        expectedMainScores = expectedScores,
        players = players,
        issues = issues,
        checkedGames = games.size,
        leaderText = leaderText,
        worstText = worstText,
        longestLossStreakText = longestLossStreakText,
        bestRateText = bestRateText
    )
}

private fun parseAuditGame(
    game: ServerDataAuditGame
): AuditGame {

    return AuditGame(
        id =
            game.id,
        type =
            game.gameType,
        isTest =
            game.isTest,
        affectsMainScore =
            game.affectsMainScore,
        mainScoreApplied =
            game.mainScoreApplied,
        targetScore =
            game.targetScore,
        playerIds =
            game.playerIds,
        finalScores =
            game.finalScores,
        roundHistory =
            game.roundHistory,
        loserId =
            game.loserId,
        finishedAt =
            game.finishedAtEpochMillis
                ?.let {
                    Timestamp(
                        Date(
                            it
                        )
                    )
                },
        hasFinalScores =
            game.hasFinalScores
    )
}


private fun parseAuditLogEvent(
    event: ServerDataAuditEvent
): AuditLogEvent {

    return AuditLogEvent(
        id =
            event.id,
        type =
            event.type,
        playerId =
            event.playerId,
        oldValue =
            event.oldValue,
        newValue =
            event.newValue,
        gameId =
            event.gameId,
        values =
            event.values,
        eventAt =
            event.eventAtEpochMillis
                ?.let {
                    Timestamp(
                        Date(
                            it
                        )
                    )
                }
    )
}


private fun validateGame(
    game: AuditGame,
    auditEvents: List<AuditLogEvent>,
    issues: MutableList<AuditIssue>
) {
    val prefix = "Партия ${shortId(game.id)}"

    if (game.finishedAt == null) {
        issues += AuditIssue(AuditSeverity.ERROR, "$prefix: нет finishedAt")
    }

    if (game.targetScore <= 0) {
        issues += AuditIssue(AuditSeverity.ERROR, "$prefix: некорректный targetScore=${game.targetScore}")
    }

    if (game.playerIds.isEmpty()) {
        issues += AuditIssue(AuditSeverity.ERROR, "$prefix: пустой список игроков")
    }

    if (!game.hasFinalScores) {
        issues += AuditIssue(
            AuditSeverity.WARNING,
            "$prefix: нет finalScores",
            "Для проверки использовано поле scores."
        )
    }

    val expectedAffectsMainScore = game.type == "classic" && !game.isTest
    if (game.affectsMainScore != expectedAffectsMainScore) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: affectsMainScore=${game.affectsMainScore}",
            "Для type=${game.type}, isTest=${game.isTest} ожидалось $expectedAffectsMainScore."
        )
    }

    if (game.mainScoreApplied != game.affectsMainScore) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: mainScoreApplied=${game.mainScoreApplied}",
            "affectsMainScore=${game.affectsMainScore}"
        )
    }

    if (game.type == "classic") {
        val expectedIds = auditCorePlayers.map { it.first }.toSet()
        val actualIds = game.playerIds.toSet()
        if (actualIds != expectedIds || game.playerIds.size != 4) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "$prefix: неверный состав classic",
                "Получено: ${game.playerIds.joinToString()}, ожидалось: ${expectedIds.joinToString()}"
            )
        }
    }

    val unknownHistoryIds = game.roundHistory.filter { it !in game.playerIds }.distinct()
    if (unknownHistoryIds.isNotEmpty()) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: roundHistory содержит неизвестных игроков",
            unknownHistoryIds.joinToString()
        )
    }

    val historyCounts = game.roundHistory.groupingBy { it }.eachCount()

    game.playerIds.forEach { playerId ->
        val finalScore = game.finalScores[playerId]
        val historyScore = historyCounts[playerId] ?: 0
        if (finalScore == null) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "$prefix: в finalScores нет $playerId"
            )
        } else if (finalScore != historyScore) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "$prefix: счёт $playerId не сходится",
                "finalScores=$finalScore, roundHistory=$historyScore"
            )
        }
    }

    val sumScores = game.finalScores.values.sum()
    if (sumScores != game.roundHistory.size) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: сумма очков не равна roundHistory",
            "sum(finalScores)=$sumScores, roundHistory=${game.roundHistory.size}"
        )
    }

    val loserId = game.loserId
    if (loserId.isNullOrBlank()) {
        issues += AuditIssue(AuditSeverity.ERROR, "$prefix: нет loserId")
    } else if (loserId !in game.playerIds) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: loserId отсутствует среди игроков",
            "loserId=$loserId"
        )
    } else {
        val loserScore = game.finalScores[loserId] ?: 0
        if (loserScore < game.targetScore) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "$prefix: проигравший не достиг лимита",
                "$loserId: $loserScore/${game.targetScore}"
            )
        } else if (loserScore > game.targetScore) {
            issues += AuditIssue(
                AuditSeverity.WARNING,
                "$prefix: проигравший выше лимита",
                "$loserId: $loserScore/${game.targetScore}. Обычно финиш должен быть ровно на лимите."
            )
        }
    }

    val gameResultLogs =
        auditEvents.filter {
            it.type == "game_result" && it.gameId == game.id
        }

    if (game.affectsMainScore && gameResultLogs.size != 1) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: неверное число game_result логов",
            "Найдено ${gameResultLogs.size}, ожидался ровно 1."
        )
    }

    if (!game.affectsMainScore && gameResultLogs.isNotEmpty()) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "$prefix: не должна менять общий счёт",
            "Но найдено game_result логов: ${gameResultLogs.size}."
        )
    }

    if (game.affectsMainScore && gameResultLogs.size == 1 && loserId != null) {
        val log = gameResultLogs.first()
        if (log.playerId != loserId) {
            issues += AuditIssue(
                AuditSeverity.ERROR,
                "$prefix: loserId не совпадает с game_result",
                "game loser=$loserId, log player=${log.playerId ?: "—"}"
            )
        }
    }
}

private fun rebuildMainScoresFromAudit(
    events: List<AuditLogEvent>,
    issues: MutableList<AuditIssue>
): Map<String, Int> {
    val important =
        events.filter {
            it.type == "initial_score_import" ||
                it.type == "manual_score_correction" ||
                it.type == "game_result"
        }

    important.filter { it.eventAt == null }.forEach { event ->
        issues += AuditIssue(
            AuditSeverity.WARNING,
            "Audit log ${shortId(event.id)} без времени",
            "Тип: ${event.type}. Порядок восстановления счёта может быть неточным."
        )
    }

    val sorted =
        important.sortedWith(
            compareBy<AuditLogEvent> { it.eventAt?.seconds ?: Long.MAX_VALUE }
                .thenBy { it.id }
        )

    val result = mutableMapOf<String, Int>()
    var importCount = 0

    sorted.forEach { event ->
        when (event.type) {
            "initial_score_import" -> {
                importCount++
                if (event.values.isEmpty()) {
                    issues += AuditIssue(
                        AuditSeverity.ERROR,
                        "initial_score_import ${shortId(event.id)} без values"
                    )
                } else {
                    event.values.forEach { (id, losses) ->
                        result[id] = losses
                    }
                }
            }

            "manual_score_correction", "game_result" -> {
                val playerId = event.playerId
                val newValue = event.newValue

                if (playerId.isNullOrBlank() || newValue == null) {
                    issues += AuditIssue(
                        AuditSeverity.ERROR,
                        "Audit log ${shortId(event.id)} неполный",
                        "type=${event.type}, playerId=${playerId ?: "—"}, newValue=${newValue ?: "—"}"
                    )
                    return@forEach
                }

                val previous = result[playerId]
                val oldValue = event.oldValue

                if (previous != null && oldValue != null && previous != oldValue) {
                    issues += AuditIssue(
                        AuditSeverity.ERROR,
                        "Разрыв цепочки auditLogs для $playerId",
                        "Перед ${event.type} ожидалось oldValue=$previous, а в логе oldValue=$oldValue. Лог ${shortId(event.id)}."
                    )
                }

                if (event.type == "game_result" && oldValue != null && newValue != oldValue + 1) {
                    issues += AuditIssue(
                        AuditSeverity.ERROR,
                        "game_result не добавляет ровно +1",
                        "$playerId: $oldValue → $newValue, лог ${shortId(event.id)}"
                    )
                }

                result[playerId] = newValue
            }
        }
    }

    if (importCount == 0) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "Не найден initial_score_import",
            "Без него невозможно надёжно подтвердить историческую часть общего счёта."
        )
    } else if (importCount > 1) {
        issues += AuditIssue(
            AuditSeverity.WARNING,
            "Найдено несколько initial_score_import: $importCount"
        )
    }

    return result.toMap()
}

private fun calculateAuditPlayerStats(
    games: List<AuditGame>,
    playerId: String,
    playerName: String,
    currentMainScore: Int?,
    expectedMainScore: Int?,
    issues: MutableList<AuditIssue>
): AuditPlayerStats {
    val playerGames =
        games
            .filter { it.finalScores.containsKey(playerId) }
            .sortedBy { it.finishedAt?.seconds ?: 0L }

    var currentLossStreak = 0
    var maxLossStreak = 0

    playerGames.forEach { game ->
        if (game.loserId == playerId) {
            currentLossStreak++
            maxLossStreak = max(maxLossStreak, currentLossStreak)
        } else {
            currentLossStreak = 0
        }
    }

    var bestSurvivedScore = 0
    var bestSurvivedTarget = 0

    playerGames
        .filter { it.loserId != playerId }
        .forEach { game ->
            val score = game.finalScores[playerId] ?: 0
            val oldDistance =
                if (bestSurvivedTarget > 0) {
                    bestSurvivedTarget - bestSurvivedScore
                } else {
                    Int.MAX_VALUE
                }
            val newDistance = game.targetScore - score
            if (newDistance < oldDistance) {
                bestSurvivedScore = score
                bestSurvivedTarget = game.targetScore
            }
        }

    val totalPointsFromHistory =
        playerGames.sumOf { game ->
            game.roundHistory.count { it == playerId }
        }

    val totalPointsFromScores =
        playerGames.sumOf { it.finalScores[playerId] ?: 0 }

    if (totalPointsFromHistory != totalPointsFromScores) {
        issues += AuditIssue(
            AuditSeverity.ERROR,
            "Суммарные +1 $playerName не сходятся",
            "По roundHistory=$totalPointsFromHistory, по finalScores=$totalPointsFromScores"
        )
    }

    val bestRoundStreak =
        playerGames.maxOfOrNull { game ->
            bestNoPointStreakAudit(game, playerId)
        } ?: 0

    val losses = playerGames.count { it.loserId == playerId }
    val cleanGames = playerGames.count { (it.finalScores[playerId] ?: 0) == 0 }
    val nearEscapeGames =
        playerGames.count { game ->
            game.loserId != playerId &&
                (game.finalScores[playerId] ?: 0) == game.targetScore - 1
        }

    val survivedOneAway =
        bestSurvivedTarget > 0 &&
            bestSurvivedScore >= bestSurvivedTarget - 1

    val achievements =
        listOf(
            AuditAchievement("Хуй пробьёшь", bestRoundStreak >= 10, "$bestRoundStreak/10"),
            AuditAchievement("Серийный дурак", maxLossStreak >= 5, "$maxLossStreak/5 подряд"),
            AuditAchievement("Ебаный пылесос +1", totalPointsFromHistory >= 100, "$totalPointsFromHistory/100"),
            AuditAchievement(
                "Живучая тварь",
                survivedOneAway,
                if (bestSurvivedTarget > 0) "$bestSurvivedScore/$bestSurvivedTarget" else "—"
            ),
            AuditAchievement("Сухой, аж бесит", cleanGames >= 3, "$cleanGames/3"),
            AuditAchievement("Смерть его не берёт", nearEscapeGames >= 3, "$nearEscapeGames/3"),
            AuditAchievement("Заслуженный долбоёб", losses >= 10, "$losses/10")
        )

    return AuditPlayerStats(
        id = playerId,
        name = playerName,
        gamesPlayed = playerGames.size,
        appLosses = losses,
        currentMainScore = currentMainScore,
        expectedMainScoreFromAudit = expectedMainScore,
        totalPoints = totalPointsFromHistory,
        bestNoPointStreak = bestRoundStreak,
        maxLossStreak = maxLossStreak,
        cleanGames = cleanGames,
        nearEscapeGames = nearEscapeGames,
        bestSurvivedScore = bestSurvivedScore,
        bestSurvivedTarget = bestSurvivedTarget,
        achievements = achievements
    )
}

private fun bestNoPointStreakAudit(
    game: AuditGame,
    playerId: String
): Int {
    var current = 0
    var best = 0

    game.roundHistory.forEach { loserId ->
        if (loserId == playerId) {
            current = 0
        } else {
            current++
            best = max(best, current)
        }
    }

    return best
}

private fun shortId(id: String): String =
    if (id.length <= 10) id else id.take(8) + "…"
