package com.seven.ikar.kotlin.feature.chat

import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Base64
import android.util.Patterns
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.Send
import androidx.compose.material.icons.outlined.AttachFile
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.Call
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Forum
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowUp
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material.icons.outlined.Mic
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.Mood
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Stop
import androidx.compose.material.icons.outlined.Videocam
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.seven.ikar.kotlin.core.model.AttachmentDto
import com.seven.ikar.kotlin.core.model.BotCommandDto
import com.seven.ikar.kotlin.core.model.ChatType
import com.seven.ikar.kotlin.core.model.MessageDeliveryState
import com.seven.ikar.kotlin.core.model.MessageDto
import com.seven.ikar.kotlin.core.model.MessageReactionDto
import com.seven.ikar.kotlin.core.network.ServerConfig
import com.seven.ikar.kotlin.core.push.IncomingCallFullScreenSetupHelper
import com.seven.ikar.kotlin.core.push.IncomingCallFullScreenSetupResult
import com.seven.ikar.kotlin.core.settings.ChatSettingsSnapshot
import com.seven.ikar.kotlin.ui.AuthenticatedAsyncImage
import com.seven.ikar.kotlin.ui.AvatarView
import com.seven.ikar.kotlin.ui.ChatWallpaperBackground
import com.seven.ikar.kotlin.ui.IkarHeaderIconBubble
import com.seven.ikar.kotlin.ui.SecureWindowEffect
import com.seven.ikar.kotlin.ui.theme.TelegramBlue
import com.seven.ikar.kotlin.ui.theme.TelegramDanger
import com.seven.ikar.kotlin.ui.theme.TelegramInk
import com.seven.ikar.kotlin.ui.theme.TelegramInkMuted
import com.seven.ikar.kotlin.ui.theme.TelegramSurface
import java.time.Instant
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch

private sealed interface ChatRow {
    data class DateSeparator(val id: String, val text: String) : ChatRow
    data class Message(val item: MessageDto) : ChatRow
}

private enum class LocalMessageStatus {
    Sending,
    Failed
}

private fun ChatUiState.localMessageStatus(messageId: String): LocalMessageStatus? = when {
    sendingLocalMessageIds.contains(messageId) -> LocalMessageStatus.Sending
    failedLocalMessageIds.contains(messageId) -> LocalMessageStatus.Failed
    else -> null
}

private data class ChatImagePreviewEntry(
    val message: MessageDto,
    val attachment: AttachmentDto
)

private data class ChatVideoPreviewEntry(
    val message: MessageDto,
    val attachment: AttachmentDto
)

private data class EmojiGridItem(
    val index: Int,
    val categoryKey: String,
    val categoryIndex: Int,
    val emoji: String
)

private val ChatTimeFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale("ru", "RU"))
private val ChatDayFormatter = DateTimeFormatter.ofPattern("d MMMM", Locale("ru", "RU"))
private val ChatDayYearFormatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale("ru", "RU"))

private sealed interface EmojiPickerMode {
    object Composer : EmojiPickerMode
    data class Reaction(val message: MessageDto) : EmojiPickerMode
}

private enum class EmojiKeyboardTab {
    Emoji,
    Gif,
    Stickers
}

private val EmojiKeyboardFallbackHeight = 300.dp
private val EmojiKeyboardMinHeight = 220.dp
private val EmojiKeyboardMaxHeight = 360.dp
private const val EmojiGridColumns = 8
private const val EmojiGridVisibleRows = 4
private val EmojiCategoryStripHeight = 47.dp
private val EmojiKeyboardBottomTabsHeight = 53.dp
private val EmojiGridCellHeight = 46.dp
private val PickerMediaGridMinCellWidth = 92.dp
private val PickerMediaGridCellHeight = 112.dp
private val PickerSuggestionPanelHeight = 176.dp
private val PickerSuggestionGridMinCellWidth = 68.dp
private val PickerSuggestionGridCellHeight = 76.dp
private const val EmojiKeyboardAnimationMillis = 220

