package com.tomhempel.rbarometer.network

import com.tomhempel.rbarometer.security.SessionPayloadCrypto
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

object QuestionnaireApiClient {
    //URL ONLY VALID ON CURRENT NETWORK; HAS TO BE CHANGED IN FUTURE
    private val jsonMediaType = "application/json; charset=UTF-8".toMediaType()
    private val client get() = HttpModule.client
    private val gson = Gson()

    suspend fun changePassword(
        token: String,
        username: String,
        oldPassword: String,
        newPassword: String
    ): LoginResult = withContext(Dispatchers.IO) {
        val body = JSONObject()
            .put("username", username)
            .put("old_password", oldPassword)
            .put("new_password", newPassword)
            .toString()
            .toRequestBody(jsonMediaType)

        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/auth/change-password")
            .post(body)
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_CHANGE_PW", "changePassword", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.getAsJsonObject("data") ?: throw ApiException("Change password response missing data")
            parseLoginResult(data)
        }
    }

    suspend fun login(username: String, password: String): LoginResult = withContext(Dispatchers.IO) {
        val body = JSONObject()
            .put("username", username)
            .put("password", password)
            .toString()
            .toRequestBody(jsonMediaType)

        val request = Request.Builder()
            .url("${HttpModule.BASE_URL}/auth/login")
            .post(body)
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_LOGIN", "login", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.getAsJsonObject("data") ?: throw ApiException("Login response missing data")
            parseLoginResult(data)
        }
    }

    /** App UI strings for the login screen — no Bearer token required. */
    suspend fun getPublicTranslations(): JsonObject = withContext(Dispatchers.IO) {
        val request = Request.Builder()
            .url("${HttpModule.BASE_URL}/app_questionnaires?translations=1")
            .get()
            .build()
        fetchTranslationsEnvelope(request, logTag = "API_TRANSLATIONS_PUBLIC")
    }

    suspend fun getTranslations(token: String): JsonObject = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires?translations=1")
            .get()
            .build()
        fetchTranslationsEnvelope(request, logTag = "API_TRANSLATIONS")
    }

    suspend fun getSession(token: String): SessionInfo = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/session")
            .get()
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_SESSION", "getSession", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.getAsJsonObject("data") ?: throw ApiException("Session response missing data")
            SessionInfo(
                valid = data.get("valid")?.asBoolean ?: false,
                user = data.get("user")?.asString.orEmpty(),
                role = data.get("role")?.asString.orEmpty(),
                mustChangePassword = data.get("mustChangePassword")?.asBoolean ?: false,
            )
        }
    }

    private fun fetchTranslationsEnvelope(request: Request, logTag: String): JsonObject =
        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse(logTag, "translations", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.get("data")
            when {
                data == null || data.isJsonNull -> JsonObject()
                data.isJsonObject -> data.asJsonObject
                else -> {
                    SyncLog.w(logTag, "unexpected data type for translations: ${data::class.simpleName}")
                    JsonObject()
                }
            }
        }

    suspend fun getQuestionnaires(token: String): List<QuestionnaireListItem> = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires")
            .get()
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_QUESTIONNAIRES", "getQuestionnaires", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.getAsJsonArray("data") ?: JsonArray()
            data.mapNotNull { item ->
                val obj = item.asJsonObject
                val id = obj.get("id")?.asString ?: return@mapNotNull null
                val conditionElement = obj.get("condition")
                val conditionJson = when {
                    conditionElement == null || conditionElement.isJsonNull -> "{}"
                    conditionElement.isJsonObject -> conditionElement.asJsonObject.toString()
                    else -> "{}"
                }
                QuestionnaireListItem(
                    id = id,
                    name = obj.get("name")?.asString ?: id,
                    showPoints = obj.get("showPoints")?.asBoolean ?: false,
                    conditionJson = conditionJson,
                    categoryKey = obj.get("categoryKey")?.asString?.takeIf { it.isNotBlank() },
                    structureRevision = obj.get("structureRevision")?.asInt?.coerceAtLeast(1) ?: 1,
                )
            }
        }
    }

    /**
     * Fetches assigned clients together with their server-side completion records.
     * Supports both the legacy plain-string array and the new rich object format:
     *   { "data": { "clients": [ { "clientCode": "...", "completedQuestionnaires": [...] } ] } }
     *   { "data": { "clients": ["CLIENT-001", ...] } }
     *   { "data": ["CLIENT-001", ...] }
     */
    suspend fun getAssignedClients(token: String): List<AssignedClientInfo> = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires?clients=1")
            .get()
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_CLIENTS", "getAssignedClients", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val dataEl = root.get("data") ?: return@use emptyList()
            val data = when {
                dataEl.isJsonObject -> SessionPayloadCrypto.decryptDataElement(dataEl.asJsonObject, token)
                else -> return@use emptyList()
            }

            val arr = when {
                data.has("clients") -> data.getAsJsonArray("clients")
                else -> null
            } ?: return@use emptyList()

            arr.mapNotNull { item ->
                when {
                    item.isJsonPrimitive -> {
                        val code = item.asString.takeIf { it.isNotBlank() } ?: return@mapNotNull null
                        AssignedClientInfo(clientCode = code)
                    }
                    item.isJsonObject -> {
                        val obj = item.asJsonObject
                        val code = obj.get("clientCode")?.asString?.takeIf { it.isNotBlank() }
                            ?: return@mapNotNull null
                        val completions = obj.getAsJsonArray("completedQuestionnaires")
                            ?.mapNotNull { cItem ->
                                if (!cItem.isJsonObject) return@mapNotNull null
                                val cObj = cItem.asJsonObject
                                val qId = cObj.get("questionnaireID")?.asString?.takeIf { it.isNotBlank() }
                                    ?: return@mapNotNull null
                                ServerCompletedQuestionnaire(
                                    questionnaireID = qId,
                                    sumPoints = cObj.get("sumPoints")?.asInt ?: 0,
                                    completedAt = cObj.get("completedAt")?.asLong ?: 0L
                                )
                            } ?: emptyList()
                        AssignedClientInfo(
                            clientCode = code,
                            completedQuestionnaires = completions,
                            programProgress = parseProgramProgress(obj.get("programProgress")),
                        )
                    }
                    else -> null
                }
            }
        }
    }

    suspend fun fetchBulkClientAnswers(token: String): List<ClientAnswersBundle> =
        withContext(Dispatchers.IO) {
            val request = authorizedRequest(token)
                .url("${HttpModule.BASE_URL}/app_questionnaires?answersBulk=1")
                .get()
                .build()

            client.newCall(request).execute().use { response ->
                val bodyText = response.body?.string()
                SyncLog.apiResponse("API_ANSWERS", "fetchBulkClientAnswers", response.code, bodyText)
                val root = parseEnvelope(response.code, bodyText)
                val dataEl = root.get("data") ?: return@use emptyList()
                val data = when {
                    dataEl.isJsonObject -> SessionPayloadCrypto.decryptDataElement(dataEl.asJsonObject, token)
                    else -> return@use emptyList()
                }
                parseClientAnswerBundles(data.getAsJsonArray("clients"))
            }
        }

    suspend fun fetchClientAnswers(token: String, clientCode: String): ClientAnswersBundle =
        withContext(Dispatchers.IO) {
            val request = authorizedRequest(token)
                .url(
                    "${HttpModule.BASE_URL}/app_questionnaires" +
                        "?clientCode=${java.net.URLEncoder.encode(clientCode, Charsets.UTF_8.name())}&answers=1",
                )
                .get()
                .build()

            client.newCall(request).execute().use { response ->
                val bodyText = response.body?.string()
                SyncLog.apiResponse("API_ANSWERS", "fetchClientAnswers(${SyncLog.clientRef(clientCode)})", response.code, bodyText)
                val root = parseEnvelope(response.code, bodyText)
                val dataEl = root.get("data") ?: return@use ClientAnswersBundle(clientCode)
                val data = when {
                    dataEl.isJsonObject -> SessionPayloadCrypto.decryptDataElement(dataEl.asJsonObject, token)
                    else -> return@use ClientAnswersBundle(clientCode)
                }
                val blocks = parseQuestionnaireAnswerBlocks(data.getAsJsonArray("questionnaires"))
                ClientAnswersBundle(
                    clientCode = data.get("clientCode")?.asString ?: clientCode,
                    questionnaires = blocks,
                )
            }
        }

    private fun parseClientAnswerBundles(clientsArr: JsonArray?): List<ClientAnswersBundle> {
        if (clientsArr == null) return emptyList()
        return clientsArr.mapNotNull { clientEl ->
            if (!clientEl.isJsonObject) return@mapNotNull null
            val clientObj = clientEl.asJsonObject
            val code = clientObj.get("clientCode")?.asString?.takeIf { it.isNotBlank() }
                ?: return@mapNotNull null
            ClientAnswersBundle(
                clientCode = code,
                questionnaires = parseQuestionnaireAnswerBlocks(clientObj.getAsJsonArray("questionnaires")),
            )
        }
    }

    private fun parseQuestionnaireAnswerBlocks(qnArr: JsonArray?): List<ServerQuestionnaireAnswers> {
        if (qnArr == null) return emptyList()
        return qnArr.mapNotNull { item ->
            if (!item.isJsonObject) return@mapNotNull null
            val obj = item.asJsonObject
            val qnId = obj.get("questionnaireID")?.asString?.takeIf { it.isNotBlank() }
                ?: return@mapNotNull null
            val answersArr = obj.getAsJsonArray("answers") ?: JsonArray()
            val answers = answersArr.mapNotNull { aEl ->
                if (!aEl.isJsonObject) return@mapNotNull null
                val a = aEl.asJsonObject
                val qid = a.get("questionID")?.asString ?: return@mapNotNull null
                SubmitAnswer(
                    questionID = qid,
                    answerOptionKey = a.get("answerOptionKey")?.asString,
                    freeTextValue = a.get("freeTextValue")?.asString,
                    numericValue = a.get("numericValue")?.takeIf { !it.isJsonNull }?.asDouble,
                    answeredAt = a.get("answeredAt")?.asLong,
                )
            }
            ServerQuestionnaireAnswers(
                questionnaireID = qnId,
                completedAt = obj.get("completedAt")?.asLong,
                sumPoints = obj.get("sumPoints")?.asInt ?: 0,
                answers = answers,
            )
        }
    }

    suspend fun getProgramSessions(token: String): ProgramSessionsPayload = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires?sessions=1")
            .get()
            .build()

        client.newCall(request).execute().use { response ->
            val bodyText = response.body?.string()
            SyncLog.apiResponse("API_PROGRAM_SESSIONS", "getProgramSessions", response.code, bodyText)
            val root = parseEnvelope(response.code, bodyText)
            val data = root.getAsJsonObject("data") ?: JsonObject()
            val sessionsArr = data.getAsJsonArray("sessions") ?: JsonArray()
            val sessions = sessionsArr.mapNotNull { item ->
                if (!item.isJsonObject) return@mapNotNull null
                val obj = item.asJsonObject
                val sessionID = obj.get("sessionID")?.asString?.takeIf { it.isNotBlank() }
                    ?: return@mapNotNull null
                val members = obj.getAsJsonArray("questionnaires")?.mapNotNull { mEl ->
                    if (!mEl.isJsonObject) return@mapNotNull null
                    val m = mEl.asJsonObject
                    val qId = m.get("questionnaireID")?.asString?.takeIf { it.isNotBlank() }
                        ?: return@mapNotNull null
                    ProgramSessionQuestionnaire(
                        questionnaireID = qId,
                        orderIndex = m.get("orderIndex")?.asInt ?: 0,
                    )
                }?.sortedBy { it.orderIndex } ?: emptyList()
                ProgramSessionDefinition(
                    sessionID = sessionID,
                    name = obj.get("name")?.asString ?: sessionID,
                    description = obj.get("description")?.asString.orEmpty(),
                    sessionNumber = obj.get("sessionNumber")?.asInt ?: 0,
                    orderIndex = obj.get("orderIndex")?.asInt ?: 0,
                    questionnaires = members,
                )
            }.sortedBy { it.orderIndex }
            val settingsObj = data.getAsJsonObject("settings")
            val settings = ProgramSessionSettings(
                autoRepeatSessions = settingsObj?.get("autoRepeatSessions")?.asBoolean ?: false,
            )
            ProgramSessionsPayload(sessions = sessions, settings = settings)
        }
    }

    private fun parseProgramProgress(element: JsonElement?): ProgramProgress? {
        if (element == null || element.isJsonNull || !element.isJsonObject) return null
        val obj = element.asJsonObject
        return ProgramProgress(
            cycleNumber = obj.get("cycleNumber")?.asInt ?: 1,
            cycleStartedAt = obj.get("cycleStartedAt")?.asLong ?: 0L,
            totalSessions = obj.get("totalSessions")?.asInt ?: 0,
            sessionsCompletedInCycle = obj.get("sessionsCompletedInCycle")?.asInt ?: 0,
            currentSessionID = obj.get("currentSessionID")?.takeIf { !it.isJsonNull }?.asString,
            currentSessionNumber = obj.get("currentSessionNumber")?.takeIf { !it.isJsonNull }?.asInt,
            currentSessionName = obj.get("currentSessionName")?.takeIf { !it.isJsonNull }?.asString,
            cycleComplete = obj.get("cycleComplete")?.asBoolean ?: false,
            programComplete = obj.get("programComplete")?.asBoolean ?: false,
            autoRepeatSessions = obj.get("autoRepeatSessions")?.asBoolean ?: false,
        )
    }

    suspend fun getQuestionnaire(token: String, questionnaireId: String): JsonObject = withContext(Dispatchers.IO) {
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires?id=$questionnaireId")
            .get()
            .build()

        client.newCall(request).execute().use { response ->
            val root = parseEnvelope(response.code, response.body?.string())
            root.getAsJsonObject("data") ?: throw ApiException("Questionnaire response missing data")
        }
    }

    suspend fun submitQuestionnaire(
        token: String,
        requestBody: SubmitQuestionnaireRequest
    ): SubmitQuestionnaireResponse = withContext(Dispatchers.IO) {
        val envelope = SessionPayloadCrypto.envelope(gson.toJson(requestBody), token)
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires")
            .post(gson.toJson(envelope).toRequestBody(jsonMediaType))
            .build()

        client.newCall(request).execute().use { response ->
            val root = parseEnvelope(response.code, response.body?.string())
            val data = root.getAsJsonObject("data") ?: JsonObject()
            gson.fromJson(data, SubmitQuestionnaireResponse::class.java)
                ?: SubmitQuestionnaireResponse()
        }
    }

    suspend fun getScoringProfiles(token: String): List<ScoringProfileDefinition> =
        withContext(Dispatchers.IO) {
            val request = authorizedRequest(token)
                .url("${HttpModule.BASE_URL}/app_questionnaires?scoringProfiles=1")
                .get()
                .build()
            client.newCall(request).execute().use { response ->
                val bodyText = response.body?.string()
                SyncLog.apiResponse("API_SCORING_PROFILES", "getScoringProfiles", response.code, bodyText)
                val root = parseEnvelope(response.code, bodyText)
                val dataEl = root.get("data") ?: return@use emptyList()
                val data = when {
                    dataEl.isJsonObject -> SessionPayloadCrypto.decryptDataElement(dataEl.asJsonObject, token)
                    else -> return@use emptyList()
                }
                parseScoringProfiles(data.getAsJsonArray("profiles"))
            }
        }

    suspend fun getScoringReview(token: String, clientCode: String? = null): List<ClientScoringReview> =
        withContext(Dispatchers.IO) {
            val urlBuilder = StringBuilder("${HttpModule.BASE_URL}/app_questionnaires?scoringReview=1")
            if (!clientCode.isNullOrBlank()) {
                urlBuilder.append("&clientCode=")
                    .append(java.net.URLEncoder.encode(clientCode, Charsets.UTF_8.name()))
            }
            val request = authorizedRequest(token).url(urlBuilder.toString()).get().build()
            client.newCall(request).execute().use { response ->
                val bodyText = response.body?.string()
                SyncLog.apiResponse("API_SCORING_REVIEW", "getScoringReview", response.code, bodyText)
                val root = parseEnvelope(response.code, bodyText)
                val dataEl = root.get("data") ?: return@use emptyList()
                val data = when {
                    dataEl.isJsonObject -> SessionPayloadCrypto.decryptDataElement(dataEl.asJsonObject, token)
                    else -> return@use emptyList()
                }
                parseScoringReviewClients(data.getAsJsonArray("clients"))
            }
        }

    suspend fun submitCoachScoringReview(
        token: String,
        requestBody: CoachScoringReviewRequest,
    ): ScoringReviewProfile = withContext(Dispatchers.IO) {
        val envelope = SessionPayloadCrypto.envelope(gson.toJson(requestBody), token)
        val request = authorizedRequest(token)
            .url("${HttpModule.BASE_URL}/app_questionnaires")
            .post(gson.toJson(envelope).toRequestBody(jsonMediaType))
            .build()

        client.newCall(request).execute().use { response ->
            val root = parseEnvelope(response.code, response.body?.string())
            val data = root.getAsJsonObject("data") ?: throw ApiException("Review response missing data")
            val profileObj = data.getAsJsonObject("profile")
                ?: throw ApiException("Review response missing profile")
            parseScoringReviewProfile(profileObj)
                ?: throw ApiException("Review response profile invalid")
        }
    }

    private fun parseScoringProfiles(arr: JsonArray?): List<ScoringProfileDefinition> {
        if (arr == null) return emptyList()
        return arr.mapNotNull { item ->
            if (!item.isJsonObject) return@mapNotNull null
            val obj = item.asJsonObject
            val profileID = obj.get("profileID")?.asString?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
            val members = obj.getAsJsonArray("questionnaires")?.mapNotNull { mEl ->
                if (!mEl.isJsonObject) return@mapNotNull null
                val m = mEl.asJsonObject
                val qId = m.get("questionnaireID")?.asString?.takeIf { it.isNotBlank() }
                    ?: return@mapNotNull null
                ScoringProfileMember(
                    questionnaireID = qId,
                    weight = m.get("weight")?.asDouble ?: 1.0,
                    orderIndex = m.get("orderIndex")?.asInt ?: 0,
                    name = m.get("name")?.asString.orEmpty(),
                )
            } ?: emptyList()
            ScoringProfileDefinition(
                profileID = profileID,
                name = obj.get("name")?.asString ?: profileID,
                description = obj.get("description")?.asString.orEmpty(),
                isActive = obj.get("isActive")?.asInt == 1 || obj.get("isActive")?.asBoolean == true,
                greenMin = obj.get("greenMin")?.asInt ?: 0,
                greenMax = obj.get("greenMax")?.asInt ?: 12,
                yellowMin = obj.get("yellowMin")?.asInt ?: 13,
                yellowMax = obj.get("yellowMax")?.asInt ?: 36,
                redMin = obj.get("redMin")?.asInt ?: 37,
                questionnaires = members,
            )
        }
    }

    private fun parseScoringReviewClients(arr: JsonArray?): List<ClientScoringReview> {
        if (arr == null) return emptyList()
        return arr.mapNotNull { item ->
            if (!item.isJsonObject) return@mapNotNull null
            val obj = item.asJsonObject
            val code = obj.get("clientCode")?.asString?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
            ClientScoringReview(
                clientCode = code,
                profiles = parseScoringReviewProfiles(obj.getAsJsonArray("profiles")),
            )
        }
    }

    private fun parseScoringReviewProfiles(arr: JsonArray?): List<ScoringReviewProfile> {
        if (arr == null) return emptyList()
        return arr.mapNotNull { item ->
            if (!item.isJsonObject) return@mapNotNull null
            parseScoringReviewProfile(item.asJsonObject)
        }
    }

    private fun parseScoringReviewProfile(obj: JsonObject): ScoringReviewProfile? {
        val profileID = obj.get("profileID")?.asString?.takeIf { it.isNotBlank() } ?: return null
        val coachBand = obj.get("coachBand")?.takeIf { !it.isJsonNull }?.asString
        return ScoringReviewProfile(
            profileID = profileID,
            name = obj.get("name")?.asString ?: profileID,
            weightedTotal = obj.get("weightedTotal")?.asDouble ?: 0.0,
            calculatedBand = obj.get("calculatedBand")?.asString ?: obj.get("band")?.asString.orEmpty(),
            greenMin = obj.get("greenMin")?.asInt ?: 0,
            greenMax = obj.get("greenMax")?.asInt ?: 12,
            yellowMin = obj.get("yellowMin")?.asInt ?: 13,
            yellowMax = obj.get("yellowMax")?.asInt ?: 36,
            redMin = obj.get("redMin")?.asInt ?: 37,
            coachBand = coachBand,
            pendingReview = obj.get("pendingReview")?.asBoolean ?: coachBand.isNullOrBlank(),
        )
    }

    private fun authorizedRequest(token: String): Request.Builder =
        Request.Builder().header("Authorization", "Bearer $token")

    private fun apiExceptionFromErrorElement(errorEl: JsonElement?, httpCode: Int): ApiException {
        if (errorEl == null || errorEl.isJsonNull) {
            return ApiException("Unexpected server response ($httpCode)")
        }
        if (errorEl.isJsonObject) {
            val o = errorEl.asJsonObject
            return ApiException(
                o.get("message")?.asString ?: "Server error ($httpCode)",
                o.get("code")?.asString,
                o.get("details"),
            )
        }
        if (errorEl.isJsonPrimitive) {
            return ApiException(errorEl.asString, null, null)
        }
        return ApiException("Unexpected server response ($httpCode)")
    }

    /** User-visible text for failed API calls (includes validation breakdown). */
    fun formatErrorMessage(exception: ApiException): String {
        if (exception.code != "VALIDATION_FAILED") {
            return exception.message ?: "Unknown error"
        }
        val details = exception.details?.takeIf { it.isJsonObject }?.asJsonObject ?: return exception.message ?: "Validation failed"
        val errors = details.getAsJsonArray("errors") ?: return exception.message ?: "Validation failed"
        val lines = mutableListOf<String>()
        errors.forEach { item ->
            if (!item.isJsonObject) return@forEach
            val o = item.asJsonObject
            val qid = o.get("questionID")?.asString?.takeIf { it.isNotBlank() }
            val msg = o.get("message")?.asString ?: o.get("code")?.asString ?: "Invalid"
            lines += if (qid != null) "$qid: $msg" else msg
        }
        return if (lines.isEmpty()) {
            exception.message ?: "Validation failed"
        } else {
            (exception.message ?: "Validation failed") + "\n" + lines.joinToString("\n")
        }
    }

    private fun parseEnvelope(httpCode: Int, body: String?): JsonObject {
        if (body.isNullOrBlank()) {
            throw ApiException("Empty server response ($httpCode)")
        }

        val root = try {
            JsonParser.parseString(body).asJsonObject
        } catch (e: Exception) {
            SyncLog.e("API", "Failed to parse JSON (HTTP $httpCode, ${body?.length ?: 0} bytes)", e)
            throw ApiException("Server response was not valid JSON ($httpCode). See logs for details.")
        }

        if (!root.has("ok")) {
            throw apiExceptionFromErrorElement(root.get("error"), httpCode)
        }

        if (!root.get("ok").asBoolean) {
            throw apiExceptionFromErrorElement(root.get("error"), httpCode)
        }

        return root
    }

    private fun parseLoginResult(data: JsonObject): LoginResult {
        val token = data.get("token")?.asString ?: throw ApiException("Login response missing token")
        val user = data.get("user")?.asString ?: ""
        val clientsPayload = data.getAsJsonObject("clientsPayload")
        val clientSource = if (clientsPayload != null) {
            SessionPayloadCrypto.decryptEnvelope(clientsPayload, token)
        } else {
            data
        }
        return LoginResult(
            token = token,
            user = user,
            role = data.get("role")?.asString,
            mustChangePassword = data.get("mustChangePassword")?.asBoolean ?: false,
            assignedClientCodes = parseAssignedClientCodes(clientSource),
        )
    }

    private fun parseAssignedClientCodes(data: JsonObject): List<String> {
        val sources = listOf("assignedClientCodes", "clientCodes", "clients")
        return sources.firstNotNullOfOrNull { key ->
            data.get(key)?.let(::extractClientCodes)
                ?.takeIf { it.isNotEmpty() }
        }.orEmpty()
    }

    private fun extractClientCodes(element: JsonElement): List<String> {
        if (!element.isJsonArray) return emptyList()
        return element.asJsonArray.mapNotNull { item ->
            when {
                item.isJsonPrimitive -> item.asString
                item.isJsonObject -> {
                    val obj = item.asJsonObject
                    obj.get("clientCode")?.asString ?: obj.get("code")?.asString
                }
                else -> null
            }
        }.filter { it.isNotBlank() }.distinct()
    }
}
