package com.rosetta.messenger.ui.chats.components

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.util.Base64
import android.util.LruCache
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.exifinterface.media.ExifInterface
import com.rosetta.messenger.R
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.MessageAttachment
import com.rosetta.messenger.network.TransportManager
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.models.MessageStatus
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.utils.AttachmentFileManager
import com.rosetta.messenger.utils.AvatarFileManager
import com.vanniktech.blurhash.BlurHash
import com.rosetta.messenger.ui.icons.TelegramIcons
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.core.content.FileProvider
import android.content.Intent
import android.os.SystemClock
import android.webkit.MimeTypeMap
import java.io.ByteArrayInputStream
import java.io.File
import java.security.MessageDigest
import kotlin.math.min

private const val TAG = "AttachmentComponents"
private const val MAX_BITMAP_DECODE_DIMENSION = 4096
private val whitespaceRegex = "\\s+".toRegex()

private fun isGroupStoredKey(value: String): Boolean = value.startsWith("group:")

private fun canonicalGroupDialogKey(value: String): String {
    val trimmed = value.trim()
    if (trimmed.isBlank()) return ""
    val groupId =
            when {
                trimmed.startsWith("#group:") -> trimmed.removePrefix("#group:").trim()
                trimmed.startsWith("group:", ignoreCase = true) ->
                        trimmed.substringAfter(':').trim()
                else -> return trimmed
            }
    return if (groupId.isBlank()) "" else "#group:$groupId"
}

private fun decodeGroupPassword(storedKey: String, privateKey: String): String? {
    if (!isGroupStoredKey(storedKey)) return null
    val encoded = storedKey.removePrefix("group:")
    if (encoded.isBlank()) return null
    return CryptoManager.decryptWithPassword(encoded, privateKey)
}

private fun decodeBase64Payload(data: String): ByteArray? {
    val raw = data.trim()
    if (raw.isBlank()) return null
    val payload =
            if (raw.startsWith("data:") && raw.contains(",")) {
                raw.substringAfter(",")
            } else {
                raw
            }
    return try {
        Base64.decode(payload, Base64.DEFAULT)
    } catch (_: Exception) {
        null
    }
}

private fun shortDebugId(value: String): String {
    if (value.isBlank()) return "empty"
    val clean = value.trim()
    return if (clean.length <= 8) clean else "${clean.take(8)}..."
}

private fun logPhotoDebug(message: String) {
    AttachmentDownloadDebugLogger.log(message)
}

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

/**
 * Анимированный текст с волнообразными точками.
 * Три точки плавно подпрыгивают каскадом с изменением прозрачности.
 */
@Composable
fun AnimatedDotsText(
    baseText: String,
    color: Color,
    fontSize: androidx.compose.ui.unit.TextUnit = 12.sp,
    fontWeight: FontWeight = FontWeight.Normal
) {
    val infiniteTransition = rememberInfiniteTransition(label = "dots")

    val dot0 by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1200
                0f at 0
                1f at 300
                0f at 600
                0f at 1200
            },
            repeatMode = RepeatMode.Restart,
            initialStartOffset = StartOffset(0)
        ),
        label = "dot0"
    )
    val dot1 by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1200
                0f at 0
                1f at 300
                0f at 600
                0f at 1200
            },
            repeatMode = RepeatMode.Restart,
            initialStartOffset = StartOffset(200)
        ),
        label = "dot1"
    )
    val dot2 by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 1200
                0f at 0
                1f at 300
                0f at 600
                0f at 1200
            },
            repeatMode = RepeatMode.Restart,
            initialStartOffset = StartOffset(400)
        ),
        label = "dot2"
    )

    val dotValues = listOf(dot0, dot1, dot2)
    val bounceHeight = with(LocalDensity.current) { fontSize.toPx() * 0.35f }

    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(
            text = baseText,
            fontSize = fontSize,
            fontWeight = fontWeight,
            color = color
        )
        dotValues.forEach { progress ->
            Text(
                text = ".",
                fontSize = fontSize,
                fontWeight = fontWeight,
                color = color.copy(alpha = 0.4f + 0.6f * progress),
                modifier = Modifier.graphicsLayer {
                    translationY = -bounceHeight * progress
                }
            )
        }
    }
}

/**
 * 🖼️ Глобальный LRU кэш для bitmap'ов изображений
 * Это предотвращает мигание при перезаходе в диалог,
 * так как изображения остаются в памяти между recomposition
 */
object ImageBitmapCache {
    // Размер кэша = 1/8 доступной памяти (стандартная практика Android)
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8

    private val cache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            // Размер в килобайтах
            return bitmap.byteCount / 1024
        }
    }

    // Flow для уведомления о новых записях (заменяет polling retry loops)
    private val _updates = kotlinx.coroutines.flow.MutableSharedFlow<String>(extraBufferCapacity = 64)
    val updates: kotlinx.coroutines.flow.SharedFlow<String> = _updates

    fun get(key: String): Bitmap? = cache.get(key)

    fun put(key: String, bitmap: Bitmap) {
        if (cache.get(key) == null) {
            cache.put(key, bitmap)
            _updates.tryEmit(key)
        }
    }

    fun remove(key: String) {
        cache.remove(key)
    }

    fun clear() {
        cache.evictAll()
    }

    /**
     * Ждёт появления bitmap в кэше по ключу (вместо polling retry loop).
     * Возвращает null при таймауте.
     */
    suspend fun awaitCached(key: String, timeoutMs: Long = 3000): Bitmap? {
        // Может уже быть в кэше
        get(key)?.let { return it }
        return kotlinx.coroutines.withTimeoutOrNull(timeoutMs) {
            updates.first { it == key }
            get(key)
        }
    }
}

/**
 * 🔒 Global semaphore to limit concurrent image decode/download operations
 * This prevents lag when opening collages with many photos
 */
object ImageLoadSemaphore {
    val semaphore = kotlinx.coroutines.sync.Semaphore(3)
}

/**
 * 📐 Telegram Bubble Specification
 * Все константы взяты из ChatMessageCell.java и Theme.java
 */
object TelegramBubbleSpec {
    // === BUBBLE RADIUS ===
    val bubbleRadius = 17.dp           // chat_msgTextPaintOneEmoji
    val nearRadius = 6.dp              // smallRad для хвостика
    val minRadius = 6.dp               // Минимальный радиус
    val photoRadius = 15.dp            // bubbleRadius - 2 для медиа
    
    // === PHOTO SIZING ===
    // maxWidth = min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.5f
    // Telegram использует ~50-65% ширины экрана
    fun maxPhotoWidth(screenWidth: Int): Int = (screenWidth * 0.65f).toInt()
    // Multi-photo collage should be slightly larger than a single media bubble.
    fun maxCollageWidth(screenWidth: Int): Int = (screenWidth * 0.72f).toInt()
    val maxPhotoHeight = 360           // dp, примерно maxWidth + 100
    val minPhotoWidth = 150            // dp
    val minPhotoHeight = 120           // dp
    
    // === CAPTION PADDING ===
    val captionPaddingTop = 9.dp       // Отступ от фото до caption
    val captionPaddingBottom = 6.dp
    val captionPaddingStartOutgoing = 11.dp  // Исходящие
    val captionPaddingStartIncoming = 13.dp  // Входящие
    val captionPaddingEnd = 10.dp
    
    // === TIME OVERLAY (на медиа) ===
    val timeOverlayPadding = 6.dp
    val timeOverlayRadius = 10.dp
    val timeOverlayAlpha = 0.5f
    val timeTextSize = 11.sp
    
    // === COLLAGE ===
    val collageSpacing = 1.dp          // Тонкий разделитель как в Telegram
    
    // === TEXT ===
    val messageTextSize = 16.sp
    val captionTextSize = 16.sp
}

/** Статус скачивания attachment (как в desktop) */
enum class DownloadStatus {
    DOWNLOADED,
    NOT_DOWNLOADED,
    PENDING,
    DECRYPTING,
    DOWNLOADING,
    ERROR
}

private enum class TelegramFileActionState {
    FILE,
    DOWNLOAD,
    CANCEL,
    PAUSE,
    ERROR
}

@Composable
private fun TelegramFileActionButton(
    state: TelegramFileActionState,
    progress: Float?,
    indeterminate: Boolean,
    modifier: Modifier = Modifier
) {
    val spinTransition = rememberInfiniteTransition(label = "file_action_spin")
    val spin by spinTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1200, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "file_action_spin_progress"
    )

    val backgroundColor = if (state == TelegramFileActionState.ERROR) Color(0xFFE53935) else PrimaryBlue
    val iconPainter = when (state) {
        TelegramFileActionState.FILE -> TelegramIcons.File
        TelegramFileActionState.DOWNLOAD -> painterResource(R.drawable.msg_download)
        TelegramFileActionState.CANCEL -> TelegramIcons.Close
        TelegramFileActionState.ERROR -> TelegramIcons.Close
        TelegramFileActionState.PAUSE -> null
    }
    val iconSize =
        when (state) {
            TelegramFileActionState.ERROR -> 18.dp
            TelegramFileActionState.PAUSE -> 18.dp
            else -> 20.dp
        }

    val showProgressRing =
        (state == TelegramFileActionState.PAUSE ||
            state == TelegramFileActionState.DOWNLOAD ||
            state == TelegramFileActionState.CANCEL) &&
            (indeterminate || progress != null)
    val sweep = when {
        indeterminate -> 104f
        progress != null -> (progress.coerceIn(0f, 1f) * 360f)
        else -> 0f
    }
    val startAngle = if (indeterminate) (spin * 360f) - 90f else -90f

    Box(
        modifier = modifier.size(40.dp),
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .clip(CircleShape)
                .background(backgroundColor),
            contentAlignment = Alignment.Center
        ) {
            if (showProgressRing && sweep > 0f) {
                Canvas(modifier = Modifier.fillMaxSize()) {
                    val strokeWidth = 2.dp.toPx()
                    val inset = 3.dp.toPx()
                    drawArc(
                        color = Color.White,
                        startAngle = startAngle,
                        sweepAngle = sweep,
                        useCenter = false,
                        topLeft = Offset(inset, inset),
                        size = Size(size.width - inset * 2f, size.height - inset * 2f),
                        style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
                    )
                }
            }

            if (state == TelegramFileActionState.PAUSE) {
                Icon(
                    imageVector = Icons.Default.Pause,
                    contentDescription = null,
                    tint = Color.White,
                    modifier = Modifier.size(iconSize)
                )
            } else {
                Icon(
                    painter = iconPainter ?: TelegramIcons.File,
                    contentDescription = null,
                    tint = Color.White,
                    modifier = Modifier.size(iconSize)
                )
            }
        }
    }
}

/**
 * Composable для отображения всех attachments в сообщении 🖼️ IMAGE attachments группируются в
 * коллаж (как в Telegram)
 */