@OptIn(FlowPreview::class)
@Composable
fun ChatScreen(
    state: ChatUiState,
    viewModel: ChatViewModel,
    chatSettings: ChatSettingsSnapshot,
    largeEmojiEnabled: Boolean,
    animatedBackgroundsEnabled: Boolean,
    onBackClick: () -> Unit,
    onPeerProfileClick: (() -> Unit)? = null,
    onChatNotificationsClick: () -> Unit,
    onChatWallpaperClick: () -> Unit,
    onOpenLinkedDiscussion: (String, String?) -> Unit = { _, _ -> },
    onOpenUrl: (String) -> Unit = {},
    onForwardMessage: (MessageDto) -> Unit
) {
    val clipboard = LocalClipboard.current
    val context = LocalContext.current
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    val rows = remember(state.messages) { buildChatRows(state.messages) }
    val latestMessageId = remember(state.messages) { state.messages.lastOrNull()?.id }
    val savedScrollPosition = remember(state.chatId) { viewModel.readSavedScrollPosition() }
    val firstUnreadRowIndex = remember(rows, state.firstUnreadMessageId) {
        state.firstUnreadMessageId
            ?.let { unreadMessageId -> rows.indexOfFirst { row -> row is ChatRow.Message && row.item.id == unreadMessageId } }
            ?.takeIf { it >= 0 }
    }
    val currentUserId = state.session?.user?.id
    val imagePreviewEntries = remember(state.messages) {
        state.messages.flatMap { message ->
            message.attachments
                .filter { attachment -> attachment.isImageAttachment() }
                .map { attachment -> ChatImagePreviewEntry(message = message, attachment = attachment) }
        }
    }
    val videoPreviewEntries = remember(state.messages) {
        state.messages.flatMap { message ->
            message.attachments
                .filter { attachment -> attachment.isVideoAttachment() }
                .map { attachment -> ChatVideoPreviewEntry(message = message, attachment = attachment) }
        }
    }
    val currentImagePreviewIndex = remember(state.previewImageAttachmentId, imagePreviewEntries) {
        state.previewImageAttachmentId
            ?.let { attachmentId -> imagePreviewEntries.indexOfFirst { it.attachment.id == attachmentId } }
            ?.takeIf { it >= 0 }
    }
    val currentImagePreviewEntry = remember(currentImagePreviewIndex, imagePreviewEntries) {
        currentImagePreviewIndex?.let(imagePreviewEntries::getOrNull)
    }
    val currentVideoPreviewEntry = remember(state.previewVideoAttachmentId, videoPreviewEntries) {
        state.previewVideoAttachmentId
            ?.let { attachmentId -> videoPreviewEntries.firstOrNull { it.attachment.id == attachmentId } }
    }
    val hasProtectedContentVisible = remember(state.messages, currentImagePreviewEntry, currentVideoPreviewEntry) {
        state.messages.any { it.hasProtectedContent && !it.isDeleted() } ||
            currentImagePreviewEntry?.message?.hasProtectedContent == true ||
            currentVideoPreviewEntry?.message?.hasProtectedContent == true
    }
    val searchResultIds = remember(state.search.resultMessageIds) { state.search.resultMessageIds.toSet() }
    val activeSearchMessageId = state.search.activeMessageId

    SecureWindowEffect(enabled = hasProtectedContentVisible)

    LaunchedEffect(state.isChatDeleted) {
        if (state.isChatDeleted) {
            onBackClick()
        }
    }

    var emojiPickerMode by remember { mutableStateOf<EmojiPickerMode?>(null) }
    var displayedEmojiPickerMode by remember { mutableStateOf<EmojiPickerMode?>(null) }
    var emojiKeyboardTab by remember { mutableStateOf(EmojiKeyboardTab.Emoji) }
    var frequentEmojis by remember(context) { mutableStateOf(loadFrequentEmojis(context)) }
    val emojiCategories = remember(frequentEmojis) { emojiPickerCategories(frequentEmojis) }
    var suggestedPickerEmoji by remember(state.chatId) { mutableStateOf<String?>(null) }
    val suggestedPickerItems = remember(suggestedPickerEmoji) {
        suggestedPickerMediaForEmoji(
            emoji = suggestedPickerEmoji,
            gifPacks = BuiltInGifPacks,
            stickerPacks = BuiltInStickerPacks
        )
    }
    val density = LocalDensity.current
    val keyboardController = LocalSoftwareKeyboardController.current
    var rememberedKeyboardHeight by remember { mutableStateOf(EmojiKeyboardFallbackHeight) }
    val currentKeyboardHeight = with(density) {
        (WindowInsets.ime.getBottom(this) - WindowInsets.navigationBars.getBottom(this))
            .coerceAtLeast(0)
            .toDp()
    }
    val emojiKeyboardHeight = rememberedKeyboardHeight.coerceIn(
        EmojiKeyboardMinHeight,
        EmojiKeyboardMaxHeight
    )
    var showHeaderMenu by remember { mutableStateOf(false) }
    var showClearHistoryConfirm by remember { mutableStateOf(false) }
    var showDeleteChatConfirm by remember { mutableStateOf(false) }
    var deleteChatForEveryone by remember { mutableStateOf(false) }
    var showBotCommandsDialog by remember { mutableStateOf(false) }
    var showBotThreadSelection by remember { mutableStateOf(false) }
    var messageMenuTarget by remember { mutableStateOf<MessageDto?>(null) }
    var deleteCandidate by remember { mutableStateOf<MessageDto?>(null) }
    var isInitialScrollApplied by remember(state.chatId) { mutableStateOf(false) }
    var observedLatestMessageId by remember(state.chatId) { mutableStateOf<String?>(null) }
    var pendingVideoCallStartAfterSetup by remember { mutableStateOf(false) }
    val showScrollToLatestButton by remember(listState, rows) {
        derivedStateOf {
            rows.isNotEmpty() &&
                (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 48)
        }
    }
    val singleSelectedMessageId = remember(state.selectedMessageIds) {
        state.selectedMessageIds.singleOrNull()
    }
    val singleSelectedMessage = remember(singleSelectedMessageId, state.messages) {
        singleSelectedMessageId?.let { selectedId ->
            state.messages.firstOrNull { it.id == selectedId }
        }
    }

    LaunchedEffect(currentKeyboardHeight) {
        if (currentKeyboardHeight > 0.dp) {
            rememberedKeyboardHeight = currentKeyboardHeight
        }
    }

    LaunchedEffect(emojiPickerMode) {
        emojiPickerMode?.let { displayedEmojiPickerMode = it }
        if (emojiPickerMode !is EmojiPickerMode.Composer) {
            suggestedPickerEmoji = null
        }
        if (emojiPickerMode is EmojiPickerMode.Reaction) {
            emojiKeyboardTab = EmojiKeyboardTab.Emoji
        }
    }

    BackHandler(enabled = state.isMessageSelectionMode) {
        viewModel.clearMessageSelection()
    }

    BackHandler(enabled = state.search.isActive && !state.isMessageSelectionMode) {
        viewModel.closeMessageSearch()
    }

    BackHandler(enabled = emojiPickerMode != null) {
        suggestedPickerEmoji = null
        emojiPickerMode = null
    }

    var continueVideoCallAfterIncomingCallSetup: (() -> Unit)? = null
    var runVideoCallIncomingCallPreflight: (() -> Unit)? = null
    var requestVideoCallFullScreenNotificationPermission: (() -> Unit)? = null

    val attachmentPicker = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
        if (uris.isNotEmpty()) {
            viewModel.consumeAttachmentUris(uris)
        }
    }
    val microphonePermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            viewModel.beginVoiceRecording()
        } else {
            viewModel.setErrorMessage("Нет доступа к микрофону.")
        }
    }

    val audioCallPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            viewModel.startAudioCall()
        } else {
            viewModel.setErrorMessage("Нет доступа к микрофону.")
        }
    }
    val videoCallPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        val hasMicrophone = permissions[Manifest.permission.RECORD_AUDIO] == true
        val hasCamera = permissions[Manifest.permission.CAMERA] == true
        if (hasMicrophone && hasCamera) {
            pendingVideoCallStartAfterSetup = false
            viewModel.startVideoCall()
        } else {
            pendingVideoCallStartAfterSetup = false
            val missing = buildList {
                if (!hasMicrophone) add("микрофону")
                if (!hasCamera) add("камере")
            }.joinToString(" и ")
            viewModel.setErrorMessage("Нет доступа к $missing.")
        }
    }
    val fullScreenCallSetupSettingsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (!pendingVideoCallStartAfterSetup) {
            return@rememberLauncherForActivityResult
        }

        when (val result = IncomingCallFullScreenSetupHelper.evaluate(context)) {
            IncomingCallFullScreenSetupResult.Ready -> {
                continueVideoCallAfterIncomingCallSetup?.invoke()
            }

            IncomingCallFullScreenSetupResult.NeedsPostNotificationsPermission -> {
                requestVideoCallFullScreenNotificationPermission?.invoke()
            }

            is IncomingCallFullScreenSetupResult.NeedsSystemSettings -> {
                pendingVideoCallStartAfterSetup = false
                viewModel.setErrorMessage(result.detail)
            }
        }
    }
    val fullScreenCallNotificationPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (!pendingVideoCallStartAfterSetup) {
            return@rememberLauncherForActivityResult
        }

        if (!granted) {
            pendingVideoCallStartAfterSetup = false
            viewModel.setErrorMessage("Без разрешения на уведомления полноэкранный входящий звонок на заблокированном экране может не появиться.")
            return@rememberLauncherForActivityResult
        }

        when (val result = IncomingCallFullScreenSetupHelper.evaluate(context)) {
            IncomingCallFullScreenSetupResult.Ready -> {
                continueVideoCallAfterIncomingCallSetup?.invoke()
            }

            IncomingCallFullScreenSetupResult.NeedsPostNotificationsPermission -> {
                pendingVideoCallStartAfterSetup = false
                viewModel.setErrorMessage("Android не выдал разрешение на уведомления для Икар.")
            }

            is IncomingCallFullScreenSetupResult.NeedsSystemSettings -> {
                viewModel.setErrorMessage(result.detail)
                fullScreenCallSetupSettingsLauncher.launch(result.intent)
            }
        }
    }
    requestVideoCallFullScreenNotificationPermission = {
        fullScreenCallNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
    }

    continueVideoCallAfterIncomingCallSetup = {
        val hasMicrophone = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
        val hasCamera = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
        if (hasMicrophone && hasCamera) {
            pendingVideoCallStartAfterSetup = false
            viewModel.startVideoCall()
        } else {
            videoCallPermissionLauncher.launch(
                arrayOf(
                    Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.CAMERA
                )
            )
        }
    }
    runVideoCallIncomingCallPreflight = {
        when (val result = IncomingCallFullScreenSetupHelper.evaluate(context)) {
            IncomingCallFullScreenSetupResult.Ready -> {
                continueVideoCallAfterIncomingCallSetup?.invoke()
            }

            IncomingCallFullScreenSetupResult.NeedsPostNotificationsPermission -> {
                pendingVideoCallStartAfterSetup = true
                requestVideoCallFullScreenNotificationPermission?.invoke()
            }

            is IncomingCallFullScreenSetupResult.NeedsSystemSettings -> {
                pendingVideoCallStartAfterSetup = true
                viewModel.setErrorMessage(result.detail)
                fullScreenCallSetupSettingsLauncher.launch(result.intent)
            }
        }
    }

    LaunchedEffect(
        state.chatId,
        rows,
        state.errorMessage,
        state.chatType,
        state.firstUnreadMessageId,
        firstUnreadRowIndex,
        state.hasMoreMessages,
        state.isLoadingOlderMessages,
        savedScrollPosition?.rowKey,
        savedScrollPosition?.itemIndex,
        savedScrollPosition?.itemScrollOffset
    ) {
        if (isInitialScrollApplied || state.errorMessage != null || rows.isEmpty()) {
            return@LaunchedEffect
        }

        val channelReadRowIndex = if (state.chatType == ChatType.Channel) {
            firstUnreadRowIndex?.let { findLastReadRowIndexBeforeFirstUnread(rows, it) }
        } else {
            null
        }
        val shouldLoadChannelReadPosition =
            state.chatType == ChatType.Channel &&
                state.firstUnreadMessageId != null &&
                channelReadRowIndex == null &&
                state.hasMoreMessages

        if (shouldLoadChannelReadPosition) {
            if (!state.isLoadingOlderMessages) {
                viewModel.loadOlderMessages()
            }
            return@LaunchedEffect
        }

        val initialScrollTarget = when {
            state.chatType == ChatType.Channel && firstUnreadRowIndex != null ->
                (channelReadRowIndex ?: firstUnreadRowIndex) to 0

            firstUnreadRowIndex != null -> firstUnreadRowIndex to 0

            else -> savedScrollPosition?.let { position ->
                val restoredIndex = rows.indexOfFirst { chatRowKey(it) == position.rowKey }
                    .takeIf { it >= 0 }
                    ?: position.itemIndex.coerceIn(0, rows.lastIndex)
                restoredIndex to position.itemScrollOffset.coerceAtLeast(0)
            }
        }

        initialScrollTarget?.let { (restoredIndex, restoredOffset) ->
            listState.scrollToItem(
                index = restoredIndex,
                scrollOffset = restoredOffset
            )
        }
        isInitialScrollApplied = true
    }

    LaunchedEffect(state.scrollRequest?.requestId, rows, isInitialScrollApplied) {
        val request = state.scrollRequest ?: return@LaunchedEffect
        if (!isInitialScrollApplied || rows.isEmpty()) {
            return@LaunchedEffect
        }

        val targetIndex = request.messageId?.let { messageId ->
            rows.indexOfFirst { row -> row is ChatRow.Message && row.item.id == messageId }
        }
        if (request.messageId != null && targetIndex == -1) {
            return@LaunchedEffect
        }

        listState.animateScrollToItem(index = targetIndex?.takeIf { it >= 0 } ?: 0)
        viewModel.consumeScrollRequest(request.requestId)
    }

    LaunchedEffect(state.chatId, rows, isInitialScrollApplied) {
        if (!isInitialScrollApplied || rows.isEmpty()) {
            return@LaunchedEffect
        }

        snapshotFlow {
            val visibleIndex = listState.firstVisibleItemIndex.coerceIn(0, rows.lastIndex)
            SavedChatScrollPosition(
                rowKey = rows.getOrNull(visibleIndex)?.let(::chatRowKey),
                itemIndex = visibleIndex,
                itemScrollOffset = listState.firstVisibleItemScrollOffset.coerceAtLeast(0)
            )
        }
            .distinctUntilChanged()
            .debounce(350)
            .collect(viewModel::saveScrollPosition)
    }

    LaunchedEffect(state.chatId, state.chatType, latestMessageId, isInitialScrollApplied) {
        if (!isInitialScrollApplied || latestMessageId == null) {
            return@LaunchedEffect
        }

        val previousLatestMessageId = observedLatestMessageId
        observedLatestMessageId = latestMessageId
        if (previousLatestMessageId == null || previousLatestMessageId == latestMessageId) {
            return@LaunchedEffect
        }
        if (state.chatType == ChatType.Channel) {
            return@LaunchedEffect
        }

        listState.animateScrollToItem(index = 0)
    }

    LaunchedEffect(state.chatId, state.chatType, rows, isInitialScrollApplied) {
        if (!isInitialScrollApplied || rows.isEmpty()) {
            return@LaunchedEffect
        }

        snapshotFlow {
            listState.layoutInfo.visibleItemsInfo
                .mapNotNull { item -> (rows.getOrNull(item.index) as? ChatRow.Message)?.item }
                .filterNot { message -> message.isDeleted() }
                .maxByOrNull { message -> parseMessageDate(message.sentAt)?.toInstant() ?: Instant.EPOCH }
                ?.id
        }
            .distinctUntilChanged()
            .debounce(500)
            .collect { messageId ->
                if (messageId != null) {
                    viewModel.acknowledgeRead(messageId)
                }
            }
    }

    LaunchedEffect(
        state.chatId,
        rows.size,
        state.hasMoreMessages,
        state.isLoadingOlderMessages,
        isInitialScrollApplied
    ) {
        if (!isInitialScrollApplied || rows.isEmpty()) {
            return@LaunchedEffect
        }

        snapshotFlow {
            if (!state.hasMoreMessages || state.isLoadingOlderMessages || rows.isEmpty()) {
                false
            } else {
                val olderEdgeIndex = (rows.lastIndex - 3).coerceAtLeast(0)
                listState.layoutInfo.visibleItemsInfo.any { item -> item.index >= olderEdgeIndex }
            }
        }
            .distinctUntilChanged()
            .collect { shouldLoadOlder ->
                if (shouldLoadOlder) {
                    viewModel.loadOlderMessages()
                }
            }
    }

    LaunchedEffect(state.isMessageSelectionMode) {
        if (state.isMessageSelectionMode) {
            messageMenuTarget = null
            emojiPickerMode = null
            suggestedPickerEmoji = null
            deleteCandidate = null
            showHeaderMenu = false
        }
    }

    LaunchedEffect(activeSearchMessageId, rows) {
        val messageId = activeSearchMessageId ?: return@LaunchedEffect
        val rowIndex = rows.indexOfFirst { row -> row is ChatRow.Message && row.item.id == messageId }
        if (rowIndex >= 0) {
            listState.animateScrollToItem(rowIndex)
        }
    }

    fun handleEmojiSelected(pickerMode: EmojiPickerMode, emoji: String) {
        frequentEmojis = storeFrequentEmoji(context, frequentEmojis, emoji)
        when (pickerMode) {
            EmojiPickerMode.Composer -> {
                viewModel.appendEmoji(emoji)
                suggestedPickerEmoji = emoji
            }
            is EmojiPickerMode.Reaction -> {
                suggestedPickerEmoji = null
                emojiPickerMode = null
                val target = pickerMode.message
                if (state.selectedMessageIds.contains(target.id)) {
                    viewModel.toggleReactionFromSelection(target, emoji)
                } else {
                    viewModel.toggleReaction(target, emoji)
                }
            }
        }
    }

    fun handlePickerMediaSelected(item: PickerMediaItem) {
        suggestedPickerEmoji = null
        emojiPickerMode = null
        viewModel.sendPickerMedia(item)
    }

    val openMessageUrl: (String) -> Unit = { url ->
        if (chatSettings.inAppBrowserEnabled) {
            onOpenUrl(url)
        } else {
            openExternalUrl(context, url)
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        ChatWallpaperBackground(
            snapshot = chatSettings,
            animated = animatedBackgroundsEnabled && chatSettings.animationsEnabled,
            modifier = Modifier.fillMaxSize()
        )

        Column(
            modifier = Modifier.fillMaxSize()
        ) {
            if (state.isMessageSelectionMode) {
                MessageSelectionHeader(
                    selectedMessagesCount = state.selectedMessagesCount,
                    selectedMessage = singleSelectedMessage,
                    currentUserId = currentUserId,
                    isDeleteEnabled = !state.isDeletingSelectedMessages,
                    onClearSelection = viewModel::clearMessageSelection,
                    onReplySelected = { message ->
                        viewModel.clearMessageSelection()
                        viewModel.beginReplyToMessage(message)
                    },
                    onCopySelected = { message ->
                        coroutineScope.launch {
                            clipboard.setClipEntry(
                                ClipEntry(ClipData.newPlainText("Ikar message", message.text))
                            )
                        }
                        viewModel.clearMessageSelection()
                    },
                    onForwardSelected = { message ->
                        viewModel.clearMessageSelection()
                        onForwardMessage(message)
                    },
                    onReactSelected = { message ->
                        keyboardController?.hide()
                        suggestedPickerEmoji = null
                        displayedEmojiPickerMode = EmojiPickerMode.Reaction(message)
                        emojiPickerMode = EmojiPickerMode.Reaction(message)
                    },
                    onPinSelected = { message ->
                        viewModel.clearMessageSelection()
                        viewModel.togglePinMessage(message)
                    },
                    onDeleteSelectedClick = viewModel::showDeleteSelectedMessagesDialog
                )
            } else if (state.search.isActive) {
                ChatSearchHeader(
                    search = state.search,
                    onQueryChanged = viewModel::updateMessageSearchQuery,
                    onPrevious = { viewModel.navigateMessageSearch(-1) },
                    onNext = { viewModel.navigateMessageSearch(1) },
                    onClose = viewModel::closeMessageSearch
                )
            } else {
                ChatHeader(
                    title = state.title.ifBlank { "Чат" },
                    subtitle = state.subtitle,
                    avatarTitle = state.title.ifBlank { state.peer?.displayName.orEmpty() },
                    avatarUrl = ServerConfig.normalizeMediaUrl(state.peer?.avatarPath),
                    avatarAccessToken = state.session?.accessToken,
                    onBackClick = onBackClick,
                    onAvatarClick = onPeerProfileClick,
                    onMenuClick = { showHeaderMenu = true }
                )
            }

            state.pinnedMessages.firstOrNull()?.let { pinnedMessage ->
                PinnedMessageBanner(
                    message = pinnedMessage,
                    onClick = {
                        val rowIndex = rows.indexOfFirst { row ->
                            row is ChatRow.Message && row.item.id == pinnedMessage.id
                        }
                        if (rowIndex >= 0) {
                            coroutineScope.launch {
                                listState.animateScrollToItem(rowIndex)
                            }
                        } else {
                            viewModel.setErrorMessage("Закреплённое сообщение находится за пределами загруженной истории. Прокрутите выше, чтобы загрузить старые сообщения.")
                        }
                    },
                    onUnpinClick = { viewModel.togglePinMessage(pinnedMessage) }
                )
            }

            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) {
            LazyColumn(
                state = listState,
                reverseLayout = true,
                modifier = Modifier.fillMaxSize(),
                contentPadding = PaddingValues(start = 10.dp, top = 8.dp, end = 10.dp, bottom = 8.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(
                    items = rows,
                    key = { row ->
                        when (row) {
                            is ChatRow.DateSeparator -> row.id
                            is ChatRow.Message -> row.item.id
                        }
                    }
                ) { row ->
                    when (row) {
                        is ChatRow.DateSeparator -> ChatDateChip(text = row.text)
                        is ChatRow.Message -> {
                            val localStatus = state.localMessageStatus(row.item.id)
                            MessageBubble(
                            message = row.item,
                            isOutgoing = row.item.sender.id == currentUserId &&
                                !(state.chatType == ChatType.Channel && row.item.postedAsChannel),
                            showImagesBeforeText = state.chatType == ChatType.Channel,
                            isSelected = state.selectedMessageIds.contains(row.item.id),
                            isSearchMatch = searchResultIds.contains(row.item.id),
                            isActiveSearchResult = activeSearchMessageId == row.item.id,
                            isSelectionMode = state.isMessageSelectionMode,
                            largeEmojiEnabled = largeEmojiEnabled,
                            localStatus = localStatus,
                            accessToken = state.session?.accessToken,
                            attachmentUrlResolver = viewModel::attachmentPreviewUrl,
                            onOpenUrl = openMessageUrl,
                            onDiscussionClick = state.linkedDiscussionChatId
                                ?.takeIf { it.isNotBlank() }
                                ?.let { discussionChatId ->
                                    { discussionMessageId -> onOpenLinkedDiscussion(discussionChatId, discussionMessageId) }
                                },
                            onClick = {
                                if (state.isMessageSelectionMode) {
                                    viewModel.toggleMessageSelection(row.item)
                                } else if (localStatus == null) {
                                    messageMenuTarget = row.item
                                }
                            },
                            onAttachmentClick = { attachment ->
                                if (state.isMessageSelectionMode) {
                                    viewModel.toggleMessageSelection(row.item)
                                } else {
                                    viewModel.openAttachment(attachment)
                                }
                            },
                            onInlineAudioClick = { audio ->
                                if (state.isMessageSelectionMode) {
                                    viewModel.toggleMessageSelection(row.item)
                                } else {
                                    viewModel.toggleInlineAudioPlayback(audio.id, audio.url)
                                }
                            },
                            onQuickActionClick = { commandText ->
                                if (state.isMessageSelectionMode) {
                                    viewModel.toggleMessageSelection(row.item)
                                } else {
                                    viewModel.executeBotCommand(commandText)
                                }
                            },
                            onReactionClick = { emoji ->
                                if (state.isMessageSelectionMode) {
                                    viewModel.toggleMessageSelection(row.item)
                                } else {
                                    viewModel.toggleReaction(row.item, emoji)
                                }
                            },
                            onDoubleTap = {
                                if (!state.isMessageSelectionMode && !row.item.isDeleted()) {
                                    viewModel.toggleReaction(row.item, "\uD83D\uDC4D")
                                }
                            },
                            onReplyPreviewClick = { replyMessageId ->
                                val rowIndex = rows.indexOfFirst { candidate ->
                                    candidate is ChatRow.Message && candidate.item.id == replyMessageId
                                }
                                if (rowIndex >= 0) {
                                    coroutineScope.launch { listState.animateScrollToItem(rowIndex) }
                                } else {
                            viewModel.setErrorMessage("Сообщение для ответа находится за пределами загруженной истории. Используйте поиск или прокрутите выше, чтобы загрузить старые сообщения.")
                                }
                            },
                            onSwipeReply = {
                                if (localStatus == null) {
                                    viewModel.beginReplyToMessage(row.item)
                                }
                            },
                            onLongPress = {
                                if (localStatus == null) {
                                    viewModel.handleMessageLongPress(row.item)
                                }
                            },
                            attachmentPlaybackState = state.attachmentPlayback,
                            bubbleCornerRadiusDp = chatSettings.bubbleCornerRadiusDp,
                            messageTextSizeSp = chatSettings.messageTextSizeSp
                        )
                        }
                    }
                }
                if (state.isLoadingOlderMessages) {
                    item(key = "older-messages-loading") {
                        OlderMessagesLoadingRow()
                    }
                }
            }

            if (state.isLoading && state.messages.isEmpty()) {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.Center),
                    color = TelegramBlue
                )
            }

            if (showScrollToLatestButton) {
                Surface(
                    color = TelegramBlue,
                    shape = CircleShape,
                    shadowElevation = 6.dp,
                    modifier = Modifier
                        .align(Alignment.BottomEnd)
                        .padding(end = 16.dp, bottom = 16.dp)
                        .clickable {
                            coroutineScope.launch {
                                listState.animateScrollToItem(index = 0)
                            }
                        }
                ) {
                    Box(
                        modifier = Modifier.padding(12.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Icon(
                            imageVector = Icons.Outlined.ArrowDownward,
                            contentDescription = "К последнему сообщению",
                            tint = Color.White
                        )
                    }
                }
                }
            }

            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .then(if (emojiPickerMode == null) Modifier.imePadding() else Modifier)
                    .navigationBarsPadding()
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 10.dp, vertical = 8.dp),
                    verticalArrangement = Arrangement.spacedBy(8.dp)
                ) {
            state.activeCall?.let {
                CallBanner(
                    title = viewModel.callBannerTitle(state),
                    subtitle = viewModel.callBannerSubtitle(state),
                    showAccept = viewModel.showAcceptCallButton(state),
                    showDecline = viewModel.showDeclineCallButton(state),
                    showEnd = viewModel.showEndCallButton(state),
                    onAccept = viewModel::acceptCall,
                    onDecline = viewModel::declineCall,
                    onEnd = viewModel::endCall
                )
            }

            if (state.chatType == ChatType.Channel && !state.canSendMessages && !state.canSubscribe) {
                InfoBanner(text = "Канал доступен только для чтения. Публикации создают администраторы.")
            }

            state.editingMessageId?.let {
                ComposerStateCard(
                    title = "Редактирование",
                    subtitle = state.editingMessageBanner,
                    actionText = "Отмена",
                    onActionClick = viewModel::cancelEditMessage
                )
            }

            state.replyTo?.let { replyTo ->
                ComposerStateCard(
                    title = "Ответ ${replyTo.senderDisplayName}",
                    subtitle = replyTo.previewText,
                    actionText = "Отмена",
                    onActionClick = viewModel::cancelReply
                )
            }

            state.pendingAttachment?.takeIf { it.imagePreviews.size > 1 }?.let { pending ->
                PendingImageAlbumCard(
                    pending = pending,
                    onRemoveClick = viewModel::removePendingAttachment
                )
            }

            state.pendingAttachment?.takeUnless { it.imagePreviews.size > 1 }?.let { pending ->
                ComposerStateCard(
                    title = pending.fileName,
                    subtitle = "${pending.contentType} • ${formatFileSize(pending.fileSizeBytes)}",
                    actionText = "Убрать",
                    onActionClick = viewModel::removePendingAttachment
                )
            }

            state.activeTransfers.forEach { transfer ->
                TransferQueueCard(
                    transfer = transfer,
                    onRetryClick = { viewModel.retryTransfer(transfer.groupId) },
                    onCancelClick = { viewModel.cancelTransfer(transfer.groupId) }
                )
            }

            if (state.isRecordingVoiceNote) {
                ComposerStateCard(
                    title = "Идёт запись голосового сообщения",
                    subtitle = state.voiceRecordingElapsedText,
                    actionText = "Отменить",
                    onActionClick = viewModel::cancelVoiceRecording
                )
            }

            state.errorMessage?.takeIf { it.isNotBlank() }?.let {
                Text(
                    text = it,
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramDanger,
                    modifier = Modifier.padding(horizontal = 4.dp)
                )
            }

            AnimatedVisibility(
                visible = state.canSendMessages &&
                    emojiPickerMode is EmojiPickerMode.Composer &&
                    emojiKeyboardTab == EmojiKeyboardTab.Emoji &&
                    suggestedPickerItems.isNotEmpty(),
                enter = expandVertically(
                    expandFrom = Alignment.Bottom,
                    animationSpec = tween(EmojiKeyboardAnimationMillis)
                ),
                exit = shrinkVertically(
                    shrinkTowards = Alignment.Bottom,
                    animationSpec = tween(EmojiKeyboardAnimationMillis)
                )
            ) {
                EmojiStickerSuggestionPanel(
                    items = suggestedPickerItems,
                    onMediaSelected = ::handlePickerMediaSelected
                )
            }

            if (state.canSendMessages) {
                ChatComposer(
                    draftText = state.draftText,
                    placeholder = if (state.chatType == ChatType.Channel) "Публикация" else "Сообщение",
                    showVoiceButton = state.draftText.isBlank() && state.pendingAttachment == null && state.editingMessageId == null,
                    showBotMenuButton = viewModel.showBotComposerMenuButton(state),
                    isRecording = state.isRecordingVoiceNote,
                    isSending = state.isSendingMessage,
                    recordingElapsedText = state.voiceRecordingElapsedText,
                    sendByEnter = chatSettings.sendByEnterEnabled,
                    onDraftChanged = viewModel::updateDraft,
                    onEmojiClick = {
                        keyboardController?.hide()
                        emojiPickerMode = if (emojiPickerMode is EmojiPickerMode.Composer) {
                            suggestedPickerEmoji = null
                            null
                        } else {
                            displayedEmojiPickerMode = EmojiPickerMode.Composer
                            EmojiPickerMode.Composer
                        }
                    },
                    onInputFocused = {
                        suggestedPickerEmoji = null
                        emojiPickerMode = null
                    },
                    onBotMenuClick = {
                        viewModel.ensureBotCommandsLoaded()
                        showBotCommandsDialog = true
                    },
                    onAttachClick = { attachmentPicker.launch("*/*") },
                    onSendClick = viewModel::sendMessage,
                    onVoiceClick = {
                        if (state.isRecordingVoiceNote) {
                            viewModel.completeVoiceRecording()
                        } else {
                            microphonePermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
                        }
                    }
                )
            } else if (state.chatType == ChatType.Channel) {
                ChannelViewerBar(
                    isSubscribed = state.isSubscribed,
                    canSubscribe = state.canSubscribe,
                    isJoining = state.isJoiningChannel,
                    publicUsername = state.publicUsername,
                    description = state.channelDescription,
                    subscriberCount = state.subscriberCount,
                    onSubscribeClick = viewModel::subscribeToChannel
                )
            }
                }

                AnimatedVisibility(
                    visible = emojiPickerMode != null,
                    enter = expandVertically(
                        expandFrom = Alignment.Bottom,
                        animationSpec = tween(EmojiKeyboardAnimationMillis)
                    ),
                    exit = shrinkVertically(
                        shrinkTowards = Alignment.Bottom,
                        animationSpec = tween(EmojiKeyboardAnimationMillis)
                    )
                ) {
                    displayedEmojiPickerMode?.let { pickerMode ->
                        EmojiKeyboardPanel(
                            categories = emojiCategories,
                            height = emojiKeyboardHeight,
                            activeTab = emojiKeyboardTab,
                            allowMediaTabs = pickerMode is EmojiPickerMode.Composer,
                            gifPacks = BuiltInGifPacks,
                            stickerPacks = BuiltInStickerPacks,
                            onTabSelected = { tab -> emojiKeyboardTab = tab },
                            onDismiss = {
                                suggestedPickerEmoji = null
                                emojiPickerMode = null
                            },
                            onEmojiSelected = { emoji -> handleEmojiSelected(pickerMode, emoji) },
                            onMediaSelected = ::handlePickerMediaSelected
                        )
                    }
                }
            }
        }
    }

    if (showHeaderMenu) {
        val canStartCalls = viewModel.showCallHeaderButton(state)
        MessageActionDialog(
            title = "Меню чата",
            onDismiss = { showHeaderMenu = false },
            actions = buildList {
                onPeerProfileClick?.let { openInfo ->
                    add(
                        MessageActionItem(
                            label = "Информация",
                            icon = Icons.Outlined.Info,
                            onClick = {
                                showHeaderMenu = false
                                openInfo()
                            }
                        )
                    )
                }
                add(
                    MessageActionItem(
                        label = "Уведомления",
                        icon = Icons.Outlined.Notifications,
                        onClick = {
                            showHeaderMenu = false
                            onChatNotificationsClick()
                        }
                    )
                )
                if (canStartCalls) {
                    add(
                        MessageActionItem(
                            label = "Видеозвонок",
                            icon = Icons.Outlined.Videocam,
                            onClick = {
                                showHeaderMenu = false
                                runVideoCallIncomingCallPreflight?.invoke()
                            }
                        )
                    )
                    add(
                        MessageActionItem(
                            label = "Аудиозвонок",
                            icon = Icons.Outlined.Call,
                            onClick = {
                                showHeaderMenu = false
                                audioCallPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
                            }
                        )
                    )
                }
                add(
                    MessageActionItem(
                        label = "Настроить обои",
                        icon = Icons.Outlined.Image,
                        onClick = {
                            showHeaderMenu = false
                            onChatWallpaperClick()
                        }
                    )
                )
                if (viewModel.showBotComposerMenuButton(state)) {
                    add(
                        MessageActionItem(
                            label = "Удалить весь контент",
                            icon = Icons.Outlined.Delete,
                            isDestructive = true,
                            onClick = {
                                showHeaderMenu = false
                                showClearHistoryConfirm = true
                            }
                        )
                    )
                }
                if (state.chatType == ChatType.Channel) {
                    state.linkedDiscussionChatId?.takeIf { it.isNotBlank() }?.let { discussionChatId ->
                        add(
                            MessageActionItem(
                                label = "Обсуждение",
                                icon = Icons.Outlined.Forum,
                                onClick = {
                                    showHeaderMenu = false
                                    onOpenLinkedDiscussion(discussionChatId, null)
                                }
                            )
                        )
                    }
                    if (state.canDeleteForEveryone) {
                        add(
                            MessageActionItem(
                                label = "Удалить канал навсегда",
                                icon = Icons.Outlined.Delete,
                                isDestructive = true,
                                onClick = {
                                    showHeaderMenu = false
                                    deleteChatForEveryone = true
                                    showDeleteChatConfirm = true
                                }
                            )
                        )
                    } else if (state.isSubscribed) {
                        add(
                            MessageActionItem(
                                label = if (state.isLeavingChannel) "Выход..." else "Выйти из канала",
                                icon = Icons.Outlined.Delete,
                                isDestructive = true,
                                onClick = {
                                    showHeaderMenu = false
                                    viewModel.unsubscribeFromChannel()
                                }
                            )
                        )
                    }
                } else if (state.isSubscribed) {
                    add(
                        MessageActionItem(
                            label = "Удалить чат",
                            icon = Icons.Outlined.Delete,
                            isDestructive = true,
                            onClick = {
                                showHeaderMenu = false
                                deleteChatForEveryone = false
                                showDeleteChatConfirm = true
                            }
                        )
                    )
                }
            }
        )
    }

    if (showDeleteChatConfirm) {
        val deleteTitle = when {
            state.chatType == ChatType.Channel && deleteChatForEveryone -> "Удалить канал навсегда?"
            state.chatType == ChatType.Channel -> "Выйти из канала?"
            else -> "Удалить чат?"
        }
        val deleteText = when {
            state.chatType == ChatType.Channel && deleteChatForEveryone ->
                "Канал будет удален для всех подписчиков и исчезнет из публичного поиска."
            state.chatType == ChatType.Channel ->
                "Канал исчезнет только из вашего списка."
            else ->
                "Чат исчезнет из вашего списка после подтверждения."
        }
        val confirmDeleteText = if (state.chatType == ChatType.Channel && deleteChatForEveryone) {
            "Удалить навсегда"
        } else {
            "Удалить"
        }
        AlertDialog(
            onDismissRequest = {
                showDeleteChatConfirm = false
                deleteChatForEveryone = false
            },
            title = { Text(deleteTitle) },
            text = { Text(deleteText) },
            confirmButton = {
                TextButton(
                    enabled = !state.isDeletingChat,
                    onClick = {
                        showDeleteChatConfirm = false
                        viewModel.deleteChat(forEveryone = deleteChatForEveryone)
                        deleteChatForEveryone = false
                    }
                ) {
                    Text(confirmDeleteText, color = MaterialTheme.colorScheme.error)
                }
            },
            dismissButton = {
                TextButton(
                    enabled = !state.isDeletingChat,
                    onClick = {
                        showDeleteChatConfirm = false
                        deleteChatForEveryone = false
                    }
                ) {
                    Text("Отмена")
                }
            }
        )
    }

    if (showClearHistoryConfirm) {
        AlertDialog(
            onDismissRequest = { showClearHistoryConfirm = false },
            title = { Text("Удалить весь контент") },
            text = { Text("Удалить все сообщения и вложения в этом чате с ботом без возможности восстановления?") },
            confirmButton = {
                TextButton(onClick = {
                    showHeaderMenu = false
                    showClearHistoryConfirm = false
                    viewModel.clearChatHistory()
                }) {
                    Text("Удалить", color = MaterialTheme.colorScheme.error)
                }
            },
            dismissButton = {
                TextButton(onClick = { showClearHistoryConfirm = false }) {
                    Text("Отмена")
                }
            }
        )
    }

    if (showBotCommandsDialog) {
        ActionDialog(
            title = "Команды бота",
            onDismiss = { showBotCommandsDialog = false },
            actions = viewModel.availableBotCommands(state).map { command ->
                val label = if (command.description.isBlank()) "/${command.command}" else "/${command.command}  ${command.description}"
                label to {
                    showBotCommandsDialog = false
                    when (command.command.lowercase(Locale.ROOT)) {
                        "use" -> {
                            val quickActions = viewModel.latestBotQuickActions(state)
                            if (quickActions.isNotEmpty()) {
                                showBotThreadSelection = true
                            } else {
                                viewModel.executeBotCommand("/threads")
                            }
                        }

                        "newthread" -> viewModel.executeBotCommand("/newthread")
                        "turn" -> {
                            if (state.draftText.isBlank()) {
                        viewModel.setErrorMessage("Для /turn нужен текст. После выбора потока можно просто отправить обычное сообщение без команды.")
                            } else {
                                viewModel.executeBotCommand("/turn ${state.draftText.trim()}")
                            }
                        }

                        else -> viewModel.executeBotCommand("/${command.command}")
                    }
                }
            }
        )
    }

    if (showBotThreadSelection) {
        ActionDialog(
            title = "Выберите поток",
            onDismiss = { showBotThreadSelection = false },
            actions = viewModel.latestBotQuickActions(state).map { action ->
                action.title to {
                    showBotThreadSelection = false
                    viewModel.executeBotCommand(action.commandText)
                }
            }
        )
    }

    messageMenuTarget?.let { target ->
        val actionItems = buildMessageMenuItems(
            message = target,
            currentUserId = currentUserId,
            canPostAsChannel = state.canPostAsChannel,
            onCopy = {
                coroutineScope.launch {
                    clipboard.setClipEntry(
                        ClipEntry(ClipData.newPlainText("Ikar message", target.text))
                    )
                }
                messageMenuTarget = null
            },
            onForward = {
                messageMenuTarget = null
                onForwardMessage(target)
            },
            onReply = {
                messageMenuTarget = null
                viewModel.beginReplyToMessage(target)
            },
            onReact = {
                messageMenuTarget = null
                keyboardController?.hide()
                displayedEmojiPickerMode = EmojiPickerMode.Reaction(target)
                emojiPickerMode = EmojiPickerMode.Reaction(target)
            },
            onPin = {
                messageMenuTarget = null
                viewModel.togglePinMessage(target)
            },
            onEdit = {
                messageMenuTarget = null
                viewModel.beginEditMessage(target)
            },
            onDelete = {
                messageMenuTarget = null
                deleteCandidate = target
            }
        )

        if (actionItems.isNotEmpty()) {
            MessageActionDialog(
                title = "Действия с сообщением",
                onDismiss = { messageMenuTarget = null },
                actions = actionItems
            )
        }
    }

    deleteCandidate?.let { target ->
        AlertDialog(
            onDismissRequest = { deleteCandidate = null },
            title = { Text("Удалить сообщение") },
            text = { Text("Удалить это сообщение для всех участников?") },
            confirmButton = {
                TextButton(onClick = {
                    deleteCandidate = null
                    viewModel.deleteMessage(target)
                }) {
                    Text("Удалить")
                }
            },
            dismissButton = {
                TextButton(onClick = { deleteCandidate = null }) {
                    Text("Отмена")
                }
            }
        )
    }

    if (state.isDeleteSelectionDialogVisible) {
        DeleteSelectedMessagesDialog(
            selectedMessagesCount = state.selectedMessagesCount,
            isDeletingSelectedMessages = state.isDeletingSelectedMessages,
            onDismiss = viewModel::dismissDeleteSelectedMessagesDialog,
            onConfirmDelete = viewModel::confirmDeleteSelectedMessages
        )
    }

    currentImagePreviewEntry?.let { previewEntry ->
        val previewIndex = currentImagePreviewIndex ?: return@let
        ChatImagePreviewDialog(
            entry = previewEntry,
            accessToken = state.session?.accessToken,
            imageUrl = viewModel.attachmentPreviewUrl(previewEntry.attachment),
            enableEdgeTapNavigation = chatSettings.mediaTapToBrowseEnabled,
            canNavigatePrevious = previewIndex > 0,
            canNavigateNext = previewIndex < imagePreviewEntries.lastIndex,
            canDelete = previewEntry.message.canDelete(currentUserId),
            isProtectedContent = previewEntry.message.hasProtectedContent,
            onDismiss = viewModel::dismissImagePreview,
            onNavigatePrevious = {
                imagePreviewEntries.getOrNull(previewIndex - 1)?.let { previousEntry ->
                    viewModel.showImagePreviewAttachment(previousEntry.attachment.id)
                }
            },
            onNavigateNext = {
                imagePreviewEntries.getOrNull(previewIndex + 1)?.let { nextEntry ->
                    viewModel.showImagePreviewAttachment(nextEntry.attachment.id)
                }
            },
            onSaveToGallery = { viewModel.saveImageAttachmentToGallery(previewEntry.attachment) },
            onReply = {
                viewModel.beginReplyToMessage(previewEntry.message)
                viewModel.dismissImagePreview()
            },
            onShare = { viewModel.shareAttachmentExternally(previewEntry.attachment) },
            onDelete = {
                viewModel.dismissImagePreview()
                deleteCandidate = previewEntry.message
            }
        )
    }

    currentVideoPreviewEntry?.let { previewEntry ->
        ChatVideoPreviewDialog(
            entry = previewEntry,
            accessToken = state.session?.accessToken,
            videoUrl = viewModel.attachmentPreviewUrl(previewEntry.attachment),
            onDismiss = viewModel::dismissVideoPreview
        )
    }
}

