package com.tomhempel.rbarometer.network

import android.content.Context
import com.tomhempel.rbarometer.MyApp
import com.tomhempel.rbarometer.data.CompletedQuestionnaire
import com.tomhempel.rbarometer.data.Questionnaire
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

object QuestionnaireSyncService {
    private const val TAG = "SYNC"
    private const val DETAIL_FETCH_PARALLELISM = 8

    suspend fun refreshAll(
        context: Context,
        token: String,
        assignedClientCodes: List<String> = emptyList(),
        pullAnswers: Boolean = true,
        forceAnswerPull: Boolean = true,
        onProgress: SyncProgressCallback? = null,
    ) = coroutineScope {
        SyncLog.d(TAG, "refreshAll start (${assignedClientCodes.size} fallback clients)")
        val pacer = SyncProgressPacer(onProgress)

        pacer.emit(SyncPhase.PREPARING)
        val translationsDeferred = async(Dispatchers.IO) { QuestionnaireApiClient.getTranslations(token) }
        val questionnairesDeferred = async(Dispatchers.IO) { QuestionnaireApiClient.getQuestionnaires(token) }
        val translations = translationsDeferred.await()
        val questionnaires = questionnairesDeferred.await()
        QuestionnaireCache.saveTranslations(context, translations)
        QuestionnaireCache.saveQuestionnaireList(context, questionnaires)
        SyncLog.d(TAG, "questionnaire list: ${questionnaires.size} items")

        val db = MyApp.database
        db.questionnaireDao().insertQuestionnaires(
            questionnaires.map { Questionnaire(it.id) },
        )

        pacer.emit(SyncPhase.DOWNLOADING_QUESTIONNAIRES)
        val clientsDeferred = async(Dispatchers.IO) {
            runCatching { QuestionnaireApiClient.getAssignedClients(token) }
        }
        val scoringProfilesDeferred = async(Dispatchers.IO) {
            runCatching { QuestionnaireApiClient.getScoringProfiles(token) }
        }
        val programSessionsDeferred = async(Dispatchers.IO) {
            runCatching { QuestionnaireApiClient.getProgramSessions(token) }
        }
        val detailsDeferred = async {
            fetchAndCacheQuestionnaireDetails(context, token, questionnaires)
        }

        val clientResult = clientsDeferred.await()
        scoringProfilesDeferred.await().onSuccess { profiles ->
            QuestionnaireCache.saveScoringProfiles(context, profiles)
            SyncLog.d(TAG, "scoring profiles cached: ${profiles.size}")
        }.onFailure { e ->
            SyncLog.w(TAG, "getScoringProfiles failed: ${e.message}")
        }
        programSessionsDeferred.await().onSuccess { payload ->
            QuestionnaireCache.saveProgramSessions(context, payload)
            SyncLog.d(TAG, "program sessions cached: ${payload.sessions.size}")
        }.onFailure { e ->
            SyncLog.w(TAG, "getProgramSessions failed: ${e.message}")
        }
        pacer.emit(SyncPhase.DOWNLOADING_CLIENT_INFO)
        val clientInfosFromApi = clientResult.getOrElse { e ->
            SyncLog.w(TAG, "getAssignedClients failed: ${e.message}")
            emptyList()
        }
        val apiSucceeded = clientResult.isSuccess

        val clientCodesToSave: List<String> = when {
            apiSucceeded -> clientInfosFromApi.map { it.clientCode }
            assignedClientCodes.isNotEmpty() -> assignedClientCodes
            else -> emptyList()
        }

        persistAssignedClients(context, apiSucceeded, assignedClientCodes, clientCodesToSave, clientInfosFromApi)

        if (!forceAnswerPull || !pullAnswers) {
            seedServerCompletions(clientInfosFromApi)
        }

        detailsDeferred.await()

        if (pullAnswers && apiSucceeded && clientCodesToSave.isNotEmpty()) {
            val toPull = QuestionnaireAnswerSync.clientsNeedingPull(
                context,
                clientCodesToSave,
                forceAnswerPull,
            )
            if (toPull.isNotEmpty()) {
                pacer.emit(SyncPhase.DOWNLOADING_ANSWERS)
                val bulkDeferred = async(Dispatchers.IO) {
                    runCatching { QuestionnaireApiClient.fetchBulkClientAnswers(token) }
                }
                val bulkResult = bulkDeferred.await()
                val prefetchedBundles = bulkResult.getOrNull()
                if (bulkResult.isFailure) {
                    SyncLog.w(
                        TAG,
                        "bulk answer pull failed, import will retry fetch: ${bulkResult.exceptionOrNull()?.message}",
                    )
                }
                QuestionnaireAnswerSync.pullForAllAssignedClients(
                    context,
                    token,
                    toPull,
                    force = forceAnswerPull,
                    prefetchedBundles = prefetchedBundles,
                    pacer = pacer,
                )
            } else {
                SyncLog.d(TAG, "all ${clientCodesToSave.size} clients fresh — skip answer pull")
            }
        }

        pacer.emitFinished()
        SyncLog.d(TAG, "refreshAll complete")
    }

    suspend fun refreshAssignedClients(context: Context, token: String): Boolean {
        SyncLog.d(TAG, "refreshAssignedClients start")
        val apiSucceeded = fetchAndPersistAssignedClients(
            context = context,
            token = token,
            fallbackCodes = emptyList(),
            pullAnswers = true,
            forceAnswerPull = false,
            onProgress = null,
        )
        val saved = QuestionnaireCache.getAssignedClients(context)
        SyncLog.d(TAG, "refreshAssignedClients done: ${saved.size} clients, api=$apiSucceeded")
        return apiSucceeded
    }

