package com.tomhempel.rbarometer.network

import android.content.Context
import com.tomhempel.rbarometer.MyApp
import com.tomhempel.rbarometer.data.AnswerEntity
import com.tomhempel.rbarometer.data.AnswerRepository
import com.tomhempel.rbarometer.data.CompletedQuestionnaire
import com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository
import com.tomhempel.rbarometer.data.Question
import com.tomhempel.rbarometer.data.Questionnaire
import com.tomhempel.rbarometer.questionnaire.AnswerKeyUtils
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext

object QuestionnaireAnswerSync {
    private const val TAG = "AnswerSync"
    /** Parallel HTTP downloads during per-client answer fallback. */
    private const val CLIENT_FETCH_PARALLELISM = 6
    /** Room/SQLite allows one writer — serialize imports to avoid lock contention. */
    private val dbImportGate = Semaphore(1)
    /** Re-download on client switch only if last successful sync is older than this. */
    const val CLIENT_PULL_STALE_MS = 3_600_000L

    private val sessionPullAtMs = mutableMapOf<String, Long>()

    private data class PreparedBlockImport(
        val questionnaireId: String,
        val questions: List<Question>,
        val answerEntities: List<AnswerEntity>,
        val completion: CompletedQuestionnaire,
    )

    private data class PreparedClientImport(
        val clientCode: String,
        val blocks: List<PreparedBlockImport>,
    )

    private data class DetailCaches(
        val parsed: Map<String, JsonObject>,
        val rawJson: Map<String, String>,
    )

    /**
     * Pulls server answers for assigned clients (offline facility prep).
     * When [force] is false, only clients not synced within [CLIENT_PULL_STALE_MS] are fetched.
     * Pass [prefetchedBundles] when bulk HTTP was started earlier (e.g. parallel with detail fetch).
     */
    suspend fun pullForAllAssignedClients(
        context: Context,
        token: String,
        clientCodes: List<String>? = null,
        force: Boolean = false,
        prefetchedBundles: List<ClientAnswersBundle>? = null,
        pacer: SyncProgressPacer? = null,
    ): Int {
        if (!NetworkUtils.isOnline(context)) {
            SyncLog.d(TAG, "offline — skip answer pull for all clients")
            return 0
        }
        val codes = (clientCodes ?: QuestionnaireCache.getAssignedClients(context))
            .filter { it.isNotBlank() }
            .distinct()
        if (codes.isEmpty()) {
            SyncLog.d(TAG, "no assigned clients — skip answer pull")
            return 0
        }
        val toPull = clientsNeedingPull(context, codes, force)
        if (toPull.isEmpty()) {
            SyncLog.d(TAG, "all ${codes.size} clients fresh (synced within 1h)")
            return codes.size
        }
        SyncLog.d(TAG, "pulling answers for ${toPull.size}/${codes.size} clients (force=$force)")
        pullClientsParallel(context, token, toPull, prefetchedBundles, pacer)
        return toPull.size
    }

    fun clientsNeedingPull(context: Context, codes: List<String>, force: Boolean): List<String> =
        if (force) codes else codes.filter { shouldPull(context, it) }

    /**
     * Pulls answers for one client when selected on the opening screen.
     * Skips network if synced within [CLIENT_PULL_STALE_MS] unless [force] is true.
     */
    suspend fun pullForClient(
        context: Context,
        token: String,
        clientCode: String,
        force: Boolean = false,
    ): Boolean {
        if (!NetworkUtils.isOnline(context)) {
            SyncLog.d(TAG, "offline — skip answer pull for ${SyncLog.clientRef(clientCode)}")
            return false
        }
        val code = clientCode.trim()
        if (code.isBlank()) return false
        if (!force && !shouldPull(context, code)) {
            SyncLog.d(TAG, "skip answer pull for ${SyncLog.clientRef(code)} (synced within 1h)")
            return true
        }
        return runCatching {
            val bundle = QuestionnaireApiClient.fetchClientAnswers(token, code)
            importClientBundle(context, code, bundle.questionnaires)
            markPulled(context, code)
            true
        }.onFailure { e ->
            SyncLog.w(TAG, "answer pull failed for ${SyncLog.clientRef(code)}: ${e.message}")
        }.getOrDefault(false)
    }

    fun invalidatePullCache(context: Context, clientCode: String? = null) {
        ClientAnswerSyncStore.clear(context, clientCode)
        if (clientCode.isNullOrBlank()) {
            sessionPullAtMs.clear()
        } else {
            sessionPullAtMs.remove(clientCode.trim().uppercase())
        }
    }