@Composable
private fun ChatHeader(
    title: String,
    subtitle: String,
    avatarTitle: String,
    avatarUrl: String?,
    avatarAccessToken: String?,
    onBackClick: () -> Unit,
    onAvatarClick: (() -> Unit)?,
    onMenuClick: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFFF9FFFF))
    ) {
        Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 10.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
        IkarHeaderIconBubble(onClick = onBackClick) {
            Icon(
                imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
                contentDescription = "Назад",
                tint = TelegramInk
            )
        }

        Row(
            modifier = Modifier
                .weight(1f)
                .clip(RoundedCornerShape(18.dp))
                .then(if (onAvatarClick != null) Modifier.clickable(onClick = onAvatarClick) else Modifier),
            horizontalArrangement = Arrangement.spacedBy(10.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            AvatarView(
                imageUrl = avatarUrl,
                title = avatarTitle,
                accessToken = avatarAccessToken,
                size = 42.dp
            )

            Column(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(1.dp)
            ) {
                Text(
                    text = title,
                    style = MaterialTheme.typography.headlineLarge,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                if (subtitle.isNotBlank()) {
                    Text(
                        text = subtitle,
                        style = MaterialTheme.typography.bodySmall,
                        color = TelegramInkMuted,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                }
            }
        }

        IkarHeaderIconBubble(onClick = onMenuClick) {
            Icon(Icons.Outlined.Menu, contentDescription = "Меню", tint = TelegramInk)
        }
    }
    }
}

@Composable
private fun ChatSearchHeader(
    search: ChatSearchUiState,
    onQueryChanged: (String) -> Unit,
    onPrevious: () -> Unit,
    onNext: () -> Unit,
    onClose: () -> Unit
) {
    val hasResults = search.resultMessageIds.isNotEmpty()
    val resultLabel = when {
        search.query.isBlank() -> ""
        search.isSearching -> "..."
        hasResults -> "${search.activeResultIndex + 1}/${search.resultMessageIds.size}"
        else -> "0/0"
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFFF9FFFF))
    ) {
        Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 10.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            IkarHeaderIconBubble(onClick = onClose) {
                Icon(Icons.Outlined.Close, contentDescription = "Закрыть поиск", tint = TelegramInk)
            }

            Surface(
                color = TelegramSurface,
                shape = RoundedCornerShape(18.dp),
                modifier = Modifier.weight(1f)
            ) {
                BasicTextField(
                    value = search.query,
                    onValueChange = onQueryChanged,
                    singleLine = true,
                    textStyle = MaterialTheme.typography.bodyLarge.copy(color = TelegramInk),
                    modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
                    decorationBox = { innerTextField ->
                        if (search.query.isBlank()) {
                            Text(
                                text = "Поиск сообщений",
                                style = MaterialTheme.typography.bodyLarge,
                                color = TelegramInkMuted
                            )
                        }
                        innerTextField()
                    }
                )
            }

            Text(
                text = resultLabel,
                style = MaterialTheme.typography.labelMedium,
                color = TelegramInkMuted
            )

            IkarHeaderIconBubble(onClick = onPrevious.takeIf { hasResults }) {
                Icon(
                    Icons.Outlined.KeyboardArrowUp,
                    contentDescription = "Предыдущий результат",
                    tint = if (hasResults) TelegramInk else TelegramInkMuted
                )
            }
            IkarHeaderIconBubble(onClick = onNext.takeIf { hasResults }) {
                Icon(
                    Icons.Outlined.KeyboardArrowDown,
                    contentDescription = "Следующий результат",
                    tint = if (hasResults) TelegramInk else TelegramInkMuted
                )
            }
        }
        if (search.isSearching) {
            LinearProgressIndicator(
                modifier = Modifier.fillMaxWidth(),
                color = TelegramBlue
            )
        }
    }
}

@Composable
private fun MessageSelectionHeader(
    selectedMessagesCount: Int,
    selectedMessage: MessageDto?,
    currentUserId: String?,
    isDeleteEnabled: Boolean,
    onClearSelection: () -> Unit,
    onReplySelected: (MessageDto) -> Unit,
    onCopySelected: (MessageDto) -> Unit,
    onForwardSelected: (MessageDto) -> Unit,
    onReactSelected: (MessageDto) -> Unit,
    onPinSelected: (MessageDto) -> Unit,
    onDeleteSelectedClick: () -> Unit
) {
    val canUseSingleActions = selectedMessagesCount == 1 && selectedMessage != null && !selectedMessage.isDeleted()
    val canCopy = canUseSingleActions &&
        selectedMessage?.hasProtectedContent != true &&
        selectedMessage?.text?.isNotBlank() == true
    val canForward = canUseSingleActions &&
        selectedMessage?.hasProtectedContent != true &&
        selectedMessage?.let { it.text.isNotBlank() || it.attachments.isNotEmpty() } == true
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFFF9FFFF))
    ) {
        Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 10.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            IkarHeaderIconBubble(onClick = onClearSelection) {
                Icon(
                    imageVector = Icons.Outlined.Close,
                    contentDescription = "Снять выделение",
                    tint = TelegramInk
                )
            }

            Column(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(1.dp)
            ) {
                Text(
                    text = messageSelectionCountLabel(selectedMessagesCount),
                    style = MaterialTheme.typography.headlineLarge,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                Text(
                    text = "Можно удалить выбранные сообщения",
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramInkMuted,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
            }

            if (canUseSingleActions && selectedMessage != null) {
                SelectionActionButton(
                    icon = Icons.AutoMirrored.Outlined.ArrowBack,
                    contentDescription = "Ответить",
                    enabled = true,
                    onClick = { onReplySelected(selectedMessage) }
                )
                SelectionActionButton(
                    icon = Icons.Outlined.ContentCopy,
                    contentDescription = "Копировать",
                    enabled = canCopy,
                    onClick = { onCopySelected(selectedMessage) }
                )
                SelectionActionButton(
                    icon = Icons.AutoMirrored.Outlined.Send,
                    contentDescription = "Переслать",
                    enabled = canForward,
                    onClick = { onForwardSelected(selectedMessage) }
                )
                SelectionActionButton(
                    icon = Icons.Outlined.Mood,
                    contentDescription = "Реакция",
                    enabled = true,
                    onClick = { onReactSelected(selectedMessage) }
                )
                SelectionActionButton(
                    icon = Icons.Outlined.PushPin,
                    contentDescription = if (selectedMessage.pinnedAt == null) "Закрепить" else "Открепить",
                    enabled = selectedMessage.canDelete(currentUserId),
                    onClick = { onPinSelected(selectedMessage) }
                )
            }

            IkarHeaderIconBubble(onClick = onDeleteSelectedClick.takeIf { isDeleteEnabled }) {
                Icon(
                    imageVector = Icons.Outlined.Delete,
                    contentDescription = "Удалить выбранные сообщения",
                    tint = if (isDeleteEnabled) TelegramDanger else TelegramInkMuted
                )
            }
        }
    }
}

@Composable
private fun SelectionActionButton(
    icon: ImageVector,
    contentDescription: String,
    enabled: Boolean,
    onClick: () -> Unit
) {
    IkarHeaderIconBubble(onClick = onClick.takeIf { enabled }) {
        Icon(
            imageVector = icon,
            contentDescription = contentDescription,
            tint = if (enabled) TelegramInk else TelegramInkMuted
        )
    }
}

