package com.tomhempel.rbarometer.network

import android.content.Context
import com.tomhempel.rbarometer.LanguageManager
import com.tomhempel.rbarometer.MyApp
import com.tomhempel.rbarometer.data.ClientRepository
import com.tomhempel.rbarometer.security.SensitiveDataCrypto
import com.tomhempel.rbarometer.questionnaire.GlobalValues
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import java.io.File

object QuestionnaireCache {
    private const val CACHE_DIR = "api_questionnaire_cache"
    private const val DETAILS_DIR = "details"
    private const val TRANSLATIONS_FILE = "translations.json"
    private const val QUESTIONNAIRES_FILE = "questionnaires.json"
    private const val CLIENTS_FILE = "assigned_clients.json"
    private const val CLIENT_INFOS_FILE = "assigned_client_infos.json"
    private const val PROGRAM_SESSIONS_FILE = "program_sessions.json"
    private const val SCORING_PROFILES_FILE = "scoring_profiles.json"

    private val gson = Gson()

    fun hasQuestionnaireCache(context: Context): Boolean =
        questionnairesFile(context).exists() && detailsDir(context).listFiles()?.isNotEmpty() == true

    fun saveTranslations(context: Context, data: JsonObject) {
        // Server may return { "translations": { "en": {...} } }, directly { "en": {...} },
        // or { "translations": [] } when no translations are configured yet.
        val translationsElement = if (data.has("translations")) data.get("translations") else data
        val translations = if (translationsElement.isJsonObject) translationsElement.asJsonObject else JsonObject()
        SyncLog.d("CACHE", "saveTranslations: ${translations.keySet().size} language(s)")
        writeCacheText(translationsFile(context), gson.toJson(translations))
        LanguageManager.setApiTranslations(parseTranslationMap(translations))
    }

    fun applyTranslations(context: Context) {
        val file = translationsFile(context)
        if (!file.exists()) return
        val parsed = runCatching { JsonParser.parseString(readCacheText(file) ?: return) }.getOrNull() ?: return
        val translations = if (parsed.isJsonObject) parsed.asJsonObject else JsonObject()
        LanguageManager.setApiTranslations(parseTranslationMap(translations))
    }

    fun parseTranslationMap(root: JsonObject): Map<String, Map<String, String>> = parseTranslations(root)

    fun applyQuestionnaireTranslationsFromJson(jsonString: String) {
        val root = runCatching { JsonParser.parseString(jsonString).asJsonObject }.getOrNull() ?: return
        val translationsElement = root.get("translations") ?: return
        if (!translationsElement.isJsonObject) return
        LanguageManager.setQuestionnaireTranslations(parseTranslations(translationsElement.asJsonObject))
    }

    fun saveQuestionnaireList(context: Context, list: List<QuestionnaireListItem>) {
        writeCacheText(questionnairesFile(context), gson.toJson(list))
    }

    fun getQuestionnaireList(context: Context): List<QuestionnaireListItem> {
        val file = questionnairesFile(context)
        if (!file.exists()) return emptyList()
        return runCatching {
            gson.fromJson(readCacheText(file), Array<QuestionnaireListItem>::class.java).toList()
        }.getOrDefault(emptyList())
    }

    fun saveQuestionnaireDetail(context: Context, questionnaireId: String, detail: JsonObject) {
        writeCacheText(File(detailsDir(context), "$questionnaireId.json"), gson.toJson(detail))
    }

    fun loadQuestionnaireJson(context: Context, fileOrId: String): String? {
        val id = fileOrId.removeSuffix(".json")
        val file = File(detailsDir(context), "$id.json")
        return if (file.exists()) readCacheText(file) else null
    }

    fun getCachedStructureRevision(context: Context, questionnaireId: String): Int? {
        val json = loadQuestionnaireJson(context, questionnaireId) ?: return null
        return runCatching {
            JsonParser.parseString(json).asJsonObject.get("structureRevision")?.asInt
        }.getOrNull()?.coerceAtLeast(1)
    }

    fun saveAssignedClients(context: Context, clientCodes: List<String>) {
        writeCacheText(clientsFile(context), gson.toJson(clientCodes.distinct()))
    }

    fun clearAssignedClients(context: Context) {
        val file = clientsFile(context)
        if (file.exists()) file.delete()
        GlobalValues.LAST_CLIENT_CODE = null
        GlobalValues.LOADED_CLIENT_CODE = null
    }

    suspend fun clearAllClientData(context: Context) {
        clearAssignedClients(context)
        ClientRepository.deleteAllClients()
    }

    /**
     * Removes all interview data, questionnaire cache files, and Room tables from the device.
     * Call on logout for security; does not clear auth tokens (see [SessionLogout.wipeAllLocalData]).
     */
    suspend fun wipeEntireLocalStore(context: Context) {
        clearAssignedClients(context)
        com.tomhempel.rbarometer.MyApp.database.clearAllTables()
        cacheDir(context).deleteRecursively()
        com.tomhempel.rbarometer.notification.UploadReminderScheduler.cancel(context)
        LanguageManager.setApiTranslations(emptyMap())
        LanguageManager.setQuestionnaireTranslations(emptyMap())
    }

