package ru.durakscore.app

import android.os.Handler
import android.os.Looper
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID
import kotlin.concurrent.thread

data class ServerMe(
    val uid: String,
    val email: String?,
    val name: String,
    val playerId: String,
    val role: String,
    val active: Boolean
)

data class ServerMainScorePlayer(
    val id: String,
    val name: String,
    val gender: String,
    val isCore: Boolean,
    val sortOrder: Int,
    val losses: Int
)

data class ServerMainScoreCorrection(
    val playerId: String,
    val oldValue: Int,
    val newValue: Int,
    val alreadyApplied: Boolean
)

data class ServerFinishGameResult(
    val gameId: String,
    val loserId: String,
    val alreadyFinished: Boolean,
    val affectsMainScore: Boolean,
    val mainScoreApplied: Boolean,
    val newLosses: Int?
)

data class ServerGamePlayer(
    val id: String,
    val name: String,
    val gender: String,
    val isCore: Boolean,
    val isGuest: Boolean
)

data class ServerLoadedGame(
    val id: String,
    val gameType: String,
    val status: String,
    val targetScore: Int,
    val isTest: Boolean,
    val affectsMainScore: Boolean,
    val players: List<ServerGamePlayer>,
    val scores: Map<String, Int>,
    val history: List<String>,
    val startedAtEpochMillis: Long
)

data class ServerGameState(
    val scores: Map<String, Int>,
    val history: List<String>
)

data class ServerFinishedGame(
    val id: String,
    val gameType: String,
    val isTest: Boolean,
    val targetScore: Int,
    val players: List<ServerGamePlayer>,
    val finalScores: Map<String, Int>,
    val history: List<String>,
    val loserId: String,
    val loserName: String,
    val loserGender: String,
    val startedAtEpochMillis: Long,
    val finishedAtEpochMillis: Long
)

data class ServerAuditLog(
    val id: String,
    val type: String,
    val eventAtEpochMillis: Long?,
    val actorName: String,
    val title: String,
    val details: List<String>,
    val isTest: Boolean
)

data class ServerLiveGame(
    val id: String,
    val gameType: String,
    val status: String,
    val targetScore: Int,
    val isTest: Boolean,
    val players: List<ServerGamePlayer>,
    val scores: Map<String, Int>,
    val finalScores: Map<String, Int>,
    val history: List<String>,
    val startedAtEpochMillis: Long,
    val lastActionType: String?,
    val lastActionAtEpochMillis: Long?,
    val finishedAtEpochMillis: Long?,
    val loserId: String?,
    val loserName: String?,
    val loserGender: String?
)

data class ServerLiveSnapshot(
    val activeGame: ServerLiveGame?,
    val lastFinishedGame: ServerLiveGame?
)

data class ServerReaction(
    val id: String,
    val emoji: String,
    val senderUid: String,
    val senderName: String?,
    val createdAtEpochMillis: Long?
)

data class ServerAccessStatus(
    val status: String
)

data class ServerPendingAccessRequest(
    val uid: String,
    val name: String,
    val email: String,
    val createdAtEpochMillis: Long?
)

data class ServerActiveMember(
    val uid: String,
    val name: String,
    val playerId: String,
    val role: String,
    val active: Boolean
)

data class ServerAdminUsers(
    val requests: List<ServerPendingAccessRequest>,
    val members: List<ServerActiveMember>
)

data class ServerDataAuditPlayer(
    val id: String,
    val name: String
)

data class ServerDataAuditGame(
    val id: String,
    val gameType: 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 finishedAtEpochMillis: Long?,
    val hasFinalScores: Boolean
)

data class ServerDataAuditEvent(
    val id: String,
    val type: String,
    val playerId: String?,
    val oldValue: Int?,
    val newValue: Int?,
    val gameId: String?,
    val values: Map<String, Int>,
    val eventAtEpochMillis: Long?
)

data class ServerDataAuditSource(
    val players: List<ServerDataAuditPlayer>,
    val scores: Map<String, Int>,
    val games: List<ServerDataAuditGame>,
    val auditEvents: List<ServerDataAuditEvent>
)

class ServerPollingHandle(
    private val cancelAction: () -> Unit
) {
    @Volatile
    private var stopped =
        false

    fun remove() {
        if (!stopped) {
            stopped = true
            cancelAction()
        }
    }
}

object DurakServerApi {

    private const val TAG =
        "DurakServerApi"

    private const val BASE_URL =
        "https://api.bearmsk.ru"

    private const val CONNECT_TIMEOUT_MS =
        7_000

    private const val READ_TIMEOUT_MS =
        10_000

    private val mainHandler =
        Handler(
            Looper.getMainLooper()
        )

    fun login(
        email: String,
        password: String,
        onSuccess: (DurakSession) -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "email",
                    email.trim()
                )
                .put(
                    "password",
                    password
                )
                .toString()