@Composable
private fun PinnedMessageBanner(
    message: MessageDto,
    onClick: () -> Unit,
    onUnpinClick: () -> Unit
) {
    Surface(
        color = Color.White.copy(alpha = 0.96f),
        shadowElevation = 2.dp,
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            Box(
                modifier = Modifier
                    .width(3.dp)
                    .height(36.dp)
                    .clip(CircleShape)
                    .background(TelegramBlue)
            )
            Icon(
                imageVector = Icons.Outlined.PushPin,
                contentDescription = "Закрепленное сообщение",
                tint = TelegramBlue,
                modifier = Modifier.size(18.dp)
            )
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = "Закрепленное сообщение",
                    style = MaterialTheme.typography.labelMedium,
                    color = TelegramBlue,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                Text(
                    text = pinnedMessagePreview(message),
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramInk,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
            }
            IkarHeaderIconBubble(onClick = onUnpinClick) {
                Icon(
                    imageVector = Icons.Outlined.Close,
                    contentDescription = "Открепить",
                    tint = TelegramInkMuted
                )
            }
        }
    }
}

@Composable
private fun CallBanner(
    title: String,
    subtitle: String,
    showAccept: Boolean,
    showDecline: Boolean,
    showEnd: Boolean,
    onAccept: () -> Unit,
    onDecline: () -> Unit,
    onEnd: () -> Unit
) {
    Surface(
        color = Color(0xFFF2F8FF),
        shape = RoundedCornerShape(16.dp),
        shadowElevation = 0.dp
    ) {
        Column(
            modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
            verticalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            Text(title, style = MaterialTheme.typography.titleLarge)
            if (subtitle.isNotBlank()) {
                Text(subtitle, style = MaterialTheme.typography.bodySmall, color = TelegramInkMuted)
            }
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                if (showAccept) {
                    ActionChip(text = "Принять", background = TelegramBlue, textColor = Color.White, onClick = onAccept)
                }
                if (showDecline) {
                    ActionChip(text = "Отклонить", background = Color(0xFFF8E1E3), textColor = TelegramDanger, onClick = onDecline)
                }
                if (showEnd) {
                    ActionChip(text = "Отбой", background = Color(0xFFF8E1E3), textColor = TelegramDanger, onClick = onEnd)
                }
            }
        }
    }
}

@Composable
private fun ActionChip(
    text: String,
    background: Color,
    textColor: Color,
    enabled: Boolean = true,
    onClick: () -> Unit
) {
    Surface(
        modifier = Modifier.clickable(enabled = enabled, onClick = onClick),
        color = background,
        shape = RoundedCornerShape(16.dp),
        shadowElevation = 0.dp
    ) {
        Text(
            text = text,
            modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp),
            style = MaterialTheme.typography.labelLarge,
            color = textColor
        )
    }
}

@Composable
private fun ChannelViewerBar(
    isSubscribed: Boolean,
    canSubscribe: Boolean,
    isJoining: Boolean,
    publicUsername: String?,
    description: String?,
    subscriberCount: Int,
    onSubscribeClick: () -> Unit
) {
    Surface(
        color = Color(0xFFF9FFFF),
        shape = RoundedCornerShape(24.dp),
        shadowElevation = 4.dp
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 14.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(
                    text = when {
                        canSubscribe -> "Публичный канал"
                        isSubscribed -> "Вы подписаны на канал"
                        else -> "Канал только для чтения"
                    },
                    style = MaterialTheme.typography.titleMedium,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                Text(
                    text = listOfNotNull(
                        publicUsername?.let { "@$it" },
                        formatChannelSubscribers(subscriberCount)
                    ).joinToString(" • "),
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramInkMuted,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                description?.takeIf { it.isNotBlank() }?.let { text ->
                    Text(
                        text = text,
                        style = MaterialTheme.typography.bodySmall,
                        color = TelegramInkMuted,
                        maxLines = 2,
                        overflow = TextOverflow.Ellipsis
                    )
                }
            }
            if (canSubscribe) {
                ActionChip(
                    text = if (isJoining) "Подписка..." else "Подписаться",
                    background = TelegramBlue,
                    textColor = Color.White,
                    enabled = !isJoining,
                    onClick = onSubscribeClick
                )
            }
        }
    }
}

@Composable
private fun InfoBanner(text: String) {
    Surface(
        color = Color(0xFFFDF6E3),
        shape = RoundedCornerShape(16.dp),
        shadowElevation = 0.dp
    ) {
        Text(
            text = text,
            modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
            style = MaterialTheme.typography.bodyMedium,
            color = Color(0xFF9C6B00)
        )
    }
}

@Composable
private fun PendingImageAlbumCard(
    pending: PendingAttachmentUiState,
    onRemoveClick: () -> Unit
) {
    Surface(
        color = Color(0xFFF9FFFF),
        shape = RoundedCornerShape(18.dp),
        shadowElevation = 0.dp
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 12.dp, vertical = 10.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                    Text(pending.fileName, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
                    Text(
                        text = "${pending.contentType} вЂў ${formatFileSize(pending.fileSizeBytes)}",
                        style = MaterialTheme.typography.bodySmall,
                        color = TelegramInkMuted,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                }
                TextButton(onClick = onRemoveClick) {
                    Text("РЈР±СЂР°С‚СЊ")
                }
            }

            PendingImageAlbumGrid(previews = pending.imagePreviews)
        }
    }
}

@Composable
private fun PendingImageAlbumGrid(previews: List<PendingImageAttachmentUiState>) {
    ImageCollageLayout(
        itemCount = previews.size,
        minHeight = 112.dp,
        maxHeight = 144.dp
    ) { index, modifier, overlayText ->
        val preview = previews[index]
        ImageCollageTile(
            imageUrl = "file://${preview.localPath}",
            accessToken = null,
            contentDescription = preview.fileName,
            modifier = modifier,
            overlayText = overlayText
        )
    }
}

@Composable
private fun ComposerStateCard(
    title: String,
    subtitle: String,
    actionText: String,
    onActionClick: () -> Unit
) {
    Surface(
        color = Color(0xFFF9FFFF),
        shape = RoundedCornerShape(18.dp),
        shadowElevation = 0.dp
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 14.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(title, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
                Text(subtitle, style = MaterialTheme.typography.bodySmall, color = TelegramInkMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
            }
            TextButton(onClick = onActionClick) {
                Text(actionText)
            }
        }
    }
}

@Composable
private fun TransferQueueCard(
    transfer: TransferQueueUiState,
    onRetryClick: () -> Unit,
    onCancelClick: () -> Unit
) {
    val isFailed = transfer.status == "failed"
    ComposerStateCard(
        title = transfer.title,
        subtitle = when {
            isFailed -> transfer.errorMessage ?: "Не удалось загрузить"
            transfer.status == "queued" -> "В очереди"
            transfer.status == "running" -> "Загрузка ${transfer.progressPercent}%"
            else -> transfer.status
        },
        actionText = if (isFailed) "Повторить" else "Отменить",
        onActionClick = if (isFailed) onRetryClick else onCancelClick
    )
}

@Composable
private fun ChatComposer(
    draftText: String,
    placeholder: String,
    showVoiceButton: Boolean,
    showBotMenuButton: Boolean,
    isRecording: Boolean,
    isSending: Boolean,
    recordingElapsedText: String,
    sendByEnter: Boolean,
    onDraftChanged: (String) -> Unit,
    onEmojiClick: () -> Unit,
    onInputFocused: () -> Unit,
    onBotMenuClick: () -> Unit,
    onAttachClick: () -> Unit,
    onSendClick: () -> Unit,
    onVoiceClick: () -> Unit
) {
    Surface(
        color = Color(0xFFF9FFFF),
        shape = RoundedCornerShape(26.dp),
        shadowElevation = 4.dp
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 4.dp, vertical = 2.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(2.dp)
        ) {
            ChatComposerIcon(
                icon = Icons.Outlined.Mood,
                description = "Эмодзи",
                onClick = onEmojiClick
            )

            BasicTextField(
                value = draftText,
                onValueChange = onDraftChanged,
                enabled = !isSending,
                keyboardOptions = KeyboardOptions(
                    capitalization = KeyboardCapitalization.Sentences,
                    imeAction = if (sendByEnter) ImeAction.Send else ImeAction.Default
                ),
                keyboardActions = KeyboardActions(
                    onSend = {
                        if (sendByEnter && draftText.isNotBlank() && !showVoiceButton && !isRecording && !isSending) {
                            onSendClick()
                        }
                    }
                ),
                modifier = Modifier
                    .weight(1f)
                    .onFocusChanged { focusState ->
                        if (focusState.isFocused) {
                            onInputFocused()
                        }
                    }
                    .padding(horizontal = 4.dp, vertical = 10.dp),
                textStyle = MaterialTheme.typography.bodyLarge.copy(
                    color = TelegramInk,
                    fontSize = 19.sp
                ),
                singleLine = false,
                maxLines = 4,
                decorationBox = { innerTextField ->
                    Box(
                        modifier = Modifier.fillMaxWidth(),
                        contentAlignment = Alignment.CenterStart
                    ) {
                        if (draftText.isBlank()) {
                            Text(
                                text = placeholder,
                                style = MaterialTheme.typography.bodyLarge.copy(fontSize = 19.sp),
                                color = TelegramInkMuted
                            )
                        }
                        innerTextField()
                    }
                }
            )

            if (showBotMenuButton) {
                ChatComposerIcon(
                    icon = Icons.Outlined.Menu,
                    description = "Команды бота",
                    onClick = onBotMenuClick
                )
            }

            ChatComposerIcon(
                icon = Icons.Outlined.AttachFile,
                description = "Вложение",
                onClick = onAttachClick
            )

            Surface(
                color = TelegramBlue,
                shape = CircleShape,
                modifier = Modifier.padding(end = 2.dp)
            ) {
                Box(
                    modifier = Modifier
                        .clickable(
                            enabled = !isSending,
                            onClick = if (showVoiceButton || isRecording) onVoiceClick else onSendClick
                        )
                        .padding(10.dp),
                    contentAlignment = Alignment.Center
                ) {
                    if (isSending) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(20.dp),
                            color = Color.White,
                            strokeWidth = 2.dp
                        )
                    } else {
                        Icon(
                            imageVector = when {
                                isRecording -> Icons.Outlined.Stop
                                showVoiceButton -> Icons.Outlined.Mic
                                else -> Icons.AutoMirrored.Outlined.Send
                            },
                            contentDescription = when {
                                isRecording -> "Остановить запись"
                                showVoiceButton -> "Голосовое сообщение"
                                else -> "Отправить"
                            },
                            tint = Color.White
                        )
                    }
                }
            }
        }
    }

    if (isRecording) {
        Text(
            text = "Запись: $recordingElapsedText",
            style = MaterialTheme.typography.bodySmall,
            color = TelegramDanger,
            modifier = Modifier.padding(horizontal = 8.dp)
        )
    }
}

@Composable
private fun ChatComposerIcon(
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    description: String,
    onClick: () -> Unit
) {
    Box(
        modifier = Modifier
            .clickable(onClick = onClick)
            .padding(8.dp),
        contentAlignment = Alignment.Center
    ) {
        Icon(icon, contentDescription = description, tint = TelegramInkMuted)
    }
}

@Composable
private fun OlderMessagesLoadingRow() {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 12.dp),
        contentAlignment = Alignment.Center
    ) {
        CircularProgressIndicator(
            modifier = Modifier.size(22.dp),
            color = TelegramBlue,
            strokeWidth = 2.dp
        )
    }
}

@Composable
private fun ChatDateChip(text: String) {
    Box(
        modifier = Modifier.fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        Surface(
            color = Color(0xFF8AA66F),
            shape = RoundedCornerShape(12.dp),
            shadowElevation = 0.dp
        ) {
            Text(
                text = text,
                modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
                style = MaterialTheme.typography.labelMedium,
                color = Color.White
            )
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun MessageBubble(
    message: MessageDto,
    isOutgoing: Boolean,
    showImagesBeforeText: Boolean,
    isSelected: Boolean,
    isSearchMatch: Boolean,
    isActiveSearchResult: Boolean,
    isSelectionMode: Boolean,
    largeEmojiEnabled: Boolean,
    localStatus: LocalMessageStatus?,
    accessToken: String?,
    attachmentUrlResolver: (AttachmentDto) -> String,
    onOpenUrl: (String) -> Unit,
    onDiscussionClick: ((String) -> Unit)?,
    onClick: () -> Unit,
    onAttachmentClick: (AttachmentDto) -> Unit,
    onInlineAudioClick: (InlineAudioUrl) -> Unit,
    onQuickActionClick: (String) -> Unit,
    onReactionClick: (String) -> Unit,
    onDoubleTap: () -> Unit,
    onReplyPreviewClick: (String) -> Unit,
    onSwipeReply: () -> Unit,
    onLongPress: () -> Unit,
    attachmentPlaybackState: Map<String, VoiceAttachmentPlaybackUiState>,
    bubbleCornerRadiusDp: Int,
    messageTextSizeSp: Int
) {
    val botQuickActions = remember(message.id, message.text) { buildBotQuickActions(message) }
    val hasAttachments = message.attachments.isNotEmpty()
    val selectionBackground = when {
        isSelected -> TelegramBlue.copy(alpha = 0.12f)
        isActiveSearchResult -> Color(0xFFFFD166).copy(alpha = 0.46f)
        isSearchMatch -> Color(0xFFFFD166).copy(alpha = 0.22f)
        else -> Color.Transparent
    }
    val displayText = if (message.isDeleted()) "Сообщение удалено." else message.text
    val displayContent = remember(message.id, displayText) { buildMessageDisplayContent(displayText) }
    val standaloneTelegramMediaAttachment = message.standaloneTelegramMediaAttachment(displayContent)
    val discussionMessageId = message.discussionMessageId?.takeIf { it.isNotBlank() }
    val canOpenDiscussion = !message.isDeleted() &&
        message.chatType == ChatType.Channel &&
        discussionMessageId != null &&
        onDiscussionClick != null
    val bubbleContentSpacing = if (!hasAttachments && displayContent.imageUrls.isEmpty() && displayContent.audioUrls.isEmpty() && botQuickActions.isEmpty()) 4.dp else 8.dp
    val bubbleShape = RoundedCornerShape(bubbleCornerRadiusDp.dp)
    val selectionShape = RoundedCornerShape((bubbleCornerRadiusDp + 4).coerceIn(8, 28).dp)
    val density = LocalDensity.current
    val maxSwipeOffsetPx = with(density) { 72.dp.toPx() }
    val swipeReplyThresholdPx = with(density) { 42.dp.toPx() }
    var swipeOffsetPx by remember(message.id, isSelectionMode) { mutableStateOf(0f) }
    val canSwipeToReply = !isSelectionMode && !message.isDeleted()
    BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
        val maxBubbleWidth = when {
            standaloneTelegramMediaAttachment?.isTelegramStickerAttachment() == true -> maxWidth * 0.58f
            standaloneTelegramMediaAttachment?.isTelegramGifAttachment() == true -> maxWidth * 0.68f
            else -> maxWidth * 0.8f
        }

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
            verticalAlignment = Alignment.Bottom
        ) {
            if (isOutgoing && isSelected) {
                SelectionIndicator()
                Spacer(modifier = Modifier.width(8.dp))
            }

            Box(
                modifier = Modifier
                    .background(selectionBackground, selectionShape)
                    .padding(2.dp)
            ) {
        Surface(
            color = if (hasAttachments) Color.Transparent else if (isOutgoing) Color(0xFFE7F7C8) else TelegramSurface,
            shape = bubbleShape,
            shadowElevation = if (hasAttachments) 0.dp else 1.dp,
            modifier = Modifier
                .widthIn(max = maxBubbleWidth)
                .graphicsLayer {
                    translationX = swipeOffsetPx
                    alpha = if (localStatus == LocalMessageStatus.Sending) 0.72f else 1f
                }
                .pointerInput(message.id, canSwipeToReply) {
                    if (!canSwipeToReply) {
                        return@pointerInput
                    }

                    detectHorizontalDragGestures(
                        onDragEnd = {
                            val shouldReply = swipeOffsetPx <= -swipeReplyThresholdPx
                            swipeOffsetPx = 0f
                            if (shouldReply) {
                                onSwipeReply()
                            }
                        },
                        onDragCancel = {
                            swipeOffsetPx = 0f
                        }
                    ) { _, dragAmount ->
                        swipeOffsetPx = (swipeOffsetPx + dragAmount).coerceIn(-maxSwipeOffsetPx, 0f)
                    }
                }
                .combinedClickable(
                    onClick = onClick,
                    onDoubleClick = onDoubleTap,
                    onLongClick = onLongPress
                )
        ) {
            Column(
                modifier = Modifier.padding(
                    horizontal = if (hasAttachments) 0.dp else 12.dp,
                    vertical = if (hasAttachments) 0.dp else 10.dp
                ),
                verticalArrangement = Arrangement.spacedBy(bubbleContentSpacing)
            ) {
                message.storyReply?.let { storyReply ->
                    StoryReplyPreviewCard(
                        title = "Ответ на сторис",
                        previewText = storyReply.previewText,
                        attachmentKind = storyReply.attachmentKind,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp)
                    )
                }

                message.replyTo?.let { replyTo ->
                    ReplyPreviewCard(
                        replyTo = replyTo,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp),
                        onClick = { onReplyPreviewClick(replyTo.messageId) }
                    )
                }

                if (!message.forwardedFromDisplayName.isNullOrBlank()) {
                    Text(
                        text = "Переслано от ${message.forwardedFromDisplayName}",
                        style = MaterialTheme.typography.labelMedium,
                        color = TelegramBlue,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp)
                    )
                }

                standaloneTelegramMediaAttachment?.let { attachment ->
                    TelegramMediaAttachment(
                        attachment = attachment,
                        attachmentUrl = attachmentUrlResolver(attachment),
                        accessToken = accessToken,
                        isOutgoing = isOutgoing,
                        modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start),
                        onClick = { onAttachmentClick(attachment) },
                        onLongPress = onLongPress
                    )
                }

                val attachmentsForDefaultRendering = if (standaloneTelegramMediaAttachment == null) {
                    message.attachments
                } else {
                    message.attachments.filterNot { it.id == standaloneTelegramMediaAttachment.id }
                }
                val imageAttachments = attachmentsForDefaultRendering.filter { it.isImageAttachment() }

                if (showImagesBeforeText) {
                    displayContent.imageUrls.forEach { imageUrl ->
                        InlineMessageImage(
                            imageUrl = imageUrl,
                            onClick = { onOpenUrl(imageUrl) },
                            onLongPress = onLongPress
                        )
                    }

                    if (imageAttachments.size > 1) {
                        AttachmentAlbumGrid(
                            attachments = imageAttachments,
                            attachmentUrlResolver = attachmentUrlResolver,
                            accessToken = accessToken,
                            onAttachmentClick = onAttachmentClick,
                            onLongPress = onLongPress
                        )
                    } else {
                        imageAttachments.forEach { attachment ->
                            AttachmentCard(
                                attachment = attachment,
                                attachmentUrl = attachmentUrlResolver(attachment),
                                accessToken = accessToken,
                                playbackState = attachmentPlaybackState[attachment.id],
                                onClick = { onAttachmentClick(attachment) },
                                onLongPress = onLongPress
                            )
                        }
                    }
                }

                displayContent.audioUrls.forEach { audio ->
                    InlineMessageAudio(
                        audio = audio,
                        playbackState = attachmentPlaybackState[audio.id],
                        onClick = { onInlineAudioClick(audio) },
                        onLongPress = onLongPress
                    )
                }

                if (displayContent.text.isNotBlank()) {
                    val useLargeEmojiText = largeEmojiEnabled &&
                        !hasAttachments &&
                        displayContent.imageUrls.isEmpty() &&
                        displayContent.audioUrls.isEmpty() &&
                        botQuickActions.isEmpty() &&
                        message.storyReply == null &&
                        message.replyTo == null &&
                        message.forwardedFromDisplayName.isNullOrBlank() &&
                        isLargeEmojiMessage(displayContent.text)
                    MessageText(
                        text = displayContent.text,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp),
                        textSizeSp = if (useLargeEmojiText) 42 else messageTextSizeSp,
                        onUrlClick = onOpenUrl,
                        onTap = onClick,
                        onLongPress = onLongPress
                    )
                }

                if (!showImagesBeforeText) {
                    displayContent.imageUrls.forEach { imageUrl ->
                        InlineMessageImage(
                            imageUrl = imageUrl,
                            onClick = { onOpenUrl(imageUrl) },
                            onLongPress = onLongPress
                        )
                    }
                }

                val remainingAttachments = if (showImagesBeforeText || imageAttachments.size > 1) {
                    attachmentsForDefaultRendering.filterNot { it.isImageAttachment() }
                } else {
                    attachmentsForDefaultRendering
                }

                if (!showImagesBeforeText && imageAttachments.size > 1) {
                    AttachmentAlbumGrid(
                        attachments = imageAttachments,
                        attachmentUrlResolver = attachmentUrlResolver,
                        accessToken = accessToken,
                        onAttachmentClick = onAttachmentClick,
                        onLongPress = onLongPress
                    )
                }

                remainingAttachments.forEach { attachment ->
                    AttachmentCard(
                        attachment = attachment,
                        attachmentUrl = attachmentUrlResolver(attachment),
                        accessToken = accessToken,
                        playbackState = attachmentPlaybackState[attachment.id],
                        onClick = { onAttachmentClick(attachment) },
                        onLongPress = onLongPress
                    )
                }

                if (botQuickActions.isNotEmpty()) {
                    FlowRow(
                        horizontalArrangement = Arrangement.spacedBy(6.dp),
                        verticalArrangement = Arrangement.spacedBy(6.dp)
                    ) {
                        botQuickActions.forEach { action ->
                            Surface(
                                color = Color(0xFFEEF5FD),
                                shape = RoundedCornerShape(16.dp),
                                modifier = Modifier.clickable { onQuickActionClick(action.commandText) }
                            ) {
                                Text(
                                    text = action.title,
                                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
                                    style = MaterialTheme.typography.labelLarge,
                                    color = TelegramBlue
                                )
                            }
                        }
                    }
                }

                if (message.reactions.isNotEmpty()) {
                    ReactionRow(
                        reactions = message.reactions,
                        onReactionClick = onReactionClick
                    )
                }

                if (canOpenDiscussion) {
                    ChannelDiscussionLink(
                        commentCount = message.commentCount ?: 0,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp),
                        onClick = { onDiscussionClick(discussionMessageId) }
                    )
                }

                MessageMeta(
                    sentAt = message.sentAt,
                    deliveryState = message.deliveryState,
                    isOutgoing = isOutgoing,
                    viewCount = message.viewCount,
                    localStatus = localStatus,
                    modifier = Modifier.align(Alignment.End)
                )
            }
        }
            }

            if (!isOutgoing && isSelected) {
                Spacer(modifier = Modifier.width(8.dp))
                SelectionIndicator()
            }
        }
    }
}