    /**
     * Updates assigned clients without wiping local answers/completions for clients that stay assigned.
     * (Previously deleteAllClients() CASCADE-deleted all interview data on every sync.)
     */
    suspend fun replaceAssignedClients(context: Context, clientCodes: List<String>) {
        val codes = clientCodes.distinct()
        saveAssignedClients(context, codes)
        ClientRepository.replaceClients(codes)
        SyncLog.d("CACHE", "replaceAssignedClients: ${codes.size} clients")
    }

    fun saveScoringProfiles(context: Context, profiles: List<ScoringProfileDefinition>) {
        writeCacheText(File(cacheDir(context), SCORING_PROFILES_FILE), gson.toJson(profiles))
    }

    fun getScoringProfiles(context: Context): List<ScoringProfileDefinition> {
        val file = File(cacheDir(context), SCORING_PROFILES_FILE)
        if (!file.exists()) return emptyList()
        return runCatching {
            gson.fromJson(readCacheText(file), Array<ScoringProfileDefinition>::class.java).toList()
        }.getOrDefault(emptyList())
    }

    fun getAssignedClients(context: Context): List<String> {
        val file = clientsFile(context)
        if (!file.exists()) return emptyList()
        return runCatching {
            gson.fromJson(readCacheText(file), Array<String>::class.java).toList()
        }.getOrDefault(emptyList())
    }

    fun saveAssignedClientInfos(context: Context, infos: List<AssignedClientInfo>) {
        writeCacheText(clientInfosFile(context), gson.toJson(infos))
    }

    fun getAssignedClientInfos(context: Context): List<AssignedClientInfo> {
        val file = clientInfosFile(context)
        if (!file.exists()) return emptyList()
        return runCatching {
            gson.fromJson(readCacheText(file), Array<AssignedClientInfo>::class.java).toList()
        }.getOrDefault(emptyList())
    }

    fun getProgramProgress(context: Context, clientCode: String): ProgramProgress? =
        getAssignedClientInfos(context)
            .firstOrNull { it.clientCode.equals(clientCode, ignoreCase = true) }
            ?.programProgress

    fun saveProgramSessions(context: Context, payload: ProgramSessionsPayload) {
        writeCacheText(programSessionsFile(context), gson.toJson(payload))
    }

    fun getProgramSessionsPayload(context: Context): ProgramSessionsPayload {
        val file = programSessionsFile(context)
        if (!file.exists()) return ProgramSessionsPayload()
        return runCatching {
            gson.fromJson(readCacheText(file), ProgramSessionsPayload::class.java)
        }.getOrDefault(ProgramSessionsPayload())
    }

    fun getProgramSessions(context: Context): List<ProgramSessionDefinition> =
        getProgramSessionsPayload(context).sessions

    fun getProgramSessionSettings(context: Context): ProgramSessionSettings =
        getProgramSessionsPayload(context).settings

    fun findProgramSession(context: Context, sessionID: String): ProgramSessionDefinition? =
        getProgramSessions(context).firstOrNull { it.sessionID == sessionID }

    private fun parseTranslations(root: JsonObject): Map<String, Map<String, String>> {
        return root.entrySet().mapNotNull { (language, values) ->
            if (!values.isJsonObject) return@mapNotNull null
            val strings = values.asJsonObject.entrySet().mapNotNull { (key, value) ->
                if (value.isJsonPrimitive) key to value.asString else null
            }.toMap()
            language to strings
        }.toMap()
    }

    private fun cacheDir(context: Context): File =
        File(context.filesDir, CACHE_DIR).apply { mkdirs() }

    private fun detailsDir(context: Context): File =
        File(cacheDir(context), DETAILS_DIR).apply { mkdirs() }

    private fun translationsFile(context: Context): File =
        File(cacheDir(context), TRANSLATIONS_FILE)

    private fun questionnairesFile(context: Context): File =
        File(cacheDir(context), QUESTIONNAIRES_FILE)

    private fun clientsFile(context: Context): File =
        File(cacheDir(context), CLIENTS_FILE)

    private fun clientInfosFile(context: Context): File =
        File(cacheDir(context), CLIENT_INFOS_FILE)

    private fun programSessionsFile(context: Context): File =
        File(cacheDir(context), PROGRAM_SESSIONS_FILE)

    private fun writeCacheText(file: File, plain: String) {
        file.writeText(SensitiveDataCrypto.encrypt(plain))
    }

    /** Reads encrypted cache files; legacy plaintext files are still accepted. */
    private fun readCacheText(file: File): String? {
        if (!file.exists()) return null
        val raw = file.readText()
        return if (raw.startsWith("v1:")) SensitiveDataCrypto.decrypt(raw) else raw
    }
}