@Composable
fun MessageAttachments(
        attachments: List<MessageAttachment>,
        chachaKey: String,
        privateKey: String,
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        senderPublicKey: String,
        dialogPublicKey: String = "",
        isGroupChat: Boolean = false,
        timestamp: java.util.Date,
        messageStatus: MessageStatus = MessageStatus.READ,
        avatarRepository: AvatarRepository? = null,
        currentUserPublicKey: String = "",
        hasCaption: Boolean = false, // Если есть caption - время показывается под фото, не на фото
        showTail: Boolean = true, // Показывать хвостик пузырька
        isSelectionMode: Boolean = false, // Блокировать клик на фото в selection mode
        onLongClick: () -> Unit = {}, // Long press на фото — запускает selection mode
        onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> },
        modifier: Modifier = Modifier
) {
    if (attachments.isEmpty()) return

    // Разделяем attachments по типам
    val imageAttachments = attachments.filter { it.type == AttachmentType.IMAGE }
    val otherAttachments =
            attachments.filter {
                it.type != AttachmentType.IMAGE && it.type != AttachmentType.MESSAGES
            }

    Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
        // 🖼️ Коллаж для изображений (если больше 1)
        if (imageAttachments.isNotEmpty()) {
            ImageCollage(
                    attachments = imageAttachments,
                    chachaKey = chachaKey,
                    privateKey = privateKey,
                    senderPublicKey = senderPublicKey,
                    isOutgoing = isOutgoing,
                    isDarkTheme = isDarkTheme,
                    timestamp = timestamp,
                    messageStatus = messageStatus,
                    hasCaption = hasCaption,
                    showTail = showTail,
                    isSelectionMode = isSelectionMode,
                    onLongClick = onLongClick,
                    onImageClick = onImageClick
            )
        }

        // Остальные attachments по отдельности
        otherAttachments.forEach { attachment ->
            when (attachment.type) {
                AttachmentType.FILE -> {
                    FileAttachment(
                            attachment = attachment,
                            chachaKey = chachaKey,
                            privateKey = privateKey,
                            currentUserPublicKey = currentUserPublicKey,
                            isOutgoing = isOutgoing,
                            isDarkTheme = isDarkTheme,
                            timestamp = timestamp,
                            messageStatus = messageStatus
                    )
                }
                AttachmentType.AVATAR -> {
                    AvatarAttachment(
                            attachment = attachment,
                            chachaKey = chachaKey,
                            privateKey = privateKey,
                            senderPublicKey = senderPublicKey,
                            dialogPublicKey = dialogPublicKey,
                            isGroupChat = isGroupChat,
                            avatarRepository = avatarRepository,
                            currentUserPublicKey = currentUserPublicKey,
                            isOutgoing = isOutgoing,
                            isDarkTheme = isDarkTheme,
                            timestamp = timestamp,
                            messageStatus = messageStatus
                    )
                }
                AttachmentType.CALL -> {
                    CallAttachment(
                            attachment = attachment,
                            isOutgoing = isOutgoing,
                            isDarkTheme = isDarkTheme,
                            timestamp = timestamp,
                            messageStatus = messageStatus
                    )
                }
                else -> {
                    /* MESSAGES обрабатываются отдельно */
                }
            }
        }
    }
}

/**
 * 🖼️ Коллаж изображений в стиле Telegram Разные layout'ы в зависимости от количества фото:
 * - 1 фото: полная ширина
 * - 2 фото: 2 колонки
 * - 3 фото: 1 большое слева + 2 маленьких справа
 * - 4 фото: 2x2 сетка
 * - 5+ фото: 2 сверху + 3 снизу (или больше рядов)
 */
