@file:Suppress("HardCodedStringLiteral")

package eu.gaudian.translator.model.repository

import android.content.Context
import com.github.luben.zstd.Zstd
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import eu.gaudian.translator.model.communication.Asset
import eu.gaudian.translator.model.communication.FileInfo
import eu.gaudian.translator.utils.Log
import java.io.File
import java.io.IOException
import java.text.Normalizer

/**
 * Data class representing a dictionary word entry with parsed fields where possible.
 */
data class DictionaryWordEntry(
    val word: String,
    val langCode: String,
    val pos: String?,
    val json: String
)

/**
 * Repository for performing dictionary lookups using the DictionaryDatabaseRepository.
 * Handles specific dictionary files named "dictionary_<langCode>.db".
 */
class DictionaryLookupRepository(private val context: Context) {

    private val databaseRepository = DictionaryDatabaseRepository(context)
    private val gson = Gson()
    @Suppress("PrivatePropertyName")
    private val TAG = "DictionaryLookupRepository"
    private val dictionaryCache = mutableMapOf<String, ByteArray>()

    /**
     * Developer helper: returns a list of ALL words in the local dictionary for a language.
     * This can be large and is intended for debugging / cycling through entries only.
     */
    @Suppress("HardCodedStringLiteral")
    fun getAllWords(langCode: String, limit: Int = 10_000, offset: Int = 0): List<String> {
        if (!hasDictionaryForLanguage(langCode)) {
            Log.w(TAG, "No dictionary available for language: $langCode")
            return emptyList()
        }

        val dbFilename = "dictionary_$langCode.db"
        val dictFilename = "dictionary_$langCode.zstdict"
        val fileInfo = FileInfo(
            id = "dictionary_$langCode",
            name = "Dictionary for $langCode",
            description = "",
            version = "",
            assets = listOf(
                Asset(filename = dbFilename, sizeBytes = 0L, checksumSha256 = ""),
                Asset(filename = dictFilename, sizeBytes = 0L, checksumSha256 = "")
            )
        )

        val db = databaseRepository.getDatabase(fileInfo) ?: return emptyList()
        val sql = "SELECT word FROM dictionary_data ORDER BY word LIMIT ? OFFSET ?"
        Log.d(TAG, "Developer getAllWords for '$langCode' with limit=$limit, offset=$offset")
        return try {
            db.rawQuery(sql, arrayOf(limit.toString(), offset.toString())).use { cursor ->
                val words = mutableListOf<String>()
                val wordIndex = cursor.getColumnIndex("word")
                while (cursor.moveToNext()) {
                    val wordValue = cursor.getString(wordIndex)
                    if (!wordValue.isNullOrBlank()) {
                        words.add(wordValue)
                    }
                }
                Log.d(TAG, "Developer getAllWords fetched ${words.size} words")
                words
            }
        } catch (e: Exception) {
            Log.e(TAG, "Developer getAllWords failed for lang '$langCode': ${e.message}", e)
            emptyList()
        }.also { db.close() }
    }

    /**
     * Checks if a dictionary file exists for the given language code.
     * Language codes are the first part in languages.xml strings, e.g., "de" for German.
     */
    fun hasDictionaryForLanguage(langCode: String): Boolean {
        val filename = "dictionary_$langCode.db"
        val exists = File(context.filesDir, filename).exists()
        return exists
    }