@Composable
private fun ChannelDiscussionLink(
    commentCount: Int,
    modifier: Modifier = Modifier,
    onClick: () -> Unit
) {
    val label = if (commentCount > 0) {
        "${formatCompactCount(commentCount)} комментариев"
    } else {
        "Комментировать"
    }

    Surface(
        color = Color(0xFFEAF4FB),
        shape = RoundedCornerShape(16.dp),
        modifier = modifier.clickable(onClick = onClick)
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = Icons.Outlined.Forum,
                contentDescription = "Комментарии",
                tint = TelegramBlue,
                modifier = Modifier.size(16.dp)
            )
            Spacer(modifier = Modifier.width(6.dp))
            Text(
                text = label,
                style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold),
                color = TelegramBlue
            )
        }
    }
}

@Composable
private fun SelectionIndicator() {
    Icon(
        imageVector = Icons.Outlined.CheckCircle,
        contentDescription = "Сообщение выбрано",
        tint = TelegramBlue,
        modifier = Modifier.size(20.dp)
    )
}

@Composable
private fun EmojiStickerSuggestionPanel(
    items: List<PickerMediaItem>,
    onMediaSelected: (PickerMediaItem) -> Unit
) {
    Surface(
        color = Color(0xDDF2FBFF),
        shape = RoundedCornerShape(18.dp),
        shadowElevation = 2.dp,
        modifier = Modifier
            .fillMaxWidth()
            .height(PickerSuggestionPanelHeight)
    ) {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = PickerSuggestionGridMinCellWidth),
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.SpaceEvenly,
            verticalArrangement = Arrangement.spacedBy(6.dp)
        ) {
            items(items = items, key = { item -> "suggestion-${item.key}" }) { item ->
                PickerMediaSuggestionTile(
                    item = item,
                    onClick = { onMediaSelected(item) }
                )
            }
        }
    }
}

@Composable
private fun PickerMediaSuggestionTile(
    item: PickerMediaItem,
    onClick: () -> Unit
) {
    val isGif = item.kind == PickerMediaKind.Gif
    val gifPreviewRequest = rememberPickerGifPreviewRequest(item = item, isGif = isGif)

    Surface(
        color = Color.Transparent,
        shape = RoundedCornerShape(14.dp),
        modifier = Modifier
            .fillMaxWidth()
            .height(PickerSuggestionGridCellHeight)
            .clickable(onClick = onClick)
    ) {
        Box(contentAlignment = Alignment.Center) {
            Surface(
                color = if (isGif) Color.White.copy(alpha = 0.72f) else Color(item.backgroundColor),
                shape = RoundedCornerShape(if (isGif) 14.dp else 18.dp),
                modifier = Modifier.size(if (isGif) 62.dp else 66.dp)
            ) {
                Box(contentAlignment = Alignment.Center) {
                    if (gifPreviewRequest != null) {
                        AsyncImage(
                            model = gifPreviewRequest,
                            contentDescription = item.title,
                            modifier = Modifier
                                .fillMaxSize()
                                .padding(4.dp),
                            contentScale = ContentScale.Fit
                        )
                    } else {
                        Text(
                            text = item.previewEmoji,
                            fontSize = if (isGif) 28.sp else 34.sp
                        )
                    }
                }
            }
            if (isGif) {
                Surface(
                    color = TelegramBlue,
                    shape = RoundedCornerShape(7.dp),
                    modifier = Modifier
                        .align(Alignment.BottomEnd)
                        .padding(end = 2.dp, bottom = 2.dp)
                ) {
                    Text(
                        text = "GIF",
                        modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp),
                        style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
                        color = Color.White
                    )
                }
            }
        }
    }
}

@Composable
private fun EmojiKeyboardPanel(
    categories: List<EmojiCategory>,
    height: Dp,
    activeTab: EmojiKeyboardTab,
    allowMediaTabs: Boolean,
    gifPacks: List<PickerMediaPack>,
    stickerPacks: List<PickerMediaPack>,
    onTabSelected: (EmojiKeyboardTab) -> Unit,
    onDismiss: () -> Unit,
    onEmojiSelected: (String) -> Unit,
    onMediaSelected: (PickerMediaItem) -> Unit
) {
    val gridState = rememberLazyGridState()
    val coroutineScope = rememberCoroutineScope()
    val categoryScrollState = rememberScrollState()
    val resolvedActiveTab = if (allowMediaTabs) activeTab else EmojiKeyboardTab.Emoji
    var selectedGifPackKey by remember(gifPacks) { mutableStateOf(gifPacks.firstOrNull()?.key.orEmpty()) }
    var selectedStickerPackKey by remember(stickerPacks) { mutableStateOf(stickerPacks.firstOrNull()?.key.orEmpty()) }
    val emojiGridItems = remember(categories) {
        var globalIndex = 0
        categories.flatMapIndexed { categoryIndex, category ->
            category.emojis.map { emoji ->
                EmojiGridItem(
                    index = globalIndex++,
                    categoryKey = category.key,
                    categoryIndex = categoryIndex,
                    emoji = emoji
                )
            }
        }
    }
    val categoryFirstIndexes = remember(categories) {
        var itemIndex = 0
        categories.map { category ->
            val firstIndex = itemIndex
            itemIndex += category.emojis.size
            firstIndex
        }
    }
    val selectedCategoryIndex by remember(emojiGridItems, gridState) {
        derivedStateOf {
            emojiGridItems
                .getOrNull(gridState.firstVisibleItemIndex)
                ?.categoryIndex
                ?.coerceIn(0, categories.lastIndex.coerceAtLeast(0))
                ?: 0
        }
    }
    val emojiGridViewportHeight = remember(height, allowMediaTabs) {
        val tabHeight = if (allowMediaTabs) EmojiKeyboardBottomTabsHeight else 0.dp
        val availableHeight = (height - EmojiCategoryStripHeight - tabHeight).coerceAtLeast(EmojiGridCellHeight * 2f)
        val fourRowsHeight = EmojiGridCellHeight * EmojiGridVisibleRows.toFloat()
        availableHeight.coerceAtMost(fourRowsHeight)
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .height(height)
            .background(TelegramSurface)
    ) {
        Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
            when (resolvedActiveTab) {
                EmojiKeyboardTab.Emoji -> EmojiKeyboardEmojiPage(
                    categories = categories,
                    categoryScrollState = categoryScrollState,
                    gridState = gridState,
                    emojiGridItems = emojiGridItems,
                    categoryFirstIndexes = categoryFirstIndexes,
                    selectedCategoryIndex = selectedCategoryIndex,
                    gridViewportHeight = emojiGridViewportHeight,
                    coroutineScope = coroutineScope,
                    onEmojiSelected = onEmojiSelected
                )

                EmojiKeyboardTab.Gif -> {
                    val selectedPack = gifPacks.firstOrNull { it.key == selectedGifPackKey } ?: gifPacks.firstOrNull()
                    EmojiKeyboardMediaPage(
                        packs = gifPacks,
                        selectedPackKey = selectedPack?.key.orEmpty(),
                        onPackSelected = { selectedGifPackKey = it },
                        items = selectedPack?.items.orEmpty(),
                        isGif = true,
                        onMediaSelected = onMediaSelected
                    )
                }

                EmojiKeyboardTab.Stickers -> {
                    val selectedPack = stickerPacks.firstOrNull { it.key == selectedStickerPackKey } ?: stickerPacks.firstOrNull()
                    EmojiKeyboardMediaPage(
                        packs = stickerPacks,
                        selectedPackKey = selectedPack?.key.orEmpty(),
                        onPackSelected = { selectedStickerPackKey = it },
                        items = selectedPack?.items.orEmpty(),
                        isGif = false,
                        onMediaSelected = onMediaSelected
                    )
                }
            }
        }

        if (allowMediaTabs) {
            HorizontalDivider(color = Color(0xFFE6EEF6))
            EmojiKeyboardTabRow(
                activeTab = resolvedActiveTab,
                onTabSelected = onTabSelected,
                onDismiss = onDismiss
            )
        }
    }
}

@Composable
private fun EmojiKeyboardTabRow(
    activeTab: EmojiKeyboardTab,
    onTabSelected: (EmojiKeyboardTab) -> Unit,
    onDismiss: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 10.dp, vertical = 7.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Spacer(modifier = Modifier.weight(1f))
        Surface(
            color = Color(0xFFFFFFFF),
            shape = RoundedCornerShape(19.dp),
            shadowElevation = 2.dp
        ) {
            Row(
                modifier = Modifier.padding(2.dp),
                horizontalArrangement = Arrangement.spacedBy(2.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                EmojiKeyboardTabChip(
                    text = "\u042d\u043c\u043e\u0434\u0437\u0438",
                    selected = activeTab == EmojiKeyboardTab.Emoji,
                    onClick = { onTabSelected(EmojiKeyboardTab.Emoji) }
                )
                EmojiKeyboardTabChip(
                    text = "GIF",
                    selected = activeTab == EmojiKeyboardTab.Gif,
                    onClick = { onTabSelected(EmojiKeyboardTab.Gif) }
                )
                EmojiKeyboardTabChip(
                    text = "\u0421\u0442\u0438\u043a\u0435\u0440\u044b",
                    selected = activeTab == EmojiKeyboardTab.Stickers,
                    onClick = { onTabSelected(EmojiKeyboardTab.Stickers) }
                )
            }
        }
        Spacer(modifier = Modifier.weight(1f))
        Surface(
            color = Color(0xFFFFFFFF),
            shape = CircleShape,
            shadowElevation = 2.dp,
            modifier = Modifier.size(38.dp).clickable(onClick = onDismiss)
        ) {
            Box(contentAlignment = Alignment.Center) {
                Icon(
                    imageVector = Icons.Outlined.Close,
                    contentDescription = "Close emoji panel",
                    tint = TelegramInkMuted,
                    modifier = Modifier.size(22.dp)
                )
            }
        }
    }
}

@Composable
private fun EmojiKeyboardTabChip(
    text: String,
    selected: Boolean,
    onClick: () -> Unit
) {
    Surface(
        color = if (selected) Color(0xFFECE8DF) else Color.Transparent,
        shape = RoundedCornerShape(17.dp),
        modifier = Modifier
            .height(34.dp)
            .widthIn(min = if (text == "GIF") 54.dp else 86.dp)
            .clickable(onClick = onClick)
    ) {
        Box(
            modifier = Modifier.padding(horizontal = 12.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = text,
                style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
                color = if (selected) TelegramInk else TelegramInkMuted,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    }
}
@Composable
private fun EmojiKeyboardEmojiPage(
    categories: List<EmojiCategory>,
    categoryScrollState: androidx.compose.foundation.ScrollState,
    gridState: androidx.compose.foundation.lazy.grid.LazyGridState,
    emojiGridItems: List<EmojiGridItem>,
    categoryFirstIndexes: List<Int>,
    selectedCategoryIndex: Int,
    gridViewportHeight: Dp,
    coroutineScope: kotlinx.coroutines.CoroutineScope,
    onEmojiSelected: (String) -> Unit
) {
    Column(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .horizontalScroll(categoryScrollState)
                .padding(horizontal = 8.dp, vertical = 6.dp),
            horizontalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            categories.forEachIndexed { index, category ->
                EmojiCategoryChip(
                    category = category,
                    selected = index == selectedCategoryIndex,
                    onClick = {
                        coroutineScope.launch {
                            gridState.animateScrollToItem(categoryFirstIndexes.getOrElse(index) { 0 })
                        }
                    }
                )
            }
        }

        HorizontalDivider(color = Color(0xFFE6EEF6))

        LazyVerticalGrid(
            columns = GridCells.Fixed(EmojiGridColumns),
            state = gridState,
            modifier = Modifier
                .fillMaxWidth()
                .height(gridViewportHeight),
            contentPadding = PaddingValues(start = 4.dp, top = 6.dp, end = 4.dp, bottom = 6.dp),
            horizontalArrangement = Arrangement.spacedBy(0.dp),
            verticalArrangement = Arrangement.spacedBy(0.dp)
        ) {
            items(
                items = emojiGridItems,
                key = { item -> "${item.index}-${item.categoryKey}-${item.emoji}" }
            ) { item ->
                EmojiPickerButton(
                    emoji = item.emoji,
                    onClick = { onEmojiSelected(item.emoji) }
                )
            }
        }
    }
}

@Composable
private fun EmojiCategoryChip(
    category: EmojiCategory,
    selected: Boolean,
    onClick: () -> Unit
) {
    Surface(
        color = if (selected) Color(0xFFE7F1FF) else Color(0xFFF4F7FA),
        shape = RoundedCornerShape(16.dp),
        modifier = Modifier
            .size(34.dp)
            .clickable(onClick = onClick)
    ) {
        Box(contentAlignment = Alignment.Center) {
            Text(
                text = category.icon,
                fontSize = 18.sp,
                color = if (selected) TelegramBlue else TelegramInk
            )
        }
    }
}

@Composable
private fun EmojiPickerButton(
    emoji: String,
    onClick: () -> Unit
) {
    Surface(
        color = Color.Transparent,
        shape = RoundedCornerShape(8.dp),
        modifier = Modifier
            .fillMaxWidth()
            .height(EmojiGridCellHeight)
            .clickable(onClick = onClick)
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = emoji,
                style = MaterialTheme.typography.headlineMedium.copy(
                    fontSize = 28.sp,
                    shadow = Shadow(
                        color = Color.Black.copy(alpha = 0.32f),
                        offset = Offset(0f, 5f),
                        blurRadius = 6f
                    )
                ),
                modifier = Modifier.graphicsLayer {
                    scaleX = 1.04f
                    scaleY = 1.04f
                }
            )
        }
    }
}