        plainJsonRequest(
            method =
                "POST",
            path =
                "/api/v1/auth/login",
            body =
                body,
            onSuccess = {
                    json ->

                onSuccess(
                    parseAuthSession(
                        json
                    )
                )
            },
            onError =
                onError
        )
    }


    fun register(
        name: String,
        email: String,
        password: String,
        onSuccess: (DurakSession) -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "name",
                    name.trim()
                )
                .put(
                    "email",
                    email.trim()
                )
                .put(
                    "password",
                    password
                )
                .toString()

        plainJsonRequest(
            method =
                "POST",
            path =
                "/api/v1/auth/register",
            body =
                body,
            onSuccess = {
                    json ->

                onSuccess(
                    parseAuthSession(
                        json
                    )
                )
            },
            onError =
                onError
        )
    }


    fun logout(
        user: DurakSession
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "POST",
            path =
                "/api/v1/auth/logout",
            body =
                null,
            allowNetworkRetry =
                false,
            onSuccess = {
            },
            onError = {
            }
        )
    }


    private fun parseAuthSession(
        json: JSONObject
    ): DurakSession {

        return DurakSession(
            uid =
                json.optString(
                    "uid",
                    ""
                ),
            name =
                json.optString(
                    "name",
                    "Пользователь"
                ),
            email =
                json.optString(
                    "email",
                    ""
                ),
            token =
                json.optString(
                    "token",
                    ""
                ),
            expiresAtEpochMillis =
                json.optLong(
                    "expiresAtEpochMillis",
                    0L
                )
        )
    }


    fun loadMe(
        user: DurakSession,
        onSuccess: (ServerMe) -> Unit,
        onForbidden: () -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path = "/api/v1/me",
            body = null,
            onForbidden = onForbidden,
            onSuccess = { json ->

                onSuccess(
                    ServerMe(
                        uid =
                            json.optString(
                                "uid",
                                user.uid
                            ),
                        email =
                            json.optString(
                                "email",
                                ""
                            )
                                .takeIf {
                                    it.isNotBlank() &&
                                        it !=
                                        "null"
                                },
                        name =
                            json.optString(
                                "name",
                                "Пользователь"
                            ),
                        playerId =
                            json.optString(
                                "playerId",
                                ""
                            ),
                        role =
                            json.optString(
                                "role",
                                "viewer"
                            ),
                        active =
                            json.optBoolean(
                                "active",
                                true
                            )
                    )
                )
            },
            onError = onError
        )
    }

    fun loadMainScore(
        user: DurakSession,
        onSuccess: (List<ServerMainScorePlayer>) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path = "/api/v1/main-score",
            body = null,
            onSuccess = { json ->

                val array =
                    json.getJSONArray(
                        "players"
                    )

                val players =
                    buildList {

                        for (
                            index in
                            0 until array.length()
                        ) {

                            val item =
                                array.getJSONObject(
                                    index
                                )

                            add(
                                ServerMainScorePlayer(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    name =
                                        item.optString(
                                            "name",
                                            ""
                                        ),
                                    gender =
                                        item.optString(
                                            "gender",
                                            ""
                                        ),
                                    isCore =
                                        item.optBoolean(
                                            "isCore",
                                            false
                                        ),
                                    sortOrder =
                                        item.optInt(
                                            "sortOrder",
                                            999
                                        ),
                                    losses =
                                        item.optInt(
                                            "losses",
                                            0
                                        )
                                )
                            )
                        }
                    }

                Log.i(
                    TAG,
                    "Main score loaded from VPS: players=${players.size}"
                )

                onSuccess(
                    players
                )
            },
            onError = onError
        )
    }

    fun correctMainScore(
        user: DurakSession,
        playerId: String,
        newValue: Int,
        requestId: String,
        onSuccess: (ServerMainScoreCorrection) -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "playerId",
                    playerId
                )
                .put(
                    "newValue",
                    newValue
                )
                .put(
                    "requestId",
                    requestId
                )
                .toString()

        authenticatedJsonRequest(
            user = user,
            method = "POST",
            path =
                "/api/v1/main-score/correct",
            body = body,
            onSuccess = { json ->

                val result =
                    ServerMainScoreCorrection(
                        playerId =
                            json.optString(
                                "playerId",
                                playerId
                            ),
                        oldValue =
                            json.optInt(
                                "oldValue",
                                newValue
                            ),
                        newValue =
                            json.optInt(
                                "newValue",
                                newValue
                            ),
                        alreadyApplied =
                            json.optBoolean(
                                "alreadyApplied",
                                false
                            )
                    )

                Log.i(
                    TAG,
                    "Main score correction synced: player=${result.playerId}, ${result.oldValue}->${result.newValue}"
                )

                onSuccess(
                    result
                )
            },
            onError = onError
        )
    }

    fun finishGame(
        user: DurakSession,
        gameId: String,
        loserId: String,
        onSuccess: (ServerFinishGameResult) -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "loserId",
                    loserId
                )
                .toString()

        authenticatedJsonRequest(
            user = user,
            method = "POST",
            path =
                "/api/v1/games/$gameId/finish",
            body = body,
            onSuccess = { json ->

                val newLosses =
                    if (
                        json.isNull(
                            "newLosses"
                        )
                    ) {
                        null
                    } else {
                        json.optInt(
                            "newLosses"
                        )
                    }

                val result =
                    ServerFinishGameResult(
                        gameId =
                            json.optString(
                                "gameId",
                                gameId
                            ),
                        loserId =
                            json.optString(
                                "loserId",
                                loserId
                            ),
                        alreadyFinished =
                            json.optBoolean(
                                "alreadyFinished",
                                false
                            ),
                        affectsMainScore =
                            json.optBoolean(
                                "affectsMainScore",
                                false
                            ),
                        mainScoreApplied =
                            json.optBoolean(
                                "mainScoreApplied",
                                false
                            ),
                        newLosses =
                            newLosses
                    )

                Log.i(
                    TAG,
                    "Game finish synced: game=${result.gameId}, loser=${result.loserId}, newLosses=${result.newLosses}"
                )

                onSuccess(
                    result
                )
            },
            onError = onError
        )
    }

    fun createGame(
        user: DurakSession,
        setup: GameSetup,
        isTest: Boolean,
        onSuccess: (ServerLoadedGame) -> Unit,
        onError: (String) -> Unit
    ) {

        val requestId =
            UUID.randomUUID()
                .toString()

        val gameType =
            if (
                setup.type ==
                GameType.CLASSIC
            ) {
                "classic"
            } else {
                "custom"
            }

        val players =
            JSONArray()

        setup.players.forEach {
                player ->

            players.put(
                JSONObject()
                    .put(
                        "id",
                        player.id
                    )
                    .put(
                        "name",
                        player.name
                    )
                    .put(
                        "gender",
                        player.gender
                    )
                    .put(
                        "isCore",
                        player.isCore
                    )
                    .put(
                        "isGuest",
                        player.isGuest
                    )
            )
        }

        val body =
            JSONObject()
                .put(
                    "requestId",
                    requestId
                )
                .put(
                    "gameType",
                    gameType
                )
                .put(
                    "targetScore",
                    setup.targetScore
                )
                .put(
                    "players",
                    players
                )
                .put(
                    "isTest",
                    isTest
                )
                .toString()

        authenticatedJsonRequest(
            user = user,
            method = "POST",
            path =
                "/api/v1/games/create",
            body = body,
            onSuccess = { json ->

                val gameJson =
                    json.getJSONObject(
                        "game"
                    )

                onSuccess(
                    parseLoadedGame(
                        gameJson
                    )
                )
            },
            onError = onError
        )
    }

    fun loadLatestUnfinishedGame(
        user: DurakSession,
        isTest: Boolean,
        onSuccess: (ServerLoadedGame?) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path =
                "/api/v1/games/unfinished?isTest=$isTest",
            body = null,
            onSuccess = { json ->

                if (
                    json.isNull(
                        "game"
                    )
                ) {
                    onSuccess(
                        null
                    )
                } else {
                    onSuccess(
                        parseLoadedGame(
                            json.getJSONObject(
                                "game"
                            )
                        )
                    )
                }
            },
            onError = onError
        )
    }

    fun loadGameState(
        user: DurakSession,
        gameId: String,
        onSuccess: (ServerGameState) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path =
                "/api/v1/games/$gameId/state",
            body = null,
            onSuccess = { json ->

                val game =
                    json.getJSONObject(
                        "game"
                    )

                onSuccess(
                    ServerGameState(
                        scores =
                            parseScores(
                                game.optJSONObject(
                                    "scores"
                                )
                            ),
                        history =
                            parseHistory(
                                game.optJSONArray(
                                    "history"
                                )
                            )
                    )
                )
            },
            onError = onError
        )
    }

    fun loadGameStatus(
        user: DurakSession,
        gameId: String,
        onSuccess: (String) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path =
                "/api/v1/games/$gameId/status",
            body = null,
            onSuccess = { json ->

                onSuccess(
                    json.optString(
                        "status",
                        "unknown"
                    )
                )
            },
            onError = onError
        )
    }

    fun applyGameActions(
        user: DurakSession,
        gameId: String,
        actions: List<QueuedGameAction>,
        onSuccess: (ServerGameState) -> Unit,
        onError: (String) -> Unit
    ) {

        val requestId =
            UUID.randomUUID()
                .toString()

        val actionsJson =
            JSONArray()

        actions.forEach {
                action ->

            val item =
                JSONObject()

            when (
                action.type
            ) {

                GameActionType.ADD_POINT -> {
                    item.put(
                        "type",
                        "add_point"
                    )

                    item.put(
                        "playerId",
                        action.playerId
                    )
                }

                GameActionType.UNDO_LAST -> {
                    item.put(
                        "type",
                        "undo_last"
                    )
                }
            }

            actionsJson.put(
                item
            )
        }

        val body =
            JSONObject()
                .put(
                    "requestId",
                    requestId
                )
                .put(
                    "actions",
                    actionsJson
                )
                .toString()

        authenticatedJsonRequest(
            user = user,
            method = "POST",
            path =
                "/api/v1/games/$gameId/actions",
            body = body,
            onSuccess = { json ->

                onSuccess(
                    ServerGameState(
                        scores =
                            parseScores(
                                json.optJSONObject(
                                    "scores"
                                )
                            ),
                        history =
                            parseHistory(
                                json.optJSONArray(
                                    "history"
                                )
                            )
                    )
                )
            },
            onError = onError
        )
    }

    fun pauseGame(
        user: DurakSession,
        gameId: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        simpleGamePost(
            user =
                user,
            path =
                "/api/v1/games/$gameId/pause",
            body =
                JSONObject()
                    .toString(),
            onSuccess =
                onSuccess,
            onError =
                onError
        )
    }

    fun resumeGame(
        user: DurakSession,
        gameId: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        simpleGamePost(
            user =
                user,
            path =
                "/api/v1/games/$gameId/resume",
            body =
                JSONObject()
                    .toString(),
            onSuccess =
                onSuccess,
            onError =
                onError
        )
    }

    fun updateTargetScore(
        user: DurakSession,
        gameId: String,
        newTargetScore: Int,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "newTargetScore",
                    newTargetScore
                )
                .toString()

        simpleGamePost(
            user =
                user,
            path =
                "/api/v1/games/$gameId/target-score",
            body =
                body,
            onSuccess =
                onSuccess,
            onError =
                onError
        )
    }

    fun cancelGame(
        user: DurakSession,
        gameId: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        simpleGamePost(
            user =
                user,
            path =
                "/api/v1/games/$gameId/cancel",
            body =
                JSONObject()
                    .toString(),
            onSuccess =
                onSuccess,
            onError =
                onError
        )
    }

    private fun simpleGamePost(
        user: DurakSession,
        path: String,
        body: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "POST",
            path = path,
            body = body,
            onSuccess = {
                onSuccess()
            },
            onError = onError
        )
    }

    private fun parseLoadedGame(
        json: JSONObject
    ): ServerLoadedGame {

        val playersJson =
            json.optJSONArray(
                "players"
            )

        val players =
            buildList {

                if (
                    playersJson !=
                    null
                ) {

                    for (
                        index in
                        0 until
                            playersJson.length()
                    ) {

                        val item =
                            playersJson
                                .getJSONObject(
                                    index
                                )

                        add(
                            ServerGamePlayer(
                                id =
                                    item.optString(
                                        "id",
                                        ""
                                    ),
                                name =
                                    item.optString(
                                        "name",
                                        ""
                                    ),
                                gender =
                                    item.optString(
                                        "gender",
                                        "male"
                                    ),
                                isCore =
                                    item.optBoolean(
                                        "isCore",
                                        false
                                    ),
                                isGuest =
                                    item.optBoolean(
                                        "isGuest",
                                        false
                                    )
                            )
                        )
                    }
                }
            }

        return ServerLoadedGame(
            id =
                json.optString(
                    "id",
                    ""
                ),
            gameType =
                json.optString(
                    "gameType",
                    "custom"
                ),
            status =
                json.optString(
                    "status",
                    "unknown"
                ),
            targetScore =
                json.optInt(
                    "targetScore",
                    15
                ),
            isTest =
                json.optBoolean(
                    "isTest",
                    false
                ),
            affectsMainScore =
                json.optBoolean(
                    "affectsMainScore",
                    false
                ),
            players =
                players,
            scores =
                parseScores(
                    json.optJSONObject(
                        "scores"
                    )
                ),
            history =
                parseHistory(
                    json.optJSONArray(
                        "history"
                    )
                ),
            startedAtEpochMillis =
                json.optLong(
                    "startedAtEpochMillis",
                    0L
                )
        )
    }

    private fun parseScores(
        json: JSONObject?
    ): Map<String, Int> {

        if (
            json ==
            null
        ) {
            return emptyMap()
        }

        val result =
            mutableMapOf<String, Int>()

        val keys =
            json.keys()

        while (
            keys.hasNext()
        ) {

            val key =
                keys.next()

            result[key] =
                json.optInt(
                    key,
                    0
                )
        }

        return result
    }

    private fun parseHistory(
        array: JSONArray?
    ): List<String> {

        if (
            array ==
            null
        ) {
            return emptyList()
        }

        return buildList {

            for (
                index in
                0 until array.length()
            ) {

                val value =
                    array.optString(
                        index,
                        ""
                    )

                if (
                    value.isNotBlank()
                ) {
                    add(
                        value
                    )
                }
            }
        }
    }

    fun loadFinishedGames(
        user: DurakSession,
        onSuccess: (List<ServerFinishedGame>) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path =
                "/api/v1/games/finished?limit=500",
            body = null,
            onSuccess = { json ->

                val array =
                    json.getJSONArray(
                        "games"
                    )

                val games =
                    buildList {

                        for (
                            index in
                            0 until array.length()
                        ) {

                            val item =
                                array.getJSONObject(
                                    index
                                )

                            val playersJson =
                                item.optJSONArray(
                                    "players"
                                )

                            val players =
                                buildList {

                                    if (
                                        playersJson !=
                                        null
                                    ) {

                                        for (
                                            playerIndex in
                                            0 until playersJson.length()
                                        ) {

                                            val player =
                                                playersJson
                                                    .getJSONObject(
                                                        playerIndex
                                                    )

                                            add(
                                                ServerGamePlayer(
                                                    id =
                                                        player.optString(
                                                            "id",
                                                            ""
                                                        ),
                                                    name =
                                                        player.optString(
                                                            "name",
                                                            ""
                                                        ),
                                                    gender =
                                                        player.optString(
                                                            "gender",
                                                            "male"
                                                        ),
                                                    isCore =
                                                        player.optBoolean(
                                                            "isCore",
                                                            false
                                                        ),
                                                    isGuest =
                                                        player.optBoolean(
                                                            "isGuest",
                                                            false
                                                        )
                                                )
                                            )
                                        }
                                    }
                                }

                            add(
                                ServerFinishedGame(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    gameType =
                                        item.optString(
                                            "gameType",
                                            "custom"
                                        ),
                                    isTest =
                                        item.optBoolean(
                                            "isTest",
                                            false
                                        ),
                                    targetScore =
                                        item.optInt(
                                            "targetScore",
                                            15
                                        ),
                                    players =
                                        players,
                                    finalScores =
                                        parseScores(
                                            item.optJSONObject(
                                                "finalScores"
                                            )
                                        ),
                                    history =
                                        parseHistory(
                                            item.optJSONArray(
                                                "history"
                                            )
                                        ),
                                    loserId =
                                        item.optString(
                                            "loserId",
                                            ""
                                        ),
                                    loserName =
                                        item.optString(
                                            "loserName",
                                            "Неизвестно"
                                        ),
                                    loserGender =
                                        item.optString(
                                            "loserGender",
                                            "male"
                                        ),
                                    startedAtEpochMillis =
                                        item.optLong(
                                            "startedAtEpochMillis",
                                            0L
                                        ),
                                    finishedAtEpochMillis =
                                        item.optLong(
                                            "finishedAtEpochMillis",
                                            0L
                                        )
                                )
                            )
                        }
                    }

                Log.i(
                    TAG,
                    "Finished games loaded from VPS: games=${games.size}"
                )

                onSuccess(
                    games
                )
            },
            onError = onError
        )
    }

    fun loadAuditLogs(
        user: DurakSession,
        onSuccess: (List<ServerAuditLog>) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user = user,
            method = "GET",
            path =
                "/api/v1/audit-logs?limit=500",
            body = null,
            onSuccess = { json ->

                val array =
                    json.getJSONArray(
                        "logs"
                    )

                val logs =
                    buildList {

                        for (
                            index in
                            0 until array.length()
                        ) {

                            val item =
                                array.getJSONObject(
                                    index
                                )

                            val detailsJson =
                                item.optJSONArray(
                                    "details"
                                )

                            val details =
                                buildList {

                                    if (
                                        detailsJson !=
                                        null
                                    ) {

                                        for (
                                            detailIndex in
                                            0 until detailsJson.length()
                                        ) {

                                            add(
                                                detailsJson.optString(
                                                    detailIndex,
                                                    ""
                                                )
                                            )
                                        }
                                    }
                                }
                                .filter {
                                    it.isNotBlank()
                                }

                            val eventAt =
                                if (
                                    item.isNull(
                                        "eventAtEpochMillis"
                                    )
                                ) {
                                    null
                                } else {
                                    item.optLong(
                                        "eventAtEpochMillis"
                                    )
                                }

                            add(
                                ServerAuditLog(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    type =
                                        item.optString(
                                            "type",
                                            "unknown"
                                        ),
                                    eventAtEpochMillis =
                                        eventAt,
                                    actorName =
                                        item.optString(
                                            "actorName",
                                            "Неизвестно"
                                        ),
                                    title =
                                        item.optString(
                                            "title",
                                            "Системное действие"
                                        ),
                                    details =
                                        details,
                                    isTest =
                                        item.optBoolean(
                                            "isTest",
                                            false
                                        )
                                )
                            )
                        }
                    }

                Log.i(
                    TAG,
                    "Audit log loaded from VPS: logs=${logs.size}"
                )

                onSuccess(
                    logs
                )
            },
            onError = onError
        )
    }

    private fun parseLiveGame(
        item: JSONObject?
    ): ServerLiveGame? {

        if (
            item ==
            null
        ) {
            return null
        }

        val playersJson =
            item.optJSONArray(
                "players"
            )

        val players =
            buildList {

                if (
                    playersJson !=
                    null
                ) {

                    for (
                        index in
                        0 until playersJson.length()
                    ) {

                        val player =
                            playersJson
                                .getJSONObject(
                                    index
                                )

                        add(
                            ServerGamePlayer(
                                id =
                                    player.optString(
                                        "id",
                                        ""
                                    ),
                                name =
                                    player.optString(
                                        "name",
                                        ""
                                    ),
                                gender =
                                    player.optString(
                                        "gender",
                                        "male"
                                    ),
                                isCore =
                                    player.optBoolean(
                                        "isCore",
                                        false
                                    ),
                                isGuest =
                                    player.optBoolean(
                                        "isGuest",
                                        false
                                    )
                            )
                        )
                    }
                }
            }

        fun nullableLong(
            key: String
        ): Long? =
            if (
                item.isNull(
                    key
                )
            ) {
                null
            } else {
                item.optLong(
                    key
                )
            }

        fun nullableString(
            key: String
        ): String? =
            item.optString(
                key,
                ""
            )
                .takeIf {
                    it.isNotBlank() &&
                        it !=
                        "null"
                }

        return ServerLiveGame(
            id =
                item.optString(
                    "id",
                    ""
                ),
            gameType =
                item.optString(
                    "gameType",
                    "custom"
                ),
            status =
                item.optString(
                    "status",
                    "active"
                ),
            targetScore =
                item.optInt(
                    "targetScore",
                    15
                ),
            isTest =
                item.optBoolean(
                    "isTest",
                    false
                ),
            players =
                players,
            scores =
                parseScores(
                    item.optJSONObject(
                        "scores"
                    )
                ),
            finalScores =
                parseScores(
                    item.optJSONObject(
                        "finalScores"
                    )
                ),
            history =
                parseHistory(
                    item.optJSONArray(
                        "history"
                    )
                ),
            startedAtEpochMillis =
                item.optLong(
                    "startedAtEpochMillis",
                    0L
                ),
            lastActionType =
                nullableString(
                    "lastActionType"
                ),
            lastActionAtEpochMillis =
                nullableLong(
                    "lastActionAtEpochMillis"
                ),
            finishedAtEpochMillis =
                nullableLong(
                    "finishedAtEpochMillis"
                ),
            loserId =
                nullableString(
                    "loserId"
                ),
            loserName =
                nullableString(
                    "loserName"
                ),
            loserGender =
                nullableString(
                    "loserGender"
                )
        )
    }

    private fun parseReaction(
        item: JSONObject
    ): ServerReaction {

        val createdAt =
            if (
                item.isNull(
                    "createdAtEpochMillis"
                )
            ) {
                null
            } else {
                item.optLong(
                    "createdAtEpochMillis"
                )
            }

        return ServerReaction(
            id =
                item.optString(
                    "id",
                    ""
                ),
            emoji =
                item.optString(
                    "emoji",
                    ""
                ),
            senderUid =
                item.optString(
                    "senderUid",
                    ""
                ),
            senderName =
                item.optString(
                    "senderName",
                    ""
                )
                    .takeIf {
                        it.isNotBlank()
                    },
            createdAtEpochMillis =
                createdAt
        )
    }

    fun loadLiveSnapshot(
        user: DurakSession,
        isTest: Boolean,
        onSuccess: (ServerLiveSnapshot) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "GET",
            path =
                "/api/v1/live/snapshot?isTest=$isTest",
            body =
                null,
            onSuccess = {
                    json ->

                val active =
                    if (
                        json.isNull(
                            "activeGame"
                        )
                    ) {
                        null
                    } else {
                        parseLiveGame(
                            json.optJSONObject(
                                "activeGame"
                            )
                        )
                    }

                val finished =
                    if (
                        json.isNull(
                            "lastFinishedGame"
                        )
                    ) {
                        null
                    } else {
                        parseLiveGame(
                            json.optJSONObject(
                                "lastFinishedGame"
                            )
                        )
                    }

                onSuccess(
                    ServerLiveSnapshot(
                        activeGame =
                            active,
                        lastFinishedGame =
                            finished
                    )
                )
            },
            onError =
                onError
        )
    }

    fun startLivePolling(
        user: DurakSession,
        isTest: Boolean,
        intervalMs: Long = 450L,
        onSnapshot: (ServerLiveSnapshot) -> Unit,
        onError: (String) -> Unit
    ): ServerPollingHandle {

        var stopped =
            false

        lateinit var tick:
            Runnable

        fun schedule(
            delayMs: Long
        ) {
            if (
                !stopped
            ) {
                mainHandler.postDelayed(
                    tick,
                    delayMs
                )
            }
        }

        tick =
            Runnable {

                if (
                    stopped
                ) {
                    return@Runnable
                }

                loadLiveSnapshot(
                    user =
                        user,
                    isTest =
                        isTest,
                    onSuccess = {
                            snapshot ->

                        if (
                            stopped
                        ) {
                            return@loadLiveSnapshot
                        }

                        onSnapshot(
                            snapshot
                        )

                        schedule(
                            intervalMs
                        )
                    },
                    onError = {
                            message ->

                        if (
                            stopped
                        ) {
                            return@loadLiveSnapshot
                        }

                        onError(
                            message
                        )

                        schedule(
                            1_200L
                        )
                    }
                )
            }

        mainHandler.post(
            tick
        )

        return ServerPollingHandle {
            stopped = true
            mainHandler.removeCallbacks(
                tick
            )
        }
    }

    fun sendReaction(
        user: DurakSession,
        gameId: String,
        requestId: String,
        emoji: String,
        onSuccess: (ServerReaction) -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "requestId",
                    requestId
                )
                .put(
                    "emoji",
                    emoji
                )
                .toString()

        authenticatedJsonRequest(
            user =
                user,
            method =
                "POST",
            path =
                "/api/v1/games/$gameId/reactions",
            body =
                body,
            onSuccess = {
                    json ->

                val reaction =
                    json.getJSONObject(
                        "reaction"
                    )

                onSuccess(
                    parseReaction(
                        reaction
                    )
                )
            },
            onError =
                onError
        )
    }

    fun loadReactions(
        user: DurakSession,
        gameId: String,
        onSuccess: (List<ServerReaction>) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "GET",
            path =
                "/api/v1/games/$gameId/reactions?limit=80",
            body =
                null,
            onSuccess = {
                    json ->

                val array =
                    json.getJSONArray(
                        "reactions"
                    )

                val reactions =
                    buildList {

                        for (
                            index in
                            0 until array.length()
                        ) {

                            add(
                                parseReaction(
                                    array.getJSONObject(
                                        index
                                    )
                                )
                            )
                        }
                    }

                onSuccess(
                    reactions
                )
            },
            onError =
                onError
        )
    }

    fun startReactionPolling(
        user: DurakSession,
        gameId: String,
        intervalMs: Long = 450L,
        onSnapshot: (List<ServerReaction>) -> Unit,
        onError: (String) -> Unit
    ): ServerPollingHandle {

        var stopped =
            false

        lateinit var tick:
            Runnable

        fun schedule(
            delayMs: Long
        ) {
            if (
                !stopped
            ) {
                mainHandler.postDelayed(
                    tick,
                    delayMs
                )
            }
        }

        tick =
            Runnable {

                if (
                    stopped
                ) {
                    return@Runnable
                }

                loadReactions(
                    user =
                        user,
                    gameId =
                        gameId,
                    onSuccess = {
                            reactions ->

                        if (
                            stopped
                        ) {
                            return@loadReactions
                        }

                        onSnapshot(
                            reactions
                        )

                        schedule(
                            intervalMs
                        )
                    },
                    onError = {
                            message ->

                        if (
                            stopped
                        ) {
                            return@loadReactions
                        }

                        onError(
                            message
                        )

                        schedule(
                            1_200L
                        )
                    }
                )
            }

        mainHandler.post(
            tick
        )

        return ServerPollingHandle {
            stopped = true
            mainHandler.removeCallbacks(
                tick
            )
        }
    }

    fun loadAccessStatus(
        user: DurakSession,
        onSuccess: (ServerAccessStatus) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "GET",
            path =
                "/api/v1/access-status",
            body =
                null,
            onSuccess = {
                    json ->

                onSuccess(
                    ServerAccessStatus(
                        status =
                            json.optString(
                                "status",
                                "none"
                            )
                    )
                )
            },
            onError =
                onError
        )
    }

    fun loadAdminUsers(
        user: DurakSession,
        onSuccess: (ServerAdminUsers) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "GET",
            path =
                "/api/v1/admin/users",
            body =
                null,
            onSuccess = {
                    json ->

                val requestsJson =
                    json.getJSONArray(
                        "requests"
                    )

                val requests =
                    buildList {

                        for (
                            index in
                            0 until requestsJson.length()
                        ) {

                            val item =
                                requestsJson
                                    .getJSONObject(
                                        index
                                    )

                            add(
                                ServerPendingAccessRequest(
                                    uid =
                                        item.optString(
                                            "uid",
                                            ""
                                        ),
                                    name =
                                        item.optString(
                                            "name",
                                            "Без имени"
                                        ),
                                    email =
                                        item.optString(
                                            "email",
                                            "Почта не указана"
                                        ),
                                    createdAtEpochMillis =
                                        if (
                                            item.isNull(
                                                "createdAtEpochMillis"
                                            )
                                        ) {
                                            null
                                        } else {
                                            item.optLong(
                                                "createdAtEpochMillis"
                                            )
                                        }
                                )
                            )
                        }
                    }

                val membersJson =
                    json.getJSONArray(
                        "members"
                    )

                val members =
                    buildList {

                        for (
                            index in
                            0 until membersJson.length()
                        ) {

                            val item =
                                membersJson
                                    .getJSONObject(
                                        index
                                    )

                            add(
                                ServerActiveMember(
                                    uid =
                                        item.optString(
                                            "uid",
                                            ""
                                        ),
                                    name =
                                        item.optString(
                                            "name",
                                            "Пользователь"
                                        ),
                                    playerId =
                                        item.optString(
                                            "playerId",
                                            ""
                                        ),
                                    role =
                                        item.optString(
                                            "role",
                                            "viewer"
                                        ),
                                    active =
                                        item.optBoolean(
                                            "active",
                                            true
                                        )
                                )
                            )
                        }
                    }

                onSuccess(
                    ServerAdminUsers(
                        requests =
                            requests,
                        members =
                            members
                    )
                )
            },
            onError =
                onError
        )
    }

    fun approveAdminUser(
        user: DurakSession,
        requestUid: String,
        playerId: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {

        val body =
            JSONObject()
                .put(
                    "requestUid",
                    requestUid
                )
                .put(
                    "playerId",
                    playerId
                )
                .toString()

        authenticatedJsonRequest(
            user =
                user,
            method =
                "POST",
            path =
                "/api/v1/admin/users/approve",
            body =
                body,
            onSuccess = {
                onSuccess()
            },
            onError =
                onError
        )
    }

    fun loadDataAuditSource(
        user: DurakSession,
        onSuccess: (ServerDataAuditSource) -> Unit,
        onError: (String) -> Unit
    ) {

        authenticatedJsonRequest(
            user =
                user,
            method =
                "GET",
            path =
                "/api/v1/data-audit/source",
            body =
                null,
            onSuccess = {
                    json ->

                val playersJson =
                    json.getJSONArray(
                        "players"
                    )

                val players =
                    buildList {

                        for (
                            index in
                            0 until playersJson.length()
                        ) {

                            val item =
                                playersJson
                                    .getJSONObject(
                                        index
                                    )

                            add(
                                ServerDataAuditPlayer(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    name =
                                        item.optString(
                                            "name",
                                            ""
                                        )
                                )
                            )
                        }
                    }

                val scores =
                    parseScores(
                        json.optJSONObject(
                            "scores"
                        )
                    )

                val gamesJson =
                    json.getJSONArray(
                        "games"
                    )

                val games =
                    buildList {

                        for (
                            index in
                            0 until gamesJson.length()
                        ) {

                            val item =
                                gamesJson
                                    .getJSONObject(
                                        index
                                    )

                            val idsJson =
                                item.optJSONArray(
                                    "playerIds"
                                )

                            val playerIds =
                                buildList {

                                    if (
                                        idsJson !=
                                        null
                                    ) {

                                        for (
                                            idIndex in
                                            0 until idsJson.length()
                                        ) {
                                            add(
                                                idsJson.optString(
                                                    idIndex,
                                                    ""
                                                )
                                            )
                                        }
                                    }
                                }
                                .filter {
                                    it.isNotBlank()
                                }

                            add(
                                ServerDataAuditGame(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    gameType =
                                        item.optString(
                                            "gameType",
                                            "unknown"
                                        ),
                                    isTest =
                                        item.optBoolean(
                                            "isTest",
                                            false
                                        ),
                                    affectsMainScore =
                                        item.optBoolean(
                                            "affectsMainScore",
                                            false
                                        ),
                                    mainScoreApplied =
                                        item.optBoolean(
                                            "mainScoreApplied",
                                            false
                                        ),
                                    targetScore =
                                        item.optInt(
                                            "targetScore",
                                            0
                                        ),
                                    playerIds =
                                        playerIds,
                                    finalScores =
                                        parseScores(
                                            item.optJSONObject(
                                                "finalScores"
                                            )
                                        ),
                                    roundHistory =
                                        parseHistory(
                                            item.optJSONArray(
                                                "roundHistory"
                                            )
                                        ),
                                    loserId =
                                        item.optString(
                                            "loserId",
                                            ""
                                        )
                                            .takeIf {
                                                it.isNotBlank()
                                            },
                                    finishedAtEpochMillis =
                                        if (
                                            item.isNull(
                                                "finishedAtEpochMillis"
                                            )
                                        ) {
                                            null
                                        } else {
                                            item.optLong(
                                                "finishedAtEpochMillis"
                                            )
                                        },
                                    hasFinalScores =
                                        item.optBoolean(
                                            "hasFinalScores",
                                            false
                                        )
                                )
                            )
                        }
                    }

                val eventsJson =
                    json.getJSONArray(
                        "auditEvents"
                    )

                val events =
                    buildList {

                        for (
                            index in
                            0 until eventsJson.length()
                        ) {

                            val item =
                                eventsJson
                                    .getJSONObject(
                                        index
                                    )

                            fun nullableInt(
                                key: String
                            ): Int? =
                                if (
                                    item.isNull(
                                        key
                                    )
                                ) {
                                    null
                                } else {
                                    item.optInt(
                                        key
                                    )
                                }

                            fun nullableLong(
                                key: String
                            ): Long? =
                                if (
                                    item.isNull(
                                        key
                                    )
                                ) {
                                    null
                                } else {
                                    item.optLong(
                                        key
                                    )
                                }

                            fun nullableString(
                                key: String
                            ): String? =
                                item.optString(
                                    key,
                                    ""
                                )
                                    .takeIf {
                                        it.isNotBlank()
                                    }

                            add(
                                ServerDataAuditEvent(
                                    id =
                                        item.optString(
                                            "id",
                                            ""
                                        ),
                                    type =
                                        item.optString(
                                            "type",
                                            "unknown"
                                        ),
                                    playerId =
                                        nullableString(
                                            "playerId"
                                        ),
                                    oldValue =
                                        nullableInt(
                                            "oldValue"
                                        ),
                                    newValue =
                                        nullableInt(
                                            "newValue"
                                        ),
                                    gameId =
                                        nullableString(
                                            "gameId"
                                        ),
                                    values =
                                        parseScores(
                                            item.optJSONObject(
                                                "values"
                                            )
                                        ),
                                    eventAtEpochMillis =
                                        nullableLong(
                                            "eventAtEpochMillis"
                                        )
                                )
                            )
                        }
                    }

                onSuccess(
                    ServerDataAuditSource(
                        players =
                            players,
                        scores =
                            scores,
                        games =
                            games,
                        auditEvents =
                            events
                    )
                )
            },
            onError =
                onError
        )
    }

    private fun authenticatedJsonRequest(
        user: DurakSession,
        method: String,
        path: String,
        body: String?,
        allowNetworkRetry: Boolean = true,
        onForbidden: (() -> Unit)? = null,
        onSuccess: (JSONObject) -> Unit,
        onError: (String) -> Unit
    ) {

        val token =
            user.token.trim()

        if (
            token.isBlank()
        ) {

            onMain {
                onError(
                    "Сессия отсутствует. Войдите снова."
                )
            }

            return
        }

        thread(
            name =
                "durak-api-request",
            isDaemon =
                true
        ) {

            var connection:
                HttpURLConnection? =
                null

            try {

                connection =
                    (
                        URL(
                            "$BASE_URL$path"
                        )
                            .openConnection()
                            as HttpURLConnection
                    )
                        .apply {

                            requestMethod =
                                method

                            connectTimeout =
                                CONNECT_TIMEOUT_MS

                            readTimeout =
                                READ_TIMEOUT_MS

                            setRequestProperty(
                                "Accept",
                                "application/json"
                            )

                            setRequestProperty(
                                "Authorization",
                                "Bearer $token"
                            )

                            useCaches =
                                false

                            if (
                                body !=
                                null
                            ) {

                                doOutput =
                                    true

                                setRequestProperty(
                                    "Content-Type",
                                    "application/json; charset=utf-8"
                                )
                            }
                        }

                if (
                    body !=
                    null
                ) {

                    connection
                        .outputStream
                        .bufferedWriter(
                            Charsets.UTF_8
                        )
                        .use {
                            it.write(
                                body
                            )
                        }
                }

                val code =
                    connection
                        .responseCode

                val responseBody =
                    readResponseBody(
                        connection,
                        code
                    )

                when {

                    code in
                        200..299 -> {

                        val json =
                            if (
                                responseBody
                                    .isBlank()
                            ) {
                                JSONObject()
                            } else {
                                JSONObject(
                                    responseBody
                                )
                            }

                        onMain {
                            onSuccess(
                                json
                            )
                        }
                    }

                    code ==
                        HttpURLConnection
                            .HTTP_UNAUTHORIZED -> {

                        onMain {
                            onError(
                                "Сессия истекла. Войдите снова."
                            )
                        }
                    }

                    code ==
                        HttpURLConnection
                            .HTTP_FORBIDDEN -> {

                        onMain {

                            if (
                                onForbidden !=
                                null
                            ) {
                                onForbidden()
                            } else {
                                onError(
                                    extractDetail(
                                        responseBody,
                                        "Нет доступа"
                                    )
                                )
                            }
                        }
                    }

                    else -> {

                        val message =
                            extractDetail(
                                responseBody,
                                "VPS ответил HTTP $code"
                            )

                        Log.w(
                            TAG,
                            "$path HTTP $code: $responseBody"
                        )

                        onMain {
                            onError(
                                message
                            )
                        }
                    }
                }

            } catch (
                error: Exception
            ) {

                Log.e(
                    TAG,
                    "$path request failed",
                    error
                )

                if (
                    allowNetworkRetry
                ) {

                    try {
                        Thread.sleep(
                            500L
                        )
                    } catch (
                        ignored: InterruptedException
                    ) {
                        Thread
                            .currentThread()
                            .interrupt()
                    }

                    onMain {

                        authenticatedJsonRequest(
                            user =
                                user,
                            method =
                                method,
                            path =
                                path,
                            body =
                                body,
                            allowNetworkRetry =
                                false,
                            onForbidden =
                                onForbidden,
                            onSuccess =
                                onSuccess,
                            onError =
                                onError
                        )
                    }

                } else {

                    onMain {
                        onError(
                            "VPS недоступен: ${error.message ?: "ошибка сети"}"
                        )
                    }
                }

            } finally {

                connection
                    ?.disconnect()
            }
        }
    }


    private fun plainJsonRequest(
        method: String,
        path: String,
        body: String?,
        allowNetworkRetry: Boolean = true,
        onSuccess: (JSONObject) -> Unit,
        onError: (String) -> Unit
    ) {

        thread(
            name =
                "durak-auth-request",
            isDaemon =
                true
        ) {

            var connection:
                HttpURLConnection? =
                null

            try {

                connection =
                    (
                        URL(
                            "$BASE_URL$path"
                        )
                            .openConnection()
                            as HttpURLConnection
                    )
                        .apply {

                            requestMethod =
                                method

                            connectTimeout =
                                CONNECT_TIMEOUT_MS

                            readTimeout =
                                READ_TIMEOUT_MS

                            setRequestProperty(
                                "Accept",
                                "application/json"
                            )

                            useCaches =
                                false

                            if (
                                body !=
                                null
                            ) {
                                doOutput =
                                    true

                                setRequestProperty(
                                    "Content-Type",
                                    "application/json; charset=utf-8"
                                )
                            }
                        }

                if (
                    body !=
                    null
                ) {
                    connection
                        .outputStream
                        .bufferedWriter(
                            Charsets.UTF_8
                        )
                        .use {
                            it.write(
                                body
                            )
                        }
                }

                val code =
                    connection
                        .responseCode

                val responseBody =
                    readResponseBody(
                        connection,
                        code
                    )

                if (
                    code in
                    200..299
                ) {

                    val json =
                        if (
                            responseBody
                                .isBlank()
                        ) {
                            JSONObject()
                        } else {
                            JSONObject(
                                responseBody
                            )
                        }

                    onMain {
                        onSuccess(
                            json
                        )
                    }

                } else {

                    val message =
                        extractDetail(
                            responseBody,
                            "VPS ответил HTTP $code"
                        )

                    onMain {
                        onError(
                            message
                        )
                    }
                }

            } catch (
                error: Exception
            ) {

                if (
                    allowNetworkRetry
                ) {

                    try {
                        Thread.sleep(
                            500L
                        )
                    } catch (
                        ignored: InterruptedException
                    ) {
                        Thread
                            .currentThread()
                            .interrupt()
                    }

                    onMain {
                        plainJsonRequest(
                            method =
                                method,
                            path =
                                path,
                            body =
                                body,
                            allowNetworkRetry =
                                false,
                            onSuccess =
                                onSuccess,
                            onError =
                                onError
                        )
                    }

                } else {

                    onMain {
                        onError(
                            "VPS недоступен: ${error.message ?: "ошибка сети"}"
                        )
                    }
                }

            } finally {

                connection
                    ?.disconnect()
            }
        }
    }


    private fun extractDetail(
        body: String,
        fallback: String
    ): String {

        if (
            body.isBlank()
        ) {
            return fallback
        }

        return try {

            JSONObject(
                body
            )
                .optString(
                    "detail",
                    fallback
                )
                .ifBlank {
                    fallback
                }

        } catch (
            ignored: Exception
        ) {

            fallback
        }
    }

    private fun readResponseBody(
        connection: HttpURLConnection,
        code: Int
    ): String {

        val stream =
            if (
                code in
                200..299
            ) {
                connection.inputStream
            } else {
                connection.errorStream
            }

        return stream
            ?.bufferedReader(
                Charsets.UTF_8
            )
            ?.use {
                it.readText()
            }
            .orEmpty()
    }

    private fun onMain(
        action: () -> Unit
    ) {

        if (
            Looper.myLooper() ==
            Looper.getMainLooper()
        ) {

            action()

        } else {

            mainHandler.post(
                action
            )
        }
    }
}
