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

import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
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.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.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.Image
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.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.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
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.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.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import com.seven.ikar.kotlin.core.model.AttachmentDto
import com.seven.ikar.kotlin.core.model.AttachmentKind
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 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 val EmojiKeyboardFallbackHeight = 300.dp
private val EmojiKeyboardMinHeight = 220.dp
private val EmojiKeyboardMaxHeight = 360.dp
private val EmojiGridMinCellWidth = 38.dp
private val EmojiGridCellHeight = 34.dp
private const val EmojiKeyboardAnimationMillis = 220

@OptIn(FlowPreview::class)
@Composable
fun ChatScreen(
    state: ChatUiState,
    viewModel: ChatViewModel,
    chatSettings: ChatSettingsSnapshot,
    onBackClick: () -> Unit,
    onPeerProfileClick: (() -> Unit)? = null,
    onChatNotificationsClick: () -> Unit,
    onChatWallpaperClick: () -> Unit,
    onForwardMessage: (MessageDto) -> Unit
) {
    val clipboard = LocalClipboardManager.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 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 hasProtectedContentVisible = remember(state.messages, currentImagePreviewEntry) {
        state.messages.any { it.hasProtectedContent && !it.isDeleted() } ||
            currentImagePreviewEntry?.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 frequentEmojis by remember(context) { mutableStateOf(loadFrequentEmojis(context)) }
    val emojiCategories = remember(frequentEmojis) { emojiPickerCategories(frequentEmojis) }
    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 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 }
    }

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

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

    BackHandler(enabled = emojiPickerMode != 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 || state.chatType != ChatType.Channel || 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
            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)
            is EmojiPickerMode.Reaction -> {
                emojiPickerMode = null
                val target = pickerMode.message
                if (state.selectedMessageIds.contains(target.id)) {
                    viewModel.toggleReactionFromSelection(target, emoji)
                } else {
                    viewModel.toggleReaction(target, emoji)
                }
            }
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        ChatWallpaperBackground(
            snapshot = chatSettings,
            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 ->
                        clipboard.setText(AnnotatedString(message.text))
                        viewModel.clearMessageSelection()
                    },
                    onForwardSelected = { message ->
                        viewModel.clearMessageSelection()
                        onForwardMessage(message)
                    },
                    onReactSelected = { message ->
                        keyboardController?.hide()
                        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,
                            localStatus = localStatus,
                            accessToken = state.session?.accessToken,
                            attachmentUrlResolver = viewModel::attachmentPreviewUrl,
                            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)
                                }
                            },
                            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)
                )
            }

            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,
                    onDraftChanged = viewModel::updateDraft,
                    onEmojiClick = {
                        keyboardController?.hide()
                        emojiPickerMode = if (emojiPickerMode is EmojiPickerMode.Composer) {
                            null
                        } else {
                            displayedEmojiPickerMode = EmojiPickerMode.Composer
                            EmojiPickerMode.Composer
                        }
                    },
                    onInputFocused = { 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,
                    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,
                            onEmojiSelected = { emoji -> handleEmojiSelected(pickerMode, emoji) }
                        )
                    }
                }
            }
        }
    }

    if (showHeaderMenu) {
        val canStartCalls = viewModel.showCallHeaderButton(state)
        MessageActionDialog(
            title = "Меню чата",
            onDismiss = { showHeaderMenu = false },
            actions = buildList {
                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.isSubscribed && !state.canManageChannel) {
                    add(
                        MessageActionItem(
                            label = if (state.isLeavingChannel) "Отписка..." else "Отписаться от канала",
                            icon = Icons.Outlined.Delete,
                            isDestructive = true,
                            onClick = {
                                showHeaderMenu = false
                                viewModel.unsubscribeFromChannel()
                            }
                        )
                    )
                } else if (state.chatType != ChatType.Channel || state.isSubscribed) {
                    add(
                        MessageActionItem(
                            label = if (state.chatType == ChatType.Channel) "Удалить канал" else "Удалить чат",
                            icon = Icons.Outlined.Delete,
                            isDestructive = true,
                            onClick = {
                                showHeaderMenu = false
                                showDeleteChatConfirm = true
                            }
                        )
                    )
                }
            }
        )
    }

    if (showDeleteChatConfirm) {
        val deleteTitle = if (state.chatType == ChatType.Channel) "Удалить канал?" else "Удалить чат?"
        val deleteText = if (state.chatType == ChatType.Channel) {
            "Канал исчезнет из вашего списка после подтверждения."
        } else {
            "Чат исчезнет из вашего списка после подтверждения."
        }
        AlertDialog(
            onDismissRequest = { showDeleteChatConfirm = false },
            title = { Text(deleteTitle) },
            text = { Text(deleteText) },
            confirmButton = {
                TextButton(
                    enabled = !state.isDeletingChat,
                    onClick = {
                        showDeleteChatConfirm = false
                        viewModel.deleteChat()
                    }
                ) {
                    Text("Удалить", color = MaterialTheme.colorScheme.error)
                }
            },
            dismissButton = {
                TextButton(
                    enabled = !state.isDeletingChat,
                    onClick = { showDeleteChatConfirm = 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,
            onCopy = {
                clipboard.setText(AnnotatedString(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
            }
        )
    }
}

@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
            )
        }

        Box(
            modifier = if (onAvatarClick != null) {
                Modifier
                    .clip(CircleShape)
                    .clickable(onClick = onAvatarClick)
            } else {
                Modifier
            }
        ) {
            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?,
    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
                )
            }
            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,
    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 = if (showBotMenuButton) Icons.Outlined.Menu else Icons.Outlined.Mood,
                description = if (showBotMenuButton) "Команды бота" else "Эмодзи",
                onClick = if (showBotMenuButton) onBotMenuClick else onEmojiClick
            )

            BasicTextField(
                value = draftText,
                onValueChange = onDraftChanged,
                enabled = !isSending,
                keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
                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()
                    }
                }
            )

            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,
    localStatus: LocalMessageStatus?,
    accessToken: String?,
    attachmentUrlResolver: (AttachmentDto) -> String,
    onClick: () -> Unit,
    onAttachmentClick: (AttachmentDto) -> 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 bubbleContentSpacing = if (!hasAttachments && displayContent.imageUrls.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 context = LocalContext.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 = 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.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)
                    )
                }

                val imageAttachments = message.attachments.filter { it.isImageAttachment() }

                if (showImagesBeforeText) {
                    displayContent.imageUrls.forEach { imageUrl ->
                        InlineMessageImage(
                            imageUrl = imageUrl,
                            onClick = { openExternalUrl(context, 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
                            )
                        }
                    }
                }

                if (displayContent.text.isNotBlank()) {
                    MessageText(
                        text = displayContent.text,
                        modifier = Modifier.padding(horizontal = if (hasAttachments) 6.dp else 0.dp),
                        textSizeSp = messageTextSizeSp,
                        onTap = onClick,
                        onLongPress = onLongPress
                    )
                }

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

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

                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
                    )
                }

                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 SelectionIndicator() {
    Icon(
        imageVector = Icons.Outlined.CheckCircle,
        contentDescription = "Сообщение выбрано",
        tint = TelegramBlue,
        modifier = Modifier.size(20.dp)
    )
}