@Composable
fun ImageCollage(
        attachments: List<MessageAttachment>,
        chachaKey: String,
        privateKey: String,
        senderPublicKey: String,
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        timestamp: java.util.Date,
        messageStatus: MessageStatus = MessageStatus.READ,
        hasCaption: Boolean = false, // Если есть caption - время показывается под фото
        showTail: Boolean = true, // Показывать хвостик пузырька
        isSelectionMode: Boolean = false, // Блокировать клик на фото в selection mode
        onLongClick: () -> Unit = {}, // Long press на фото — запускает selection mode
        onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> },
        modifier: Modifier = Modifier
) {
    val count = attachments.size
    val spacing = TelegramBubbleSpec.collageSpacing
    val collageDividerColor =
            if (isOutgoing) {
                Color.White.copy(alpha = if (isDarkTheme) 0.18f else 0.22f)
            } else {
                if (isDarkTheme) Color(0xFF2D3036) else Color(0xFFD8DEE8)
            }

    // Показываем время и статус только если нет caption
    val showOverlayOnLast = !hasCaption

    // Telegram: bubbleRadius=17dp, photoRadius=15dp (bubbleRadius-2), minRadius=6dp
    val collageShape =
            if (hasCaption) {
                // Фото над caption: верх=15dp, низ=0dp (плавный переход к caption)
                RoundedCornerShape(
                        topStart = TelegramBubbleSpec.photoRadius,
                        topEnd = TelegramBubbleSpec.photoRadius,
                        bottomStart = 0.dp,
                        bottomEnd = 0.dp
                )
            } else {
                // Фото без caption - простое закругление
                RoundedCornerShape(TelegramBubbleSpec.photoRadius)
            }

    Box(modifier = modifier.clip(collageShape).background(collageDividerColor)) {
        when (count) {
            1 -> {
                // Одно фото - размер определяется самим фото (Telegram style)
                ImageAttachment(
                        attachment = attachments[0],
                        chachaKey = chachaKey,
                        privateKey = privateKey,
                        senderPublicKey = senderPublicKey,
                        isOutgoing = isOutgoing,
                        isDarkTheme = isDarkTheme,
                        timestamp = timestamp,
                        messageStatus = messageStatus,
                        showTimeOverlay = showOverlayOnLast,
                        hasCaption = hasCaption,
                        isSelectionMode = isSelectionMode,
                        onLongClick = onLongClick,
                        onImageClick = onImageClick
                )
            }
            2 -> {
                // Два фото - горизонтально
                Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.spacedBy(spacing)
                ) {
                    attachments.forEachIndexed { index, attachment ->
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachment,
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = showOverlayOnLast && index == count - 1,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                    }
                }
            }
            3 -> {
                // Три фото: 1 большое слева + 2 маленьких справа
                Row(
                        modifier = Modifier.fillMaxWidth().height(200.dp),
                        horizontalArrangement = Arrangement.spacedBy(spacing)
                ) {
                    // Большое фото слева
                    Box(modifier = Modifier.weight(1.5f).fillMaxHeight()) {
                        ImageAttachment(
                                attachment = attachments[0],
                                chachaKey = chachaKey,
                                privateKey = privateKey,
                                senderPublicKey = senderPublicKey,
                                isOutgoing = isOutgoing,
                                isDarkTheme = isDarkTheme,
                                timestamp = timestamp,
                                messageStatus = messageStatus,
                                showTimeOverlay = false,
                                fillMaxSize = true,
                                isSelectionMode = isSelectionMode,
                                onLongClick = onLongClick,
                                onImageClick = onImageClick
                        )
                    }
                    // Два маленьких справа
                    Column(
                            modifier = Modifier.weight(1f).fillMaxHeight(),
                            verticalArrangement = Arrangement.spacedBy(spacing)
                    ) {
                        Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
                            ImageAttachment(
                                    attachment = attachments[1],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                        Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
                            ImageAttachment(
                                    attachment = attachments[2],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = showOverlayOnLast,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                    }
                }
            }
            4 -> {
                // Четыре фото: 2x2 сетка
                Column(
                        modifier = Modifier.fillMaxWidth(),
                        verticalArrangement = Arrangement.spacedBy(spacing)
                ) {
                    Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.spacedBy(spacing)
                    ) {
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[0],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[1],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                    }
                    Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.spacedBy(spacing)
                    ) {
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[2],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[3],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = showOverlayOnLast,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                    }
                }
            }
            else -> {
                // 5+ фото: 2 сверху + остальные снизу по 3 в ряд
                Column(
                        modifier = Modifier.fillMaxWidth(),
                        verticalArrangement = Arrangement.spacedBy(spacing)
                ) {
                    // Первые 2 фото сверху
                    Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.spacedBy(spacing)
                    ) {
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[0],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                        Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                            ImageAttachment(
                                    attachment = attachments[1],
                                    chachaKey = chachaKey,
                                    privateKey = privateKey,
                                    senderPublicKey = senderPublicKey,
                                    isOutgoing = isOutgoing,
                                    isDarkTheme = isDarkTheme,
                                    timestamp = timestamp,
                                    messageStatus = messageStatus,
                                    showTimeOverlay = false,
                                    fillMaxSize = true,
                                    isSelectionMode = isSelectionMode,
                                    onLongClick = onLongClick,
                                    onImageClick = onImageClick
                            )
                        }
                    }
                    // Остальные по 3 в ряд
                    val remaining = attachments.drop(2)
                    val rows = remaining.chunked(3)
                    val totalRows = rows.size
                    rows.forEachIndexed { rowIndex, rowItems ->
                        val isLastRow = rowIndex == totalRows - 1
                        Row(
                                modifier = Modifier.fillMaxWidth(),
                                horizontalArrangement = Arrangement.spacedBy(spacing)
                        ) {
                            rowItems.forEachIndexed { index, attachment ->
                                val isLastItem = isLastRow && index == rowItems.size - 1
                                Box(modifier = Modifier.weight(1f).aspectRatio(1f)) {
                                    ImageAttachment(
                                            attachment = attachment,
                                            chachaKey = chachaKey,
                                            privateKey = privateKey,
                                            senderPublicKey = senderPublicKey,
                                            isOutgoing = isOutgoing,
                                            isDarkTheme = isDarkTheme,
                                            timestamp = timestamp,
                                            messageStatus = messageStatus,
                                            showTimeOverlay = showOverlayOnLast && isLastItem,
                                            fillMaxSize = true,
                                            isSelectionMode = isSelectionMode,
                                            onLongClick = onLongClick,
                                            onImageClick = onImageClick
                                    )
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

/**
 * Image attachment - Telegram style с blurhash placeholder
 *
 * Логика определения статуса (как в Desktop):
 * 1. Если preview НЕ содержит UUID → это локальный файл → DOWNLOADED
 * 2. Если есть UUID (download tag) → проверяем файловую систему через AttachmentFileManager
 * 3. Если файл есть локально → DOWNLOADED (загружаем из файла)
 * 4. Иначе → NOT_DOWNLOADED (нужно скачать с CDN)
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ImageAttachment(
        attachment: MessageAttachment,
        chachaKey: String,
        privateKey: String,
        senderPublicKey: String,
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        timestamp: java.util.Date,
        messageStatus: MessageStatus = MessageStatus.READ,
        showTimeOverlay: Boolean = true,
        aspectRatio: Float? = null,
        fillMaxSize: Boolean = false,
        hasCaption: Boolean = false,
        isSelectionMode: Boolean = false,
        onLongClick: () -> Unit = {},
        onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> }
) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    // Bounds для shared element transition
    var imageBounds by remember { mutableStateOf<Rect?>(null) }

    // 🔥 Ключ для глобального кэша
    val cacheKey = "img_${attachment.id}"
    
    // 🔥 Сначала проверяем глобальный кэш
    var downloadStatus by remember(attachment.id) { 
        mutableStateOf(
            if (ImageBitmapCache.get(cacheKey) != null) DownloadStatus.DOWNLOADED 
            else DownloadStatus.PENDING
        ) 
    }
    var imageBitmap by remember(attachment.id) { 
        mutableStateOf(ImageBitmapCache.get(cacheKey)) 
    }
    var blurhashBitmap by remember(attachment.id) { mutableStateOf<Bitmap?>(null) }
    var downloadProgress by remember(attachment.id) { mutableStateOf(0f) }
    var errorLabel by remember(attachment.id) { mutableStateOf("Error") }

    val preview = getPreview(attachment.preview)
    val downloadTag = getDownloadTag(attachment.preview)

    // Анимация прогресса
    val animatedProgress by
            animateFloatAsState(
                    targetValue = downloadProgress,
                    animationSpec = tween(durationMillis = 300),
                    label = "progress"
            )

    // Определяем начальный статус и декодируем blurhash (как в Desktop calcDownloadStatus)
    LaunchedEffect(attachment.id, attachment.localUri) {
        // 🔥 Если изображение уже в глобальном кэше - используем его!
        val cachedBitmap = ImageBitmapCache.get(cacheKey)
        if (cachedBitmap != null) {
            imageBitmap = cachedBitmap
            downloadStatus = DownloadStatus.DOWNLOADED
            return@LaunchedEffect
        }
        
        // 🔥 Если изображение уже загружено локально - не перезагружаем!
        if (imageBitmap != null && downloadStatus == DownloadStatus.DOWNLOADED) {
            return@LaunchedEffect
        }
        
        // Определяем статус (логика из Desktop useAttachment.ts)
        withContext(Dispatchers.IO) {
            downloadStatus =
                    when {
                        // 🚀 0. Если есть localUri - это optimistic UI, показываем сразу
                        attachment.localUri.isNotEmpty() -> {
                            DownloadStatus.DOWNLOADED
                        }
                        // 1. Если blob уже есть в памяти → DOWNLOADED
                        attachment.blob.isNotEmpty() -> {
                            DownloadStatus.DOWNLOADED
                        }
                        // 2. Если preview НЕ содержит UUID → это наш локальный файл → DOWNLOADED
                        !isDownloadTag(attachment.preview) -> {
                            DownloadStatus.DOWNLOADED
                        }
                        // 3. Есть UUID (download tag) → проверяем файловую систему
                        else -> {
                            // Проверяем есть ли файл локально (как в Desktop:
                            // readFile(`m/${md5hash}`))
                            val hasLocal =
                                    AttachmentFileManager.hasAttachment(
                                            context,
                                            attachment.id,
                                            senderPublicKey
                                    )
                            if (hasLocal) {
                                DownloadStatus.DOWNLOADED
                            } else {
                                DownloadStatus.NOT_DOWNLOADED
                            }
                        }
                    }
        }

        // Декодируем blurhash для placeholder (если есть)
        if (preview.isNotEmpty() && !isDownloadTag(preview)) {
            withContext(Dispatchers.IO) {
                try {
                    blurhashBitmap = BlurHash.decode(preview, 200, 200)
                    if (blurhashBitmap != null) {
                    } else {
                    }
                } catch (e: Exception) {
                }
            }
        } else {
        }

        // Загружаем изображение если статус DOWNLOADED
        if (downloadStatus == DownloadStatus.DOWNLOADED) {
            // 🔒 Ограничиваем параллельные загрузки через семафор
            ImageLoadSemaphore.semaphore.withPermit {
                withContext(Dispatchers.IO) {
                    // 🚀 0. Если есть localUri - загружаем напрямую из URI (optimistic UI)
                    if (imageBitmap == null && attachment.localUri.isNotEmpty()) {
                    try {
                        val uri = android.net.Uri.parse(attachment.localUri)
                        
                        // 📐 Читаем EXIF ориентацию
                        val orientation = try {
                            context.contentResolver.openInputStream(uri)?.use { inputStream ->
                                val exif = ExifInterface(inputStream)
                                exif.getAttributeInt(
                                    ExifInterface.TAG_ORIENTATION,
                                    ExifInterface.ORIENTATION_NORMAL
                                )
                            } ?: ExifInterface.ORIENTATION_NORMAL
                        } catch (e: Exception) {
                            ExifInterface.ORIENTATION_NORMAL
                        }
                        
                        // Загружаем bitmap
                        val inputStream = context.contentResolver.openInputStream(uri)
                        var bitmap = BitmapFactory.decodeStream(inputStream)
                        inputStream?.close()
                        
                        // 🔄 Применяем EXIF ориентацию
                        if (bitmap != null && orientation != ExifInterface.ORIENTATION_NORMAL) {
                            val matrix = Matrix()
                            when (orientation) {
                                ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
                                ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
                                ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
                                ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
                                ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
                                ExifInterface.ORIENTATION_TRANSPOSE -> {
                                    matrix.postRotate(90f)
                                    matrix.preScale(-1f, 1f)
                                }
                                ExifInterface.ORIENTATION_TRANSVERSE -> {
                                    matrix.postRotate(270f)
                                    matrix.preScale(-1f, 1f)
                                }
                            }
                            val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
                            if (rotated != bitmap) {
                                bitmap.recycle()
                                bitmap = rotated
                            }
                        }
                        
                        if (bitmap != null) {
                            imageBitmap = bitmap
                            // 🔥 Сохраняем в глобальный кэш
                            ImageBitmapCache.put(cacheKey, bitmap)
                        }
                    } catch (e: Exception) {
                        // Fallback to other methods
                    }
                }
                
                // 1. Если нет bitmap, пробуем blob из памяти
                if (imageBitmap == null && attachment.blob.isNotEmpty()) {
                    val bitmap = base64ToBitmap(attachment.blob)
                    if (bitmap != null) {
                        imageBitmap = bitmap
                        // 🔥 Сохраняем в глобальный кэш
                        ImageBitmapCache.put(cacheKey, bitmap)
                    }
                }
                
                // 2. Если всё ещё нет, читаем из файловой системы
                if (imageBitmap == null) {
                    val localBlob =
                            AttachmentFileManager.readAttachment(
                                    context,
                                    attachment.id,
                                    senderPublicKey,
                                    privateKey
                            )
                    if (localBlob != null) {
                        val bitmap = base64ToBitmap(localBlob)
                        if (bitmap != null) {
                            imageBitmap = bitmap
                            // 🔥 Сохраняем в глобальный кэш
                            ImageBitmapCache.put(cacheKey, bitmap)
                        } else {
                            downloadStatus = DownloadStatus.ERROR
                        }
                    } else if (attachment.localUri.isEmpty()) {
                        // Только если нет localUri - помечаем как NOT_DOWNLOADED
                        downloadStatus = DownloadStatus.NOT_DOWNLOADED
                    }
                }
                }
            }

            if (imageBitmap == null && downloadStatus == DownloadStatus.DOWNLOADED) {
                downloadStatus =
                        if (downloadTag.isNotEmpty()) {
                            DownloadStatus.NOT_DOWNLOADED
                        } else {
                            DownloadStatus.ERROR
                        }
            }
        }
    }

    val download: () -> Unit = {
        if (downloadTag.isNotEmpty()) {
            scope.launch {
                val idShort = shortDebugId(attachment.id)
                val tagShort = shortDebugId(downloadTag)
                val server = TransportManager.getTransportServer() ?: "unset"
                logPhotoDebug("Start image download: id=$idShort, tag=$tagShort, server=$server")
                try {
                    downloadStatus = DownloadStatus.DOWNLOADING

                    // Скачиваем зашифрованный контент (с одним авто-ретраем)
                    val startTime = System.currentTimeMillis()
                    val encryptedContent: String
                    try {
                        encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
                    } catch (e: Exception) {
                        // Один авто-ретрай через 1с
                        logPhotoDebug("CDN download failed, retrying: id=$idShort, reason=${e.message}")
                        kotlinx.coroutines.delay(1000)
                        try {
                            val retryResult = TransportManager.downloadFile(attachment.id, downloadTag)
                            @Suppress("NAME_SHADOWING")
                            val encryptedContent = retryResult
                            logPhotoDebug("CDN retry OK: id=$idShort")
                            // Продолжаем с retryResult
                            processDownloadedImage(
                                encryptedContent = encryptedContent,
                                chachaKey = chachaKey,
                                privateKey = privateKey,
                                attachment = attachment,
                                senderPublicKey = senderPublicKey,
                                context = context,
                                cacheKey = cacheKey,
                                idShort = idShort,
                                onProgress = { downloadProgress = it },
                                onStatus = { downloadStatus = it },
                                onBitmap = { imageBitmap = it },
                                onError = { label -> errorLabel = label }
                            )
                            return@launch
                        } catch (retryEx: Exception) {
                            // CDN ошибка (файл истёк или сервер недоступен)
                            val isExpired = retryEx.message?.contains("404") == true || retryEx.message?.contains("410") == true
                            errorLabel = if (isExpired) "Expired" else "Error"
                            downloadStatus = DownloadStatus.ERROR
                            logPhotoDebug("CDN retry also failed: id=$idShort, reason=${retryEx.message}")
                            return@launch
                        }
                    }
                    val downloadTime = System.currentTimeMillis() - startTime
                    logPhotoDebug(
                            "CDN download OK: id=$idShort, bytes=${encryptedContent.length}, time=${downloadTime}ms"
                    )
                    
                    processDownloadedImage(
                        encryptedContent = encryptedContent,
                        chachaKey = chachaKey,
                        privateKey = privateKey,
                        attachment = attachment,
                        senderPublicKey = senderPublicKey,
                        context = context,
                        cacheKey = cacheKey,
                        idShort = idShort,
                        onProgress = { downloadProgress = it },
                        onStatus = { downloadStatus = it },
                        onBitmap = { imageBitmap = it },
                        onError = { label -> errorLabel = label }
                    )
                } catch (e: Exception) {
                    e.printStackTrace()
                    downloadStatus = DownloadStatus.ERROR
                    errorLabel = "Error"
                    logPhotoDebug(
                            "Image download ERROR: id=$idShort, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}, stack=${e.stackTraceToString().take(200)}"
                    )
                } catch (e: OutOfMemoryError) {
                    System.gc()
                    downloadStatus = DownloadStatus.ERROR
                    errorLabel = "Error"
                    logPhotoDebug("Image OOM: id=$idShort, availMem=${Runtime.getRuntime().freeMemory() / 1024}KB")
                }
            }
        } else {
            logPhotoDebug("Skip image download: empty tag for id=${shortDebugId(attachment.id)}")
        }
    }

    // Telegram-style image с blurhash placeholder и тонким бордером
    val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) }
    val borderColor =
            if (isOutgoing) {
                Color.White.copy(alpha = 0.15f)
            } else {
                if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
            }

    // 📐 TELEGRAM-STYLE sizing (из ChatMessageCell.java)
    // maxWidth = min(displaySize.x, displaySize.y) * 0.5f (~65% в портрете)
    // Сохраняем aspect ratio, ограничиваем max/min размеры
    val configuration = LocalConfiguration.current
    val screenWidthDp = configuration.screenWidthDp
    
    // For incoming media where server dimensions are often missing, keep stable bubble-based fallback size
    // instead of reflowing from decoded bitmap dimensions after download.
    val actualWidth = attachment.width.takeIf { it > 0 } ?: 0
    val actualHeight = attachment.height.takeIf { it > 0 } ?: 0

    val (imageWidth, imageHeight) =
            remember(actualWidth, actualHeight, fillMaxSize, aspectRatio, screenWidthDp) {
                if (fillMaxSize) {
                    null to null
                } else {
                    // Telegram: maxWidth = screenWidth * 0.65
                    val maxPhotoWidthPx = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp)

                    if (actualWidth > 0 && actualHeight > 0) {
                        val ar = actualWidth.toFloat() / actualHeight.toFloat()
                        
                        // Telegram: scale = imageW / photoWidth, пропорционально масштабируем
                        var w: Float
                        var h: Float
                        
                        if (ar >= 1f) {
                            // Горизонтальное фото - по ширине
                            w = maxPhotoWidthPx.toFloat()
                            h = w / ar
                        } else {
                            // Вертикальное фото - уменьшаем ширину
                            w = maxPhotoWidthPx * 0.75f
                            h = w / ar
                        }
                        
                        // Ограничение максимальной высоты
                        if (h > TelegramBubbleSpec.maxPhotoHeight) {
                            h = TelegramBubbleSpec.maxPhotoHeight.toFloat()
                            w = h * ar
                        }
                        
                        // Минимальная высота
                        if (h < TelegramBubbleSpec.minPhotoHeight) {
                            h = TelegramBubbleSpec.minPhotoHeight.toFloat()
                            w = h * ar
                        }
                        
                        // Ограничения по ширине
                        w = w.coerceIn(TelegramBubbleSpec.minPhotoWidth.toFloat(), maxPhotoWidthPx.toFloat())
                        h = h.coerceIn(TelegramBubbleSpec.minPhotoHeight.toFloat(), TelegramBubbleSpec.maxPhotoHeight.toFloat())
                        
                        w.dp to h.dp
                    } else {
                        // Match placeholder size to bubble width when media dimensions are still unknown.
                        val fallbackWidthPx = maxPhotoWidthPx.toFloat()
                        val fallbackHeightPx =
                                (fallbackWidthPx * 0.75f).coerceIn(
                                        TelegramBubbleSpec.minPhotoHeight.toFloat(),
                                        TelegramBubbleSpec.maxPhotoHeight.toFloat()
                                )
                        fallbackWidthPx.dp to fallbackHeightPx.dp
                    }
                }
            }

    // Модификатор размера
    val sizeModifier =
            when {
                fillMaxSize -> Modifier.fillMaxSize()
                aspectRatio != null -> Modifier.fillMaxWidth().aspectRatio(aspectRatio)
                imageWidth != null && imageHeight != null ->
                        Modifier.width(imageWidth).height(imageHeight)
                else -> Modifier.size(200.dp)
            }

    // Telegram: photoRadius = 15dp (bubbleRadius - 2)
    // Если есть caption - нижние углы прямые
    val photoRadius = TelegramBubbleSpec.photoRadius
    val imageShape = when {
        fillMaxSize -> RoundedCornerShape(0.dp)
        hasCaption -> RoundedCornerShape(
            topStart = photoRadius,
            topEnd = photoRadius,
            bottomStart = 0.dp,
            bottomEnd = 0.dp
        )
        else -> RoundedCornerShape(photoRadius)
    }
    val cornerRadius = if (fillMaxSize || hasCaption) 0f else 15f

    Box(
            modifier =
                    sizeModifier
                            .clip(imageShape)
                            .background(Color.Transparent)
                            .onGloballyPositioned { coordinates ->
                                // Capture bounds for shared element transition
                                imageBounds = coordinates.boundsInWindow()
                            }
                            .combinedClickable(
                                    indication = null,
                                    interactionSource = remember { MutableInteractionSource() },
                                    onLongClick = onLongClick,
                                    onClick = {
                                        if (!isSelectionMode) {
                                            when (downloadStatus) {
                                                DownloadStatus.NOT_DOWNLOADED -> download()
                                                DownloadStatus.DOWNLOADED -> {
                                                    // 📸 Open image viewer with bounds for animation
                                                    val bounds = imageBounds?.let {
                                                        ImageSourceBounds(
                                                            left = it.left,
                                                            top = it.top,
                                                            width = it.width,
                                                            height = it.height,
                                                            cornerRadius = cornerRadius,
                                                            thumbnailBitmap = imageBitmap
                                                        )
                                                    }
                                                    onImageClick(attachment.id, bounds)
                                                }
                                                DownloadStatus.ERROR -> download()
                                                else -> {}
                                            }
                                        } else {
                                            onLongClick()
                                        }
                                    }
                            ),
            contentAlignment = Alignment.Center
    ) {
        // Фоновый слой - blurhash или placeholder
        when {
            imageBitmap != null -> {
                // Показываем полное изображение
                Image(
                        bitmap = imageBitmap!!.asImageBitmap(),
                        contentDescription = "Image",
                        modifier = Modifier.fillMaxSize(),
                        contentScale = ContentScale.Crop
                )
            }
            blurhashBitmap != null -> {
                // Показываем blurhash placeholder
                Image(
                        bitmap = blurhashBitmap!!.asImageBitmap(),
                        contentDescription = "Preview",
                        modifier = Modifier.fillMaxSize(),
                        contentScale = ContentScale.Crop
                )
            }
            else -> {
                // Простой placeholder с приятным gradient фоном
                Box(
                        modifier =
                                Modifier.fillMaxSize()
                                        .background(
                                                Brush.verticalGradient(
                                                        colors =
                                                                if (isDarkTheme)
                                                                        listOf(
                                                                                Color(0xFF2A2A2A),
                                                                                Color(0xFF1A1A1A)
                                                                        )
                                                                else
                                                                        listOf(
                                                                                Color(0xFFE8E8E8),
                                                                                Color(0xFFD0D0D0)
                                                                        )
                                                )
                                        )
                )
            }
        }

        // Время в правом нижнем углу (показываем только если showTimeOverlay = true)
        if (showTimeOverlay) {
            Box(
                    modifier =
                            Modifier.align(Alignment.BottomEnd)
                                    .padding(TelegramBubbleSpec.timeOverlayPadding)
                                    .background(
                                            Color.Black.copy(alpha = TelegramBubbleSpec.timeOverlayAlpha),
                                            shape = RoundedCornerShape(TelegramBubbleSpec.timeOverlayRadius)
                                    )
                                    .padding(horizontal = 6.dp, vertical = 3.dp)
            ) {
                Row(
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(2.dp)
                ) {
                    Text(
                            text = timeFormat.format(timestamp),
                            color = Color.White,
                            fontSize = TelegramBubbleSpec.timeTextSize,
                            fontWeight = FontWeight.Medium
                    )
                    if (isOutgoing) {
                        // Статус доставки для исходящих
                        when (messageStatus) {
                            MessageStatus.SENDING -> {
                                Icon(
                                        painter = TelegramIcons.Clock,
                                        contentDescription = null,
                                        tint = Color.White.copy(alpha = 0.7f),
                                        modifier = Modifier.size(14.dp)
                                )
                            }
                            MessageStatus.SENT -> {
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = null,
                                        tint =
                                                if (isDarkTheme) Color.White
                                                else Color.White.copy(alpha = 0.7f),
                                        modifier = Modifier.size(14.dp)
                                )
                            }
                            MessageStatus.DELIVERED -> {
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = null,
                                        tint =
                                                if (isDarkTheme) Color.White
                                                else Color.White.copy(alpha = 0.7f),
                                        modifier = Modifier.size(14.dp)
                                )
                            }
                            MessageStatus.READ -> {
                                Box(modifier = Modifier.height(14.dp)) {
                                    Icon(
                                            painter = TelegramIcons.Done,
                                            contentDescription = null,
                                            tint = Color.White,
                                            modifier = Modifier.size(14.dp)
                                    )
                                    Icon(
                                            painter = TelegramIcons.Done,
                                            contentDescription = null,
                                            tint = Color.White,
                                            modifier = Modifier.size(14.dp).offset(x = 4.dp)
                                    )
                                }
                            }
                            else -> {}
                        }
                    }
                }
            }
        }

        // ✈️ Telegram-style: Loader при отправке фото (самолётик/кружок)
        // Показываем когда сообщение отправляется И это исходящее сообщение
        if (isOutgoing && messageStatus == MessageStatus.SENDING && downloadStatus == DownloadStatus.DOWNLOADED) {
            Box(
                    modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.35f)),
                    contentAlignment = Alignment.Center
            ) {
                // Круглый индикатор отправки
                Box(
                        modifier = Modifier
                                .size(48.dp)
                                .clip(CircleShape)
                                .background(Color.Black.copy(alpha = 0.5f)),
                        contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator(
                            modifier = Modifier.size(28.dp),
                            color = Color.White,
                            strokeWidth = 2.5.dp
                    )
                }
            }
        }

        // Оверлей для статуса скачивания
        if (downloadStatus != DownloadStatus.DOWNLOADED) {
            Box(
                    modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.3f)),
                    contentAlignment = Alignment.Center
            ) {
                when (downloadStatus) {
                    DownloadStatus.NOT_DOWNLOADED -> {
                        // Кнопка скачивания
                        Box(
                                modifier =
                                        Modifier.size(48.dp)
                                                .clip(CircleShape)
                                                .background(Color.Black.copy(alpha = 0.5f)),
                                contentAlignment = Alignment.Center
                        ) {
                            Icon(
                                    Icons.Default.ArrowDownward,
                                    contentDescription = "Download",
                                    tint = Color.White,
                                    modifier = Modifier.size(24.dp)
                            )
                        }
                    }
                    DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
                        // Индикатор загрузки с прогрессом
                        Box(
                                modifier =
                                        Modifier.size(48.dp)
                                                .clip(CircleShape)
                                                .background(Color.Black.copy(alpha = 0.5f)),
                                contentAlignment = Alignment.Center
                        ) {
                            CircularProgressIndicator(
                                    modifier = Modifier.size(36.dp),
                                    color = Color.White,
                                    strokeWidth = 3.dp
                            )
                        }
                    }
                    DownloadStatus.ERROR -> {
                        // Ошибка с кнопкой повтора
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Box(
                                    modifier =
                                            Modifier.size(48.dp)
                                                    .clip(CircleShape)
                                                    .background(
                                                            Color(0xFFE53935).copy(alpha = 0.8f)
                                                    ),
                                    contentAlignment = Alignment.Center
                            ) {
                                Icon(
                                        Icons.Default.Refresh,
                                        contentDescription = "Retry",
                                        tint = Color.White,
                                        modifier = Modifier.size(24.dp)
                                )
                            }
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(errorLabel, fontSize = 12.sp, color = Color.White)
                        }
                    }
                    else -> {}
                }
            }
        }
    }
}

private data class DesktopCallUi(
    val title: String,
    val subtitle: String,
    val isError: Boolean
)

private fun parseCallDurationSeconds(preview: String): Int {
    if (preview.isBlank()) return 0

    val tail = preview.substringAfterLast("::").trim()
    tail.toIntOrNull()?.let { return it.coerceAtLeast(0) }

    val durationRegex = Regex("duration(?:Sec|Seconds)?\\s*[:=]\\s*(\\d+)", RegexOption.IGNORE_CASE)
    durationRegex.find(preview)?.groupValues?.getOrNull(1)?.toIntOrNull()?.let {
        return it.coerceAtLeast(0)
    }

    return preview.trim().toIntOrNull()?.coerceAtLeast(0) ?: 0
}

private fun formatDesktopCallDuration(durationSec: Int): String {
    val minutes = durationSec / 60
    val seconds = durationSec % 60
    return "$minutes:${seconds.toString().padStart(2, '0')}"
}

private fun resolveDesktopCallUi(preview: String, isOutgoing: Boolean): DesktopCallUi {
    val durationSec = parseCallDurationSeconds(preview)
    val isError = durationSec == 0
    val title =
        if (isError) {
            if (isOutgoing) "Rejected call" else "Missed call"
        } else {
            if (isOutgoing) "Outgoing call" else "Incoming call"
        }
    val subtitle =
        if (isError) {
            "Call was not answered or was rejected"
        } else {
            formatDesktopCallDuration(durationSec)
        }
    return DesktopCallUi(title = title, subtitle = subtitle, isError = isError)
}

/** Call attachment bubble */
@Composable
fun CallAttachment(
        attachment: MessageAttachment,
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        timestamp: java.util.Date,
        messageStatus: MessageStatus = MessageStatus.READ
) {
    val callUi = remember(attachment.preview, isOutgoing) {
        resolveDesktopCallUi(attachment.preview, isOutgoing)
    }
    val containerShape = RoundedCornerShape(10.dp)
    val containerBackground =
        if (isOutgoing) {
            Color.White.copy(alpha = 0.12f)
        } else {
            if (isDarkTheme) Color(0xFF1F2733) else Color(0xFFF3F8FF)
        }
    val containerBorder =
        if (isOutgoing) {
            Color.White.copy(alpha = 0.2f)
        } else {
            if (isDarkTheme) Color(0xFF33435A) else Color(0xFFD8E5F4)
        }
    val iconBackground = if (callUi.isError) Color(0xFFE55A5A) else PrimaryBlue
    val iconVector =
        when {
            callUi.isError -> Icons.Default.Close
            isOutgoing -> Icons.Default.CallMade
            else -> Icons.Default.CallReceived
        }

    Box(
        modifier =
            Modifier
                .padding(vertical = 4.dp)
                .widthIn(min = 200.dp)
                .heightIn(min = 60.dp)
                .clip(containerShape)
                .background(containerBackground)
                .border(width = 1.dp, color = containerBorder, shape = containerShape)
                .padding(horizontal = 10.dp, vertical = 8.dp)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                    modifier =
                            Modifier.size(40.dp)
                                    .clip(CircleShape)
                                    .background(iconBackground),
                    contentAlignment = Alignment.Center
            ) {
                Icon(
                        imageVector = iconVector,
                        contentDescription = null,
                        tint = Color.White,
                        modifier = Modifier.size(20.dp)
                )
            }

            Spacer(modifier = Modifier.width(10.dp))

            Column(modifier = Modifier.weight(1f)) {
                Text(
                        text = callUi.title,
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Medium,
                        color = if (isOutgoing) Color.White else if (isDarkTheme) Color.White else Color.Black,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                )
                Spacer(modifier = Modifier.height(2.dp))
                Text(
                    text = callUi.subtitle,
                    fontSize = 12.sp,
                    color =
                        if (callUi.isError) {
                            Color(0xFFE55A5A)
                        } else if (isOutgoing) {
                            Color.White.copy(alpha = 0.72f)
                        } else {
                            if (isDarkTheme) Color(0xFF8EC9FF) else PrimaryBlue
                        },
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
            }

            if (isOutgoing) {
                Spacer(modifier = Modifier.width(8.dp))
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text(
                            text = android.text.format.DateFormat.format("HH:mm", timestamp).toString(),
                            fontSize = 11.sp,
                            color = Color.White.copy(alpha = 0.7f)
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    when (messageStatus) {
                        MessageStatus.SENDING -> {
                            Icon(
                                    painter = TelegramIcons.Clock,
                                    contentDescription = null,
                                    tint = Color.White.copy(alpha = 0.7f),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                        MessageStatus.SENT, MessageStatus.DELIVERED -> {
                            Icon(
                                    painter = TelegramIcons.Done,
                                    contentDescription = null,
                                    tint = Color.White.copy(alpha = 0.8f),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                        MessageStatus.READ -> {
                            Box(modifier = Modifier.height(14.dp)) {
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = null,
                                        tint = Color.White,
                                        modifier = Modifier.size(14.dp)
                                )
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = null,
                                        tint = Color.White,
                                        modifier = Modifier.size(14.dp).offset(x = 4.dp)
                                )
                            }
                        }
                        MessageStatus.ERROR -> {
                            Icon(
                                    imageVector = Icons.Default.Error,
                                    contentDescription = null,
                                    tint = Color(0xFFE53935),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                    }
                }
            }
        }    
    }
}

/** File attachment - Telegram style */
@Composable
fun FileAttachment(
        attachment: MessageAttachment,
        chachaKey: String,
        privateKey: String,
        currentUserPublicKey: String = "",
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        timestamp: java.util.Date,
        messageStatus: MessageStatus = MessageStatus.READ
) {
    val context = LocalContext.current
    var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
    var downloadProgress by remember { mutableStateOf(0f) }
    var isPaused by remember { mutableStateOf(false) }
    var lastActionAtMs by remember { mutableLongStateOf(0L) }

    // Bounce animation for icon
    val iconScale = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        iconScale.animateTo(
            targetValue = 1f,
            animationSpec = spring(
                dampingRatio = 0.5f, // Bouncy effect
                stiffness = 400f
            )
        )
    }

    val preview = attachment.preview
    val downloadTag = getDownloadTag(preview)
    val (fileSize, fileName) = parseFilePreview(preview)

    // Анимация прогресса
    val animatedProgress by
            animateFloatAsState(
                    targetValue = downloadProgress,
                    animationSpec = tween(durationMillis = 300),
                    label = "progress"
            )

    // Путь к скачанному файлу (как Desktop: ~/Rosetta Downloads/filename)
    val downloadsDir = remember { File(context.filesDir, "rosetta_downloads").apply { mkdirs() } }
    val savedFile = remember(fileName) { File(downloadsDir, fileName) }

    // 📥 Подписываемся на глобальный FileDownloadManager для live-прогресса
    val managerState by com.rosetta.messenger.network.FileDownloadManager
        .progressOf(attachment.id)
        .collectAsState(initial = null)

    // Синхронизируем локальный UI с глобальным менеджером
    LaunchedEffect(managerState) {
        val state = managerState ?: return@LaunchedEffect
        downloadProgress = state.progress
        downloadStatus = when (state.status) {
            com.rosetta.messenger.network.FileDownloadStatus.QUEUED,
            com.rosetta.messenger.network.FileDownloadStatus.DOWNLOADING -> DownloadStatus.DOWNLOADING
            com.rosetta.messenger.network.FileDownloadStatus.PAUSED -> DownloadStatus.DOWNLOADING
            com.rosetta.messenger.network.FileDownloadStatus.DECRYPTING -> DownloadStatus.DECRYPTING
            com.rosetta.messenger.network.FileDownloadStatus.DONE -> DownloadStatus.DOWNLOADED
            com.rosetta.messenger.network.FileDownloadStatus.ERROR -> DownloadStatus.ERROR
        }
        isPaused = state.status == com.rosetta.messenger.network.FileDownloadStatus.PAUSED
    }

    LaunchedEffect(attachment.id) {
        val existingState = com.rosetta.messenger.network.FileDownloadManager.stateOf(attachment.id)
        if (existingState != null) {
            downloadProgress = existingState.progress
            downloadStatus = when (existingState.status) {
                com.rosetta.messenger.network.FileDownloadStatus.QUEUED,
                com.rosetta.messenger.network.FileDownloadStatus.DOWNLOADING ->
                    DownloadStatus.DOWNLOADING
                com.rosetta.messenger.network.FileDownloadStatus.PAUSED ->
                    DownloadStatus.DOWNLOADING
                com.rosetta.messenger.network.FileDownloadStatus.DECRYPTING ->
                    DownloadStatus.DECRYPTING
                com.rosetta.messenger.network.FileDownloadStatus.DONE ->
                    DownloadStatus.DOWNLOADED
                com.rosetta.messenger.network.FileDownloadStatus.ERROR ->
                    DownloadStatus.ERROR
            }
            isPaused =
                existingState.status == com.rosetta.messenger.network.FileDownloadStatus.PAUSED
            return@LaunchedEffect
        }

        // Если менеджер уже качает этот файл — подхватим состояние оттуда
        if (com.rosetta.messenger.network.FileDownloadManager.isDownloading(attachment.id)) {
            downloadStatus = DownloadStatus.DOWNLOADING
            isPaused = false
            return@LaunchedEffect
        }
        downloadStatus = if (isDownloadTag(preview)) {
            // Проверяем, был ли файл уже скачан ранее
            if (savedFile.exists()) DownloadStatus.DOWNLOADED
            else DownloadStatus.NOT_DOWNLOADED
        } else {
            // Для отправленных файлов без тега — проверяем локальный файл
            if (savedFile.exists()) DownloadStatus.DOWNLOADED
            else DownloadStatus.DOWNLOADED // blob есть в памяти
        }
        isPaused = false
    }

    // Открыть файл через системное приложение
    val openFile: () -> Unit = openFile@{
        try {
            if (!savedFile.exists()) {
                // Файл ещё не скачан — переключаем статус чтобы UI показал кнопку загрузки
                if (downloadTag.isNotEmpty()) {
                    downloadStatus = DownloadStatus.NOT_DOWNLOADED
                } else {
                    android.widget.Toast.makeText(context, "File not available", android.widget.Toast.LENGTH_SHORT).show()
                }
                return@openFile
            }
            val uri = FileProvider.getUriForFile(
                context,
                "${context.packageName}.provider",
                savedFile
            )
            val ext = fileName.substringAfterLast('.', "").lowercase()
            val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
                ?: "application/octet-stream"
            val intent = Intent(Intent.ACTION_VIEW).apply {
                setDataAndType(uri, mimeType)
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }
            context.startActivity(intent)
        } catch (e: Exception) {
            android.widget.Toast.makeText(context, "Cannot open file: ${e.message}", android.widget.Toast.LENGTH_SHORT).show()
        }
    }

    // 📥 Запуск скачивания через глобальный FileDownloadManager
    val download: () -> Unit = {
        if (downloadTag.isNotEmpty()) {
            isPaused = false
            com.rosetta.messenger.network.FileDownloadManager.download(
                attachmentId = attachment.id,
                downloadTag = downloadTag,
                chachaKey = chachaKey,
                privateKey = privateKey,
                accountPublicKey = currentUserPublicKey,
                fileName = fileName,
                savedFile = savedFile
            )
        }
    }

    val pauseDownload: () -> Unit = {
        com.rosetta.messenger.network.FileDownloadManager.pause(attachment.id)
        isPaused = true
    }

    val resumeDownload: () -> Unit = {
        isPaused = false
        com.rosetta.messenger.network.FileDownloadManager.resume(attachment.id)
    }

    val isSendingUpload = isOutgoing && messageStatus == MessageStatus.SENDING
    val isDownloadInProgress =
        !isPaused &&
            (downloadStatus == DownloadStatus.DOWNLOADING ||
                downloadStatus == DownloadStatus.DECRYPTING)
    val actionState = when {
        downloadStatus == DownloadStatus.ERROR -> TelegramFileActionState.ERROR
        isSendingUpload -> TelegramFileActionState.CANCEL
        isDownloadInProgress -> TelegramFileActionState.PAUSE
        isPaused -> TelegramFileActionState.DOWNLOAD
        downloadStatus == DownloadStatus.NOT_DOWNLOADED -> TelegramFileActionState.DOWNLOAD
        else -> TelegramFileActionState.FILE
    }
    val actionProgress = if (isDownloadInProgress || isPaused) animatedProgress else null

    // Telegram-style файл - как в desktop: без внутреннего фона, просто иконка + текст
    Box(modifier = Modifier.fillMaxWidth()) {
        Row(
                modifier =
                        Modifier.fillMaxWidth()
                                .clickable(
                                        interactionSource = remember { MutableInteractionSource() },
                                        indication = null,
                                        enabled =
                                                !isSendingUpload &&
                                                        (downloadStatus == DownloadStatus.NOT_DOWNLOADED ||
                                                                downloadStatus == DownloadStatus.DOWNLOADED ||
                                                                downloadStatus == DownloadStatus.ERROR ||
                                                                isDownloadInProgress ||
                                                                isPaused)
                                ) {
                                    val now = SystemClock.elapsedRealtime()
                                    if (now - lastActionAtMs < 220L) return@clickable
                                    lastActionAtMs = now

                                    when {
                                        isPaused -> resumeDownload()
                                        downloadStatus == DownloadStatus.DOWNLOADING ||
                                                downloadStatus == DownloadStatus.DECRYPTING -> pauseDownload()
                                        downloadStatus == DownloadStatus.DOWNLOADED -> openFile()
                                        else -> download()
                                    }
                                }
                                .padding(vertical = 4.dp),
                verticalAlignment = Alignment.CenterVertically
        ) {
            // File icon с индикатором прогресса - круглая иконка как в desktop
            Box(
                modifier = Modifier
                    .size(40.dp)
                    .graphicsLayer {
                        scaleX = iconScale.value
                        scaleY = iconScale.value
                    },
                contentAlignment = Alignment.Center
            ) {
                TelegramFileActionButton(
                    state = actionState,
                    progress = actionProgress,
                    indeterminate = isSendingUpload,
                    modifier = Modifier.fillMaxSize()
                )
            }

            Spacer(modifier = Modifier.width(10.dp))

            // File info
            Column(modifier = Modifier.weight(1f)) {
                Text(
                        text = fileName,
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Normal,
                        color =
                                if (isOutgoing) Color.White
                                else (if (isDarkTheme) Color.White else Color.Black),
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                )
                Spacer(modifier = Modifier.height(2.dp))

                // Размер файла и тип
                val fileExtension = fileName.substringAfterLast('.', "").uppercase()
                val statusColor = if (downloadStatus == DownloadStatus.ERROR) {
                    Color(0xFFE53935)
                } else if (isOutgoing) {
                    Color.White.copy(alpha = 0.7f)
                } else {
                    PrimaryBlue
                }
                
                if (isSendingUpload) {
                    AnimatedDotsText(
                        baseText = "Uploading",
                        color = statusColor,
                        fontSize = 12.sp
                    )
                } else {
                    when (downloadStatus) {
                        DownloadStatus.DOWNLOADING -> {
                            if (isPaused) {
                                val cdnFraction = (downloadProgress / 0.8f).coerceIn(0f, 1f)
                                val downloadedBytes = (cdnFraction * fileSize).toLong()
                                Text(
                                    text = "Paused • ${formatFileSize(downloadedBytes)} / ${formatFileSize(fileSize)}",
                                    fontSize = 12.sp,
                                    color = statusColor
                                )
                            } else {
                                // Telegram-style: "1.2 MB / 5.4 MB"
                                // CDN download maps to progress 0..0.8
                                val cdnFraction = (downloadProgress / 0.8f).coerceIn(0f, 1f)
                                val downloadedBytes = (cdnFraction * fileSize).toLong()
                                Text(
                                    text = "${formatFileSize(downloadedBytes)} / ${formatFileSize(fileSize)}",
                                    fontSize = 12.sp,
                                    color = statusColor
                                )
                            }
                        }
                        DownloadStatus.DECRYPTING -> {
                            AnimatedDotsText(
                                baseText = "Decrypting",
                                color = statusColor,
                                fontSize = 12.sp
                            )
                        }
                        else -> {
                            Text(
                                text = when (downloadStatus) {
                                    DownloadStatus.ERROR -> "File expired"
                                    else -> "${formatFileSize(fileSize)} $fileExtension"
                                },
                                fontSize = 12.sp,
                                color = statusColor
                            )
                        }
                    }
                }
            }
        }

        // Time and checkmarks (bottom-right overlay) for outgoing files
        if (isOutgoing) {
            Row(
                    modifier =
                            Modifier.align(Alignment.BottomEnd).padding(bottom = 6.dp, end = 4.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.End
            ) {
                // Time
                Text(
                        text = android.text.format.DateFormat.format("HH:mm", timestamp).toString(),
                        fontSize = 11.sp,
                        color = Color.White.copy(alpha = 0.7f)
                )

                Spacer(modifier = Modifier.width(4.dp))

                // Checkmarks
                when (messageStatus) {
                    MessageStatus.SENDING -> {
                        Icon(
                                painter = TelegramIcons.Clock,
                                contentDescription = null,
                                tint = Color.White.copy(alpha = 0.7f),
                                modifier = Modifier.size(14.dp)
                        )
                    }
                    MessageStatus.SENT -> {
                        Icon(
                                painter = TelegramIcons.Done,
                                contentDescription = null,
                                tint =
                                        if (isDarkTheme) Color.White
                                        else Color.White.copy(alpha = 0.7f),
                                modifier = Modifier.size(14.dp)
                        )
                    }
                    MessageStatus.DELIVERED -> {
                        Icon(
                                painter = TelegramIcons.Done,
                                contentDescription = null,
                                tint =
                                        if (isDarkTheme) Color.White
                                        else Color.White.copy(alpha = 0.7f),
                                modifier = Modifier.size(14.dp)
                        )
                    }
                    MessageStatus.READ -> {
                        Box(modifier = Modifier.height(14.dp)) {
                            Icon(
                                    painter = TelegramIcons.Done,
                                    contentDescription = null,
                                    tint = Color.White,
                                    modifier = Modifier.size(14.dp)
                            )
                            Icon(
                                    painter = TelegramIcons.Done,
                                    contentDescription = null,
                                    tint = Color.White,
                                    modifier = Modifier.size(14.dp).offset(x = 4.dp)
                            )
                        }
                    }
                    MessageStatus.ERROR -> {
                        Icon(
                                Icons.Default.Error,
                                contentDescription = null,
                                tint = Color(0xFFE53935),
                                modifier = Modifier.size(14.dp)
                        )
                    }
                }
            }
        }
    }
}

@Composable
fun AvatarAttachment(
        attachment: MessageAttachment,
        chachaKey: String,
        privateKey: String,
        senderPublicKey: String,
        dialogPublicKey: String = "",
        isGroupChat: Boolean = false,
        avatarRepository: AvatarRepository?,
        currentUserPublicKey: String = "",
        isOutgoing: Boolean,
        isDarkTheme: Boolean,
        timestamp: java.util.Date = java.util.Date(),
        messageStatus: MessageStatus = MessageStatus.READ
) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
    var avatarBitmap by remember { mutableStateOf<Bitmap?>(null) }
    var blurhashBitmap by remember { mutableStateOf<Bitmap?>(null) }

    val preview = getPreview(attachment.preview)
    val downloadTag = getDownloadTag(attachment.preview)

    val timeFormat = remember { java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()) }

    // Bounce animation for avatar
    val avatarScale = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        avatarScale.animateTo(
            targetValue = 1f,
            animationSpec = spring(
                dampingRatio = 0.5f, // Bouncy effect
                stiffness = 400f
            )
        )
    }

    // Определяем начальный статус (как в Desktop calcDownloadStatus для AVATAR)
    LaunchedEffect(attachment.id) {
        withContext(Dispatchers.IO) {
            downloadStatus =
                    when {
                        // 1. Если blob уже есть в памяти → DOWNLOADED
                        attachment.blob.isNotEmpty() -> {
                            DownloadStatus.DOWNLOADED
                        }
                        // 2. Если preview НЕ содержит UUID → локальный файл → DOWNLOADED
                        !isDownloadTag(attachment.preview) -> {
                            DownloadStatus.DOWNLOADED
                        }
                        // 3. Есть UUID (download tag) → проверяем файловую систему
                        // Desktop: readFile(`a/${md5(attachment.id + publicKey)}`)
                        else -> {
                            val hasLocal =
                                    AvatarFileManager.hasAvatarByAttachmentId(
                                            context,
                                            attachment.id,
                                            senderPublicKey
                                    )
                            if (hasLocal) {
                                DownloadStatus.DOWNLOADED
                            } else {
                                DownloadStatus.NOT_DOWNLOADED
                            }
                        }
                    }
        }

        // Decode blurhash
        if (preview.length >= 20) {
            withContext(Dispatchers.IO) {
                try {
                    blurhashBitmap = BlurHash.decode(preview, 100, 100)
                } catch (e: Exception) {
                }
            }
        }

        // Загружаем аватар если статус DOWNLOADED
        if (downloadStatus == DownloadStatus.DOWNLOADED) {
            withContext(Dispatchers.IO) {
                // 1. Сначала пробуем blob из памяти
                if (attachment.blob.isNotEmpty()) {
                    avatarBitmap = base64ToBitmap(attachment.blob)
                } else {
                    // 2. Читаем из файловой системы (как в Desktop getBlob)
                    val localBlob =
                            AvatarFileManager.readAvatarByAttachmentId(
                                    context,
                                    attachment.id,
                                    senderPublicKey
                            )
                    if (localBlob != null) {
                        avatarBitmap = base64ToBitmap(localBlob)
                    } else {
                        downloadStatus = DownloadStatus.NOT_DOWNLOADED
                    }
                }
            }
        }
    }

    val download: () -> Unit = {
        if (downloadTag.isNotEmpty()) {
            scope.launch {
                try {
                    downloadStatus = DownloadStatus.DOWNLOADING

                    val startTime = System.currentTimeMillis()
                    val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
                    val downloadTime = System.currentTimeMillis() - startTime

                    downloadStatus = DownloadStatus.DECRYPTING

                    val decryptStartTime = System.currentTimeMillis()
                    val decrypted =
                            if (isGroupStoredKey(chachaKey)) {
                                val groupPassword = decodeGroupPassword(chachaKey, privateKey)
                                if (groupPassword.isNullOrBlank()) {
                                    null
                                } else {
                                    CryptoManager.decryptWithPassword(encryptedContent, groupPassword)
                                }
                            } else {
                                // КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
                                // Сначала расшифровываем его, получаем raw bytes
                                val decryptedKeyAndNonce =
                                        MessageCrypto.decryptKeyFromSender(chachaKey, privateKey)

                                // Используем decryptAttachmentBlobWithPlainKey который правильно
                                // конвертирует bytes в password
                                MessageCrypto.decryptAttachmentBlobWithPlainKey(
                                        encryptedContent,
                                        decryptedKeyAndNonce
                                )
                            }
                    val decryptTime = System.currentTimeMillis() - decryptStartTime

                    if (decrypted != null) {
                        withContext(Dispatchers.IO) {
                            avatarBitmap = base64ToBitmap(decrypted)

                            // 💾 Сохраняем в файловую систему по attachment.id (как в Desktop)
                            // Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted)
                            val path =
                                    AvatarFileManager.saveAvatarByAttachmentId(
                                            context = context,
                                            base64Image = decrypted,
                                            attachmentId = attachment.id,
                                            publicKey = senderPublicKey
                                    )
                        }
                        // Сохраняем аватар в репозиторий (для UI обновления)
                        // Если это исходящее сообщение с аватаром, сохраняем для текущего
                        // пользователя
                        val normalizedDialogKey = dialogPublicKey.trim()
                        val canonicalDialogKey = canonicalGroupDialogKey(normalizedDialogKey)
                        val isGroupAvatarAttachment = isGroupChat || isGroupStoredKey(chachaKey)
                        val targetPublicKey =
                                when {
                                    isGroupAvatarAttachment && canonicalDialogKey.isNotEmpty() ->
                                            canonicalDialogKey
                                    isOutgoing && currentUserPublicKey.isNotEmpty() ->
                                            currentUserPublicKey
                                    else -> senderPublicKey
                                }

                        // ВАЖНО: ждем завершения сохранения в репозиторий
                        if (avatarRepository != null) {
                            try {
                                avatarRepository.saveAvatar(targetPublicKey, decrypted)
                            } catch (e: Exception) {
                            }
                        } else {
                        }

                        downloadStatus = DownloadStatus.DOWNLOADED
                    } else {
                        downloadStatus = DownloadStatus.ERROR
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                    downloadStatus = DownloadStatus.ERROR
                }
            }
        } else {
        }
    }

    // Telegram-style avatar attachment с временем и статусом
    Row(
            modifier =
                    Modifier.fillMaxWidth()
                            .clip(RoundedCornerShape(12.dp))
                            .background(
                                    if (isOutgoing) {
                                        Color.White.copy(alpha = 0.12f)
                                    } else {
                                        if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
                                    }
                            )
                            .clickable(
                                    enabled =
                                            downloadStatus == DownloadStatus.NOT_DOWNLOADED ||
                                                    downloadStatus == DownloadStatus.ERROR
                            ) { download() }
                            .padding(12.dp),
            verticalAlignment = Alignment.CenterVertically
    ) {
        // Avatar preview - круглое изображение
        Box(
                modifier =
                        Modifier.size(56.dp)
                                .graphicsLayer {
                                    scaleX = avatarScale.value
                                    scaleY = avatarScale.value
                                }
                                .clip(CircleShape)
                                .background(
                                        if (isOutgoing) Color.White.copy(0.15f)
                                        else
                                                (if (isDarkTheme) Color(0xFF3A3A3A)
                                                else Color(0xFFE8E8E8))
                                ),
                contentAlignment = Alignment.Center
        ) {
            when {
                avatarBitmap != null -> {
                    Image(
                            bitmap = avatarBitmap!!.asImageBitmap(),
                            contentDescription = "Avatar",
                            modifier = Modifier.fillMaxSize(),
                            contentScale = ContentScale.Crop
                    )
                }
                blurhashBitmap != null -> {
                    Image(
                            bitmap = blurhashBitmap!!.asImageBitmap(),
                            contentDescription = "Avatar preview",
                            modifier = Modifier.fillMaxSize(),
                            contentScale = ContentScale.Crop
                    )
                }
                downloadStatus == DownloadStatus.DOWNLOADING ||
                        downloadStatus == DownloadStatus.DECRYPTING -> {
                    CircularProgressIndicator(
                            modifier = Modifier.size(24.dp),
                            color = if (isOutgoing) Color.White else PrimaryBlue,
                            strokeWidth = 2.dp
                    )
                }
                else -> {
                    Icon(
                            Icons.Default.Person,
                            contentDescription = null,
                            tint =
                                    if (isOutgoing) Color.White.copy(0.6f)
                                    else (if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray),
                            modifier = Modifier.size(28.dp)
                    )
                }
            }

            // Иконка скачивания поверх аватара если нужно скачать
            if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
                Box(
                        modifier =
                                Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.4f)),
                        contentAlignment = Alignment.Center
                ) {
                    Icon(
                            Icons.Default.ArrowDownward,
                            contentDescription = "Download",
                            tint = Color.White,
                            modifier = Modifier.size(24.dp)
                    )
                }
            }
        }

        Spacer(modifier = Modifier.width(12.dp))

        // Info и время/статус
        Column(modifier = Modifier.weight(1f)) {
            // Заголовок с иконкой замка
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(
                        text = "Profile Photo",
                        fontSize = 15.sp,
                        fontWeight = FontWeight.SemiBold,
                        color =
                                if (isOutgoing) Color.White
                                else (if (isDarkTheme) Color.White else Color.Black)
                )
                Spacer(modifier = Modifier.width(6.dp))
                Icon(
                        Icons.Default.Lock,
                        contentDescription = "End-to-end encrypted",
                        tint =
                                if (isOutgoing) Color.White.copy(0.6f)
                                else (if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray),
                        modifier = Modifier.size(12.dp)
                )
            }

            Spacer(modifier = Modifier.height(2.dp))

            // Описание статуса
            val avatarStatusColor = if (isOutgoing) Color.White.copy(alpha = 0.7f)
                else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray)
            
            when (downloadStatus) {
                DownloadStatus.DOWNLOADING -> {
                    AnimatedDotsText(
                        baseText = "Downloading",
                        color = avatarStatusColor,
                        fontSize = 13.sp
                    )
                }
                DownloadStatus.DECRYPTING -> {
                    AnimatedDotsText(
                        baseText = "Decrypting",
                        color = avatarStatusColor,
                        fontSize = 13.sp
                    )
                }
                else -> {
                    Text(
                        text = when (downloadStatus) {
                            DownloadStatus.ERROR -> "Tap to retry"
                            DownloadStatus.DOWNLOADED -> "Shared profile photo"
                            else -> "Tap to download"
                        },
                        fontSize = 13.sp,
                        color = avatarStatusColor
                    )
                }
            }

            Spacer(modifier = Modifier.height(4.dp))

            // Время и статус доставки
            Row(
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                Text(
                        text = timeFormat.format(timestamp),
                        fontSize = 11.sp,
                        color =
                                if (isOutgoing) Color.White.copy(alpha = 0.6f)
                                else
                                        (if (isDarkTheme) Color.White.copy(alpha = 0.4f)
                                        else Color.Gray)
                )

                // Галочки статуса для исходящих сообщений
                if (isOutgoing) {
                    when (messageStatus) {
                        MessageStatus.SENDING -> {
                            Icon(
                                    painter = TelegramIcons.Clock,
                                    contentDescription = "Sending",
                                    tint = Color.White.copy(alpha = 0.6f),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                        MessageStatus.SENT -> {
                            Icon(
                                    painter = TelegramIcons.Done,
                                    contentDescription = "Sent",
                                    tint =
                                            if (isDarkTheme) Color.White
                                            else Color.White.copy(alpha = 0.6f),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                        MessageStatus.DELIVERED -> {
                            Icon(
                                    painter = TelegramIcons.Done,
                                    contentDescription = "Delivered",
                                    tint =
                                            if (isDarkTheme) Color.White
                                            else Color.White.copy(alpha = 0.6f),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                        MessageStatus.READ -> {
                            Box(modifier = Modifier.height(14.dp)) {
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = "Read",
                                        tint = Color.White,
                                        modifier = Modifier.size(14.dp)
                                )
                                Icon(
                                        painter = TelegramIcons.Done,
                                        contentDescription = "Read",
                                        tint = Color.White,
                                        modifier = Modifier.size(14.dp).offset(x = 4.dp)
                                )
                            }
                        }
                        MessageStatus.ERROR -> {
                            Icon(
                                    Icons.Default.Error,
                                    contentDescription = "Error",
                                    tint = Color(0xFFE53935),
                                    modifier = Modifier.size(14.dp)
                            )
                        }
                    }
                }
            }
        }

        // Иконка ошибки справа
        if (downloadStatus == DownloadStatus.ERROR) {
            Icon(
                    Icons.Default.Refresh,
                    contentDescription = "Retry",
                    tint = Color(0xFFE53935),
                    modifier = Modifier.size(24.dp)
            )
        }
    }
}

// ================================
// Helper functions
// ================================

private val uuidRegex =
        Regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")

/**
 * Проверка является ли preview UUID тегом для скачивания Как в desktop:
 * attachment.preview.split("::")[0].match(uuidRegex)
 */
internal fun isDownloadTag(preview: String): Boolean {
    val firstPart = preview.split("::").firstOrNull() ?: return false
    return uuidRegex.matches(firstPart)
}

/** Получить download tag из preview */
internal fun getDownloadTag(preview: String): String {
    val parts = preview.split("::")
    if (parts.isNotEmpty() && uuidRegex.matches(parts[0])) {
        return parts[0]
    }
    return ""
}

/** Получить preview без download tag Как в desktop: preview.split("::").splice(1).join("::") */
internal fun getPreview(preview: String): String {
    val parts = preview.split("::")
    if (parts.isNotEmpty() && uuidRegex.matches(parts[0])) {
        return parts.drop(1).joinToString("::")
    }
    return preview
}

/** Парсинг preview для файлов Формат: "UUID::filesize::filename" или "filesize::filename" */
private fun parseFilePreview(preview: String): Pair<Long, String> {
    val parts = preview.split("::")
    return when {
        parts.size >= 3 && uuidRegex.matches(parts[0]) -> {
            // UUID::size::filename
            val size = parts[1].toLongOrNull() ?: 0L
            val name = parts.drop(2).joinToString("::")
            Pair(size, name)
        }
        parts.size >= 2 && !uuidRegex.matches(parts[0]) -> {
            // size::filename
            val size = parts[0].toLongOrNull() ?: 0L
            Pair(size, parts.drop(1).joinToString("::"))
        }
        parts.size >= 2 -> {
            // UUID::filename (no size)
            Pair(0L, parts.drop(1).joinToString("::"))
        }
        else -> Pair(0L, "File")
    }
}

/** Декодирование base64 в Bitmap */
internal fun base64ToBitmap(base64: String): Bitmap? {
    return try {
        val payload = extractBase64Payload(base64) ?: return null
        val bytes = Base64.decode(payload, Base64.DEFAULT)
        if (bytes.isEmpty()) return null

        val decoded = decodeBitmapWithSampling(bytes) ?: return null

        val orientation =
                runCatching {
                            ByteArrayInputStream(bytes).use { stream ->
                                ExifInterface(stream)
                                        .getAttributeInt(
                                                ExifInterface.TAG_ORIENTATION,
                                                ExifInterface.ORIENTATION_NORMAL
                                        )
                            }
                        }
                        .getOrDefault(ExifInterface.ORIENTATION_NORMAL)

        applyExifOrientation(decoded, orientation)
    } catch (e: Exception) {
        null
    } catch (e: OutOfMemoryError) {
        null
    }
}

private fun extractBase64Payload(value: String): String? {
    val trimmed = value.trim()
    if (trimmed.isEmpty()) return null

    val payload =
            when {
                trimmed.startsWith("data:", ignoreCase = true) &&
                        trimmed.contains("base64,", ignoreCase = true) -> {
                    trimmed.substringAfter("base64,", "")
                }
                trimmed.contains(",") &&
                        trimmed.substringBefore(",").contains("base64", ignoreCase = true) -> {
                    trimmed.substringAfter(",", "")
                }
                else -> trimmed
            }

    val clean = payload.replace(whitespaceRegex, "")
    return clean.takeIf { it.isNotEmpty() }
}

private fun decodeBitmapWithSampling(bytes: ByteArray): Bitmap? {
    val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
    BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
    if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null

    var sampleSize = 1
    while ((bounds.outWidth / sampleSize) > MAX_BITMAP_DECODE_DIMENSION ||
            (bounds.outHeight / sampleSize) > MAX_BITMAP_DECODE_DIMENSION) {
        sampleSize *= 2
    }

    repeat(4) {
        val options =
                BitmapFactory.Options().apply {
                    inSampleSize = sampleSize
                    inPreferredConfig = Bitmap.Config.ARGB_8888
                }
        val bitmap = runCatching { BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) }.getOrNull()
        if (bitmap != null) return bitmap
        sampleSize *= 2
    }

    return null
}

private fun applyExifOrientation(bitmap: Bitmap, orientation: Int): Bitmap {
    if (orientation == ExifInterface.ORIENTATION_NORMAL ||
            orientation == ExifInterface.ORIENTATION_UNDEFINED) {
        return bitmap
    }

    val matrix = Matrix()
    when (orientation) {
        ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
        ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
        ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
        ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
        ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
        ExifInterface.ORIENTATION_TRANSPOSE -> {
            matrix.postRotate(90f)
            matrix.preScale(-1f, 1f)
        }
        ExifInterface.ORIENTATION_TRANSVERSE -> {
            matrix.postRotate(270f)
            matrix.preScale(-1f, 1f)
        }
        else -> return bitmap
    }

    return try {
        val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        if (rotated != bitmap) {
            bitmap.recycle()
        }
        rotated
    } catch (_: Exception) {
        bitmap
    } catch (_: OutOfMemoryError) {
        bitmap
    }
}

/**
 * Process downloaded encrypted content: decrypt → decode → cache → save.
 * Extracted to avoid duplication between first attempt and retry.
 */
private suspend fun processDownloadedImage(
    encryptedContent: String,
    chachaKey: String,
    privateKey: String,
    attachment: MessageAttachment,
    senderPublicKey: String,
    context: android.content.Context,
    cacheKey: String,
    idShort: String,
    onProgress: (Float) -> Unit,
    onStatus: (DownloadStatus) -> Unit,
    onBitmap: (Bitmap?) -> Unit,
    onError: (String) -> Unit
) {
    onProgress(0.5f)
    onStatus(DownloadStatus.DECRYPTING)

    // Расшифровываем ключ
    var keyCandidates: List<ByteArray> = emptyList()
    var groupPassword: String? = null
    try {
        if (isGroupStoredKey(chachaKey)) {
            groupPassword = decodeGroupPassword(chachaKey, privateKey)
            if (groupPassword.isNullOrBlank()) {
                throw IllegalArgumentException("empty group password")
            }
            logPhotoDebug("Group key decrypt OK: id=$idShort, keyLen=${groupPassword.length}")
        } else {
            keyCandidates = MessageCrypto.decryptKeyFromSenderCandidates(chachaKey, privateKey)
            if (keyCandidates.isEmpty()) {
                throw IllegalArgumentException("empty key candidates")
            }
            logPhotoDebug("Key decrypt OK: id=$idShort, candidates=${keyCandidates.size}, keySize=${keyCandidates.first().size}")
            keyCandidates.forEachIndexed { idx, candidate ->
                val keyHead = candidate.take(8).joinToString("") { "%02x".format(it.toInt() and 0xff) }
                logPhotoDebug("Key material[$idx]: id=$idShort, keyFp=${shortDebugHash(candidate)}, keyHead=$keyHead, keySize=${candidate.size}")
            }
        }
    } catch (e: Exception) {
        onError("Error")
        onStatus(DownloadStatus.ERROR)
        val keyPrefix =
                when {
                    chachaKey.startsWith("sync:") -> "sync"
                    chachaKey.startsWith("group:") -> "group"
                    else -> "ecdh"
                }
        logPhotoDebug("Key decrypt FAILED: id=$idShort, keyType=$keyPrefix, keyLen=${chachaKey.length}, err=${e.javaClass.simpleName}: ${e.message?.take(80)}")
        return
    }

    // Расшифровываем контент
    val decryptStartTime = System.currentTimeMillis()
    var successKeyIdx = -1
    var decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(null, emptyList())
    val decrypted =
            if (groupPassword != null) {
                val plain = CryptoManager.decryptWithPassword(encryptedContent, groupPassword!!)
                decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(plain, emptyList())
                plain
            } else {
                var value: String? = null
                for ((idx, keyCandidate) in keyCandidates.withIndex()) {
                    val attempt = MessageCrypto.decryptAttachmentBlobWithPlainKeyDebug(encryptedContent, keyCandidate)
                    if (attempt.decrypted != null) {
                        successKeyIdx = idx
                        decryptDebug = attempt
                        value = attempt.decrypted
                        break
                    }
                    // Keep last trace for diagnostics if all fail.
                    decryptDebug = attempt
                }
                value
            }
    val decryptTime = System.currentTimeMillis() - decryptStartTime
    onProgress(0.8f)

    if (decrypted != null) {
        var decodedBitmap: Bitmap? = null
        var saved = false
        logPhotoDebug("Blob decrypt OK: id=$idShort, time=${decryptTime}ms, decryptedLen=${decrypted.length}, keyIdx=$successKeyIdx")
        decryptDebug.trace.lastOrNull { it.contains("SUCCESS") }?.let { successLine ->
            logPhotoDebug("Blob decrypt trace: id=$idShort, $successLine")
        }
        withContext(Dispatchers.IO) {
            try {
                decodedBitmap = base64ToBitmap(decrypted)
            } catch (oom: OutOfMemoryError) {
                System.gc()
                logPhotoDebug("Bitmap OOM: id=$idShort, decryptedLen=${decrypted.length}")
            }
            if (decodedBitmap != null) {
                onBitmap(decodedBitmap)
                ImageBitmapCache.put(cacheKey, decodedBitmap!!)

                saved = AttachmentFileManager.saveAttachment(
                    context = context,
                    blob = decrypted,
                    attachmentId = attachment.id,
                    publicKey = senderPublicKey,
                    privateKey = privateKey
                )
            }
        }
        if (decodedBitmap != null) {
            onProgress(1f)
            onStatus(DownloadStatus.DOWNLOADED)
            logPhotoDebug("Image ready: id=$idShort, saved=$saved")
        } else {
            onError("Error")
            onStatus(DownloadStatus.ERROR)
            val preview = decrypted.take(30).replace("\n", "")
            logPhotoDebug("Image decode FAILED: id=$idShort, decryptedLen=${decrypted.length}, preview=$preview")
        }
    } else {
        onError("Error")
        onStatus(DownloadStatus.ERROR)
        val keyPrefix =
                when {
                    chachaKey.startsWith("sync:") -> "sync"
                    chachaKey.startsWith("group:") -> "group"
                    else -> "ecdh"
                }
        val firstKeySize = keyCandidates.firstOrNull()?.size ?: -1
        logPhotoDebug("Blob decrypt FAILED: id=$idShort, time=${decryptTime}ms, contentLen=${encryptedContent.length}, keyType=$keyPrefix, keyNonceSize=$firstKeySize, keyCandidates=${keyCandidates.size}")
        decryptDebug.trace.take(96).forEachIndexed { index, line ->
            logPhotoDebug("Blob decrypt TRACE[$index]: id=$idShort, $line")
        }
    }
}

/**
 * CDN download + decrypt + cache + save.
 * Shared between ReplyBubble and ForwardedImagePreview.
 *
 * @return loaded Bitmap or null
 */
internal suspend fun downloadAndDecryptImage(
    attachmentId: String,
    downloadTag: String,
    chachaKey: String,
    privateKey: String,
    cacheKey: String,
    context: android.content.Context,
    senderPublicKey: String,
    recipientPrivateKey: String
): Bitmap? {
    if (downloadTag.isEmpty() || chachaKey.isEmpty() || privateKey.isEmpty()) return null

    return withContext(Dispatchers.IO) {
        val idShort = shortDebugId(attachmentId)
        val tagShort = shortDebugId(downloadTag)
        val server = TransportManager.getTransportServer() ?: "unset"
        try {
            logPhotoDebug("Start helper image download: id=$idShort, tag=$tagShort, server=$server")
            val encryptedContent = TransportManager.downloadFile(attachmentId, downloadTag)
            if (encryptedContent.isEmpty()) return@withContext null
            logPhotoDebug(
                    "Helper CDN download OK: id=$idShort, bytes=${encryptedContent.length}"
            )

            val decrypted: String? =
                    if (isGroupStoredKey(chachaKey)) {
                        val groupPassword = decodeGroupPassword(chachaKey, privateKey) ?: return@withContext null
                        CryptoManager.decryptWithPassword(encryptedContent, groupPassword)
                    } else {
                        val keyCandidates = MessageCrypto.decryptKeyFromSenderCandidates(chachaKey, privateKey)
                        if (keyCandidates.isEmpty()) return@withContext null
                        val plainKeyAndNonce = keyCandidates.first()
                        logPhotoDebug(
                                "Helper key decrypt OK: id=$idShort, candidates=${keyCandidates.size}, keySize=${plainKeyAndNonce.size}"
                        )
                        logPhotoDebug(
                                "Helper key material: id=$idShort, keyFp=${shortDebugHash(plainKeyAndNonce)}"
                        )

                        // Primary path for image attachments
                        var decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(null, emptyList())
                        var value: String? = null
                        for ((idx, keyCandidate) in keyCandidates.withIndex()) {
                            val attempt =
                                    MessageCrypto.decryptAttachmentBlobWithPlainKeyDebug(
                                            encryptedContent,
                                            keyCandidate
                                    )
                            if (attempt.decrypted != null) {
                                decryptDebug = attempt
                                value = attempt.decrypted
                                logPhotoDebug("Helper decrypt OK: id=$idShort, keyIdx=$idx")
                                break
                            }
                            decryptDebug = attempt
                        }

                        // Fallback for legacy payloads
                        if (value.isNullOrEmpty()) {
                            decryptDebug.trace.takeLast(12).forEachIndexed { index, line ->
                                logPhotoDebug("Helper decrypt TRACE[$index]: id=$idShort, $line")
                            }
                            value =
                                    try {
                                        MessageCrypto.decryptReplyBlob(encryptedContent, plainKeyAndNonce)
                                                .takeIf { it.isNotEmpty() && it != encryptedContent }
                                    } catch (_: Exception) {
                                        null
                                    }
                        }
                        value
                    }

            if (decrypted.isNullOrEmpty()) return@withContext null

            val base64Data = extractBase64Payload(decrypted) ?: return@withContext null
            val bitmap = base64ToBitmap(base64Data) ?: return@withContext null

            ImageBitmapCache.put(cacheKey, bitmap)
            val saved = AttachmentFileManager.saveAttachment(
                context = context,
                blob = base64Data,
                attachmentId = attachmentId,
                publicKey = senderPublicKey,
                privateKey = recipientPrivateKey
            )
            logPhotoDebug("Helper image ready: id=$idShort, saved=$saved")

            bitmap
        } catch (e: Exception) {
            logPhotoDebug(
                    "Helper image ERROR: id=$idShort, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}"
            )
            null
        }
    }
}

/** Форматирование размера файла */
private fun formatFileSize(bytes: Long): String {
    return when {
        bytes < 1024 -> "$bytes B"
        bytes < 1024 * 1024 -> "${bytes / 1024} KB"
        bytes < 1024 * 1024 * 1024 -> String.format("%.1f MB", bytes / (1024.0 * 1024.0))
        else -> String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0))
    }
}