@Composable
private fun ReplyPreviewCard(
    replyTo: com.seven.ikar.kotlin.core.model.MessageReplyDto,
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {}
) {
    Surface(
        color = Color(0x11000000),
        shape = RoundedCornerShape(12.dp),
        shadowElevation = 0.dp,
        modifier = modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Column(
            modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
            verticalArrangement = Arrangement.spacedBy(2.dp)
        ) {
            Text(
                text = replyTo.senderDisplayName,
                style = MaterialTheme.typography.labelMedium,
                color = TelegramBlue,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
            Text(
                text = replyTo.previewText,
                style = MaterialTheme.typography.bodySmall,
                color = TelegramInkMuted,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis
            )
        }
    }
}

@Composable
private fun StoryReplyPreviewCard(
    title: String,
    previewText: String,
    attachmentKind: String?,
    modifier: Modifier = Modifier
) {
    Surface(
        color = Color(0xFFEAF4FF),
        shape = RoundedCornerShape(12.dp),
        shadowElevation = 0.dp,
        modifier = modifier.fillMaxWidth()
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Surface(
                color = TelegramBlue,
                shape = CircleShape,
                modifier = Modifier.size(28.dp)
            ) {
                Box(contentAlignment = Alignment.Center) {
                    Icon(
                        imageVector = storyReplyKindIcon(attachmentKind),
                        contentDescription = null,
                        tint = Color.White,
                        modifier = Modifier.size(17.dp)
                    )
                }
            }
            Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(
                    text = title,
                    style = MaterialTheme.typography.labelMedium,
                    color = TelegramBlue,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                Text(
                    text = previewText,
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramInk,
                    maxLines = 2,
                    overflow = TextOverflow.Ellipsis
                )
            }
        }
    }
}

@Composable
private fun EmojiKeyboardMediaPage(
    packs: List<PickerMediaPack>,
    selectedPackKey: String,
    onPackSelected: (String) -> Unit,
    items: List<PickerMediaItem>,
    isGif: Boolean,
    onMediaSelected: (PickerMediaItem) -> Unit
) {
    val packScrollState = rememberScrollState()

    Column(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .horizontalScroll(packScrollState)
                .padding(horizontal = 8.dp, vertical = 6.dp),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            packs.forEach { pack ->
                PickerMediaPackChip(
                    pack = pack,
                    selected = pack.key == selectedPackKey,
                    onClick = { onPackSelected(pack.key) }
                )
            }
        }

        HorizontalDivider(color = Color(0xFFE6EEF6))

        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = PickerMediaGridMinCellWidth),
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f),
            contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 10.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(items = items, key = { item -> item.key }) { item ->
                PickerMediaButton(
                    item = item,
                    isGif = isGif,
                    onClick = { onMediaSelected(item) }
                )
            }
        }
    }
}

@Composable
private fun PickerMediaPackChip(
    pack: PickerMediaPack,
    selected: Boolean,
    onClick: () -> Unit
) {
    Surface(
        color = if (selected) Color(0xFFE7F1FF) else Color(0xFFF4F7FA),
        shape = RoundedCornerShape(16.dp),
        modifier = Modifier
            .height(34.dp)
            .clickable(onClick = onClick)
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 10.dp),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = pack.icon, fontSize = 16.sp)
            Text(
                text = pack.title,
                style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold),
                color = if (selected) TelegramBlue else TelegramInk
            )
        }
    }
}

@Composable
private fun PickerMediaButton(
    item: PickerMediaItem,
    isGif: Boolean,
    onClick: () -> Unit
) {
    val gifPreviewRequest = rememberPickerGifPreviewRequest(item = item, isGif = isGif)

    Surface(
        color = Color(0xFFF7FAFD),
        shape = RoundedCornerShape(14.dp),
        modifier = Modifier
            .fillMaxWidth()
            .height(PickerMediaGridCellHeight)
            .clickable(onClick = onClick)
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 8.dp, vertical = 8.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Box(contentAlignment = Alignment.Center) {
                Surface(
                    color = Color(item.backgroundColor).copy(alpha = if (isGif) 0.58f else 1f),
                    shape = RoundedCornerShape(if (isGif) 16.dp else 20.dp),
                    modifier = Modifier.size(58.dp)
                ) {
                    Box(contentAlignment = Alignment.Center) {
                        if (gifPreviewRequest != null) {
                            AsyncImage(
                                model = gifPreviewRequest,
                                contentDescription = item.title,
                                modifier = Modifier
                                    .fillMaxSize()
                                    .padding(4.dp),
                                contentScale = ContentScale.Fit
                            )
                        } else {
                            Text(text = item.previewEmoji, fontSize = if (isGif) 26.sp else 30.sp)
                        }
                    }
                }
                if (isGif) {
                    Surface(
                        color = TelegramBlue,
                        shape = RoundedCornerShape(7.dp),
                        modifier = Modifier.align(Alignment.BottomEnd)
                    ) {
                        Text(
                            text = "GIF",
                            modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp),
                            style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
                            color = Color.White
                        )
                    }
                }
            }
            Spacer(modifier = Modifier.height(6.dp))
            Text(
                text = item.title,
                style = MaterialTheme.typography.labelSmall,
                color = TelegramInkMuted,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    }
}

@Composable
private fun rememberPickerGifPreviewRequest(
    item: PickerMediaItem,
    isGif: Boolean
): ImageRequest? {
    val context = LocalContext.current
    val gifPreviewBytes = remember(item.key, item.base64Data, isGif) {
        if (!isGif || item.base64Data.isNullOrBlank()) {
            null
        } else {
            runCatching { Base64.decode(item.base64Data, Base64.DEFAULT) }.getOrNull()
        }
    }
    return remember(context, item.key, gifPreviewBytes) {
        gifPreviewBytes?.let { bytes ->
            ImageRequest.Builder(context)
                .data(bytes)
                .memoryCacheKey("picker-gif-${item.key}")
                .diskCacheKey("picker-gif-${item.key}")
                .crossfade(false)
                .build()
        }
    }
}

private val SuggestedPickerMediaDefaultKeys = listOf(
    "gif_ikar_spark",
    "gif_heart",
    "gif_fire",
    "gif_ikar_pulse",
    "gif_ikar_launch",
    "sticker_laugh",
    "sticker_love",
    "sticker_ok",
    "sticker_ikar_wow",
    "sticker_ikar_power",
    "sticker_ikar_focus",
    "sticker_ikar_hug"
)

private fun suggestedPickerMediaForEmoji(
    emoji: String?,
    gifPacks: List<PickerMediaPack>,
    stickerPacks: List<PickerMediaPack>
): List<PickerMediaItem> {
    val emojiKey = emoji?.normalizedEmojiSuggestionKey().orEmpty()
    if (emojiKey.isBlank()) {
        return emptyList()
    }

    val allItems = (gifPacks + stickerPacks).flatMap { pack -> pack.items }
    val exactItems = allItems.filter { item -> item.matchesSuggestedEmoji(emojiKey) }
    val defaultItems = SuggestedPickerMediaDefaultKeys.mapNotNull { key ->
        allItems.firstOrNull { item -> item.key == key }
    }

    return (exactItems + defaultItems + allItems)
        .distinctBy { item -> item.key }
        .take(12)
}

private fun PickerMediaItem.matchesSuggestedEmoji(emojiKey: String): Boolean =
    previewEmoji.normalizedEmojiSuggestionKey() == emojiKey ||
        stickerEmoji?.normalizedEmojiSuggestionKey() == emojiKey

private fun String.normalizedEmojiSuggestionKey(): String =
    replace("\uFE0F", "")
        .replace("\uFE0E", "")
        .trim()

private fun storyReplyKindIcon(kind: String?): ImageVector =
    when (kind) {
        "image" -> Icons.Outlined.Image
        "video" -> Icons.Outlined.Videocam
        "audio" -> Icons.Outlined.PlayArrow
        "media" -> Icons.Outlined.Image
        else -> Icons.Outlined.Visibility
    }

@Composable
private fun ReactionRow(
    reactions: List<MessageReactionDto>,
    onReactionClick: (String) -> Unit
) {
    FlowRow(
        horizontalArrangement = Arrangement.spacedBy(6.dp),
        verticalArrangement = Arrangement.spacedBy(6.dp)
    ) {
        reactions.forEach { reaction ->
            Surface(
                color = if (reaction.isMine) Color(0xFFE7F1FF) else Color(0xFFF4F7FA),
                shape = RoundedCornerShape(14.dp),
                shadowElevation = 0.dp,
                modifier = Modifier.clickable { onReactionClick(reaction.emoji) }
            ) {
                Text(
                    text = "${reaction.emoji} ${reaction.count}",
                    modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
                    style = MaterialTheme.typography.labelMedium,
                    color = if (reaction.isMine) TelegramBlue else TelegramInk
                )
            }
        }
    }
}

@Composable
private fun MessageText(
    text: String,
    modifier: Modifier = Modifier,
    textSizeSp: Int,
    onUrlClick: (String) -> Unit,
    onTap: () -> Unit,
    onLongPress: () -> Unit
) {
    var revealedSpoilers by remember(text) { mutableStateOf<Set<String>>(emptySet()) }
    val annotatedText = remember(text, revealedSpoilers) {
        buildMessageAnnotatedText(text, revealedSpoilers)
    }
    var layoutResult by remember(annotatedText) { mutableStateOf<TextLayoutResult?>(null) }

    Text(
        text = annotatedText,
        modifier = modifier.pointerInput(annotatedText) {
            detectTapGestures(
                onTap = { position ->
                    val offset = layoutResult?.getOffsetForPosition(position) ?: return@detectTapGestures
                    val urlAnnotation = annotatedText
                        .getStringAnnotations(tag = MessageUrlAnnotationTag, start = offset, end = offset)
                        .firstOrNull()
                    val spoilerAnnotation = annotatedText
                        .getStringAnnotations(tag = MessageSpoilerAnnotationTag, start = offset, end = offset)
                        .firstOrNull()
                    when {
                        urlAnnotation != null -> onUrlClick(urlAnnotation.item)
                        spoilerAnnotation != null -> {
                            val key = spoilerAnnotation.item
                            revealedSpoilers = if (revealedSpoilers.contains(key)) {
                                revealedSpoilers - key
                            } else {
                                revealedSpoilers + key
                            }
                        }
                        else -> onTap()
                    }
                },
                onLongPress = { onLongPress() }
            )
        },
        style = MaterialTheme.typography.bodyLarge.copy(color = TelegramInk, fontSize = textSizeSp.sp),
        onTextLayout = { layoutResult = it }
    )
}

private fun isLargeEmojiMessage(text: String): Boolean {
    val trimmed = text.trim()
    if (trimmed.isEmpty() || trimmed.length > 32) {
        return false
    }

    var emojiCount = 0
    var index = 0
    while (index < trimmed.length) {
        val codePoint = trimmed.codePointAt(index)
        when {
            Character.isWhitespace(codePoint) -> Unit
            isEmojiBaseCodePoint(codePoint) -> emojiCount += 1
            isEmojiJoinerOrModifier(codePoint) -> Unit
            else -> return false
        }
        index += Character.charCount(codePoint)
    }
    return emojiCount in 1..3
}

private fun isEmojiBaseCodePoint(codePoint: Int): Boolean =
    codePoint in 0x1F1E6..0x1F1FF ||
        codePoint in 0x1F300..0x1FAFF ||
        codePoint in 0x2600..0x27BF ||
        codePoint in 0x2B00..0x2BFF

private fun isEmojiJoinerOrModifier(codePoint: Int): Boolean =
    codePoint == 0x200D ||
        codePoint == 0x20E3 ||
        codePoint == 0xFE0E ||
        codePoint == 0xFE0F ||
        codePoint in 0x1F3FB..0x1F3FF

@Composable
private fun AttachmentAlbumGrid(
    attachments: List<AttachmentDto>,
    attachmentUrlResolver: (AttachmentDto) -> String,
    accessToken: String?,
    onAttachmentClick: (AttachmentDto) -> Unit,
    onLongPress: () -> Unit
) {
    ImageCollageLayout(itemCount = attachments.size) { index, modifier, overlayText ->
        val attachment = attachments[index]
        ImageCollageTile(
            imageUrl = attachmentUrlResolver(attachment),
            accessToken = accessToken,
            contentDescription = attachment.fileName,
            modifier = modifier.combinedClickable(
                onClick = { onAttachmentClick(attachment) },
                onLongClick = onLongPress
            ),
            overlayText = overlayText
        )
    }
}