@Composable
private fun EmojiKeyboardPanel(
    categories: List<EmojiCategory>,
    height: Dp,
    onEmojiSelected: (String) -> Unit
) {
    val gridState = rememberLazyGridState()
    val coroutineScope = rememberCoroutineScope()
    val categoryScrollState = rememberScrollState()
    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
        }
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .height(height)
            .background(TelegramSurface)
    ) {
        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.Adaptive(minSize = EmojiGridMinCellWidth),
            state = gridState,
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f),
            contentPadding = PaddingValues(start = 6.dp, top = 8.dp, end = 6.dp, bottom = 8.dp),
            horizontalArrangement = Arrangement.SpaceEvenly,
            verticalArrangement = Arrangement.spacedBy(2.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, fontSize = 22.sp)
        }
    }
}

@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 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,
    onTap: () -> Unit,
    onLongPress: () -> Unit
) {
    val context = LocalContext.current
    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 -> openExternalUrl(context, 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 }
    )
}

@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
) {
    AuthenticatedAsyncImage(
        imageUrl = imageUrl,
        accessToken = "",
        contentDescription = "",
        modifier = Modifier
            .fillMaxWidth()
            .heightIn(min = 120.dp, max = 420.dp)
            .clip(RoundedCornerShape(16.dp))
            .combinedClickable(onClick = onClick, onLongClick = onLongPress)
            .background(Color(0xFFF7FAFD)),
        contentScale = ContentScale.Fit
    )
}

@Composable
private fun AttachmentCard(
    attachment: AttachmentDto,
    attachmentUrl: String,
    accessToken: String?,
    playbackState: VoiceAttachmentPlaybackUiState?,
    onClick: () -> Unit,
    onLongPress: () -> Unit
) {
    when {
        attachment.isImageAttachment() -> {
            AuthenticatedAsyncImage(
                imageUrl = attachmentUrl,
                accessToken = accessToken,
                contentDescription = attachment.fileName,
                modifier = Modifier
                    .fillMaxWidth()
                    .heightIn(min = 120.dp, max = 420.dp)
                    .clip(RoundedCornerShape(16.dp))
                    .combinedClickable(onClick = onClick, onLongClick = onLongPress)
                    .background(Color(0xFFF7FAFD)),
                contentScale = ContentScale.Fit
            )
        }

        attachment.kind == AttachmentKind.VoiceNote -> {
            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
                        )
                    }
                }
            }
        }

        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>
)

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

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

    val imageUrls = mutableListOf<String>()
    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) }
        if (imageRanges.isEmpty()) {
            visibleLines += line
            pendingImageLabel = false
            return@forEach
        }

        imageUrls += imageRanges.map { it.url }
        val cleanedLine = cleanInlineImageLine(line, imageRanges)
        if (cleanedLine.isNotBlank()) {
            visibleLines += cleanedLine
        }
        pendingImageLabel = false
    }

    return MessageDisplayContent(
        text = visibleLines.joinToString("\n").trim(),
        imageUrls = imageUrls.distinct()
    )
}

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 {
    var result = line
    imageRanges
        .sortedByDescending { it.start }
        .forEach { range -> result = result.removeRange(range.start, range.end) }

    return ImageLabelRegex
        .replace(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 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 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?,
    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)) {
        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) && 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
}