    private suspend fun fetchAndPersistAssignedClients(
        context: Context,
        token: String,
        fallbackCodes: List<String>,
        pullAnswers: Boolean = false,
        forceAnswerPull: Boolean = false,
        onProgress: SyncProgressCallback? = null,
    ): Boolean {
        val pacer = SyncProgressPacer(onProgress)
        pacer.emit(SyncPhase.DOWNLOADING_CLIENT_INFO)
        val clientResult = runCatching {
            QuestionnaireApiClient.getAssignedClients(token)
        }
        val clientInfosFromApi = clientResult.getOrElse { e ->
            SyncLog.w(TAG, "getAssignedClients failed: ${e.message}")
            emptyList()
        }
        val apiSucceeded = clientResult.isSuccess

        val clientCodesToSave: List<String> = when {
            apiSucceeded -> clientInfosFromApi.map { it.clientCode }
            fallbackCodes.isNotEmpty() -> fallbackCodes
            else -> emptyList()
        }

        persistAssignedClients(context, apiSucceeded, fallbackCodes, clientCodesToSave, clientInfosFromApi)

        if (!forceAnswerPull || !pullAnswers) {
            seedServerCompletions(clientInfosFromApi)
        }

        if (pullAnswers && apiSucceeded && clientCodesToSave.isNotEmpty()) {
            val toPull = QuestionnaireAnswerSync.clientsNeedingPull(
                context,
                clientCodesToSave,
                forceAnswerPull,
            )
            if (toPull.isNotEmpty()) {
                QuestionnaireAnswerSync.pullForAllAssignedClients(
                    context,
                    token,
                    toPull,
                    force = forceAnswerPull,
                    pacer = pacer,
                )
            }
        }

        return apiSucceeded
    }

    private suspend fun persistAssignedClients(
        context: Context,
        apiSucceeded: Boolean,
        fallbackCodes: List<String>,
        clientCodesToSave: List<String>,
        clientInfosFromApi: List<AssignedClientInfo> = emptyList(),
    ) {
        when {
            apiSucceeded -> {
                QuestionnaireCache.replaceAssignedClients(context, clientCodesToSave)
                QuestionnaireCache.saveAssignedClientInfos(context, clientInfosFromApi)
                SyncLog.d(TAG, "assigned clients from API: ${clientCodesToSave.size}")
            }
            fallbackCodes.isNotEmpty() -> {
                QuestionnaireCache.replaceAssignedClients(context, clientCodesToSave)
                SyncLog.d(TAG, "assigned clients from login fallback: ${clientCodesToSave.size}")
            }
            else -> SyncLog.w(TAG, "client fetch failed; keeping clients from login step only")
        }
    }

    private suspend fun seedServerCompletions(clientInfosFromApi: List<AssignedClientInfo>) {
        val toInsert = mutableListOf<CompletedQuestionnaire>()
        for (clientInfo in clientInfosFromApi) {
            val existingByQn = com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository
                .getAllForClient(clientInfo.clientCode)
                .associateBy { it.questionnaireId }
            for (serverCompletion in clientInfo.completedQuestionnaires) {
                val serverTimestampMs = serverCompletion.completedAt * 1000L
                val existing = existingByQn[serverCompletion.questionnaireID]
                if (existing == null || serverTimestampMs > existing.timestamp) {
                    toInsert.add(
                        CompletedQuestionnaire(
                            clientCode = clientInfo.clientCode,
                            questionnaireId = serverCompletion.questionnaireID,
                            timestamp = serverTimestampMs,
                            isDone = true,
                            sumPoints = serverCompletion.sumPoints,
                            uploadedAt = serverTimestampMs,
                        ),
                    )
                }
            }
        }
        if (toInsert.isNotEmpty()) {
            runCatching {
                com.tomhempel.rbarometer.data.CompletedQuestionnaireRepository.insertAll(toInsert)
            }.onSuccess {
                SyncLog.d(TAG, "seeded ${toInsert.size} server completion(s)")
            }.onFailure { e ->
                SyncLog.w(TAG, "batch seed completions failed: ${e.message}")
            }
        }
    }

    fun loadCachedTranslations(context: Context) {
        QuestionnaireCache.applyTranslations(context)
    }

    private suspend fun fetchAndCacheQuestionnaireDetails(
        context: Context,
        token: String,
        questionnaires: List<QuestionnaireListItem>,
    ) = coroutineScope {
        if (questionnaires.isEmpty()) return@coroutineScope
        val gate = Semaphore(DETAIL_FETCH_PARALLELISM)
        questionnaires.map { item ->
            async(Dispatchers.IO) {
                gate.withPermit {
                    val cachedRev = QuestionnaireCache.getCachedStructureRevision(context, item.id)
                    if (cachedRev != null && item.structureRevision <= cachedRev) {
                        return@withPermit
                    }
                    val detail = QuestionnaireApiClient.getQuestionnaire(token, item.id)
                    QuestionnaireCache.saveQuestionnaireDetail(context, item.id, detail)
                }
            }
        }.awaitAll()
        SyncLog.d(TAG, "questionnaire details cached: ${questionnaires.size}")
    }
}