@Composable
private fun ImageCollageLayout(
    itemCount: Int,
    minHeight: androidx.compose.ui.unit.Dp = 168.dp,
    maxHeight: androidx.compose.ui.unit.Dp = 224.dp,
    tileContent: @Composable (index: Int, modifier: Modifier, overlayText: String?) -> Unit
) {
    if (itemCount <= 0) {
        return
    }

    BoxWithConstraints(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
    ) {
        val gap = 2.dp
        val collageHeight = when (itemCount) {
            1 -> minHeight
            2 -> (maxWidth * 0.62f).coerceIn(minHeight, maxHeight)
            else -> maxHeight
        }
        val visibleCount = itemCount.coerceAtMost(4)

        when (visibleCount) {
            1 -> {
                tileContent(0, Modifier.fillMaxWidth().height(collageHeight), null)
            }

            2 -> {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(collageHeight),
                    horizontalArrangement = Arrangement.spacedBy(gap)
                ) {
                    repeat(2) { index ->
                        tileContent(index, Modifier.weight(1f).fillMaxHeight(), null)
                    }
                }
            }

            3 -> {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(collageHeight),
                    horizontalArrangement = Arrangement.spacedBy(gap)
                ) {
                    tileContent(0, Modifier.weight(1.35f).fillMaxHeight(), null)
                    Column(
                        modifier = Modifier.weight(1f).fillMaxHeight(),
                        verticalArrangement = Arrangement.spacedBy(gap)
                    ) {
                        tileContent(1, Modifier.fillMaxWidth().weight(1f), null)
                        tileContent(2, Modifier.fillMaxWidth().weight(1f), null)
                    }
                }
            }

            else -> {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(collageHeight),
                    verticalArrangement = Arrangement.spacedBy(gap)
                ) {
                    repeat(2) { row ->
                        Row(
                            modifier = Modifier.fillMaxWidth().weight(1f),
                            horizontalArrangement = Arrangement.spacedBy(gap)
                        ) {
                            repeat(2) { column ->
                                val index = row * 2 + column
                                val overlayText = if (index == 3 && itemCount > 4) "+${itemCount - 4}" else null
                                tileContent(index, Modifier.weight(1f).fillMaxHeight(), overlayText)
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun ImageCollageTile(
    imageUrl: String?,
    accessToken: String?,
    contentDescription: String,
    modifier: Modifier,
    overlayText: String? = null
) {
    Box(
        modifier = modifier
            .background(Color(0xFFF7FAFD))
            .clip(RoundedCornerShape(3.dp))
    ) {
        AuthenticatedAsyncImage(
            imageUrl = imageUrl,
            accessToken = accessToken,
            contentDescription = contentDescription,
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Crop
        )
        if (overlayText != null) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Black.copy(alpha = 0.42f)),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = overlayText,
                    style = MaterialTheme.typography.headlineLarge,
                    color = Color.White
                )
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InlineMessageImage(
    imageUrl: String,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    ChatPhotoImage(
        imageUrl = imageUrl,
        accessToken = "",
        contentDescription = "",
        onClick = onClick,
        onLongPress = onLongPress
    )
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ChatPhotoImage(
    imageUrl: String?,
    accessToken: String?,
    contentDescription: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    var imageAspectRatio by remember(imageUrl) { mutableStateOf<Float?>(null) }

    BoxWithConstraints(
        modifier = modifier.fillMaxWidth(),
        contentAlignment = Alignment.Center
    ) {
        val maxPhotoHeight = 420.dp
        val knownRatio = imageAspectRatio
        val imageModifier = if (knownRatio != null) {
            val safeRatio = knownRatio.coerceIn(0.12f, 8f)
            val targetWidth = if (maxWidth / safeRatio > maxPhotoHeight) {
                maxPhotoHeight * safeRatio
            } else {
                maxWidth
            }
            Modifier
                .width(targetWidth)
                .aspectRatio(safeRatio)
        } else {
            Modifier
                .fillMaxWidth()
                .height(220.dp)
        }

        Box(
            modifier = imageModifier
                .clip(RoundedCornerShape(16.dp))
                .combinedClickable(onClick = onClick, onLongClick = onLongPress)
        ) {
            AuthenticatedAsyncImage(
                imageUrl = imageUrl,
                accessToken = accessToken,
                contentDescription = contentDescription,
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop,
                onSuccess = { state ->
                    val drawable = state.result.drawable
                    val intrinsicWidth = drawable.intrinsicWidth
                    val intrinsicHeight = drawable.intrinsicHeight
                    if (intrinsicWidth > 0 && intrinsicHeight > 0) {
                        imageAspectRatio = intrinsicWidth.toFloat() / intrinsicHeight.toFloat()
                    }
                }
            )
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InlineMessageAudio(
    audio: InlineAudioUrl,
    playbackState: VoiceAttachmentPlaybackUiState?,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    Surface(
        color = Color(0xFFF3F8FD),
        shape = RoundedCornerShape(16.dp),
        shadowElevation = 0.dp,
        modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 12.dp, vertical = 12.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            Surface(color = TelegramBlue, shape = CircleShape) {
                Box(modifier = Modifier.padding(10.dp), contentAlignment = Alignment.Center) {
                    Icon(
                        imageVector = if (playbackState?.isPlaying == true) Icons.Outlined.Stop else Icons.Outlined.PlayArrow,
                        contentDescription = "Voice message",
                        tint = Color.White
                    )
                }
            }
            val durationMs = playbackState?.durationMs?.takeIf { it > 0L } ?: 0L
            val progress = when {
                durationMs > 0L -> (playbackState?.positionMs ?: 0L).toFloat() / durationMs.toFloat()
                else -> 0f
            }.coerceIn(0f, 1f)

            Column(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(6.dp)
            ) {
                Text(
                    text = audio.title,
                    style = MaterialTheme.typography.titleSmall,
                    color = TelegramInk,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
                LinearProgressIndicator(
                    progress = { progress },
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(4.dp)
                        .clip(CircleShape),
                    color = TelegramBlue,
                    trackColor = Color(0xFFD6E5F5)
                )
                Text(
                    text = buildInlineAudioSecondaryText(playbackState),
                    style = MaterialTheme.typography.bodySmall,
                    color = TelegramInkMuted
                )
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TelegramMediaAttachment(
    attachment: AttachmentDto,
    attachmentUrl: String,
    accessToken: String?,
    isOutgoing: Boolean,
    modifier: Modifier = Modifier,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    val isSticker = attachment.isTelegramStickerAttachment()
    val mediaSize = if (isSticker) 164.dp else 220.dp
    val shape = if (isSticker) RoundedCornerShape(0.dp) else RoundedCornerShape(18.dp)
    val backgroundColor = if (isSticker) Color.Transparent else Color(0xFFF7FAFD)

    Box(
        modifier = modifier
            .size(mediaSize)
            .clip(shape)
            .background(backgroundColor)
            .combinedClickable(onClick = onClick, onLongClick = onLongPress),
        contentAlignment = if (isOutgoing) Alignment.BottomEnd else Alignment.BottomStart
    ) {
        AuthenticatedAsyncImage(
            imageUrl = attachmentUrl,
            accessToken = accessToken,
            contentDescription = if (isSticker) "Sticker" else "GIF",
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Fit
        )
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun VideoAttachmentCard(
    attachment: AttachmentDto,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    Surface(
        color = Color(0xFFF7FAFD),
        shape = RoundedCornerShape(16.dp),
        shadowElevation = 0.dp,
        modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)
    ) {
        Column(modifier = Modifier.fillMaxWidth()) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(208.dp)
                    .background(Color(0xFF17212B)),
                contentAlignment = Alignment.Center
            ) {
                Icon(
                    imageVector = Icons.Outlined.Videocam,
                    contentDescription = null,
                    tint = Color.White.copy(alpha = 0.24f),
                    modifier = Modifier
                        .align(Alignment.TopStart)
                        .padding(14.dp)
                        .size(28.dp)
                )
                Surface(
                    color = Color.White.copy(alpha = 0.92f),
                    shape = CircleShape,
                    shadowElevation = 2.dp
                ) {
                    Icon(
                        imageVector = Icons.Outlined.PlayArrow,
                        contentDescription = "Play video",
                        tint = TelegramBlue,
                        modifier = Modifier
                            .padding(16.dp)
                            .size(34.dp)
                    )
                }
            }
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 12.dp, vertical = 10.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                Icon(
                    imageVector = Icons.Outlined.Videocam,
                    contentDescription = null,
                    tint = TelegramBlue,
                    modifier = Modifier.size(20.dp)
                )
                Column(
                    modifier = Modifier.weight(1f),
                    verticalArrangement = Arrangement.spacedBy(2.dp)
                ) {
                    Text(
                        text = attachment.fileName,
                        style = MaterialTheme.typography.titleSmall,
                        color = TelegramInk,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                    Text(
                        text = "${attachment.contentType} \u2022 ${formatFileSize(attachment.fileSizeBytes)}",
                        style = MaterialTheme.typography.bodySmall,
                        color = TelegramInkMuted,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                }
            }
        }
    }
}

@Composable
private fun AttachmentCard(
    attachment: AttachmentDto,
    attachmentUrl: String,
    accessToken: String?,
    playbackState: VoiceAttachmentPlaybackUiState?,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    when {
        attachment.isImageAttachment() -> {
            ChatPhotoImage(
                imageUrl = attachmentUrl,
                accessToken = accessToken,
                contentDescription = attachment.fileName,
                onClick = onClick,
                onLongPress = onLongPress
            )
        }

        attachment.isVoiceNoteAttachment() -> {
            Surface(
                color = Color(0xFFF3F8FD),
                shape = RoundedCornerShape(16.dp),
                shadowElevation = 0.dp,
                modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)
            ) {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 12.dp, vertical = 12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    Surface(color = TelegramBlue, shape = CircleShape) {
                        Box(modifier = Modifier.padding(10.dp), contentAlignment = Alignment.Center) {
                            Icon(
                                imageVector = if (playbackState?.isPlaying == true) Icons.Outlined.Stop else Icons.Outlined.PlayArrow,
                                contentDescription = "Голосовое",
                                tint = Color.White
                            )
                        }
                    }
                    val durationMs = when {
                        playbackState?.durationMs?.let { it > 0L } == true -> playbackState.durationMs
                        else -> 0L
                    }
                    val progress = when {
                        durationMs > 0L -> (playbackState?.positionMs ?: 0L).toFloat() / durationMs.toFloat()
                        else -> 0f
                    }.coerceIn(0f, 1f)

                    Column(
                        modifier = Modifier.weight(1f),
                        verticalArrangement = Arrangement.spacedBy(6.dp)
                    ) {
                        LinearProgressIndicator(
                            progress = { progress },
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(4.dp)
                                .clip(CircleShape),
                            color = TelegramBlue,
                            trackColor = Color(0xFFD6E5F5)
                        )
                        Text(
                            text = buildVoiceNoteSecondaryText(attachment, playbackState),
                            style = MaterialTheme.typography.bodySmall,
                            color = TelegramInkMuted
                        )
                    }
                }
            }
        }

        attachment.isVideoAttachment() -> {
            VideoAttachmentCard(
                attachment = attachment,
                onClick = onClick,
                onLongPress = onLongPress
            )
        }

        else -> {
            Surface(
                color = Color(0xFFF7FAFD),
                shape = RoundedCornerShape(16.dp),
                shadowElevation = 0.dp,
                modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 12.dp, vertical = 10.dp),
                    verticalArrangement = Arrangement.spacedBy(4.dp)
                ) {
                    Text(text = attachment.fileName, style = MaterialTheme.typography.titleMedium, color = TelegramInk)
                    Text(
                        text = "${attachment.contentType} • ${formatFileSize(attachment.fileSizeBytes)}",
                        style = MaterialTheme.typography.bodySmall,
                        color = TelegramInkMuted
                    )
                }
            }
        }
    }
}

@Composable
private fun MessageMeta(
    sentAt: String,
    deliveryState: MessageDeliveryState,
    isOutgoing: Boolean,
    viewCount: Int?,
    localStatus: LocalMessageStatus?,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = formatMessageTime(sentAt),
            style = MaterialTheme.typography.labelSmall,
            color = if (isOutgoing) Color(0xFF6F8966) else TelegramInkMuted
        )

        if (viewCount != null) {
            Spacer(modifier = Modifier.width(6.dp))
            Icon(
                imageVector = Icons.Outlined.Visibility,
                contentDescription = null,
                tint = if (isOutgoing) Color(0xFF6F8966) else TelegramInkMuted,
                modifier = Modifier.size(13.dp)
            )
            Spacer(modifier = Modifier.width(2.dp))
            Text(
                text = formatCompactCount(viewCount),
                style = MaterialTheme.typography.labelSmall,
                color = if (isOutgoing) Color(0xFF6F8966) else TelegramInkMuted
            )
        }

        if (isOutgoing) {
            Spacer(modifier = Modifier.width(4.dp))
            Text(
                text = when (localStatus) {
                    LocalMessageStatus.Sending -> "..."
                    LocalMessageStatus.Failed -> "!"
                    null -> when (deliveryState) {
                        MessageDeliveryState.Read -> "\u2713\u2713"
                        MessageDeliveryState.Delivered -> "\u2713"
                        MessageDeliveryState.Sent -> "\u2713"
                    }
                },
                style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.SemiBold),
                color = when (localStatus) {
                    LocalMessageStatus.Failed -> Color(0xFFE53935)
                    LocalMessageStatus.Sending -> Color(0xFF6F8966)
                    null -> if (deliveryState == MessageDeliveryState.Read) TelegramBlue else Color(0xFF6F8966)
                }
            )
        }
    }
}

private const val MessageUrlAnnotationTag = "message-url"
private const val MessageSpoilerAnnotationTag = "message-spoiler"

private data class MessageUrlRange(
    val start: Int,
    val end: Int,
    val url: String
)

private data class MessageDisplayContent(
    val text: String,
    val imageUrls: List<String>,
    val audioUrls: List<InlineAudioUrl>
)

private data class InlineAudioUrl(
    val id: String,
    val url: String,
    val title: String
)

private fun MessageDto.standaloneTelegramMediaAttachment(displayContent: MessageDisplayContent): AttachmentDto? {
    if (attachments.size != 1 ||
        displayContent.text.isNotBlank() ||
        displayContent.imageUrls.isNotEmpty() ||
        displayContent.audioUrls.isNotEmpty()
    ) {
        return null
    }

    return attachments.firstOrNull()?.takeIf { it.isTelegramMediaAttachment() }
}

private fun AttachmentDto.isTelegramMediaAttachment(): Boolean =
    isTelegramGifAttachment() || isTelegramStickerAttachment()

private fun AttachmentDto.isTelegramGifAttachment(): Boolean {
    val normalizedContentType = contentType
        .substringBefore(';')
        .trim()
        .lowercase(Locale.US)
    val normalizedFileName = fileName
        .substringBefore('?')
        .substringBefore('#')
        .trim()
        .lowercase(Locale.US)
    return normalizedContentType == "image/gif" ||
        normalizedFileName.endsWith(".gif")
}

private fun AttachmentDto.isTelegramStickerAttachment(): Boolean {
    val normalizedContentType = contentType
        .substringBefore(';')
        .trim()
        .lowercase(Locale.US)
    val normalizedFileName = fileName
        .substringBefore('?')
        .substringBefore('#')
        .trim()
        .lowercase(Locale.US)
    val isBuiltInStickerName = normalizedFileName.startsWith("ikar_sticker_") ||
        normalizedFileName.startsWith("ikar-sticker-") ||
        normalizedFileName.contains("_sticker_")
    val isImageFile = normalizedContentType in setOf("image/png", "image/webp") ||
        normalizedFileName.endsWith(".png") ||
        normalizedFileName.endsWith(".webp")
    return isBuiltInStickerName && isImageFile
}

private val ImageLabelRegex = Regex("""(^|[\s:;,.!?()\[\]{}-])изображени[еяю]?([\s:;,.!?()\[\]{}-]|$)""", RegexOption.IGNORE_CASE)
private val ImageFileExtensions = setOf("jpg", "jpeg", "png", "webp", "gif", "bmp", "heic", "heif")
private val AudioFileExtensions = setOf("aac", "flac", "m4a", "mp3", "oga", "ogg", "opus", "wav", "webm")

private fun buildMessageDisplayContent(text: String): MessageDisplayContent {
    if (text.isBlank()) {
        return MessageDisplayContent(text = text, imageUrls = emptyList(), audioUrls = emptyList())
    }

    val imageUrls = mutableListOf<String>()
    val audioUrls = mutableListOf<InlineAudioUrl>()
    val visibleLines = mutableListOf<String>()
    var pendingImageLabel = false

    text.lines().forEach { line ->
        val hasImageLabel = ImageLabelRegex.containsMatchIn(line)
        val ranges = extractMessageUrlRanges(line)
        if (ranges.isEmpty()) {
            val cleanedLine = if (hasImageLabel) cleanInlineImageLine(line, emptyList()) else line
            if (hasImageLabel && cleanedLine.isBlank()) {
                pendingImageLabel = true
                return@forEach
            }

            visibleLines += line
            pendingImageLabel = false
            return@forEach
        }

        val imageRanges = ranges
            .filter { range -> isInlineImageUrl(line, range, pendingImageLabel || hasImageLabel) }
        val audioRanges = ranges
            .filter { range -> isAudioUrl(range.url) }
        if (imageRanges.isEmpty() && audioRanges.isEmpty()) {
            visibleLines += line
            pendingImageLabel = false
            return@forEach
        }

        imageUrls += imageRanges.map { it.url }
        audioUrls += audioRanges.map { range ->
            InlineAudioUrl(
                id = range.url,
                url = range.url,
                title = buildInlineAudioTitle(line, range)
            )
        }
        val cleanedLine = cleanInlineMediaLine(line, imageRanges, audioRanges)
        if (cleanedLine.isNotBlank()) {
            visibleLines += cleanedLine
        }
        pendingImageLabel = false
    }

    return MessageDisplayContent(
        text = visibleLines.joinToString("\n").trim(),
        imageUrls = imageUrls.distinct(),
        audioUrls = audioUrls.distinctBy { it.url }
    )
}

private fun isInlineImageUrl(line: String, range: MessageUrlRange, forcedByLabel: Boolean): Boolean =
    forcedByLabel || isImageUrl(range.url) || ImageLabelRegex.containsMatchIn(line)

private fun cleanInlineImageLine(line: String, imageRanges: List<MessageUrlRange>): String {
    return cleanInlineMediaLine(line, imageRanges, emptyList(), removeImageLabel = true)
}

private fun cleanInlineMediaLine(
    line: String,
    imageRanges: List<MessageUrlRange>,
    audioRanges: List<MessageUrlRange>,
    removeImageLabel: Boolean = imageRanges.isNotEmpty()
): String {
    var result = line
    (imageRanges + audioRanges)
        .distinctBy { it.start to it.end }
        .sortedByDescending { it.start }
        .forEach { range -> result = result.removeRange(range.start, range.end) }

    if (removeImageLabel) {
        result = ImageLabelRegex.replace(result, " ")
    }

    if (audioRanges.isNotEmpty() && result.trim().looksLikeAudioFileName()) {
        result = ""
    }

    return result
        .replace(Regex("""[ \t]{2,}"""), " ")
        .trim()
        .trim(':', ';', ',', '.', '-', '–', '—', '•')
        .trim()
}

private fun isImageUrl(url: String): Boolean {
    val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return false
    val pathExtension = uri.path
        ?.substringAfterLast('.', missingDelimiterValue = "")
        ?.lowercase(Locale.US)
        .orEmpty()
    if (pathExtension in ImageFileExtensions) {
        return true
    }

    val query = uri.query.orEmpty().lowercase(Locale.US)
    return ImageFileExtensions.any { extension ->
        query.contains("format=$extension") ||
            query.contains("fm=$extension") ||
            query.contains("ext=$extension") ||
            query.contains("image/$extension")
    }
}

private fun isAudioUrl(url: String): Boolean {
    val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return false
    val path = uri.path.orEmpty()
    if (path.looksLikeAudioFileName()) {
        return true
    }

    val lastPathSegment = uri.lastPathSegment.orEmpty()
    if (lastPathSegment.looksLikeAudioFileName()) {
        return true
    }

    val query = uri.query.orEmpty().lowercase(Locale.US)
    return AudioFileExtensions.any { extension ->
        query.contains("format=$extension") ||
            query.contains("fm=$extension") ||
            query.contains("ext=$extension") ||
            query.contains("audio/$extension")
    }
}

private fun buildInlineAudioTitle(line: String, range: MessageUrlRange): String {
    val titleFromLine = cleanInlineMediaLine(line, emptyList(), listOf(range))
    if (titleFromLine.isNotBlank() && !titleFromLine.equals(range.url, ignoreCase = true)) {
        return titleFromLine
    }

    return runCatching { Uri.parse(range.url).lastPathSegment }
        .getOrNull()
        ?.takeIf { it.isNotBlank() }
        ?: "Voice message"
}

private fun buildMessageAnnotatedText(text: String, revealedSpoilers: Set<String> = emptySet()): AnnotatedString {
    val builder = AnnotatedString.Builder(text)
    extractMessageUrlRanges(text).forEach { range ->
        builder.addStyle(
            style = SpanStyle(
                color = TelegramBlue,
                textDecoration = TextDecoration.Underline
            ),
            start = range.start,
            end = range.end
        )
        builder.addStringAnnotation(
            tag = MessageUrlAnnotationTag,
            annotation = range.url,
            start = range.start,
            end = range.end
        )
    }
    MentionHashtagRegex.findAll(text).forEach { match ->
        builder.addStyle(
            style = SpanStyle(color = TelegramBlue, fontWeight = FontWeight.SemiBold),
            start = match.range.first,
            end = match.range.last + 1
        )
    }
    SpoilerRegex.findAll(text).forEach { match ->
        val key = match.range.first.toString()
        val isRevealed = revealedSpoilers.contains(key)
        builder.addStyle(
            style = SpanStyle(
                color = if (isRevealed) TelegramInk else Color.Transparent,
                background = if (isRevealed) Color(0xFFE4EEF6) else Color(0xFF9AA7B2)
            ),
            start = match.range.first,
            end = match.range.last + 1
        )
        builder.addStringAnnotation(
            tag = MessageSpoilerAnnotationTag,
            annotation = key,
            start = match.range.first,
            end = match.range.last + 1
        )
    }
    return builder.toAnnotatedString()
}

private val MentionHashtagRegex = Regex("""(?<![\p{L}\p{N}_])[@#][\p{L}\p{N}_]{2,}""")
private val SpoilerRegex = Regex("""\|\|.+?\|\|""")

private fun extractMessageUrlRanges(text: String): List<MessageUrlRange> {
    val matcher = Patterns.WEB_URL.matcher(text)
    val ranges = mutableListOf<MessageUrlRange>()
    while (matcher.find()) {
        val rawMatch = matcher.group().orEmpty()
        val trimmedMatch = rawMatch.trimEnd('.', ',', '!', '?', ';', ':', ')', ']', '}', '>')
        if (trimmedMatch.isBlank()) {
            continue
        }

        val start = matcher.start()
        val end = start + trimmedMatch.length
        ranges += MessageUrlRange(
            start = start,
            end = end,
            url = normalizeExternalUrl(trimmedMatch)
        )
    }
    return ranges
}

private fun normalizeExternalUrl(rawUrl: String): String =
    if (rawUrl.startsWith("http://", ignoreCase = true) || rawUrl.startsWith("https://", ignoreCase = true)) {
        rawUrl
    } else {
        "https://$rawUrl"
    }

private fun openExternalUrl(context: Context, url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    try {
        context.startActivity(intent)
    } catch (_: ActivityNotFoundException) {
        // Ignore when there is no browser available on the device.
    }
}

private data class MessageActionItem(
    val label: String,
    val icon: ImageVector,
    val isDestructive: Boolean = false,
    val onClick: () -> Unit
)

@Composable
private fun MessageActionDialog(
    title: String,
    onDismiss: () -> Unit,
    actions: List<MessageActionItem>
) {
    AlertDialog(
        onDismissRequest = onDismiss,
        confirmButton = {},
        text = {
            Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
                actions.forEachIndexed { index, action ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable(onClick = action.onClick)
                            .padding(horizontal = 4.dp, vertical = 6.dp),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(14.dp)
                    ) {
                        Icon(
                            imageVector = action.icon,
                            contentDescription = null,
                            tint = if (action.isDestructive) MaterialTheme.colorScheme.error else TelegramInkMuted
                        )
                        Text(
                            text = action.label,
                            modifier = Modifier.weight(1f),
                            style = MaterialTheme.typography.titleMedium,
                            color = if (action.isDestructive) MaterialTheme.colorScheme.error else TelegramInk
                        )
                    }
                    if (index != actions.lastIndex) {
                        HorizontalDivider()
                    }
                }
            }
        }
    )
}

@Composable
private fun ActionDialog(
    title: String,
    onDismiss: () -> Unit,
    actions: List<Pair<String, () -> Unit>>
) {
    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text(title) },
        text = {
            Column {
                actions.forEachIndexed { index, (label, onClick) ->
                    TextButton(
                        modifier = Modifier.fillMaxWidth(),
                        onClick = onClick
                    ) {
                        Text(label, modifier = Modifier.fillMaxWidth())
                    }
                    if (index != actions.lastIndex) {
                        HorizontalDivider()
                    }
                }
            }
        },
        confirmButton = {},
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Отмена")
            }
        }
    )
}