    fun needsPull(context: Context, clientCode: String): Boolean =
        shouldPull(context, clientCode.trim())

    private fun shouldPull(context: Context, clientCode: String): Boolean {
        val key = clientCode.trim().uppercase()
        val last = maxOf(
            sessionPullAtMs[key] ?: 0L,
            ClientAnswerSyncStore.lastPullMs(context, key),
        )
        if (last <= 0L) return true
        return System.currentTimeMillis() - last > CLIENT_PULL_STALE_MS
    }

    private fun markPulled(context: Context, clientCode: String) {
        val now = System.currentTimeMillis()
        val key = clientCode.trim().uppercase()
        sessionPullAtMs[key] = now
        ClientAnswerSyncStore.markPulled(context, clientCode, now)
    }

    /**
     * Bulk HTTP when possible; one Room transaction per chunk. Falls back to parallel per-client fetches.
     */
    private suspend fun pullClientsParallel(
        context: Context,
        token: String,
        codes: List<String>,
        prefetchedBundles: List<ClientAnswersBundle>?,
        pacer: SyncProgressPacer? = null,
    ): Int {
        if (!NetworkUtils.isOnline(context)) {
            SyncLog.d(TAG, "offline — skip answer pull")
            return 0
        }
        if (prefetchedBundles != null) {
            return importBulkBundles(context, codes, prefetchedBundles, pacer)
        }
        pacer?.emit(SyncPhase.DOWNLOADING_ANSWERS)
        val bulkResult = runCatching {
            withContext(Dispatchers.IO) {
                QuestionnaireApiClient.fetchBulkClientAnswers(token)
            }
        }
        if (bulkResult.isSuccess) {
            return importBulkBundles(context, codes, bulkResult.getOrDefault(emptyList()), pacer)
        }
        SyncLog.w(TAG, "bulk answer pull failed, falling back to per-client: ${bulkResult.exceptionOrNull()?.message}")
        return pullClientsParallelFallback(context, token, codes, pacer)
    }

    private suspend fun importBulkBundles(
        context: Context,
        codes: List<String>,
        bundles: List<ClientAnswersBundle>,
        pacer: SyncProgressPacer?,
    ): Int {
        val byCode = bundles.associateBy { it.clientCode.trim().uppercase() }
        val imported = runCatching {
            importManyClients(
                context = context,
                codes = codes,
                blocksByCode = { code -> byCode[code.trim().uppercase()]?.questionnaires.orEmpty() },
                pacer = pacer,
            )
            ClientAnswerSyncStore.markPulledBatch(context, codes)
            codes.size
        }.getOrElse {
            SyncLog.w(TAG, "bulk import failed: ${it.message}")
            0
        }
        SyncLog.d(TAG, "bulk answer pull finished: $imported/${codes.size} clients")
        return imported
    }

    private suspend fun pullClientsParallelFallback(
        context: Context,
        token: String,
        codes: List<String>,
        pacer: SyncProgressPacer? = null,
    ): Int = coroutineScope {
        pacer?.emit(SyncPhase.DOWNLOADING_ANSWERS)
        val fetchGate = Semaphore(CLIENT_FETCH_PARALLELISM)

        data class Fetched(val code: String, val blocks: List<ServerQuestionnaireAnswers>)

        val fetched = codes.map { code ->
            async(Dispatchers.IO) {
                fetchGate.withPermit {
                    runCatching {
                        val bundle = QuestionnaireApiClient.fetchClientAnswers(token, code)
                        Fetched(code, bundle.questionnaires)
                    }.getOrElse {
                        SyncLog.w(TAG, "fetch failed for ${SyncLog.clientRef(code)}: ${it.message}")
                        null
                    }
                }
            }
        }.awaitAll().filterNotNull()

        val fetchedByCode = fetched.associate { it.code.trim().uppercase() to it.blocks }
        val imported = runCatching {
            importManyClients(
                context = context,
                codes = fetched.map { it.code },
                blocksByCode = { code -> fetchedByCode[code.trim().uppercase()].orEmpty() },
                pacer = pacer,
            )
            ClientAnswerSyncStore.markPulledBatch(context, fetched.map { it.code })
            fetched.size
        }.getOrElse {
            SyncLog.w(TAG, "fallback import failed: ${it.message}")
            0
        }

        SyncLog.d(TAG, "answer pull finished: $imported/${codes.size} clients (${fetched.size} fetched)")
        imported
    }

