package com.rosetta.messenger.crypto

import android.util.Base64
import org.bouncycastle.crypto.engines.ChaCha7539Engine
import org.bouncycastle.crypto.macs.Poly1305
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.math.BigInteger
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Security
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

/**
 * Шифрование сообщений как в React Native версии
 * XChaCha20-Poly1305 для текста + ECDH + AES для ключа
 */
object MessageCrypto {
    
    private const val CHACHA_KEY_SIZE = 32
    private const val XCHACHA_NONCE_SIZE = 24
    private const val POLY1305_TAG_SIZE = 16
    private const val SYNC_KEY_PREFIX = "sync:"

    // Кэш PBKDF2-SHA256 ключей: password → derived key bytes
    // PBKDF2 с 1000 итерациями ~50-100ms, кэш убирает повторные вычисления
    private val pbkdf2Cache = java.util.concurrent.ConcurrentHashMap<String, ByteArray>()
    
    init {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(BouncyCastleProvider())
        }
    }
    
    /**
     * Результат шифрования сообщения
     */
    data class EncryptedMessage(
        val ciphertext: String,   // Hex-encoded XChaCha20-Poly1305 ciphertext
        val key: String,          // Hex-encoded 32-byte key
        val nonce: String         // Hex-encoded 24-byte nonce
    )
    
    /**
     * Результат расшифровки входящего сообщения
     */
    data class DecryptedIncoming(
        val plaintext: String,          // Расшифрованный текст
        val plainKeyAndNonce: ByteArray // Raw key+nonce для расшифровки attachments
    )

    data class AttachmentDecryptDebugResult(
        val decrypted: String?,
        val trace: List<String>
    )

    private data class AttachmentDecryptAttemptResult(
        val decrypted: String?,
        val reason: String
    )
    
    /**
     * XChaCha20-Poly1305 шифрование (совместимо с @noble/ciphers в RN)
     * 
     * XChaCha20-Poly1305 использует 24-byte nonce:
     * - Первые 16 байт nonce используются в HChaCha20 для derive subkey
     * - Последние 8 байт nonce используются как counter для ChaCha20
     */
    fun encryptMessage(plaintext: String): EncryptedMessage {
        val secureRandom = SecureRandom()
        
        // Генерируем случайный ключ (32 байта) и nonce (24 байта для XChaCha20)
        val key = ByteArray(CHACHA_KEY_SIZE)
        val nonce = ByteArray(XCHACHA_NONCE_SIZE)
        secureRandom.nextBytes(key)
        secureRandom.nextBytes(nonce)
        
        val ciphertext = xchacha20Poly1305Encrypt(plaintext.toByteArray(Charsets.UTF_8), key, nonce)
        
        return EncryptedMessage(
            ciphertext = ciphertext.toHex(),
            key = key.toHex(),
            nonce = nonce.toHex()
        )
    }
    
    /**
     * XChaCha20-Poly1305 encrypt implementation
     */
    private fun xchacha20Poly1305Encrypt(plaintext: ByteArray, key: ByteArray, nonce: ByteArray): ByteArray {
        
        // Step 1: Derive subkey using HChaCha20
        val subkey = hchacha20(key, nonce.copyOfRange(0, 16))
        
        // Step 2: Create ChaCha20 nonce (4 zeros + last 8 bytes of original nonce)
        val chacha20Nonce = ByteArray(12)
        // First 4 bytes are 0
        System.arraycopy(nonce, 16, chacha20Nonce, 4, 8)
        
        // Step 3: Initialize ChaCha20 engine ONCE
        val engine = ChaCha7539Engine()
        engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
        
        // Step 4: Generate Poly1305 key from first 64 bytes of keystream (counter=0)
        val poly1305KeyBlock = ByteArray(64)
        engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0)
        
        // Step 5: Encrypt plaintext (engine continues from counter=1 automatically)
        val ciphertext = ByteArray(plaintext.size)
        engine.processBytes(plaintext, 0, plaintext.size, ciphertext, 0)
        
        
        // Step 6: Generate Poly1305 tag
        val mac = Poly1305()
        mac.init(KeyParameter(poly1305KeyBlock.copyOfRange(0, 32)))
        
        // No AAD in our case, just process ciphertext
        mac.update(ciphertext, 0, ciphertext.size)
        
        // Pad to 16 bytes
        val padding = (16 - (ciphertext.size % 16)) % 16
        if (padding > 0) {
            mac.update(ByteArray(padding), 0, padding)
        }
        
        // Length of AAD (0) as 64-bit LE
        mac.update(ByteArray(8), 0, 8)
        
        // Length of ciphertext as 64-bit LE
        val ctLen = longToLittleEndian(ciphertext.size.toLong())
        mac.update(ctLen, 0, 8)
        
        val tag = ByteArray(POLY1305_TAG_SIZE)
        mac.doFinal(tag, 0)
        
        // Return ciphertext + tag
        return ciphertext + tag
    }
    
    /**
     * HChaCha20 - derives a 256-bit subkey from a 256-bit key and 128-bit nonce
     */
    private fun hchacha20(key: ByteArray, nonce: ByteArray): ByteArray {
        val state = IntArray(16)
        
        // Constants "expand 32-byte k"
        state[0] = 0x61707865
        state[1] = 0x3320646e
        state[2] = 0x79622d32
        state[3] = 0x6b206574
        
        // Key
        for (i in 0..7) {
            state[4 + i] = littleEndianToInt(key, i * 4)
        }
        
        // Nonce
        for (i in 0..3) {
            state[12 + i] = littleEndianToInt(nonce, i * 4)
        }
        
        // 20 rounds
        for (i in 0 until 10) {
            // Column rounds
            quarterRound(state, 0, 4, 8, 12)
            quarterRound(state, 1, 5, 9, 13)
            quarterRound(state, 2, 6, 10, 14)
            quarterRound(state, 3, 7, 11, 15)
            // Diagonal rounds
            quarterRound(state, 0, 5, 10, 15)
            quarterRound(state, 1, 6, 11, 12)
            quarterRound(state, 2, 7, 8, 13)
            quarterRound(state, 3, 4, 9, 14)
        }
        
        // Output first 4 and last 4 words
        val subkey = ByteArray(32)
        intToLittleEndian(state[0], subkey, 0)
        intToLittleEndian(state[1], subkey, 4)
        intToLittleEndian(state[2], subkey, 8)
        intToLittleEndian(state[3], subkey, 12)
        intToLittleEndian(state[12], subkey, 16)
        intToLittleEndian(state[13], subkey, 20)
        intToLittleEndian(state[14], subkey, 24)
        intToLittleEndian(state[15], subkey, 28)
        
        return subkey
    }
    
    private fun quarterRound(state: IntArray, a: Int, b: Int, c: Int, d: Int) {
        state[a] += state[b]; state[d] = rotl(state[d] xor state[a], 16)
        state[c] += state[d]; state[b] = rotl(state[b] xor state[c], 12)
        state[a] += state[b]; state[d] = rotl(state[d] xor state[a], 8)
        state[c] += state[d]; state[b] = rotl(state[b] xor state[c], 7)
    }
    
    private fun rotl(v: Int, c: Int): Int = (v shl c) or (v ushr (32 - c))
    
    private fun littleEndianToInt(bs: ByteArray, off: Int): Int {
        return (bs[off].toInt() and 0xff) or
               ((bs[off + 1].toInt() and 0xff) shl 8) or
               ((bs[off + 2].toInt() and 0xff) shl 16) or
               ((bs[off + 3].toInt() and 0xff) shl 24)
    }
    
    private fun intToLittleEndian(n: Int, bs: ByteArray, off: Int) {
        bs[off] = n.toByte()
        bs[off + 1] = (n ushr 8).toByte()
        bs[off + 2] = (n ushr 16).toByte()
        bs[off + 3] = (n ushr 24).toByte()
    }
    
    private fun longToLittleEndian(n: Long): ByteArray {
        val bs = ByteArray(8)
        bs[0] = n.toByte()
        bs[1] = (n ushr 8).toByte()
        bs[2] = (n ushr 16).toByte()
        bs[3] = (n ushr 24).toByte()
        bs[4] = (n ushr 32).toByte()
        bs[5] = (n ushr 40).toByte()
        bs[6] = (n ushr 48).toByte()
        bs[7] = (n ushr 56).toByte()
        return bs
    }
    
    /**
     * Расшифровка текста сообщения (XChaCha20-Poly1305)
     */
    fun decryptMessage(ciphertext: String, keyHex: String, nonceHex: String): String {
        val key = keyHex.hexToBytes()
        val nonce = nonceHex.hexToBytes()
        val ciphertextWithTag = ciphertext.hexToBytes()
        
        val plaintext = xchacha20Poly1305Decrypt(ciphertextWithTag, key, nonce)
        return String(plaintext, Charsets.UTF_8)
    }
    
    /**
     * XChaCha20-Poly1305 decrypt implementation
     */
    private fun xchacha20Poly1305Decrypt(ciphertextWithTag: ByteArray, key: ByteArray, nonce: ByteArray): ByteArray {
        if (ciphertextWithTag.size < POLY1305_TAG_SIZE) {
            throw IllegalArgumentException("Ciphertext too short")
        }
        
        val ciphertext = ciphertextWithTag.copyOfRange(0, ciphertextWithTag.size - POLY1305_TAG_SIZE)
        val tag = ciphertextWithTag.copyOfRange(ciphertextWithTag.size - POLY1305_TAG_SIZE, ciphertextWithTag.size)
        
        // Step 1: Derive subkey using HChaCha20
        val subkey = hchacha20(key, nonce.copyOfRange(0, 16))
        
        // Step 2: Create ChaCha20 nonce
        val chacha20Nonce = ByteArray(12)
        System.arraycopy(nonce, 16, chacha20Nonce, 4, 8)
        
        // Step 3: Initialize ChaCha20 engine ONCE
        val engine = ChaCha7539Engine()
        engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
        
        // Step 4: Generate Poly1305 key from first 64 bytes of keystream (counter=0)
        val poly1305KeyBlock = ByteArray(64)
        engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0)
        
        // Step 5: Verify Poly1305 tag
        val mac = Poly1305()
        mac.init(KeyParameter(poly1305KeyBlock.copyOfRange(0, 32)))
        mac.update(ciphertext, 0, ciphertext.size)
        
        val padding = (16 - (ciphertext.size % 16)) % 16
        if (padding > 0) {
            mac.update(ByteArray(padding), 0, padding)
        }
        
        mac.update(ByteArray(8), 0, 8)
        val ctLen = longToLittleEndian(ciphertext.size.toLong())
        mac.update(ctLen, 0, 8)
        
        val computedTag = ByteArray(POLY1305_TAG_SIZE)
        mac.doFinal(computedTag, 0)
        
        if (!tag.contentEquals(computedTag)) {
            throw SecurityException("Authentication failed")
        }
        
        // Step 6: Decrypt with ChaCha20 (engine continues from where it left off)
        // Note: We already consumed 64 bytes for Poly1305 key, now decrypt ciphertext
        // BUT: We need a fresh engine for decryption OR reset it
        // Actually, for decryption we need to re-init the engine
        val decryptEngine = ChaCha7539Engine()
        decryptEngine.init(false, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
        
        // Skip first 64 bytes (Poly1305 key block)
        val skipBlock = ByteArray(64)
        decryptEngine.processBytes(ByteArray(64), 0, 64, skipBlock, 0)
        
        val plaintext = ByteArray(ciphertext.size)
        decryptEngine.processBytes(ciphertext, 0, ciphertext.size, plaintext, 0)
        
        return plaintext
    }
    
    /**
     * ECDH шифрование ключа для получателя
     * Использует secp256k1 + AES как в RN версии
     * Формат: Base64(iv:ciphertext:ephemeralPrivateKeyHex)
     * 
     * JS эквивалент:
     * const key = Buffer.concat([keyBytes, nonceBytes]);
     * const encryptedKey = await encrypt(key.toString('binary'), publicKey);
     * 
     * КРИТИЧНО: ephemeralKey.getPrivate('hex') в JS может быть БЕЗ ведущих нулей!
     */
    fun encryptKeyForRecipient(keyAndNonce: ByteArray, recipientPublicKeyHex: String): String {
        
        val secureRandom = SecureRandom()
        val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
        
        // Генерируем эфемерный приватный ключ (32 байта)
        val ephemeralPrivateKeyBytes = ByteArray(32)
        secureRandom.nextBytes(ephemeralPrivateKeyBytes)
        val ephemeralPrivateKey = BigInteger(1, ephemeralPrivateKeyBytes)
        
        // ⚠️ КРИТИЧНО: JS elliptic.js может вернуть hex БЕЗ ведущих нулей!
        // ephemeralKey.getPrivate('hex') - это BigInteger.toString(16)
        // Не добавляет ведущие нули если первый байт < 0x10
        val ephemeralPrivateKeyHex = ephemeralPrivateKey.toString(16).let {
            // Но если нечётная длина, добавим ведущий 0 для правильного парсинга
            if (it.length % 2 != 0) {
                "0$it"
            } else it
        }
        
        
        // Получаем эфемерный публичный ключ
        val ephemeralPublicKey = ecSpec.g.multiply(ephemeralPrivateKey)
        val ephemeralPublicKeyHex = ephemeralPublicKey.getEncoded(false).toHex()
        
        // Парсим публичный ключ получателя
        val recipientPublicKeyBytes = recipientPublicKeyHex.hexToBytes()
        val recipientPublicKey = ecSpec.curve.decodePoint(recipientPublicKeyBytes)
        
        // ECDH: ephemeralPrivate * recipientPublic = sharedSecret
        val sharedPoint = recipientPublicKey.multiply(ephemeralPrivateKey)
        
        // ⚠️ КРИТИЧНО: Эмулируем JS поведение!
        // JS: BN.toString(16) НЕ добавляет ведущие нули
        // crypto.enc.Hex.parse(hex) парсит как есть
        // Если X coordinate = 0x00abc..., JS получит "abc..." (меньше 32 байт)
        val xCoordBigInt = sharedPoint.normalize().xCoord.toBigInteger()
        var sharedSecretHex = xCoordBigInt.toString(16)
        
        // JS: если hex нечётной длины, crypto.enc.Hex.parse добавит ведущий 0
        if (sharedSecretHex.length % 2 != 0) {
            sharedSecretHex = "0$sharedSecretHex"
        }
        val sharedSecret = sharedSecretHex.hexToBytes()
        
        // Генерируем IV для AES (16 байт)
        val iv = ByteArray(16)
        secureRandom.nextBytes(iv)
        val ivHex = iv.toHex()
        
        // ⚠️ КРИТИЧНО: Эмулируем поведение React Native + crypto-js!
        // React Native: Buffer.toString('binary') → строка с символами (включая > 127)
        // crypto-js: AES.encrypt(string, ...) → кодирует строку в UTF-8 перед шифрованием
        // Итого: байты > 127 превращаются в многобайтовые UTF-8 последовательности
        
        // Шаг 1: Байты → Latin1 строка (как Buffer.toString('binary'))
        val latin1String = String(keyAndNonce, Charsets.ISO_8859_1)
        
        // Шаг 2: Latin1 строка → UTF-8 байты (как crypto-js делает внутри)
        val utf8Bytes = latin1String.toByteArray(Charsets.UTF_8)
        
        // AES шифрование
        
        val aesKey = SecretKeySpec(sharedSecret, "AES")
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(Cipher.ENCRYPT_MODE, aesKey, IvParameterSpec(iv))
        val encryptedKey = cipher.doFinal(utf8Bytes)
        val encryptedKeyHex = encryptedKey.toHex()
        
        
        // Формат как в RN: btoa(ivHex:encryptedHex:ephemeralPrivateHex)
        val combined = "$ivHex:$encryptedKeyHex:$ephemeralPrivateKeyHex"
        
        val result = Base64.encodeToString(combined.toByteArray(), Base64.NO_WRAP)
        return result
    }
    
    /**
     * ECDH расшифровка ключа
     * Формат: Base64(ivHex:encryptedHex:ephemeralPrivateHex)
     * 
     * КРИТИЧНО: ephemeralPrivateKeyHex может иметь нечётную длину!
     */
    fun decryptKeyFromSender(encryptedKeyBase64: String, myPrivateKeyHex: String): ByteArray {
        val candidates = decryptKeyFromSenderCandidates(encryptedKeyBase64, myPrivateKeyHex)
        return candidates.firstOrNull()
            ?: throw IllegalArgumentException("Failed to decrypt key: no valid candidates")
    }

    fun decryptKeyFromSenderCandidates(
        encryptedKeyBase64: String,
        myPrivateKeyHex: String
    ): List<ByteArray> {
        if (encryptedKeyBase64.startsWith(SYNC_KEY_PREFIX)) {
            val aesChachaKey = encryptedKeyBase64.removePrefix(SYNC_KEY_PREFIX)
            if (aesChachaKey.isBlank()) {
                throw IllegalArgumentException("Invalid sync key format: empty aesChachaKey")
            }
            val decoded =
                CryptoManager.decryptWithPassword(aesChachaKey, myPrivateKeyHex)
                    ?: throw IllegalArgumentException("Failed to decrypt sync chacha key")
            return listOf(decoded.toByteArray(Charsets.ISO_8859_1))
        }
        
        val combined = String(Base64.decode(encryptedKeyBase64, Base64.NO_WRAP))
        
        val parts = combined.split(":")
        if (parts.size != 3) {
            throw IllegalArgumentException("Invalid encrypted key format: expected 3 parts, got ${parts.size}")
        }
        
        val ivHex = parts[0]
        val encryptedKeyHex = parts[1]
        var ephemeralPrivateKeyHex = parts[2]
        
        
        // ⚠️ КРИТИЧНО: JS toString(16) может вернуть hex нечётной длины!
        // Добавляем ведущий 0 если нужно для правильного парсинга
        if (ephemeralPrivateKeyHex.length % 2 != 0) {
            ephemeralPrivateKeyHex = "0$ephemeralPrivateKeyHex"
        }
        
        val iv = ivHex.hexToBytes()
        val encryptedKey = encryptedKeyHex.hexToBytes()
        
        val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
        
        // Парсим эфемерный приватный ключ
        val ephemeralPrivateKey = BigInteger(ephemeralPrivateKeyHex, 16)
        val ephemeralPublicKey = ecSpec.g.multiply(ephemeralPrivateKey)
        val ephemeralPublicKeyHex = ephemeralPublicKey.getEncoded(false).toHex()
        
        // Парсим мой приватный ключ
        val myPrivateKey = BigInteger(myPrivateKeyHex, 16)
        val myPublicKey = ecSpec.g.multiply(myPrivateKey)
        val myPublicKeyHex = myPublicKey.getEncoded(false).toHex()
        
        // ECDH: ephemeralPrivate * myPublic = sharedSecret
        val sharedPoint = myPublicKey.multiply(ephemeralPrivateKey)

        // Desktop parity: noble/secp256k1 getSharedSecret(...).slice(1, 33)
        // => ровно 32 байта X-координаты С сохранением ведущих нулей.
        // Для обратной совместимости пробуем и старый Android-вариант (trim leading zeros).
        val xCoordBigInt = sharedPoint.normalize().xCoord.toBigInteger()
        val sharedSecretExact = bigIntegerToFixed32Bytes(xCoordBigInt)

        // Legacy (старый Android): BN.toString(16) + hex parse (теряет ведущие нули)
        var sharedSecretHexLegacy = xCoordBigInt.toString(16)
        if (sharedSecretHexLegacy.length % 2 != 0) {
            sharedSecretHexLegacy = "0$sharedSecretHexLegacy"
        }
        val sharedSecretLegacy = sharedSecretHexLegacy.hexToBytes()

        val candidateMap = LinkedHashMap<String, ByteArray>()
        val decryptedVariants = listOf(
            decryptKeyAesPayload(encryptedKey, iv, sharedSecretExact),
            decryptKeyAesPayload(encryptedKey, iv, sharedSecretLegacy)
        )

        for (decryptedUtf8Bytes in decryptedVariants) {
            if (decryptedUtf8Bytes == null) continue
            // ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
            // Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
            // Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
            val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
            val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)

            // В протоколе это key(32)+nonce(24). Фильтруем мусор после ложнопозитивной PKCS5 padding.
            if (originalBytes.size < 56) continue

            val fp = shortSha256(originalBytes)
            candidateMap.putIfAbsent(fp, originalBytes)
        }

        return candidateMap.values.toList()
    }

    private fun decryptKeyAesPayload(
        encryptedKey: ByteArray,
        iv: ByteArray,
        sharedSecret: ByteArray
    ): ByteArray? {
        return try {
            val aesKey = SecretKeySpec(sharedSecret, "AES")
            val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
            cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
            cipher.doFinal(encryptedKey)
        } catch (_: Exception) {
            null
        }
    }

    private fun bigIntegerToFixed32Bytes(value: BigInteger): ByteArray {
        val raw = value.toByteArray()
        return when {
            raw.size == 32 -> raw
            raw.size > 32 -> raw.copyOfRange(raw.size - 32, raw.size)
            else -> ByteArray(32 - raw.size) + raw
        }
    }
    
    /**
     * Результат шифрования для отправки
     */
    data class EncryptedForSending(
        val ciphertext: String,    // Hex-encoded encrypted message
        val encryptedKey: String,  // ECDH+AES encrypted key (for recipient)
        val plainKeyAndNonce: ByteArray  // Raw key+nonce for encrypting attachments
    )
    
    /**
     * Полное шифрование сообщения для отправки
     */
    fun encryptForSending(plaintext: String, recipientPublicKey: String): EncryptedForSending {
        
        // 1. Шифруем текст
        val encrypted = encryptMessage(plaintext)
        
        // 2. Собираем key + nonce
        val keyAndNonce = encrypted.key.hexToBytes() + encrypted.nonce.hexToBytes()
        
        // 3. Шифруем ключ для получателя
        val encryptedKey = encryptKeyForRecipient(keyAndNonce, recipientPublicKey)
        
        
        return EncryptedForSending(encrypted.ciphertext, encryptedKey, keyAndNonce)
    }
    
    /**
     * Полная расшифровка входящего сообщения
     * Возвращает текст и plainKeyAndNonce для расшифровки attachments
     */
    fun decryptIncomingFull(
        ciphertext: String,
        encryptedKey: String,
        myPrivateKey: String
    ): DecryptedIncoming {
        val keyCandidates = decryptKeyFromSenderCandidates(encryptedKey, myPrivateKey)
        if (keyCandidates.isEmpty()) {
            throw IllegalArgumentException("Failed to decrypt message key: no candidates")
        }

        var lastError: Exception? = null
        for (keyAndNonce in keyCandidates) {
            if (keyAndNonce.size < 56) continue

            val key = keyAndNonce.copyOfRange(0, 32)
            val nonce = keyAndNonce.copyOfRange(32, 56)

            try {
                val plaintext = decryptMessage(ciphertext, key.toHex(), nonce.toHex())
                return DecryptedIncoming(plaintext, keyAndNonce)
            } catch (e: Exception) {
                lastError = e
            }
        }

        throw (lastError ?: IllegalArgumentException("Failed to decrypt message content with all key candidates"))
    }
    
    /**
     * Совместимая версия decryptIncoming (возвращает только текст)
     */
    fun decryptIncoming(
        ciphertext: String,
        encryptedKey: String,
        myPrivateKey: String
    ): String = decryptIncomingFull(ciphertext, encryptedKey, myPrivateKey).plaintext

    fun decryptIncomingFullWithPlainKey(
        ciphertext: String,
        plainKeyAndNonce: ByteArray
    ): DecryptedIncoming {
        require(plainKeyAndNonce.size >= 56) { "Invalid plainKeyAndNonce size: ${plainKeyAndNonce.size}" }

        val key = plainKeyAndNonce.copyOfRange(0, 32)
        val nonce = plainKeyAndNonce.copyOfRange(32, 56)
        val plaintext = decryptMessage(ciphertext, key.toHex(), nonce.toHex())
        return DecryptedIncoming(plaintext, plainKeyAndNonce)
    }

    fun decryptIncomingWithPlainKey(
        ciphertext: String,
        plainKeyAndNonce: ByteArray
    ): String = decryptIncomingFullWithPlainKey(ciphertext, plainKeyAndNonce).plaintext
    
    /**
     * Расшифровка MESSAGES attachment blob
     * Формат: ivBase64:ciphertextBase64
     * Использует PBKDF2 + AES-256-CBC + zlib decompression
     * 
     * КРИТИЧНО: Desktop использует ВЕСЬ keyAndNonce (56 bytes) как password!
     * Desktop: chachaDecryptedKey.toString('utf-8') - конвертирует все 56 байт в UTF-8 строку
     * 
     * @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
     * @param chachaKeyPlain Уже расшифрованный ChaCha ключ+nonce (56 bytes: 32 key + 24 nonce)
     */
    /**
     * Расшифровка аттачмента зашифрованного через encodeWithPassword (desktop parity)
     * 
     * Десктоп: decodeWithPassword(keyPlain, data)
     *   1. keyPlain = chachaDecryptedKey.toString('utf-8') — JS Buffer → UTF-8 string
     *   2. PBKDF2(keyPlain, 'rosetta', {keySize: 256/32, iterations: 1000}) — SHA256
     *   3. AES-CBC decrypt
     *   4. pako.inflate → string
     * 
     * Ровно то же самое делаем здесь.
     */
    fun decryptAttachmentBlobWithPlainKey(
        encryptedData: String,
        chachaKeyPlain: ByteArray
    ): String? {
        return decryptAttachmentBlobWithPlainKeyDebug(encryptedData, chachaKeyPlain).decrypted
    }

    fun decryptAttachmentBlobWithPlainKeyDebug(
        encryptedData: String,
        chachaKeyPlain: ByteArray
    ): AttachmentDecryptDebugResult {
        val trace = mutableListOf<String>()
        return try {
            trace += "payload=${describeAttachmentPayload(encryptedData)}"
            trace += "key=size${chachaKeyPlain.size},fp=${shortSha256(chachaKeyPlain)}"

            val candidates = buildAttachmentPasswordCandidates(chachaKeyPlain)
            trace += "candidates=${candidates.size}"

            for ((index, password) in candidates.withIndex()) {
                val passwordBytesUtf8 = password.toByteArray(Charsets.UTF_8)
                trace +=
                    "cand[$index]=chars${password.length},utf8${passwordBytesUtf8.size},fp=${shortSha256(passwordBytesUtf8)}"

                val pbkdf2KeySha256 = generatePBKDF2Key(password)
                trace += "cand[$index]=pbkdf2-sha256:${shortSha256(pbkdf2KeySha256)}"

                val sha256Attempt = decryptWithPBKDF2KeyDebug(encryptedData, pbkdf2KeySha256)
                if (sha256Attempt.decrypted != null) {
                    trace += "cand[$index]=SUCCESS:sha256,len${sha256Attempt.decrypted.length}"
                    return AttachmentDecryptDebugResult(sha256Attempt.decrypted, trace)
                }
                trace += "cand[$index]=fail:sha256:${sha256Attempt.reason}"

                val pbkdf2KeySha1 = generatePBKDF2KeySha1FromBytes(passwordBytesUtf8)
                trace += "cand[$index]=pbkdf2-sha1:${shortSha256(pbkdf2KeySha1)}"
                val sha1Attempt = decryptWithPBKDF2KeyDebug(encryptedData, pbkdf2KeySha1)
                if (sha1Attempt.decrypted != null) {
                    trace += "cand[$index]=SUCCESS:sha1,len${sha1Attempt.decrypted.length}"
                    return AttachmentDecryptDebugResult(sha1Attempt.decrypted, trace)
                }
                trace += "cand[$index]=fail:sha1:${sha1Attempt.reason}"
            }

            trace += "result=failed_all_candidates"
            AttachmentDecryptDebugResult(null, trace)
        } catch (e: Exception) {
            trace += "exception=${e.javaClass.simpleName}:${e.message?.take(120)}"
            AttachmentDecryptDebugResult(null, trace)
        } catch (_: OutOfMemoryError) {
            System.gc()
            trace += "exception=OutOfMemoryError"
            AttachmentDecryptDebugResult(null, trace)
        }
    }

    /**
     * Streaming decrypt: зашифрованный файл → расшифрованный файл на диске.
     * Не загружает весь контент в память — использует потоковый пайплайн:
     *   File → Base64Decode → AES-CBC → Inflate → Base64Decode → outputFile
     * Пиковое потребление памяти: ~128KB вместо ~200MB для 30МБ файла.
     *
     * Поддерживает форматы:
     *   - ivBase64:ciphertextBase64 (обычный)
     *   - CHNK:iv1:ct1::iv2:ct2::... (чанкованный, Desktop >10MB)
     *
     * @param inputFile  temp file с зашифрованным контентом (с CDN)
     * @param chachaKeyPlain  расшифрованный ChaCha ключ (56 bytes)
     * @param outputFile  куда записать результат (raw bytes файла)
     * @return true если успешно
     */
    fun decryptAttachmentFileStreaming(
        inputFile: java.io.File,
        chachaKeyPlain: ByteArray,
        outputFile: java.io.File
    ): Boolean {
        return try {
            val passwordCandidates = buildAttachmentPasswordCandidates(chachaKeyPlain)
            if (passwordCandidates.isEmpty()) return false

            // Проверяем формат: CHNK: или обычный ivBase64:ciphertextBase64
            val header = ByteArray(5)
            var headerLen = 0
            java.io.FileInputStream(inputFile).use { fis ->
                while (headerLen < 5) {
                    val n = fis.read(header, headerLen, 5 - headerLen)
                    if (n == -1) break
                    headerLen += n
                }
            }
            val isChunked = headerLen == 5 && String(header, Charsets.US_ASCII) == "CHNK:"

            for (password in passwordCandidates) {
                val passwordBytesUtf8 = password.toByteArray(Charsets.UTF_8)
                val pbkdf2Sha256 = generatePBKDF2KeyFromBytes(passwordBytesUtf8)
                val pbkdf2Sha1 = generatePBKDF2KeySha1FromBytes(passwordBytesUtf8)
                val pbkdf2Candidates = linkedSetOf(pbkdf2Sha256, pbkdf2Sha1)

                for (pbkdf2Key in pbkdf2Candidates) {
                    outputFile.delete()
                    val ok =
                        if (isChunked) {
                            decryptChunkedFileStreaming(inputFile, pbkdf2Key, outputFile)
                        } else {
                            decryptSingleFileStreaming(inputFile, pbkdf2Key, outputFile)
                        }
                    if (ok) {
                        return true
                    }
                }
            }
            false
        } catch (e: Exception) {
            android.util.Log.e("MessageCrypto", "Streaming decrypt failed", e)
            outputFile.delete()
            false
        } catch (_: OutOfMemoryError) {
            System.gc()
            outputFile.delete()
            false
        }
    }

    /**
     * Streaming decrypt обычного формата: ivBase64:ciphertextBase64
     * Пайплайн: File → Base64Decode → AES-CBC → Inflate → strip data URL → Base64Decode → file
     */
    private fun decryptSingleFileStreaming(
        inputFile: java.io.File,
        pbkdf2Key: ByteArray,
        outputFile: java.io.File
    ): Boolean {
        // 1. Считываем IV (всё до первого ':')
        var colonOffset = 0L
        val ivBuf = java.io.ByteArrayOutputStream(64)
        java.io.FileInputStream(inputFile).use { fis ->
            while (true) {
                val b = fis.read()
                if (b == -1) return false
                colonOffset++
                if (b.toChar() == ':') break
                ivBuf.write(b)
            }
        }
        val iv = Base64.decode(ivBuf.toByteArray(), Base64.DEFAULT)

        // 2. AES-256-CBC cipher
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(
            Cipher.DECRYPT_MODE,
            SecretKeySpec(pbkdf2Key, "AES"),
            IvParameterSpec(iv)
        )

        // 3. Streaming pipeline: File[после ':'] → Base64 → AES → Inflate
        val fis = java.io.FileInputStream(inputFile)
        // Skip IV and ':'
        var skipped = 0L
        while (skipped < colonOffset) {
            val s = fis.skip(colonOffset - skipped)
            if (s <= 0) { fis.read(); skipped++ }
            else skipped += s
        }

        val pipeline = java.util.zip.InflaterInputStream(
            javax.crypto.CipherInputStream(
                android.util.Base64InputStream(fis, Base64.DEFAULT),
                cipher
            )
        )

        pipeline.use { inflated ->
            // 4. Результат — base64 текст файла, возможно с data URL prefix
            // Читаем первый кусок чтобы определить и пропустить prefix
            val headerBuf = ByteArray(256)
            var headerReadLen = 0
            while (headerReadLen < headerBuf.size) {
                val n = inflated.read(headerBuf, headerReadLen, headerBuf.size - headerReadLen)
                if (n == -1) break
                headerReadLen += n
            }
            if (headerReadLen == 0) return false

            // Ищем "base64," чтобы пропустить data URL prefix
            val headerStr = String(headerBuf, 0, headerReadLen, Charsets.ISO_8859_1)
            val marker = "base64,"
            val markerIdx = headerStr.indexOf(marker)
            val skip = if (markerIdx in 0..199) markerIdx + marker.length else 0

            // 5. Объединяем остаток header + остаток inflated stream → Base64 decode → файл
            val remaining = java.io.ByteArrayInputStream(headerBuf, skip, headerReadLen - skip)
            val bodyStream = java.io.SequenceInputStream(remaining, inflated)

            android.util.Base64InputStream(bodyStream, Base64.DEFAULT).use { decoded ->
                outputFile.outputStream().use { out ->
                    val buf = ByteArray(64 * 1024)
                    while (true) {
                        val n = decoded.read(buf)
                        if (n == -1) break
                        out.write(buf, 0, n)
                    }
                }
            }
        }

        return outputFile.length() > 0
    }

    /**
     * Streaming decrypt CHNK формата (Desktop >10MB).
     * Формат: CHNK:iv1Base64:ct1Base64::iv2Base64:ct2Base64::...
     * Каждый чанк — отдельный AES-CBC шифротекст (до 10MB compressed).
     * Все чанки после расшифровки + конкатенации → inflate → base64 файла.
     */
    private fun decryptChunkedFileStreaming(
        inputFile: java.io.File,
        pbkdf2Key: ByteArray,
        outputFile: java.io.File
    ): Boolean {
        // Для CHNK: читаем весь файл, разбиваем на чанки, расшифровываем каждый,
        // конкатенируем compressed bytes → inflate → strip data URL → base64 decode → file
        //
        // Оптимизация: обрабатываем чанки по одному, записываем decrypted bytes
        // во временный файл, потом inflate оттуда.

        val cacheDir = inputFile.parentFile ?: return false
        val decryptedTmp = java.io.File(cacheDir, "chnk_dec_${System.currentTimeMillis()}.tmp")

        try {
            // Читаем содержимое файла после "CHNK:" и разбиваем на чанки по "::"
            val content = inputFile.readText(Charsets.UTF_8)
            val chunksStr = content.removePrefix("CHNK:")
            val chunks = chunksStr.split("::")

            // Расшифровываем каждый чанк и записываем compressed bytes в tmp файл
            decryptedTmp.outputStream().use { tmpOut ->
                for (chunk in chunks) {
                    if (chunk.isBlank()) continue
                    val parts = chunk.split(":")
                    if (parts.size != 2) continue

                    val chunkIv = Base64.decode(parts[0], Base64.DEFAULT)
                    val chunkCt = Base64.decode(parts[1], Base64.DEFAULT)

                    val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
                    cipher.init(
                        Cipher.DECRYPT_MODE,
                        SecretKeySpec(pbkdf2Key, "AES"),
                        IvParameterSpec(chunkIv)
                    )
                    val decrypted = cipher.doFinal(chunkCt)
                    tmpOut.write(decrypted)
                }
            }

            // Inflate compressed concatenated data → base64 текст файла
            java.util.zip.InflaterInputStream(
                java.io.FileInputStream(decryptedTmp)
            ).use { inflated ->
                // Читаем header для data URL prefix
                val headerBuf = ByteArray(256)
                var headerReadLen = 0
                while (headerReadLen < headerBuf.size) {
                    val n = inflated.read(headerBuf, headerReadLen, headerBuf.size - headerReadLen)
                    if (n == -1) break
                    headerReadLen += n
                }
                if (headerReadLen == 0) return false

                val headerStr = String(headerBuf, 0, headerReadLen, Charsets.ISO_8859_1)
                val marker = "base64,"
                val markerIdx = headerStr.indexOf(marker)
                val skip = if (markerIdx in 0..199) markerIdx + marker.length else 0

                val remaining = java.io.ByteArrayInputStream(headerBuf, skip, headerReadLen - skip)
                val bodyStream = java.io.SequenceInputStream(remaining, inflated)

                android.util.Base64InputStream(bodyStream, Base64.DEFAULT).use { decoded ->
                    outputFile.outputStream().use { out ->
                        val buf = ByteArray(64 * 1024)
                        while (true) {
                            val n = decoded.read(buf)
                            if (n == -1) break
                            out.write(buf, 0, n)
                        }
                    }
                }
            }

            return outputFile.length() > 0
        } finally {
            decryptedTmp.delete()
        }
    }
    
    /**
     * Расшифровка attachment blob с уже готовым паролем (Latin1 string)
     * Используется когда chachaKey сохранён в БД как Latin1 string (raw bytes)
     * 
     * КРИТИЧНО: Desktop делает Buffer.from(str, 'binary').toString('utf-8')
     * Это эквивалентно взятию charCode каждого символа (0-255) как байт,
     * потом UTF-8 decode этих байтов с заменой невалидных на U+FFFD (�)
     * 
     * @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
     * @param passwordLatin1 Latin1 string (56 chars, каждый char = один байт 0-255)
     */
    fun decryptAttachmentBlobWithPassword(
        encryptedData: String,
        passwordLatin1: String
    ): String? {
        return try {

            // Конвертируем Latin1 string → bytes → UTF-8 string (эмулируем Desktop)
            // Desktop: Buffer.from(str, 'binary').toString('utf-8')
            val passwordBytes = passwordLatin1.toByteArray(Charsets.ISO_8859_1)
            val passwordCandidates = linkedSetOf(
                bytesToBufferPolyfillUtf8String(passwordBytes),
                bytesToJsUtf8String(passwordBytes)
            )

            for (passwordUtf8 in passwordCandidates) {
                // Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations)
                val pbkdf2Key = generatePBKDF2Key(passwordUtf8)
                decryptWithPBKDF2Key(encryptedData, pbkdf2Key)?.let { return it }

                // Legacy fallback (sha1)
                val pbkdf2KeySha1 = generatePBKDF2KeySha1FromBytes(passwordUtf8.toByteArray(Charsets.UTF_8))
                decryptWithPBKDF2Key(encryptedData, pbkdf2KeySha1)?.let { return it }
            }
            null
        } catch (e: Exception) {
            null
        } catch (_: OutOfMemoryError) {
            System.gc()
            null
        }
    }
    
    /**
     * Расшифровка MESSAGES attachment blob (Legacy - с RSA расшифровкой ключа)
     * Формат: ivBase64:ciphertextBase64
     * Использует PBKDF2 + AES-256-CBC + zlib decompression
     */
    fun decryptAttachmentBlob(
        encryptedData: String,
        encryptedKey: String,
        myPrivateKey: String
    ): String? {
        return try {
            val keyCandidates = decryptKeyFromSenderCandidates(encryptedKey, myPrivateKey)
            for (keyAndNonce in keyCandidates) {
                decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)?.let { return it }
            }
            null
        } catch (e: Exception) {
            null
        } catch (_: OutOfMemoryError) {
            System.gc()
            null
        }
    }
    
    /**
     * Генерация PBKDF2 ключа (совместимо с crypto-js)
     * ВАЖНО: crypto-js использует PBKDF2 с SHA256 по умолчанию (НЕ SHA1!)
     * 
     * КРИТИЧНО: crypto-js конвертирует password через UTF-8 encoding,
     * но PBEKeySpec в Java использует UTF-16! Поэтому используем ручную реализацию.
     */
    private fun generatePBKDF2Key(password: String, salt: String = "rosetta", iterations: Int = 1000): ByteArray {
        // Кэшируем только для дефолтных salt/iterations (99% вызовов)
        if (salt == "rosetta" && iterations == 1000) {
            return pbkdf2Cache.getOrPut(password) {
                val passwordBytes = password.toByteArray(Charsets.UTF_8)
                generatePBKDF2KeyFromBytes(passwordBytes, salt.toByteArray(Charsets.UTF_8), iterations)
            }
        }
        // Crypto-js: WordArray.create(password) использует UTF-8
        val passwordBytes = password.toByteArray(Charsets.UTF_8)
        return generatePBKDF2KeyFromBytes(passwordBytes, salt.toByteArray(Charsets.UTF_8), iterations)
    }
    
    /**
     * Генерация PBKDF2 ключа из raw bytes (без string conversion)
     */
    private fun generatePBKDF2KeyFromBytes(passwordBytes: ByteArray, saltBytes: ByteArray = "rosetta".toByteArray(Charsets.UTF_8), iterations: Int = 1000): ByteArray {
        // PBKDF2-HMAC-SHA256 ручная реализация для совместимости с crypto-js
        // ВАЖНО: crypto-js PBKDF2 по умолчанию использует SHA256, НЕ SHA1!
        return generatePBKDF2KeyFromBytesWithHmac(
            passwordBytes = passwordBytes,
            saltBytes = saltBytes,
            iterations = iterations,
            hmacAlgo = "HmacSHA256"
        )
    }

    /**
     * Генерация PBKDF2-SHA1 ключа из raw bytes.
     * Нужна как fallback для legacy сообщений.
     */
    private fun generatePBKDF2KeySha1FromBytes(
        passwordBytes: ByteArray,
        saltBytes: ByteArray = "rosetta".toByteArray(Charsets.UTF_8),
        iterations: Int = 1000
    ): ByteArray {
        return generatePBKDF2KeyFromBytesWithHmac(
            passwordBytes = passwordBytes,
            saltBytes = saltBytes,
            iterations = iterations,
            hmacAlgo = "HmacSHA1"
        )
    }

    private fun generatePBKDF2KeyFromBytesWithHmac(
        passwordBytes: ByteArray,
        saltBytes: ByteArray,
        iterations: Int,
        hmacAlgo: String
    ): ByteArray {
        val keyLength = 32 // 256 bits
        val mac = javax.crypto.Mac.getInstance(hmacAlgo)
        val keySpec = javax.crypto.spec.SecretKeySpec(passwordBytes, hmacAlgo)
        mac.init(keySpec)
        
        // PBKDF2 алгоритм
        val hLen = mac.macLength
        val dkLen = keyLength
        val l = (dkLen + hLen - 1) / hLen
        val r = dkLen - (l - 1) * hLen
        
        val derivedKey = ByteArray(dkLen)
        var offset = 0
        
        for (i in 1..l) {
            val block = pbkdf2Block(mac, saltBytes, iterations, i)
            val copyLen = if (i < l) hLen else r
            System.arraycopy(block, 0, derivedKey, offset, copyLen)
            offset += copyLen
        }
        
        return derivedKey
    }
    
    /**
     * Генерация PBKDF2 ключа через стандартный Java SecretKeyFactory
     * Это работает по-другому - использует char[] и UTF-16 encoding!
     * Используем SHA256 для совместимости с crypto-js
     */
    private fun generatePBKDF2KeyJava(password: String): ByteArray {
        val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
        val spec = javax.crypto.spec.PBEKeySpec(
            password.toCharArray(),
            "rosetta".toByteArray(Charsets.UTF_8),
            1000,
            256
        )
        return factory.generateSecret(spec).encoded
    }
    
    /**
     * PBKDF2 block функция (для совместимости с crypto-js)
     */
    private fun pbkdf2Block(mac: javax.crypto.Mac, salt: ByteArray, iterations: Int, blockIndex: Int): ByteArray {
        // U1 = PRF(Password, Salt || INT_32_BE(i))
        mac.reset()
        mac.update(salt)
        mac.update(byteArrayOf(
            (blockIndex shr 24).toByte(),
            (blockIndex shr 16).toByte(),
            (blockIndex shr 8).toByte(),
            blockIndex.toByte()
        ))
        var u = mac.doFinal()
        val result = u.clone()
        
        // U2 = PRF(Password, U1), ... , Uc = PRF(Password, Uc-1)
        for (j in 2..iterations) {
            mac.reset()
            u = mac.doFinal(u)
            for (k in result.indices) {
                result[k] = (result[k].toInt() xor u[k].toInt()).toByte()
            }
        }
        
        return result
    }
    
    /**
     * Расшифровка с PBKDF2 ключом (AES-256-CBC + zlib)
     * Формат: ivBase64:ciphertextBase64
     */
    private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
        return decryptWithPBKDF2KeyDebug(encryptedData, pbkdf2Key).decrypted
    }

    private fun decryptWithPBKDF2KeyDebug(
        encryptedData: String,
        pbkdf2Key: ByteArray
    ): AttachmentDecryptAttemptResult {
        return try {
            if (encryptedData.startsWith("CHNK:")) {
                val chunked = decryptChunkedWithPBKDF2KeyDebug(encryptedData, pbkdf2Key)
                return AttachmentDecryptAttemptResult(chunked, if (chunked != null) "ok:chunked" else "chunked:failed")
            }

            val parts = encryptedData.split(":", limit = 2)
            if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank()) {
                // Legacy desktop format: base64(ivHex:cipherHex)
                val old = decryptOldFormatWithPBKDF2KeyDebug(encryptedData, pbkdf2Key)
                return AttachmentDecryptAttemptResult(old.decrypted, "legacy:${old.reason}")
            }

            val iv = decodeBase64Compat(parts[0])
            val ciphertext = decodeBase64Compat(parts[1])
            if (iv == null || ciphertext == null) {
                val old = decryptOldFormatWithPBKDF2KeyDebug(encryptedData, pbkdf2Key)
                return AttachmentDecryptAttemptResult(old.decrypted, "new_base64_invalid->legacy:${old.reason}")
            }

            // AES-256-CBC расшифровка
            val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
            val secretKey = javax.crypto.spec.SecretKeySpec(pbkdf2Key, "AES")
            val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
            cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
            val decrypted = cipher.doFinal(ciphertext)

            val inflated = inflateToUtf8(decrypted)
            AttachmentDecryptAttemptResult(inflated, "ok:new")
        } catch (e: javax.crypto.BadPaddingException) {
            AttachmentDecryptAttemptResult(null, "aes_bad_padding")
        } catch (e: java.util.zip.DataFormatException) {
            AttachmentDecryptAttemptResult(null, "inflate_data_format")
        } catch (e: Exception) {
            AttachmentDecryptAttemptResult(null, "${e.javaClass.simpleName}:${e.message?.take(80)}")
        } catch (_: OutOfMemoryError) {
            System.gc()
            AttachmentDecryptAttemptResult(null, "OutOfMemoryError")
        }
    }

    private fun decryptChunkedWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
        return decryptChunkedWithPBKDF2KeyDebug(encryptedData, pbkdf2Key)
    }

    private fun decryptChunkedWithPBKDF2KeyDebug(encryptedData: String, pbkdf2Key: ByteArray): String? {
        return try {
            val raw = encryptedData.removePrefix("CHNK:")
            if (raw.isBlank()) return null

            val encryptedChunks = raw.split("::").filter { it.isNotBlank() }
            if (encryptedChunks.isEmpty()) return null

            val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
            val secretKey = javax.crypto.spec.SecretKeySpec(pbkdf2Key, "AES")
            val compressedOutput = java.io.ByteArrayOutputStream()

            for (chunk in encryptedChunks) {
                val parts = chunk.split(":", limit = 2)
                if (parts.size != 2) return null

                val iv = decodeBase64Compat(parts[0]) ?: return null
                val ciphertext = decodeBase64Compat(parts[1]) ?: return null

                val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
                cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
                val decryptedChunk = cipher.doFinal(ciphertext)
                compressedOutput.write(decryptedChunk)
            }

            inflateToUtf8(compressedOutput.toByteArray())
        } catch (e: Exception) {
            android.util.Log.w(
                "MessageCrypto",
                "decryptChunkedWithPBKDF2Key: ${e.javaClass.simpleName}: ${e.message}"
            )
            null
        } catch (_: OutOfMemoryError) {
            System.gc()
            null
        }
    }

    private fun decryptWithPBKDF2KeySha1(encryptedData: String, passwordBytes: ByteArray): String? {
        return try {
            val keyBytesSha1 = generatePBKDF2KeySha1FromBytes(passwordBytes)
            decryptWithPBKDF2Key(encryptedData, keyBytesSha1)
        } catch (_: Exception) {
            null
        } catch (_: OutOfMemoryError) {
            System.gc()
            null
        }
    }

    /**
     * Desktop legacy decodeWithPassword support:
     * data = base64("ivHex:cipherHex")
     */
    private fun decryptOldFormatWithPBKDF2Key(
        encryptedData: String,
        pbkdf2Key: ByteArray
    ): String? {
        return decryptOldFormatWithPBKDF2KeyDebug(encryptedData, pbkdf2Key).decrypted
    }

    private fun decryptOldFormatWithPBKDF2KeyDebug(
        encryptedData: String,
        pbkdf2Key: ByteArray
    ): AttachmentDecryptAttemptResult {
        return try {
            val decoded = decodeBase64Compat(encryptedData)
                ?: return AttachmentDecryptAttemptResult(null, "base64_decode_failed")
            val decodedText = String(decoded, Charsets.UTF_8)
            val parts = decodedText.split(":", limit = 2)
            if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank()) {
                return AttachmentDecryptAttemptResult(null, "decoded_not_ivhex_cthex")
            }

            val iv = parts[0].hexToBytes()
            val ciphertext = parts[1].hexToBytes()

            val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
            val secretKey = javax.crypto.spec.SecretKeySpec(pbkdf2Key, "AES")
            val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
            cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
            val decryptedUtf8 = cipher.doFinal(ciphertext)

            val text = String(decryptedUtf8, Charsets.UTF_8)
            if (text.isEmpty()) {
                AttachmentDecryptAttemptResult(null, "decrypted_empty")
            } else {
                AttachmentDecryptAttemptResult(text, "ok")
            }
        } catch (e: javax.crypto.BadPaddingException) {
            AttachmentDecryptAttemptResult(null, "aes_bad_padding")
        } catch (e: Exception) {
            AttachmentDecryptAttemptResult(null, "${e.javaClass.simpleName}:${e.message?.take(80)}")
        }
    }

    private fun decodeBase64Compat(raw: String): ByteArray? {
        val cleaned = raw.trim().replace("\n", "").replace("\r", "")
        if (cleaned.isEmpty()) return null
        return try {
            Base64.decode(cleaned, Base64.DEFAULT)
        } catch (_: IllegalArgumentException) {
            try {
                val pad = (4 - (cleaned.length % 4)) % 4
                Base64.decode(cleaned + "=".repeat(pad), Base64.DEFAULT)
            } catch (_: IllegalArgumentException) {
                null
            }
        }
    }

    private fun shortSha256(bytes: ByteArray): String {
        return try {
            val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
            digest.copyOfRange(0, 6).joinToString("") { "%02x".format(it) }
        } catch (_: Exception) {
            "shaerr"
        }
    }

    private fun describeAttachmentPayload(encryptedData: String): String {
        return try {
            val colonCount = encryptedData.count { it == ':' }

            if (encryptedData.startsWith("CHNK:")) {
                val raw = encryptedData.removePrefix("CHNK:")
                val chunks = raw.split("::").filter { it.isNotBlank() }
                val firstChunk = chunks.firstOrNull().orEmpty()
                val firstParts = firstChunk.split(":", limit = 2)
                val firstIvB64Len = firstParts.getOrNull(0)?.length ?: 0
                val firstCtB64Len = firstParts.getOrNull(1)?.length ?: 0
                val firstIvBytes = firstParts.getOrNull(0)?.let { decodeBase64Compat(it)?.size } ?: -1
                return "chunked,len=${encryptedData.length},colons=$colonCount,chunks=${chunks.size},firstIvB64=$firstIvB64Len,firstCtB64=$firstCtB64Len,firstIvBytes=$firstIvBytes"
            }

            val parts = encryptedData.split(":", limit = 2)
            if (parts.size == 2) {
                val ivBytes = decodeBase64Compat(parts[0])?.size ?: -1
                val ctBytes = decodeBase64Compat(parts[1])?.size ?: -1
                return "new-or-legacy2,len=${encryptedData.length},colons=$colonCount,ivB64=${parts[0].length},ctB64=${parts[1].length},ivBytes=$ivBytes,ctBytes=$ctBytes"
            }

            val decoded = decodeBase64Compat(encryptedData)
            if (decoded != null) {
                val decodedText = String(decoded, Charsets.UTF_8)
                val legacyParts = decodedText.split(":", limit = 2)
                if (legacyParts.size == 2) {
                    return "legacy-base64,len=${encryptedData.length},decoded=${decoded.size},ivHex=${legacyParts[0].length},ctHex=${legacyParts[1].length}"
                }
                return "base64-unknown,len=${encryptedData.length},decoded=${decoded.size},colons=$colonCount"
            }

            "unknown,len=${encryptedData.length},colons=$colonCount"
        } catch (_: Exception) {
            "inspect-error,len=${encryptedData.length}"
        }
    }

    /**
     * Собираем пароль-кандидаты для полной desktop совместимости:
     * - full key+nonce (56 bytes) и legacy key-only (32 bytes)
     * - hex password (актуальный desktop формат для attachments)
     * - Buffer polyfill UTF-8 decode (desktop runtime parity: window.Buffer from "buffer")
     * - WHATWG/Node UTF-8 decode
     * - JVM UTF-8 / Latin1 fallback
     */
    private fun buildAttachmentPasswordCandidates(chachaKeyPlain: ByteArray): List<String> {
        val candidates = LinkedHashSet<String>(16)

        fun addVariants(bytes: ByteArray) {
            if (bytes.isEmpty()) return
            candidates.add(bytes.joinToString("") { "%02x".format(it) })
            candidates.add(bytesToBufferPolyfillUtf8String(bytes))
            candidates.add(bytesToJsUtf8String(bytes))
            candidates.add(String(bytes, Charsets.UTF_8))
            candidates.add(String(bytes, Charsets.ISO_8859_1))
        }

        addVariants(chachaKeyPlain)
        addVariants(chachaKeyPlain.copyOfRange(0, minOf(chachaKeyPlain.size, 32)))
        return candidates.toList()
    }
    
    /**
     * Шифрование reply blob для передачи по сети
     * 
     * Совместим с React Native:
     * 1. Compress with pako (deflate)
     * 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
     * 3. AES-256-CBC encryption
     * 
     * @param replyJson - JSON string to encrypt
     * @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
     * 
     * Формат выхода: "ivBase64:ciphertextBase64" (совместим с desktop)
     */
    fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
        return try {
            
            // Desktop runtime parity: App sets window.Buffer from "buffer" polyfill.
            // Use the same UTF-8 decoding semantics for password derivation.
            val password = bytesToBufferPolyfillUtf8String(plainKeyAndNonce)
            
            // Compress with pako (deflate)
            val deflater = java.util.zip.Deflater()
            deflater.setInput(replyJson.toByteArray(Charsets.UTF_8))
            deflater.finish()
            val outputStream = java.io.ByteArrayOutputStream()
            val buffer = ByteArray(8192)
            while (!deflater.finished()) {
                val count = deflater.deflate(buffer)
                outputStream.write(buffer, 0, count)
            }
            deflater.end()
            val compressed = outputStream.toByteArray()
            
            // PBKDF2 key derivation (matching crypto-js: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000}))
            // Используем generatePBKDF2Key() для совместимости с crypto-js (UTF-8 encoding)
            val keyBytes = generatePBKDF2Key(password)
            
            // Generate random IV (16 bytes)
            val iv = ByteArray(16)
            java.security.SecureRandom().nextBytes(iv)
            
            // AES-CBC encryption
            val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
            val keySpec = SecretKeySpec(keyBytes, "AES")
            val ivSpec = IvParameterSpec(iv)
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
            val ciphertext = cipher.doFinal(compressed)
            
            // Format: "ivBase64:ciphertextBase64" (same as RN new format)
            val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
            val ctBase64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP)
            val result = "$ivBase64:$ctBase64"
            
            result
        } catch (e: Exception) {
            android.util.Log.e("MessageCrypto", "encryptReplyBlob failed", e)
            throw e
        }
    }
    
    /**
     * Desktop runtime parity: Buffer is from npm package "buffer" (feross),
     * not Node native decoder in all execution contexts.
     *
     * This implementation mirrors utf8Slice() from buffer@6:
     * - determines bytesPerSequence from first byte
     * - on invalid sequence emits U+FFFD and consumes exactly 1 byte
     * - produces surrogate pairs for code points > U+FFFF
     */
    private fun bytesToBufferPolyfillUtf8String(bytes: ByteArray): String {
        val codePoints = ArrayList<Int>(bytes.size * 2)
        var index = 0
        val end = bytes.size

        while (index < end) {
            val firstByte = bytes[index].toInt() and 0xff
            var codePoint: Int? = null
            var bytesPerSequence = when {
                firstByte > 0xef -> 4
                firstByte > 0xdf -> 3
                firstByte > 0xbf -> 2
                else -> 1
            }

            if (index + bytesPerSequence <= end) {
                when (bytesPerSequence) {
                    1 -> {
                        if (firstByte < 0x80) {
                            codePoint = firstByte
                        }
                    }
                    2 -> {
                        val secondByte = bytes[index + 1].toInt() and 0xff
                        if ((secondByte and 0xc0) == 0x80) {
                            val tempCodePoint = ((firstByte and 0x1f) shl 6) or (secondByte and 0x3f)
                            if (tempCodePoint > 0x7f) {
                                codePoint = tempCodePoint
                            }
                        }
                    }
                    3 -> {
                        val secondByte = bytes[index + 1].toInt() and 0xff
                        val thirdByte = bytes[index + 2].toInt() and 0xff
                        if ((secondByte and 0xc0) == 0x80 && (thirdByte and 0xc0) == 0x80) {
                            val tempCodePoint =
                                ((firstByte and 0x0f) shl 12) or
                                        ((secondByte and 0x3f) shl 6) or
                                        (thirdByte and 0x3f)
                            if (tempCodePoint > 0x7ff &&
                                    (tempCodePoint < 0xd800 || tempCodePoint > 0xdfff)
                            ) {
                                codePoint = tempCodePoint
                            }
                        }
                    }
                    4 -> {
                        val secondByte = bytes[index + 1].toInt() and 0xff
                        val thirdByte = bytes[index + 2].toInt() and 0xff
                        val fourthByte = bytes[index + 3].toInt() and 0xff
                        if ((secondByte and 0xc0) == 0x80 &&
                                (thirdByte and 0xc0) == 0x80 &&
                                (fourthByte and 0xc0) == 0x80
                        ) {
                            val tempCodePoint =
                                ((firstByte and 0x0f) shl 18) or
                                        ((secondByte and 0x3f) shl 12) or
                                        ((thirdByte and 0x3f) shl 6) or
                                        (fourthByte and 0x3f)
                            if (tempCodePoint > 0xffff && tempCodePoint < 0x110000) {
                                codePoint = tempCodePoint
                            }
                        }
                    }
                }
            }

            if (codePoint == null) {
                codePoint = 0xfffd
                bytesPerSequence = 1
            } else if (codePoint > 0xffff) {
                val adjusted = codePoint - 0x10000
                codePoints.add(((adjusted ushr 10) and 0x3ff) or 0xd800)
                codePoint = 0xdc00 or (adjusted and 0x3ff)
            }

            codePoints.add(codePoint)
            index += bytesPerSequence
        }

        val builder = StringBuilder(codePoints.size)
        codePoints.forEach { builder.append(it.toChar()) }
        return builder.toString()
    }

    /**
     * WHATWG/Node-like UTF-8 decoder fallback.
     * Kept for backwards compatibility with already persisted payloads.
     *
     * Public wrapper for use in MessageRepository
     */
    fun bytesToJsUtf8StringPublic(bytes: ByteArray): String = bytesToJsUtf8String(bytes)
    
    /**
     * Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
     * Implements the WHATWG Encoding Standard UTF-8 decoder algorithm EXACTLY:
     * https://encoding.spec.whatwg.org/#utf-8-decoder
     *
     * CRITICAL RULES (differ from naive implementations):
     * 1. Failed multi-byte sequence → emit exactly ONE U+FFFD, reprocess the bad byte
     * 2. Truncated sequence at end of input → emit exactly ONE U+FFFD
     * 3. Overlong / surrogate rejection via lower/upper continuation bounds (not post-hoc check)
     * 4. Bytes 0x80-0xC1, 0xF5-0xFF as starters → ONE U+FFFD per byte
     *
     * This is critical for cross-platform compatibility with desktop (Node.js)!
     */
    private fun bytesToJsUtf8String(bytes: ByteArray): String {
        val result = StringBuilder()

        // WHATWG state variables
        var codePoint = 0
        var bytesNeeded = 0
        var bytesSeen = 0
        var lowerBoundary = 0x80
        var upperBoundary = 0xBF

        var i = 0
        // Process all bytes + one extra iteration for end-of-input handling
        while (i <= bytes.size) {
            if (i == bytes.size) {
                // End of input
                if (bytesNeeded > 0) {
                    // Truncated multi-byte sequence → exactly ONE U+FFFD
                    result.append('\uFFFD')
                }
                break
            }

            val b = bytes[i].toInt() and 0xFF

            if (bytesNeeded == 0) {
                // Initial state — expecting a starter byte
                when {
                    b <= 0x7F -> {
                        result.append(b.toChar())
                        i++
                    }
                    b in 0xC2..0xDF -> {
                        bytesNeeded = 1
                        codePoint = b and 0x1F
                        i++
                    }
                    b in 0xE0..0xEF -> {
                        bytesNeeded = 2
                        codePoint = b and 0x0F
                        // WHATWG: tighter bounds to reject overlong & surrogates early
                        when (b) {
                            0xE0 -> lowerBoundary = 0xA0  // reject overlong < U+0800
                            0xED -> upperBoundary = 0x9F  // reject surrogates U+D800..U+DFFF
                        }
                        i++
                    }
                    b in 0xF0..0xF4 -> {
                        bytesNeeded = 3
                        codePoint = b and 0x07
                        when (b) {
                            0xF0 -> lowerBoundary = 0x90  // reject overlong < U+10000
                            0xF4 -> upperBoundary = 0x8F  // reject > U+10FFFF
                        }
                        i++
                    }
                    else -> {
                        // 0x80-0xC1, 0xF5-0xFF → invalid starter → ONE U+FFFD per byte
                        result.append('\uFFFD')
                        i++
                    }
                }
            } else {
                // Continuation state — expecting byte in [lowerBoundary, upperBoundary]
                if (b in lowerBoundary..upperBoundary) {
                    // Valid continuation
                    codePoint = (codePoint shl 6) or (b and 0x3F)
                    bytesSeen++
                    // Reset bounds to default after first continuation
                    lowerBoundary = 0x80
                    upperBoundary = 0xBF
                    if (bytesSeen == bytesNeeded) {
                        // Sequence complete — emit code point
                        if (codePoint <= 0xFFFF) {
                            result.append(codePoint.toChar())
                        } else {
                            // Supplementary: surrogate pair
                            val adjusted = codePoint - 0x10000
                            result.append((0xD800 + (adjusted shr 10)).toChar())
                            result.append((0xDC00 + (adjusted and 0x3FF)).toChar())
                        }
                        // Reset state
                        codePoint = 0
                        bytesNeeded = 0
                        bytesSeen = 0
                    }
                    i++
                } else {
                    // Invalid continuation → emit exactly ONE U+FFFD for the whole
                    // failed sequence, then REPROCESS this byte (don't consume it)
                    result.append('\uFFFD')
                    // Reset state
                    codePoint = 0
                    bytesNeeded = 0
                    bytesSeen = 0
                    lowerBoundary = 0x80
                    upperBoundary = 0xBF
                    // Do NOT increment i — reprocess this byte as a potential starter
                }
            }
        }

        return result.toString()
    }

    /**
     * Расшифровка reply blob полученного по сети
     * 
     * Совместим с React Native:
     * 1. Parse "ivBase64:ciphertextBase64" format
     * 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
     * 3. AES-256-CBC decryption
     * 4. Decompress with pako (inflate)
     * 
     * @param encryptedBlob - "ivBase64:ciphertextBase64" format
     * @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
     */
    fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
        return try {
            
            // Check if it's encrypted format (contains ':')
            if (!encryptedBlob.contains(':')) {
                return encryptedBlob
            }
            
            // Parse ivBase64:ciphertextBase64
            val parts = encryptedBlob.split(':', limit = 2)
            if (parts.size != 2) {
                return encryptedBlob
            }
            
            
            val iv = decodeBase64Compat(parts[0]) ?: return encryptedBlob
            val ciphertext = decodeBase64Compat(parts[1]) ?: return encryptedBlob

            val passwordCandidates = buildAttachmentPasswordCandidates(plainKeyAndNonce)
            for (password in passwordCandidates) {
                val passwordBytes = password.toByteArray(Charsets.UTF_8)
                val keyCandidates = listOf(
                    generatePBKDF2KeyFromBytes(passwordBytes),
                    generatePBKDF2KeySha1FromBytes(passwordBytes)
                )

                for (keyBytes in keyCandidates) {
                    try {
                        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
                        val keySpec = SecretKeySpec(keyBytes, "AES")
                        val ivSpec = IvParameterSpec(iv)
                        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
                        val compressedBytes = cipher.doFinal(ciphertext)
                        return inflateToUtf8(compressedBytes)
                    } catch (_: Exception) { }
                }
            }

            encryptedBlob
        } catch (e: Exception) {
            // Return as-is, might be plain JSON
            encryptedBlob
        }
    }

    private fun inflateToUtf8(compressedBytes: ByteArray): String {
        val inflater = java.util.zip.Inflater()
        return try {
            inflater.setInput(compressedBytes)
            val output = java.io.ByteArrayOutputStream()
            val buffer = ByteArray(8 * 1024)
            while (!inflater.finished()) {
                val count = inflater.inflate(buffer)
                if (count > 0) {
                    output.write(buffer, 0, count)
                    continue
                }
                if (inflater.needsInput()) {
                    break
                }
                if (inflater.needsDictionary()) {
                    throw java.util.zip.DataFormatException("Inflater requires dictionary")
                }
                throw java.util.zip.DataFormatException("Inflater stalled")
            }
            if (!inflater.finished()) {
                throw java.util.zip.DataFormatException("Inflater did not finish")
            }
            String(output.toByteArray(), Charsets.UTF_8)
        } finally {
            inflater.end()
        }
    }
}

// Extension functions для конвертации
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }

private fun String.hexToBytes(): ByteArray {
    check(length % 2 == 0) { "Hex string must have even length" }
    return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