@Composable
private fun DeleteSelectedMessagesDialog(
    selectedMessagesCount: Int,
    isDeletingSelectedMessages: Boolean,
    onDismiss: () -> Unit,
    onConfirmDelete: () -> Unit
) {
    AlertDialog(
        onDismissRequest = {
            if (!isDeletingSelectedMessages) {
                onDismiss()
            }
        },
        title = { Text(deleteSelectedMessagesDialogTitle(selectedMessagesCount)) },
        text = { Text(deleteSelectedMessagesDialogMessage(selectedMessagesCount), color = TelegramInkMuted) },
        confirmButton = {
            TextButton(
                enabled = !isDeletingSelectedMessages,
                onClick = onConfirmDelete
            ) {
                Text("Удалить", color = MaterialTheme.colorScheme.error)
            }
        },
        dismissButton = {
            TextButton(
                enabled = !isDeletingSelectedMessages,
                onClick = onDismiss
            ) {
                Text("Отмена")
            }
        }
    )
}

@Composable
private fun ImagePreviewDialog(
    attachment: AttachmentDto,
    accessToken: String?,
    imageUrl: String,
    onDismiss: () -> Unit
) {
    AlertDialog(
        onDismissRequest = onDismiss,
        confirmButton = {},
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Закрыть")
            }
        },
        text = {
            AuthenticatedAsyncImage(
                imageUrl = imageUrl,
                accessToken = accessToken,
                contentDescription = attachment.fileName,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(360.dp)
                    .clip(RoundedCornerShape(16.dp)),
                contentScale = ContentScale.Fit
            )
        }
    )
}

@Composable
private fun ChatVideoPreviewDialog(
    entry: ChatVideoPreviewEntry,
    accessToken: String?,
    videoUrl: String,
    onDismiss: () -> Unit
) {
    val context = LocalContext.current
    val player = remember(videoUrl, accessToken) {
        val httpDataSourceFactory = DefaultHttpDataSource.Factory().apply {
            accessToken?.takeIf { it.isNotBlank() }?.let { token ->
                setDefaultRequestProperties(mapOf("Authorization" to "Bearer $token"))
            }
        }
        val dataSourceFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
        ExoPlayer.Builder(context).build().apply {
            repeatMode = Player.REPEAT_MODE_OFF
            playWhenReady = true
            setMediaSource(
                ProgressiveMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(MediaItem.fromUri(videoUrl))
            )
            prepare()
        }
    }

    DisposableEffect(player) {
        onDispose { player.release() }
    }

    Dialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(usePlatformDefaultWidth = false)
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Black)
        ) {
            Column(modifier = Modifier.fillMaxSize()) {
                Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Black.copy(alpha = 0.72f))
                        .padding(horizontal = 12.dp, vertical = 10.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(10.dp)
                ) {
                    ImagePreviewHeaderButton(
                        icon = Icons.AutoMirrored.Outlined.ArrowBack,
                        contentDescription = "Back",
                        onClick = onDismiss
                    )
                    Column(
                        modifier = Modifier.weight(1f),
                        verticalArrangement = Arrangement.spacedBy(2.dp)
                    ) {
                        Text(
                            text = entry.message.sender.displayName,
                            style = MaterialTheme.typography.titleMedium,
                            color = Color.White,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                        Text(
                            text = entry.attachment.fileName,
                            style = MaterialTheme.typography.bodySmall,
                            color = Color.White.copy(alpha = 0.8f),
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                    }
                }

                AndroidView(
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxWidth()
                        .background(Color.Black),
                    factory = { viewContext ->
                        PlayerView(viewContext).apply {
                            useController = true
                            resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                            this.player = player
                        }
                    },
                    update = { view ->
                        view.player = player
                    }
                )
            }
        }
    }
}

@Composable
private fun ChatImagePreviewDialog(
    entry: ChatImagePreviewEntry,
    accessToken: String?,
    imageUrl: String,
    enableEdgeTapNavigation: Boolean,
    canNavigatePrevious: Boolean,
    canNavigateNext: Boolean,
    canDelete: Boolean,
    isProtectedContent: Boolean,
    onDismiss: () -> Unit,
    onNavigatePrevious: () -> Unit,
    onNavigateNext: () -> Unit,
    onSaveToGallery: () -> Unit,
    onReply: () -> Unit,
    onShare: () -> Unit,
    onDelete: () -> Unit
) {
    var showMenu by remember(entry.attachment.id) { mutableStateOf(false) }
    var scale by remember(entry.attachment.id) { mutableStateOf(1f) }
    var translation by remember(entry.attachment.id) { mutableStateOf(Offset.Zero) }
    val density = LocalDensity.current

    Dialog(
        onDismissRequest = onDismiss,
        properties = DialogProperties(usePlatformDefaultWidth = false)
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Black)
        ) {
            Column(modifier = Modifier.fillMaxSize()) {
                Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Black.copy(alpha = 0.72f))
                        .padding(horizontal = 12.dp, vertical = 10.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(10.dp)
                ) {
                    ImagePreviewHeaderButton(
                        icon = Icons.AutoMirrored.Outlined.ArrowBack,
                        contentDescription = "Назад",
                        onClick = onDismiss
                    )

                    Column(
                        modifier = Modifier.weight(1f),
                        verticalArrangement = Arrangement.spacedBy(2.dp)
                    ) {
                        Text(
                            text = entry.message.sender.displayName,
                            style = MaterialTheme.typography.titleMedium,
                            color = Color.White,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                        Text(
                            text = formatImagePreviewTimestamp(entry.message.sentAt),
                            style = MaterialTheme.typography.bodySmall,
                            color = Color.White.copy(alpha = 0.8f),
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                    }

                    Box {
                        ImagePreviewHeaderButton(
                            icon = Icons.Outlined.Menu,
                            contentDescription = "Меню",
                            onClick = { showMenu = true }
                        )
                        DropdownMenu(
                            expanded = showMenu,
                            onDismissRequest = { showMenu = false }
                        ) {
                            if (!isProtectedContent) {
                                DropdownMenuItem(
                                    text = { Text("Сохранить в галерею") },
                                    onClick = {
                                        showMenu = false
                                        onSaveToGallery()
                                    }
                                )
                            }
                            DropdownMenuItem(
                                text = { Text("Ответить") },
                                onClick = {
                                    showMenu = false
                                    onReply()
                                }
                            )
                            if (!isProtectedContent) {
                                DropdownMenuItem(
                                    text = { Text("Поделиться") },
                                    onClick = {
                                        showMenu = false
                                        onShare()
                                    }
                                )
                            }
                            if (canDelete) {
                                DropdownMenuItem(
                                    text = { Text("Удалить", color = TelegramDanger) },
                                    onClick = {
                                        showMenu = false
                                        onDelete()
                                    }
                                )
                            }
                        }
                    }
                }

                BoxWithConstraints(
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxWidth()
                ) {
                    val containerWidthPx = with(density) { maxWidth.toPx() }
                    val containerHeightPx = with(density) { maxHeight.toPx() }

                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        Box(
                            modifier = Modifier
                                .fillMaxSize()
                                .pointerInput(entry.attachment.id) {
                                    detectTransformGestures { _, pan, zoom, _ ->
                                        val nextScale = (scale * zoom).coerceIn(1f, 4f)
                                        val maxTranslationX = ((nextScale - 1f) * containerWidthPx / 2f).coerceAtLeast(0f)
                                        val maxTranslationY = ((nextScale - 1f) * containerHeightPx / 2f).coerceAtLeast(0f)
                                        translation = if (nextScale == 1f) {
                                            Offset.Zero
                                        } else {
                                            Offset(
                                                x = (translation.x + pan.x).coerceIn(-maxTranslationX, maxTranslationX),
                                                y = (translation.y + pan.y).coerceIn(-maxTranslationY, maxTranslationY)
                                            )
                                        }
                                        scale = nextScale
                                    }
                                },
                            contentAlignment = Alignment.Center
                        ) {
                            AuthenticatedAsyncImage(
                                imageUrl = imageUrl,
                                accessToken = accessToken,
                                contentDescription = entry.attachment.fileName,
                                modifier = Modifier
                                    .fillMaxSize()
                                    .graphicsLayer {
                                        scaleX = scale
                                        scaleY = scale
                                        translationX = translation.x
                                        translationY = translation.y
                                    },
                                contentScale = ContentScale.Fit
                            )
                        }

                        if (enableEdgeTapNavigation && canNavigatePrevious) {
                            ImagePreviewEdgeTapZone(
                                modifier = Modifier.align(Alignment.CenterStart),
                                onTap = onNavigatePrevious
                            )
                        }

                        if (enableEdgeTapNavigation && canNavigateNext) {
                            ImagePreviewEdgeTapZone(
                                modifier = Modifier.align(Alignment.CenterEnd),
                                onTap = onNavigateNext
                            )
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun ImagePreviewHeaderButton(
    icon: ImageVector,
    contentDescription: String,
    onClick: () -> Unit
) {
    Surface(
        color = Color.White.copy(alpha = 0.14f),
        shape = CircleShape,
        modifier = Modifier
            .size(40.dp)
            .clickable(onClick = onClick)
    ) {
        Box(contentAlignment = Alignment.Center) {
            Icon(
                imageVector = icon,
                contentDescription = contentDescription,
                tint = Color.White
            )
        }
    }
}

@Composable
private fun ImagePreviewEdgeTapZone(
    modifier: Modifier = Modifier,
    onTap: () -> Unit
) {
    Box(
        modifier = modifier
            .fillMaxHeight()
            .width(72.dp)
            .pointerInput(onTap) {
                detectTapGestures(onTap = { onTap() })
            }
    )
}

private fun buildMessageMenuItems(
    message: MessageDto,
    currentUserId: String?,
    canPostAsChannel: Boolean,
    onCopy: () -> Unit,
    onForward: () -> Unit,
    onReply: () -> Unit,
    onReact: () -> Unit,
    onPin: () -> Unit,
    onEdit: () -> Unit,
    onDelete: () -> Unit
): List<MessageActionItem> {
    val actions = mutableListOf<MessageActionItem>()
    if (!message.isDeleted() && !message.hasProtectedContent && message.text.isNotBlank()) {
        actions.add(
            MessageActionItem(
                label = "Копировать",
                icon = Icons.Outlined.ContentCopy,
                onClick = onCopy
            )
        )
    }

    if (!message.isDeleted() && (message.attachments.isNotEmpty() || message.text.isNotBlank())) {
        if (!message.hasProtectedContent) {
            actions.add(
            MessageActionItem(
                label = "Переслать",
                icon = Icons.AutoMirrored.Outlined.Send,
                onClick = onForward
            )
            )
        }
        actions.add(
            MessageActionItem(
                label = "Ответить",
                icon = Icons.AutoMirrored.Outlined.ArrowBack,
                onClick = onReply
            )
        )
        actions.add(
            MessageActionItem(
                label = "Реакция",
                icon = Icons.Outlined.Mood,
                onClick = onReact
            )
        )
    }

    if (!message.isDeleted() && (message.attachments.isNotEmpty() || message.text.isNotBlank())) {
        actions.add(
            MessageActionItem(
                label = if (message.pinnedAt == null) "Закрепить" else "Открепить",
                icon = Icons.Outlined.PushPin,
                onClick = onPin
            )
        )
    }

    if (message.canManage(currentUserId, canPostAsChannel)) {
        actions.add(
            MessageActionItem(
                label = "Изменить",
                icon = Icons.Outlined.Edit,
                onClick = onEdit
            )
        )
        actions.add(
            MessageActionItem(
                label = "Удалить",
                icon = Icons.Outlined.Delete,
                isDestructive = true,
                onClick = onDelete
            )
        )
    }

    if (!message.canManage(currentUserId, canPostAsChannel) && message.canDelete(currentUserId)) {
        actions.add(buildDeleteMessageAction(onDelete))
    }

    return actions
}

private fun buildDeleteMessageAction(onDelete: () -> Unit): MessageActionItem =
    MessageActionItem(
        label = "\u0423\u0434\u0430\u043b\u0438\u0442\u044c",
        icon = Icons.Outlined.Delete,
        isDestructive = true,
        onClick = onDelete
    )

private fun buildMessageActionItems(
    message: MessageDto,
    currentUserId: String?,
    onCopy: () -> Unit,
    onForward: () -> Unit,
    onReply: () -> Unit,
    onReact: () -> Unit,
    onEdit: () -> Unit,
    onDelete: () -> Unit
): List<Pair<String, () -> Unit>> {
    val actions = mutableListOf<Pair<String, () -> Unit>>()
    if (!message.isDeleted() && !message.hasProtectedContent && message.text.isNotBlank()) {
        actions.add("Копировать" to onCopy)
    }

    if (!message.isDeleted() && !message.hasProtectedContent && (message.attachments.isNotEmpty() || message.text.isNotBlank())) {
        actions.add("Переслать" to onForward)
        actions.add("Ответить" to onReply)
        actions.add("Реакция" to onReact)
    }

    if (message.canManage(currentUserId)) {
        actions.add("Изменить" to onEdit)
        actions.add("Удалить" to onDelete)
    }

    return actions
}

private fun messageSelectionCountLabel(selectedMessagesCount: Int): String = when {
    selectedMessagesCount % 10 == 1 && selectedMessagesCount % 100 != 11 -> "Выбрано $selectedMessagesCount сообщение"
    selectedMessagesCount % 10 in 2..4 && selectedMessagesCount % 100 !in 12..14 -> "Выбрано $selectedMessagesCount сообщения"
    else -> "Выбрано $selectedMessagesCount сообщений"
}

private fun pinnedMessagePreview(message: MessageDto): String = when {
    message.text.isNotBlank() -> message.text
    message.attachments.size == 1 -> message.attachments.first().fileName
    message.attachments.isNotEmpty() -> "${message.attachments.size} files"
    else -> "Message"
}

private fun deleteSelectedMessagesDialogTitle(selectedMessagesCount: Int): String =
    if (selectedMessagesCount == 1) "Удалить выбранное сообщение?" else "Удалить выбранные сообщения?"

private fun deleteSelectedMessagesDialogMessage(selectedMessagesCount: Int): String =
    if (selectedMessagesCount == 1) {
        "Выбранное сообщение исчезнет из чата после подтверждения."
    } else {
        "Выбранные сообщения исчезнут из чата после подтверждения."
    }

private fun buildChatRows(messages: List<MessageDto>): List<ChatRow> {
    if (messages.isEmpty()) {
        return emptyList()
    }

    val chronologicalRows = buildList<ChatRow> {
        var previousDate: LocalDate? = null
        messages.forEach { message ->
            val currentDate = parseMessageDate(message.sentAt)?.toLocalDate()
            if (currentDate != null && currentDate != previousDate) {
                add(ChatRow.DateSeparator("date-$currentDate", formatDateSeparator(currentDate)))
                previousDate = currentDate
            }
            add(ChatRow.Message(message))
        }
    }

    return chronologicalRows.asReversed()
}

private fun chatRowKey(row: ChatRow): String = when (row) {
    is ChatRow.DateSeparator -> row.id
    is ChatRow.Message -> row.item.id
}

private fun findLastReadRowIndexBeforeFirstUnread(rows: List<ChatRow>, firstUnreadRowIndex: Int): Int? {
    if (firstUnreadRowIndex >= rows.lastIndex) {
        return null
    }

    return ((firstUnreadRowIndex + 1)..rows.lastIndex)
        .firstOrNull { index -> rows[index] is ChatRow.Message }
}

private fun formatMessageTime(value: String): String =
    parseMessageDate(value)?.format(ChatTimeFormatter) ?: value

private fun formatCompactCount(value: Int): String = when {
    value >= 1_000_000 -> String.format(Locale.US, "%.1fM", value / 1_000_000f).trimTrailingZero()
    value >= 1_000 -> String.format(Locale.US, "%.1fK", value / 1_000f).trimTrailingZero()
    else -> value.toString()
}

private fun formatImagePreviewTimestamp(value: String): String =
    parseMessageDate(value)?.let { timestamp ->
        val dateFormatter = if (timestamp.year == LocalDate.now().year) ChatDayFormatter else ChatDayYearFormatter
        "${timestamp.format(dateFormatter)} • ${timestamp.format(ChatTimeFormatter)}"
    } ?: value

private fun formatDateSeparator(date: LocalDate): String {
    val today = LocalDate.now()
    return when (date) {
        today -> "Сегодня"
        today.minusDays(1) -> "Вчера"
        else -> if (date.year == today.year) date.format(ChatDayFormatter) else date.format(ChatDayYearFormatter)
    }
}

private fun parseMessageDate(value: String): OffsetDateTime? =
    runCatching {
        OffsetDateTime.parse(value)
            .atZoneSameInstant(ZoneId.systemDefault())
            .toOffsetDateTime()
    }.getOrNull()

private fun formatFileSize(sizeBytes: Long): String = when {
    sizeBytes >= 1024L * 1024L -> String.format(Locale.US, "%.1f MB", sizeBytes / 1024f / 1024f)
    sizeBytes >= 1024L -> "${sizeBytes / 1024L} KB"
    else -> "$sizeBytes B"
}

private fun formatChannelSubscribers(count: Int): String =
    "$count ${russianPlural(count, "подписчик", "подписчика", "подписчиков")}"

private fun russianPlural(count: Int, one: String, few: String, many: String): String {
    val value = kotlin.math.abs(count) % 100
    val lastDigit = value % 10
    if (value in 11..14) {
        return many
    }

    return when (lastDigit) {
        1 -> one
        in 2..4 -> few
        else -> many
    }
}

private fun String.trimTrailingZero(): String =
    replace(".0", "")

private fun buildVoiceNoteSecondaryText(
    attachment: AttachmentDto,
    playbackState: VoiceAttachmentPlaybackUiState?
): String {
    val durationText = playbackState?.durationMs?.takeIf { it > 0L }?.let(::formatDurationMs)
        ?: formatFileSize(attachment.fileSizeBytes)
    val positionText = playbackState?.positionMs?.takeIf { it > 0L }?.let(::formatDurationMs)

    return if (positionText != null) "$positionText / $durationText" else durationText
}

private fun buildInlineAudioSecondaryText(playbackState: VoiceAttachmentPlaybackUiState?): String {
    val durationText = playbackState?.durationMs?.takeIf { it > 0L }?.let(::formatDurationMs)
    val positionText = playbackState?.positionMs?.takeIf { it > 0L }?.let(::formatDurationMs)
    return when {
        durationText != null && positionText != null -> "$positionText / $durationText"
        durationText != null -> durationText
        else -> "Voice message"
    }
}