    /**
     * Checks if a specific word exists in the dictionary for the given language code.
     * This performs an actual lookup to verify the word is available, not just that
     * the dictionary file exists.
     */
    fun hasWordInDictionary(word: String, langCode: String): Boolean {
        if (!hasDictionaryForLanguage(langCode)) {
            return false
        }

        val dbFilename = "dictionary_$langCode.db"
        val dictFilename = "dictionary_$langCode.zstdict"
        val fileInfo = FileInfo(
            id = "dictionary_$langCode",
            name = "Dictionary for $langCode",
            description = "",
            version = "",
            assets = listOf(
                Asset(filename = dbFilename, sizeBytes = 0L, checksumSha256 = ""),
                Asset(filename = dictFilename, sizeBytes = 0L, checksumSha256 = "")
            )
        )

        val db = databaseRepository.getDatabase(fileInfo) ?: return false
        val sql = "SELECT 1 FROM dictionary_data WHERE word = ? COLLATE NOCASE LIMIT 1"
        
        return try {
            db.rawQuery(sql, arrayOf(word)).use { cursor ->
                val found = cursor.moveToFirst()
                found
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to check word '$word' in dictionary '$langCode': ${e.message}", e)
            false
        }.also { db.close() }
    }

    /**
     * Searches for words in the dictionary for the specified language code.
     * Returns a list of DictionaryWordEntry matching the exact word.
     * Uses case-insensitive exact match on the word column.
     */
    @Suppress("HardCodedStringLiteral")
    fun searchWord(word: String, langCode: String): List<DictionaryWordEntry> {
        if (!hasDictionaryForLanguage(langCode)) {
            Log.w(TAG, "No dictionary available for language: $langCode")
            return emptyList()
        }

        val dbFilename = "dictionary_$langCode.db"
        val dictFilename = "dictionary_$langCode.zstdict"
        val fileInfo = FileInfo(
            id = "dictionary_$langCode",
            name = "Dictionary for $langCode",
            description = "",
            version = "",
            assets = listOf(
                Asset(filename = dbFilename, sizeBytes = 0L, checksumSha256 = ""),
                Asset(filename = dictFilename, sizeBytes = 0L, checksumSha256 = "")
            )
        )

        val db = databaseRepository.getDatabase(fileInfo) ?: return emptyList()
        val sql = "SELECT word, pos, data_blob, uncompressed_size FROM dictionary_data WHERE word = ? COLLATE NOCASE LIMIT 100"
        //Log.d(TAG, "Attempting query for exact word match '$word': $sql")
        return try {
            db.rawQuery(sql, arrayOf(word)).use { cursor ->
                val entries = mutableListOf<DictionaryWordEntry>()
                val wordIndex = cursor.getColumnIndex("word")
                val posIndex = cursor.getColumnIndex("pos")
                val blobIndex = cursor.getColumnIndex("data_blob")
                val sizeIndex = cursor.getColumnIndex("uncompressed_size")

                if(cursor.count != 1){
                Log.d(TAG, "Cursor has ${cursor.count} rows")}
                while (cursor.moveToNext()) {
                    val wordValue = cursor.getString(wordIndex)
                    val posValue = if (posIndex >= 0) cursor.getString(posIndex) else null
                    val blob = cursor.getBlob(blobIndex)
                    val uncompressedSize = cursor.getInt(sizeIndex)

                    // Decompress the blob to get the JSON data
                    val dict = getDecompressionDict(langCode)
                    val jsonData = if (dict != null && blob != null) {
                        try {
                            String(Zstd.decompress(blob, dict, uncompressedSize))
                        } catch (e: Exception) {
                            Log.e(TAG, "Failed to decompress data for word '$word'", e)
                            ""
                        }
                    } else {
                        ""
                    }

                    val entry = DictionaryWordEntry(
                        word = wordValue,
                        langCode = langCode,
                        pos = posValue,
                        json = jsonData
                    )
                    entries.add(entry)
                }
                if(entries.size != 1){
                Log.d(TAG, "Processed ${entries.size} results")}
                entries
            }
        } catch (e: Exception) {
            Log.e(TAG, "Query failed for word '$word' in dictionary '$langCode': ${e.message}", e)
            Log.d(TAG, "Attemped query for exact word match '$word': $sql")
            emptyList()
        }.also { db.close() }
    }

    /**
     * Parses JSON string into a list of strings, or null if parsing fails.
     */
    @Suppress("unused")
    private fun parseJsonList(json: String?): List<String>? {
        return json?.let {
            try {
                gson.fromJson(it, object : TypeToken<List<String>>() {}.type)
            } catch (e: Exception) {
                @Suppress("HardCodedStringLiteral")
                Log.w(TAG, "Failed to parse JSON list: $json", e)
                null
            }
        }
    }

    /**
     * Retrieves word suggestions based on prefix match.
     * The match is accent-insensitive, so e.g. "haufig" will still suggest "häufig".
     */
    fun getSuggestions(prefix: String, langCode: String, limit: Int): List<String> {
        if (!hasDictionaryForLanguage(langCode)) {
            return emptyList()
        }

        // Normalize the prefix: lowercase and strip diacritics (accents)
        val normalizedPrefix = prefix.trim().lowercase().removeDiacritics()
        if (normalizedPrefix.isEmpty()) {
            return emptyList()
        }

        val dbFilename = "dictionary_$langCode.db"
        val dictFilename = "dictionary_$langCode.zstdict"
        @Suppress("HardCodedStringLiteral") val fileInfo = FileInfo(
            id = "dictionary_$langCode",
            name = "Dictionary for $langCode",
            description = "",
            version = "",
            assets = listOf(
                Asset(filename = dbFilename, sizeBytes = 0L, checksumSha256 = ""),
                Asset(filename = dictFilename, sizeBytes = 0L, checksumSha256 = "")
            )
        )

        val db = databaseRepository.getDatabase(fileInfo) ?: return emptyList()

        // To support accent-insensitive matching, query a broader set of candidates
        // (all words starting with the same first base letter), then filter in Kotlin.
        val broadPrefix = normalizedPrefix[0].toString()
        val sql = "SELECT word FROM dictionary_fts WHERE word MATCH ? LIMIT ?"
        val matchQuery = "$broadPrefix*"

        return try {
            db.rawQuery(sql, arrayOf(matchQuery, (limit * 20).toString())).use { cursor ->
                val suggestions = mutableListOf<String>()
                val wordIndex = cursor.getColumnIndex("word")
                while (cursor.moveToNext()) {
                    val word = cursor.getString(wordIndex)
                    if (!word.isNullOrBlank()) {
                        suggestions.add(word)
                    }
                }

                suggestions
                    .distinct() // Remove duplicates
                    .filter { candidate ->
                        val candidateNorm = candidate.lowercase().removeDiacritics()
                        candidateNorm.startsWith(normalizedPrefix)
                    }
                    .take(limit)
            }
        } catch (e: Exception) {
            Log.e(TAG, "Suggestion query failed: ${e.message}", e)
            emptyList()
        }.also { db.close() }
    }

    /**
     * Parses translations JSON into a map of language code to list of translations, or null if parsing fails.
     */
    @Suppress("unused")
    private fun parseTranslationsJson(json: String?): Map<String, List<String>>? {
        return json?.let {
            try {
                gson.fromJson(it, object : TypeToken<Map<String, List<String>>>() {}.type)
            } catch (e: Exception) {
                @Suppress("HardCodedStringLiteral")
                Log.w(TAG, "Failed to parse translations JSON: $json", e)
                null
            }
        }
    }

    /**
     * Gets a cached decompression dictionary for the given language, creating it if necessary.
     */
    private fun getDecompressionDict(langCode: String): ByteArray? {
        // Return the cached dictionary if it's already loaded
        if (dictionaryCache.containsKey(langCode)) {
            return dictionaryCache[langCode]
        }

        return try {
            val dictFileName = "dictionary_${langCode}.zstdict"
            Log.d(TAG, "Loading compression dictionary: $dictFileName")

            // Read the dictionary file from filesDir (consistent with database location)
            val dictFile = File(context.filesDir, dictFileName)
            if (dictFile.exists()) {
                val dictBytes = dictFile.readBytes()
                // Cache the dictionary bytes for later use
                dictionaryCache[langCode] = dictBytes
                dictBytes
            } else {
                Log.e(TAG, "Zstd dictionary file does not exist: $dictFileName")
                null
            }
        } catch (e: IOException) {
            Log.e(TAG, "Failed to load Zstd dictionary file for lang '$langCode'", e)
            null
        }
    }
}

private fun String.removeDiacritics(): String {
    val normalized = Normalizer.normalize(this, Normalizer.Form.NFD)
    return normalized.replace("\\p{Mn}+".toRegex(), "")
}