    /**
     * Prepares all clients on a worker thread (encrypt + JSON), then one Room transaction for SQLite writes.
     */
    private suspend fun importManyClients(
        context: Context,
        codes: List<String>,
        blocksByCode: (String) -> List<ServerQuestionnaireAnswers>,
        pacer: SyncProgressPacer? = null,
    ) {
        if (codes.isEmpty()) return
        pacer?.emit(SyncPhase.PROCESSING)

        val listQnIds = QuestionnaireCache.getQuestionnaireList(context).map { it.id }.toSet()
        val allQnIds = buildSet {
            addAll(listQnIds)
            codes.forEach { code ->
                blocksByCode(code).forEach { add(it.questionnaireID) }
            }
        }
        val detailCaches = loadDetailCaches(context, allQnIds)
        val localAnswerPresence = AnswerRepository.localAnswerPresence(codes)
        val completionsByClient = CompletedQuestionnaireRepository.getAllGroupedByClient(codes)

        val prepared = withContext(Dispatchers.Default) {
            buildList {
                for (code in codes) {
                    val blocks = blocksByCode(code)
                    if (blocks.isEmpty()) continue
                    val completionByQn = completionsByClient[code].orEmpty()
                        .associateBy { it.questionnaireId }
                    val clientBlocks = mutableListOf<PreparedBlockImport>()
                    for (block in blocks) {
                        prepareBlockImport(
                            clientCode = code,
                            block = block,
                            localAnswerPresence = localAnswerPresence,
                            completionByQn = completionByQn,
                            detailCaches = detailCaches,
                        )?.let { clientBlocks.add(it) }
                    }
                    if (clientBlocks.isNotEmpty()) {
                        add(PreparedClientImport(code, clientBlocks))
                    }
                }
            }
        }

        if (prepared.isEmpty()) return

        pacer?.emit(SyncPhase.SECURELY_SAVING)
        dbImportGate.withPermit {
            val db = MyApp.database
            db.withTransaction {
                val allQuestionnaires = prepared.flatMap { it.blocks }
                    .map { Questionnaire(id = it.questionnaireId) }
                    .distinctBy { it.id }
                db.questionnaireDao().insertQuestionnaires(allQuestionnaires)
                for (client in prepared) {
                    val qnIds = client.blocks.map { it.questionnaireId }
                    AnswerRepository.deleteAnswersForClientAndQuestionnaires(client.clientCode, qnIds)
                    val allQuestions = client.blocks.flatMap { it.questions }
                    if (allQuestions.isNotEmpty()) {
                        db.questionDao().insertQuestions(allQuestions)
                    }
                    val allAnswers = client.blocks.flatMap { it.answerEntities }
                    if (allAnswers.isNotEmpty()) {
                        db.answerDao().insertAnswers(allAnswers)
                    }
                    db.completedQuestionnaireDao().insertAll(
                        client.blocks.map { it.completion.toEntity(client.clientCode) },
                    )
                }
            }
        }
    }

    private suspend fun importClientBundle(
        context: Context,
        clientCode: String,
        blocks: List<ServerQuestionnaireAnswers>,
    ) {
        if (blocks.isEmpty()) return
        importManyClients(
            context = context,
            codes = listOf(clientCode),
            blocksByCode = { if (it == clientCode) blocks else emptyList() },
        )
    }

    private fun loadDetailCaches(context: Context, questionnaireIds: Set<String>): DetailCaches {
        val parsed = mutableMapOf<String, JsonObject>()
        val rawJson = mutableMapOf<String, String>()
        for (id in questionnaireIds) {
            if (id.isBlank()) continue
            val json = QuestionnaireCache.loadQuestionnaireJson(context, id) ?: continue
            rawJson[id] = json
            parsed[id] = JsonParser.parseString(json).asJsonObject
        }
        return DetailCaches(parsed, rawJson)
    }

    private fun prepareBlockImport(
        clientCode: String,
        block: ServerQuestionnaireAnswers,
        localAnswerPresence: Set<Pair<String, String>>,
        completionByQn: Map<String, CompletedQuestionnaire>,
        detailCaches: DetailCaches,
    ): PreparedBlockImport? {
        val questionnaireId = block.questionnaireID
        if (questionnaireId.isBlank()) return null

        val status = completionByQn[questionnaireId]
        val pendingEdit = status?.isDone == true &&
            (status.uploadedAt == null || (status.uploadedAt ?: 0L) < status.timestamp)
        if (localAnswerPresence.contains(clientCode to questionnaireId) && pendingEdit) {
            return null
        }

        val detail = detailCaches.parsed[questionnaireId] ?: return null
        val questions = detail.getAsJsonArray("questions") ?: return null
        val stored = mapServerAnswersToStored(block, questions) ?: return null

        val completedAtMs = (block.completedAt ?: 0L).let { if (it > 0) it * 1000L else System.currentTimeMillis() }
        val sumPoints = block.sumPoints
        val clientKey = com.tomhempel.rbarometer.security.SensitiveDataCrypto.clientCodeKey(clientCode)

        return PreparedBlockImport(
            questionnaireId = questionnaireId,
            questions = stored.keys.map { key ->
                Question(
                    questionId = AnswerKeyUtils.roomQuestionId(questionnaireId, key),
                    questionnaireId = questionnaireId,
                    question = key,
                )
            },
            answerEntities = stored.map { (key, value) ->
                AnswerEntity(
                    clientCodeKey = clientKey,
                    questionId = AnswerKeyUtils.roomQuestionId(questionnaireId, key),
                    answerValueEnc = com.tomhempel.rbarometer.security.SensitiveDataCrypto.encrypt(value),
                )
            },
            completion = CompletedQuestionnaire(
                clientCode = clientCode,
                questionnaireId = questionnaireId,
                isDone = true,
                sumPoints = sumPoints,
                timestamp = completedAtMs,
                uploadedAt = completedAtMs,
            ),
        )
    }

    private fun mapServerAnswersToStored(
        block: ServerQuestionnaireAnswers,
        questions: JsonArray,
    ): Map<String, String>? {
        val submitByQuestion = block.answers.groupBy { it.questionID }
        val stored = mutableMapOf<String, String>()
        val symptomKeys = mutableSetOf<String>()

        questions.forEach { el ->
            val q = el.asJsonObject
            val shortId = q.get("id")?.asString ?: return@forEach
            val layout = q.get("layout")?.asString.orEmpty()
            val questionKey = q.get("question")?.asString?.takeIf { it.isNotBlank() } ?: shortId
            val rows = submitByQuestion[shortId].orEmpty()

            q.getAsJsonArray("symptoms")?.forEach { s ->
                symptomKeys.add(s.asString)
            }

            when (layout) {
                "multi_check_box_question" -> {
                    val keys = rows.mapNotNull { it.answerOptionKey?.takeIf { k -> k.isNotBlank() } }
                    if (keys.isNotEmpty()) {
                        stored[questionKey] = keys.joinToString(
                            prefix = "[",
                            postfix = "]",
                        ) { "\"$it\"" }
                    }
                }
                "glass_scale_question" -> Unit
                "radio_question", "string_spinner" -> {
                    val row = rows.firstOrNull() ?: return@forEach
                    val value = row.answerOptionKey?.takeIf { it.isNotBlank() }
                        ?: row.freeTextValue?.takeIf { it.isNotBlank() }
                    if (value != null) stored[questionKey] = value
                }
                "value_spinner", "slider_question" -> {
                    val row = rows.firstOrNull() ?: return@forEach
                    val num = row.numericValue
                    val value = when {
                        num != null -> num.toString()
                        else -> row.freeTextValue
                    }
                    if (!value.isNullOrBlank()) stored[questionKey] = value
                }
                else -> {
                    val row = rows.firstOrNull() ?: return@forEach
                    val value = row.freeTextValue?.takeIf { it.isNotBlank() }
                        ?: row.answerOptionKey?.takeIf { it.isNotBlank() }
                        ?: row.numericValue?.toString()
                    if (!value.isNullOrBlank()) stored[questionKey] = value
                }
            }
        }

        symptomKeys.forEach { symptomKey ->
            val row = block.answers.find { it.questionID == symptomKey } ?: return@forEach
            val value = row.answerOptionKey?.takeIf { it.isNotBlank() } ?: return@forEach
            stored[symptomKey] = value
        }

        return stored.takeIf { it.isNotEmpty() }
    }

    private fun CompletedQuestionnaire.toEntity(clientCode: String) =
        com.tomhempel.rbarometer.data.CompletedQuestionnaireEntity(
            clientCodeKey = com.tomhempel.rbarometer.security.SensitiveDataCrypto.clientCodeKey(clientCode),
            questionnaireId = questionnaireId,
            timestamp = timestamp,
            isDone = isDone,
            sumPoints = sumPoints,
            uploadedAt = uploadedAt,
        )
}
