package com.rosetta.messenger.ui.chats

import android.app.Activity
import android.content.Context
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
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.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
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.grid.itemsIndexed as gridItemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import compose.icons.TablerIcons
import compose.icons.tablericons.*
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Message
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.PopupProperties
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.data.GroupRepository
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.di.ProtocolGateway
import com.rosetta.messenger.database.MessageEntity
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.MessageAttachment
import com.rosetta.messenger.network.OnlineState
import com.rosetta.messenger.network.PacketOnlineState
import com.rosetta.messenger.network.PacketOnlineSubscribe
import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.components.ImageAttachment
import com.rosetta.messenger.ui.chats.components.ImageSourceBounds
import com.rosetta.messenger.ui.chats.components.ImageViewerScreen
import com.rosetta.messenger.ui.chats.components.ViewableImage
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.SharedMediaFastScrollOverlay
import com.rosetta.messenger.ui.components.SwipeBackContainer
import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.icons.TelegramIcons
import com.rosetta.messenger.ui.settings.FullScreenAvatarViewer
import com.rosetta.messenger.ui.settings.ProfilePhotoPicker
import com.rosetta.messenger.utils.AvatarFileManager
import com.rosetta.messenger.utils.ImageCropHelper
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.core.view.WindowCompat
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.roundToInt

private enum class GroupInfoTab(val title: String) {
    MEMBERS("Members"),
    MEDIA("Media"),
    FILES("Files"),
    LINKS("Links")
}

private data class GroupMemberUi(
    val publicKey: String,
    val title: String,
    val subtitle: String,
    val verified: Int,
    val online: Boolean,
    val isAdmin: Boolean,
    val searchUser: SearchUser
)

private data class GroupSharedStats(
    val mediaCount: Int = 0,
    val fileCount: Int = 0,
    val linksCount: Int = 0
)

private data class GroupMembersCacheEntry(
    val members: List<String>,
    val memberInfoByKey: Map<String, SearchUser>,
    val updatedAtMs: Long
)

private object GroupMembersMemoryCache {
    private const val TTL_MS = 90_000L
    private val cache = mutableMapOf<String, GroupMembersCacheEntry>()

    fun getAny(key: String): GroupMembersCacheEntry? = synchronized(cache) { cache[key] }

    fun getFresh(key: String): GroupMembersCacheEntry? = synchronized(cache) {
        val entry = cache[key] ?: return@synchronized null
        if (System.currentTimeMillis() - entry.updatedAtMs <= TTL_MS) entry else null
    }

    fun put(key: String, members: List<String>, memberInfoByKey: Map<String, SearchUser>) {
        if (key.isBlank()) return
        synchronized(cache) {
            cache[key] =
                GroupMembersCacheEntry(
                    members = members,
                    memberInfoByKey = memberInfoByKey,
                    updatedAtMs = System.currentTimeMillis()
                )
        }
    }

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

private data class GroupMembersDiskCacheEntry(
    val members: List<String>,
    val updatedAtMs: Long
)

private object GroupMembersDiskCache {
    private const val PREFS_NAME = "group_members_cache"
    private const val KEY_PREFIX = "entry_"
    private const val TTL_MS = 12 * 60 * 60 * 1000L

    fun getAny(context: Context, key: String): GroupMembersDiskCacheEntry? = read(context, key)

    fun getFresh(context: Context, key: String): GroupMembersDiskCacheEntry? {
        val entry = read(context, key) ?: return null
        return if (System.currentTimeMillis() - entry.updatedAtMs <= TTL_MS) entry else null
    }

    fun put(context: Context, key: String, members: List<String>) {
        if (key.isBlank()) return
        val normalizedMembers = members.map { it.trim() }.filter { it.isNotBlank() }.distinct()
        if (normalizedMembers.isEmpty()) return
        val payload =
            JSONObject().apply {
                put("updatedAtMs", System.currentTimeMillis())
                put("members", JSONArray().apply { normalizedMembers.forEach { put(it) } })
            }.toString()
        context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            .edit()
            .putString(KEY_PREFIX + key, payload)
            .apply()
    }

    fun remove(context: Context, key: String) {
        if (key.isBlank()) return
        context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            .edit()
            .remove(KEY_PREFIX + key)
            .apply()
    }

    private fun read(context: Context, key: String): GroupMembersDiskCacheEntry? {
        if (key.isBlank()) return null
        val raw =
            context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
                .getString(KEY_PREFIX + key, null) ?: return null
        return runCatching {
                val json = JSONObject(raw)
                val membersArray = json.optJSONArray("members") ?: JSONArray()
                val membersList =
                    buildList {
                            repeat(membersArray.length()) { index ->
                                val value = membersArray.optString(index).trim()
                                if (value.isNotBlank()) add(value)
                            }
                        }
                        .distinct()
                if (membersList.isEmpty()) {
                    null
                } else {
                    GroupMembersDiskCacheEntry(
                        members = membersList,
                        updatedAtMs = json.optLong("updatedAtMs", 0L)
                    )
                }
            }
            .getOrNull()
    }
}

private data class GroupMediaItem(
    val key: String,
    val attachment: MessageAttachment,
    val chachaKey: String,
    val senderPublicKey: String,
    val senderName: String,
    val timestamp: Long,
    val caption: String
)

private val URL_REGEX = Regex("(?i)\\b((?:https?://|www\\.)[^\\s<>()]+)")

// Identicon colors — same palette for both themes (like Telegram)
// White stays white so the pattern is visible on dark backgrounds
private val IDENTICON_COLORS = intArrayOf(
    0xFFFFFFFF.toInt(),
    0xFFD0E8FF.toInt(),
    0xFF228BE6.toInt(),
    0xFF1971C2.toInt()
)

@Composable
fun GroupInfoScreen(
    groupUser: SearchUser,
    currentUserPublicKey: String,
    currentUserPrivateKey: String,
    isDarkTheme: Boolean,
    protocolGateway: ProtocolGateway,
    messageRepository: MessageRepository,
    preferencesManager: PreferencesManager,
    groupRepository: GroupRepository,
    avatarRepository: AvatarRepository? = null,
    onBack: () -> Unit,
    onMemberClick: (SearchUser) -> Unit = {},
    onGroupLeft: () -> Unit = {},
    onSwipeBackEnabledChanged: (Boolean) -> Unit = {}
) {
    val context = androidx.compose.ui.platform.LocalContext.current
    val view = LocalView.current
    val focusManager = LocalFocusManager.current
    val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
    val hapticFeedback = LocalHapticFeedback.current
    val scope = rememberCoroutineScope()

    val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
    val topSurfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF228BE6)
    val cardColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
    val sectionColor = if (isDarkTheme) Color(0xFF222224) else Color.White
    val listRowColor = if (isDarkTheme) Color(0xFF1F1F21) else Color.White
    val borderColor = if (isDarkTheme) Color.White.copy(alpha = 0.08f) else Color(0xFFE5E5EA)
    val memberDividerColor = if (isDarkTheme) Color(0xFF2E3239) else Color(0xFFDDE3EC)
    val primaryText = if (isDarkTheme) Color.White else Color.Black
    val secondaryText = Color(0xFF8E8E93)
    val accentColor = if (isDarkTheme) Color(0xFF5AA5FF) else Color(0xFF228BE6)
    val actionContentColor = if (isDarkTheme) Color.White else Color(0xFF1C1C1E)
    val groupActionButtonBlue = if (isDarkTheme) Color(0xFF4A4A4D) else Color(0xFF2478C2)
    val groupMenuTrailingIconSize = 22.dp

    LaunchedEffect(Unit) {
        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        repeat(3) {
            imm.hideSoftInputFromWindow(view.windowToken, 0)
            (context as? Activity)?.window?.let { window ->
                WindowCompat.getInsetsController(window, view)
                    .hide(androidx.core.view.WindowInsetsCompat.Type.ime())
            }
            focusManager.clearFocus(force = true)
            delay(16)
        }
    }

    // Keep status bar unified with group header color.
    DisposableEffect(topSurfaceColor, view) {
        val window = (view.context as? Activity)?.window
        if (window == null) {
            onDispose { }
        } else {
            val controller = WindowCompat.getInsetsController(window, view)
            val previousColor = window.statusBarColor
            val previousLightIcons = controller.isAppearanceLightStatusBars

            window.statusBarColor = topSurfaceColor.toArgb()
            controller.isAppearanceLightStatusBars = false

            onDispose {
                window.statusBarColor = previousColor
                controller.isAppearanceLightStatusBars = previousLightIcons
            }
        }
    }

    val database = remember { RosettaDatabase.getDatabase(context) }
    val groupDao = remember { database.groupDao() }
    val messageDao = remember { database.messageDao() }
    val chatsListViewModel: ChatsListViewModel = hiltViewModel()
    val forwardDialogs by chatsListViewModel.dialogs.collectAsState()

    val normalizedGroupId = remember(groupUser.publicKey) {
        groupRepository.normalizeGroupId(groupUser.publicKey)
    }
    val dialogPublicKey = remember(normalizedGroupId) {
        groupRepository.toGroupDialogPublicKey(normalizedGroupId)
    }

    var selectedTab by rememberSaveable(dialogPublicKey) { mutableStateOf(GroupInfoTab.MEMBERS) }
    var searchQuery by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
    var showSearch by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var showMenu by remember { mutableStateOf(false) }
    var showLeaveConfirm by remember { mutableStateOf(false) }
    var isLeaving by remember { mutableStateOf(false) }
    var showEncryptionPage by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var encryptionKey by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
    var encryptionKeyLoading by remember { mutableStateOf(false) }
    var membersLoading by remember(dialogPublicKey) { mutableStateOf(false) }
    var isMuted by remember { mutableStateOf(false) }
    var showGroupAvatarPicker by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var showGroupAvatarViewer by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var groupAvatarViewerTimestamp by rememberSaveable(dialogPublicKey) { mutableStateOf(0L) }
    var groupAvatarViewerBitmap by remember(dialogPublicKey) { mutableStateOf<android.graphics.Bitmap?>(null) }
    var showAddMembersPicker by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
    var isRefreshingMembers by remember(dialogPublicKey) { mutableStateOf(false) }

    val membersCacheKey = remember(currentUserPublicKey, normalizedGroupId) {
        "${currentUserPublicKey.trim().lowercase()}|${normalizedGroupId.trim().lowercase()}"
    }
    val initialMemoryMembersCache = remember(membersCacheKey) {
        GroupMembersMemoryCache.getAny(membersCacheKey)
    }
    val initialDiskMembersCache = remember(membersCacheKey) {
        GroupMembersDiskCache.getAny(context, membersCacheKey)
    }
    val hasInitialMembersCache = remember(initialMemoryMembersCache, initialDiskMembersCache) {
        initialMemoryMembersCache != null || initialDiskMembersCache != null
    }
    val initialMembers = remember(initialMemoryMembersCache, initialDiskMembersCache) {
        initialMemoryMembersCache?.members ?: initialDiskMembersCache?.members.orEmpty()
    }
    var members by remember(dialogPublicKey, membersCacheKey) { mutableStateOf(initialMembers) }
    val memberInfoByKey =
        remember(dialogPublicKey, membersCacheKey) {
            mutableStateMapOf<String, SearchUser>().apply {
                initialMemoryMembersCache?.memberInfoByKey?.let { putAll(it) }
            }
        }
    // Real online status from PacketOnlineState (0x05), NOT from SearchUser.online
    val memberOnlineStatus = remember(dialogPublicKey, membersCacheKey) { mutableStateMapOf<String, Boolean>() }

    val groupEntity by produceState<com.rosetta.messenger.database.GroupEntity?>(
        initialValue = null,
        key1 = currentUserPublicKey,
        key2 = normalizedGroupId
    ) {
        value = withContext(Dispatchers.IO) {
            groupDao.getGroup(currentUserPublicKey, normalizedGroupId)
        }
    }

    val messagesFlow = remember(messageDao, currentUserPublicKey, dialogPublicKey) {
        if (currentUserPublicKey.isBlank() || dialogPublicKey.isBlank()) {
            flowOf(emptyList())
        } else {
            messageDao.getMessagesFlow(currentUserPublicKey, dialogPublicKey)
        }
    }
    val groupMessages by messagesFlow.collectAsState(initial = emptyList())

    val sharedStats by produceState(
        initialValue = GroupSharedStats(),
        key1 = groupMessages,
        key2 = currentUserPrivateKey
    ) {
        value = withContext(Dispatchers.Default) {
            buildGroupSharedStats(groupMessages, currentUserPrivateKey)
        }
    }

    val groupMediaItems by produceState(
        initialValue = emptyList<GroupMediaItem>(),
        groupMessages,
        currentUserPrivateKey,
        memberInfoByKey.toMap(),
        currentUserPublicKey
    ) {
        val memberSnapshot = memberInfoByKey.toMap()
        value = withContext(Dispatchers.Default) {
            buildGroupMediaItems(
                protocolGateway = protocolGateway,
                messages = groupMessages,
                privateKey = currentUserPrivateKey,
                currentUserPublicKey = currentUserPublicKey,
                memberInfoByKey = memberSnapshot
            )
        }
    }

    val mediaViewerImages = remember(groupMediaItems) {
        groupMediaItems.map { mediaItem ->
            ViewableImage(
                attachmentId = mediaItem.attachment.id,
                preview = mediaItem.attachment.preview,
                blob = mediaItem.attachment.blob,
                chachaKey = mediaItem.chachaKey,
                senderPublicKey = mediaItem.senderPublicKey,
                senderName = mediaItem.senderName,
                timestamp = Date(mediaItem.timestamp),
                width = mediaItem.attachment.width,
                height = mediaItem.attachment.height,
                caption = mediaItem.caption
            )
        }
    }
    var showImageViewer by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
    var imageViewerInitialIndex by rememberSaveable(dialogPublicKey) { mutableStateOf(0) }
    var imageViewerSourceBounds by remember(dialogPublicKey) { mutableStateOf<ImageSourceBounds?>(null) }
    val groupMediaGridState = rememberLazyGridState()
    var groupMediaFastScrollHintDismissed by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }

    val groupTitle = remember(groupEntity, groupUser.title) {
        groupEntity?.title?.trim().takeUnless { it.isNullOrBlank() }
            ?: groupUser.title.trim().ifBlank { "Group" }
    }
    val groupDescription = remember(groupEntity) {
        groupEntity?.description?.trim().orEmpty()
    }

    val cropLauncher =
        rememberLauncherForActivityResult(
            contract = ActivityResultContracts.StartActivityForResult()
        ) { result ->
            val croppedUri = ImageCropHelper.getCroppedImageUri(result)
            val cropError = ImageCropHelper.getCropError(result)
            if (croppedUri != null) {
                scope.launch {
                    val preparedBase64 =
                        withContext(Dispatchers.IO) {
                            val imageBytes =
                                runCatching {
                                    context.contentResolver.openInputStream(croppedUri)?.use { stream ->
                                        stream.readBytes()
                                    }
                                }.getOrNull()
                            imageBytes?.let { bytes ->
                                val prepared = AvatarFileManager.imagePrepareForNetworkTransfer(context, bytes)
                                if (prepared.isBlank()) null else "data:image/png;base64,$prepared"
                            }
                        }

                    val repository = avatarRepository
                    val saved =
                        preparedBase64 != null &&
                            repository != null &&
                            runCatching {
                                withContext(Dispatchers.IO) {
                                    repository.saveAvatar(dialogPublicKey, preparedBase64)
                                }
                            }.isSuccess

                    Toast.makeText(
                        context,
                        if (saved) "Group avatar updated" else "Failed to update group avatar",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            } else if (cropError != null) {
                Toast.makeText(context, "Failed to crop photo", Toast.LENGTH_SHORT).show()
            }
        }

    LaunchedEffect(currentUserPublicKey, dialogPublicKey) {
        if (currentUserPublicKey.isNotBlank() && dialogPublicKey.isNotBlank()) {
            isMuted = preferencesManager.isChatMuted(currentUserPublicKey, dialogPublicKey)
        }
    }

    LaunchedEffect(currentUserPublicKey, currentUserPrivateKey) {
        if (currentUserPublicKey.isNotBlank() && currentUserPrivateKey.isNotBlank()) {
            chatsListViewModel.setAccount(currentUserPublicKey, currentUserPrivateKey)
            messageRepository.initialize(currentUserPublicKey, currentUserPrivateKey)
        }
    }

    fun refreshMembers(force: Boolean = false, showLoader: Boolean = true) {
        if (normalizedGroupId.isBlank()) return
        if (isRefreshingMembers && !force) return

        scope.launch {
            if (!force) {
                GroupMembersMemoryCache.getFresh(membersCacheKey)?.let { cached ->
                    members = cached.members
                    memberInfoByKey.clear()
                    memberInfoByKey.putAll(cached.memberInfoByKey)
                    return@launch
                }
            }

            val hasAnyCache =
                GroupMembersMemoryCache.getAny(membersCacheKey) != null ||
                    GroupMembersDiskCache.getAny(context, membersCacheKey) != null
            val shouldShowLoader = showLoader && members.isEmpty() && !hasAnyCache
            if (shouldShowLoader) membersLoading = true
            isRefreshingMembers = true
            try {
                val fetchedMembers = withContext(Dispatchers.IO) {
                    groupRepository.requestGroupMembers(normalizedGroupId).orEmpty()
                }
                val distinctMembers = fetchedMembers.distinct()
                if (distinctMembers.isNotEmpty() || members.isEmpty()) {
                    members = distinctMembers
                }

                if (members.isEmpty()) return@launch

                val resolvedUsers = withContext(Dispatchers.IO) {
                    val resolvedMap = LinkedHashMap<String, SearchUser>()
                    members.forEach { memberKey ->
                        val cached = protocolGateway.getCachedUserInfo(memberKey)
                        if (cached != null) {
                            resolvedMap[memberKey] = cached
                        } else {
                            protocolGateway.resolveUserInfo(memberKey, timeoutMs = 2500L)?.let { resolvedUser ->
                                resolvedMap[memberKey] = resolvedUser
                            }
                        }
                    }
                    resolvedMap
                }
                if (resolvedUsers.isNotEmpty()) {
                    memberInfoByKey.putAll(resolvedUsers)
                }

                GroupMembersMemoryCache.put(
                    key = membersCacheKey,
                    members = members,
                    memberInfoByKey = memberInfoByKey.toMap()
                )
                GroupMembersDiskCache.put(context, membersCacheKey, members)
            } finally {
                if (shouldShowLoader) membersLoading = false
                isRefreshingMembers = false
            }
        }
    }

    LaunchedEffect(membersCacheKey) {
        val memoryCachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
        val diskCachedEntry = GroupMembersDiskCache.getAny(context, membersCacheKey)

        memoryCachedEntry?.let { cached ->
            members = cached.members
            memberInfoByKey.clear()
            memberInfoByKey.putAll(cached.memberInfoByKey)
        } ?: diskCachedEntry?.let { cached ->
            members = cached.members
            if (memberInfoByKey.isEmpty()) {
                val resolvedUsers = withContext(Dispatchers.IO) {
                    val resolvedMap = LinkedHashMap<String, SearchUser>()
                    cached.members.forEach { memberKey ->
                        protocolGateway.getCachedUserInfo(memberKey)?.let { resolved ->
                            resolvedMap[memberKey] = resolved
                        }
                    }
                    resolvedMap
                }
                if (resolvedUsers.isNotEmpty()) {
                    memberInfoByKey.putAll(resolvedUsers)
                }
            }
            GroupMembersMemoryCache.put(
                key = membersCacheKey,
                members = members,
                memberInfoByKey = memberInfoByKey.toMap()
            )
        }

        val hasFreshCache =
            GroupMembersMemoryCache.getFresh(membersCacheKey) != null ||
                GroupMembersDiskCache.getFresh(context, membersCacheKey) != null
        if (!hasFreshCache) {
            refreshMembers(
                force = true,
                showLoader = memoryCachedEntry == null && diskCachedEntry == null
            )
        }
    }

    // 🟢 Subscribe to online status for group members via PacketOnlineSubscribe / PacketOnlineState
    // Desktop parity: users start as OFFLINE, only become ONLINE when server sends 0x05
    val onlinePacketHandler = remember<(com.rosetta.messenger.network.Packet) -> Unit>(dialogPublicKey) {
        { packet ->
            val onlinePacket = packet as PacketOnlineState
            onlinePacket.publicKeysState.forEach { item ->
                memberOnlineStatus[item.publicKey] = (item.state == OnlineState.ONLINE)
            }
        }
    }

    DisposableEffect(dialogPublicKey) {
        protocolGateway.waitPacket(0x05, onlinePacketHandler)
        onDispose {
            protocolGateway.unwaitPacket(0x05, onlinePacketHandler)
        }
    }

    // Subscribe to online status whenever members list changes
    LaunchedEffect(members, currentUserPrivateKey) {
        if (members.isEmpty() || currentUserPrivateKey.isBlank()) return@LaunchedEffect
        withContext(Dispatchers.IO) {
            try {
                val privateKeyHash = CryptoManager.generatePrivateKeyHash(currentUserPrivateKey)
                val keysToSubscribe = members.filter { key ->
                    !key.trim().equals(currentUserPublicKey.trim(), ignoreCase = true)
                }
                if (keysToSubscribe.isNotEmpty()) {
                    val packet = PacketOnlineSubscribe().apply {
                        this.privateKey = privateKeyHash
                        keysToSubscribe.forEach { addPublicKey(it) }
                    }
                    protocolGateway.send(packet)
                }
            } catch (_: Exception) {}
        }
    }

    val normalizedCurrentUserKey = remember(currentUserPublicKey) { currentUserPublicKey.trim() }

    val onlineCount by remember(members, memberOnlineStatus, normalizedCurrentUserKey) {
        derivedStateOf {
            if (members.isEmpty()) {
                0
            } else {
                val selfOnline = if (normalizedCurrentUserKey.isNotBlank()) 1 else 0
                val othersOnline =
                    members.count { key ->
                        val isCurrentUser =
                            normalizedCurrentUserKey.isNotBlank() &&
                                key.trim().equals(normalizedCurrentUserKey, ignoreCase = true)
                        !isCurrentUser && (memberOnlineStatus[key] == true)
                    }
                selfOnline + othersOnline
            }
        }
    }

    val memberItems by remember(members, memberInfoByKey, memberOnlineStatus, searchQuery) {
        derivedStateOf {
            val query = searchQuery.trim().lowercase()
            members.mapIndexed { index, key ->
                val info = memberInfoByKey[key]
                val isOnline = memberOnlineStatus[key] == true ||
                    key.trim().equals(currentUserPublicKey.trim(), ignoreCase = true)
                val fallbackName = shortPublicKey(key)
                val displayTitle =
                    info?.title?.takeIf { it.isNotBlank() }
                        ?: info?.username?.takeIf { it.isNotBlank() }
                        ?: fallbackName
                val subtitle = if (isOnline) "online" else "offline"
                GroupMemberUi(
                    publicKey = key,
                    title = displayTitle,
                    subtitle = subtitle,
                    verified = info?.verified ?: 0,
                    online = isOnline,
                    isAdmin = index == 0,
                    searchUser = SearchUser(
                        publicKey = key,
                        title = info?.title ?: displayTitle,
                        username = info?.username.orEmpty(),
                        verified = info?.verified ?: 0,
                        online = if (isOnline) 1 else 0
                    )
                )
            }.filter { member ->
                if (query.isBlank()) {
                    true
                } else {
                    val username = memberInfoByKey[member.publicKey]?.username?.lowercase().orEmpty()
                    member.title.lowercase().contains(query) ||
                        member.subtitle.lowercase().contains(query) ||
                        member.publicKey.lowercase().contains(query) ||
                        username.contains(query)
                }
            }
        }
    }
    val currentUserIsAdmin by remember(members, normalizedCurrentUserKey) {
        derivedStateOf {
            members.firstOrNull()?.trim()?.equals(normalizedCurrentUserKey, ignoreCase = true) == true
        }
    }

    fun openGroupAvatarViewer() {
        val repository = avatarRepository ?: return
        scope.launch {
            val latestAvatar = repository.getAvatars(dialogPublicKey, allDecode = false).first().firstOrNull()
                ?: return@launch
            groupAvatarViewerTimestamp = latestAvatar.timestamp / 1000L
            groupAvatarViewerBitmap =
                withContext(Dispatchers.IO) {
                    AvatarFileManager.base64ToBitmap(latestAvatar.base64Data)
                }
            showGroupAvatarViewer = true
        }
    }
    var swipedMemberKey by remember(dialogPublicKey) { mutableStateOf<String?>(null) }
    var memberToKick by remember(dialogPublicKey) { mutableStateOf<GroupMemberUi?>(null) }
    var isKickingMember by remember(dialogPublicKey) { mutableStateOf(false) }

    BackHandler {
        if (showEncryptionPage) {
            showEncryptionPage = false
        } else {
            onBack()
        }
    }

    LaunchedEffect(selectedTab) {
        if (selectedTab != GroupInfoTab.MEMBERS) {
            swipedMemberKey = null
        }
    }
    LaunchedEffect(swipedMemberKey, showEncryptionPage) {
        onSwipeBackEnabledChanged(swipedMemberKey == null && !showEncryptionPage)
    }
    DisposableEffect(Unit) {
        onDispose {
            onSwipeBackEnabledChanged(true)
        }
    }
    LaunchedEffect(memberItems) {
        val currentSwiped = swipedMemberKey ?: return@LaunchedEffect
        if (memberItems.none { it.publicKey == currentSwiped }) {
            swipedMemberKey = null
        }
    }

    fun copyInvite() {
        scope.launch {
            if (currentUserPublicKey.isBlank() || currentUserPrivateKey.isBlank()) {
                Toast.makeText(context, "Account keys not ready", Toast.LENGTH_SHORT).show()
                return@launch
            }

            val groupKey = withContext(Dispatchers.IO) {
                groupRepository.getGroupKey(currentUserPublicKey, currentUserPrivateKey, normalizedGroupId)
            }
            if (groupKey.isNullOrBlank()) {
                Toast.makeText(context, "Failed to load group key", Toast.LENGTH_SHORT).show()
                return@launch
            }

            val invite = groupRepository.constructInviteString(
                groupId = normalizedGroupId,
                title = groupTitle,
                encryptKey = groupKey,
                description = groupDescription
            )
            if (invite.isBlank()) {
                Toast.makeText(context, "Failed to create invite", Toast.LENGTH_SHORT).show()
                return@launch
            }

            clipboardManager.setText(AnnotatedString(invite))
            Toast.makeText(context, "Invite copied", Toast.LENGTH_SHORT).show()
        }
    }

    fun openInviteSharePicker() {
        scope.launch {
            if (currentUserPublicKey.isBlank() || currentUserPrivateKey.isBlank()) {
                Toast.makeText(context, "Account keys not ready", Toast.LENGTH_SHORT).show()
                return@launch
            }

            val groupKey = withContext(Dispatchers.IO) {
                groupRepository.getGroupKey(currentUserPublicKey, currentUserPrivateKey, normalizedGroupId)
            }
            if (groupKey.isNullOrBlank()) {
                Toast.makeText(context, "Failed to load group key", Toast.LENGTH_SHORT).show()
                return@launch
            }

            val invite = groupRepository.constructInviteString(
                groupId = normalizedGroupId,
                title = groupTitle,
                encryptKey = groupKey,
                description = groupDescription
            )
            if (invite.isBlank()) {
                Toast.makeText(context, "Failed to create invite", Toast.LENGTH_SHORT).show()
                return@launch
            }

            pendingInviteText = invite
            ForwardManager.setForwardMessages(
                messages = listOf(
                    ForwardManager.ForwardMessage(
                        messageId = UUID.randomUUID().toString().replace("-", "").take(32),
                        text = invite,
                        timestamp = System.currentTimeMillis(),
                        isOutgoing = true,
                        senderPublicKey = currentUserPublicKey,
                        originalChatPublicKey = dialogPublicKey,
                        senderName = "You",
                        attachments = emptyList()
                    )
                ),
                showPicker = false
            )
            showAddMembersPicker = true
        }
    }

    fun leaveGroup() {
        if (isLeaving) return
        scope.launch {
            isLeaving = true
            val left = withContext(Dispatchers.IO) {
                groupRepository.leaveGroup(currentUserPublicKey, normalizedGroupId)
            }
            isLeaving = false
            if (left) {
                GroupMembersMemoryCache.remove(membersCacheKey)
                GroupMembersDiskCache.remove(context, membersCacheKey)
                onGroupLeft()
            } else {
                Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
            }
        }
    }

    fun kickMember(member: GroupMemberUi) {
        if (isKickingMember) return
        val memberKey = member.publicKey.trim()
        if (memberKey.isBlank()) return
        if (!currentUserIsAdmin) {
            Toast.makeText(context, "Only admin can remove members", Toast.LENGTH_SHORT).show()
            return
        }
        if (memberKey.equals(normalizedCurrentUserKey, ignoreCase = true)) {
            Toast.makeText(context, "You cannot remove yourself", Toast.LENGTH_SHORT).show()
            return
        }
        if (member.isAdmin) {
            Toast.makeText(context, "Admin cannot be removed", Toast.LENGTH_SHORT).show()
            return
        }

        scope.launch {
            isKickingMember = true
            val removed = withContext(Dispatchers.IO) {
                groupRepository.kickMember(
                    groupPublicKeyOrId = normalizedGroupId,
                    memberPublicKey = memberKey
                )
            }
            isKickingMember = false
            memberToKick = null
            swipedMemberKey = null

            if (removed) {
                members = members.filterNot { it.trim().equals(memberKey, ignoreCase = true) }
                memberInfoByKey.remove(member.publicKey)
                GroupMembersMemoryCache.put(
                    key = membersCacheKey,
                    members = members,
                    memberInfoByKey = memberInfoByKey.toMap()
                )
                GroupMembersDiskCache.put(context, membersCacheKey, members)
                refreshMembers(force = true, showLoader = false)
                Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(context, "Failed to remove member", Toast.LENGTH_SHORT).show()
            }
        }
    }

    fun openEncryptionKey() {
        if (encryptionKeyLoading) return
        scope.launch {
            encryptionKeyLoading = true
            val key = withContext(Dispatchers.IO) {
                groupRepository.getGroupKey(
                    accountPublicKey = currentUserPublicKey,
                    accountPrivateKey = currentUserPrivateKey,
                    groupPublicKeyOrId = normalizedGroupId
                )
            }
            encryptionKeyLoading = false
            if (key.isNullOrBlank()) {
                Toast.makeText(context, "Failed to load encryption key", Toast.LENGTH_SHORT).show()
                return@launch
            }
            encryptionKey = key
            showEncryptionPage = true
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(backgroundColor)
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .background(topSurfaceColor)
                .statusBarsPadding()
                .padding(horizontal = 14.dp, vertical = 10.dp)
        ) {
            IconButton(onClick = onBack, modifier = Modifier.align(Alignment.TopStart)) {
                Icon(
                    imageVector = TablerIcons.ChevronLeft,
                    contentDescription = "Back",
                    tint = Color.White
                )
            }

            Box(modifier = Modifier.align(Alignment.TopEnd)) {
                IconButton(onClick = { showMenu = true }) {
                    Icon(
                        imageVector = Icons.Default.MoreVert,
                        contentDescription = "Menu",
                        tint = Color.White
                    )
                }
                val menuBgColor = if (isDarkTheme) Color(0xFF272829) else Color.White
                val menuTextColor = if (isDarkTheme) Color.White else Color(0xFF222222)
                val menuIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.47f) else Color(0xFF676B70)
                val menuDividerColor = if (isDarkTheme) Color(0xFF1C1D1F) else Color(0xFFF5F5F5)
                val menuDangerColor = Color(0xFFFF3B30)

                MaterialTheme(
                    colorScheme = MaterialTheme.colorScheme.copy(
                        surface = menuBgColor,
                        onSurface = menuTextColor
                    )
                ) {
                    DropdownMenu(
                        expanded = showMenu,
                        onDismissRequest = { showMenu = false },
                        modifier = Modifier.widthIn(min = 196.dp).background(menuBgColor),
                        properties = PopupProperties(
                            focusable = true,
                            dismissOnBackPress = true,
                            dismissOnClickOutside = true
                        )
                    ) {
                        GroupInfoMenuItem(
                            icon = Icons.Default.Search,
                            text = "Search members",
                            tintColor = menuIconColor,
                            textColor = menuTextColor,
                            onClick = {
                                showMenu = false
                                selectedTab = GroupInfoTab.MEMBERS
                                showSearch = true
                            }
                        )
                        GroupInfoMenuItem(
                            icon = Icons.Default.PersonAdd,
                            text = "Copy invite",
                            tintColor = menuIconColor,
                            textColor = menuTextColor,
                            onClick = {
                                showMenu = false
                                copyInvite()
                            }
                        )
                        GroupInfoMenuItem(
                            icon = Icons.Default.Lock,
                            text = "Encryption key",
                            tintColor = menuIconColor,
                            textColor = menuTextColor,
                            onClick = {
                                showMenu = false
                                openEncryptionKey()
                            }
                        )
                        Divider(color = menuDividerColor)
                        GroupInfoMenuItem(
                            icon = Icons.Default.ExitToApp,
                            text = "Leave group",
                            tintColor = menuDangerColor,
                            textColor = menuDangerColor,
                            onClick = {
                                showMenu = false
                                showLeaveConfirm = true
                            }
                        )
                    }
                }
            }

            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 36.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                AvatarImage(
                    publicKey = dialogPublicKey,
                    avatarRepository = avatarRepository,
                    size = 86.dp,
                    isDarkTheme = isDarkTheme,
                    onClick = {
                        if (currentUserIsAdmin) {
                            showGroupAvatarPicker = true
                        } else {
                            openGroupAvatarViewer()
                        }
                    },
                    displayName = groupTitle
                )

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

                AppleEmojiText(
                    text = groupTitle,
                    color = Color.White,
                    fontSize = 24.sp,
                    fontWeight = FontWeight.SemiBold,
                    maxLines = 1,
                    overflow = android.text.TextUtils.TruncateAt.END,
                    enableLinks = false
                )

                Text(
                    text = if (membersLoading || (members.isEmpty() && !hasInitialMembersCache)) {
                        "Loading members..."
                    } else {
                        "${members.size} members, $onlineCount online"
                    },
                    color = Color.White.copy(alpha = 0.72f),
                    fontSize = 14.sp
                )

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

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    GroupActionButton(
                        modifier = Modifier.weight(1f),
                        icon = Icons.Default.Message,
                        label = "Message",
                        backgroundColor = groupActionButtonBlue,
                        contentColor = Color.White,
                        iconColor = Color.White,
                        onClick = onBack
                    )
                    GroupActionButton(
                        modifier = Modifier.weight(1f),
                        icon = if (isMuted) Icons.Default.Notifications else Icons.Default.NotificationsOff,
                        label = if (isMuted) "Unmute" else "Mute",
                        backgroundColor = groupActionButtonBlue,
                        contentColor = Color.White,
                        iconColor = Color.White,
                        onClick = {
                            scope.launch {
                                val newMutedState = !isMuted
                                preferencesManager.setChatMuted(
                                    currentUserPublicKey,
                                    dialogPublicKey,
                                    newMutedState
                                )
                                isMuted = newMutedState
                            }
                        }
                    )
                    GroupActionButton(
                        modifier = Modifier.weight(1f),
                        icon = Icons.Default.ExitToApp,
                        label = "Leave",
                        backgroundColor = groupActionButtonBlue,
                        contentColor = Color.White,
                        iconColor = Color.White,
                        onClick = { showLeaveConfirm = true }
                    )
                }

            }
        }

        if (groupDescription.isNotBlank()) {
            Surface(
                modifier = Modifier.fillMaxWidth(),
                color = sectionColor
            ) {
                Column(
                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
                ) {
                    AppleEmojiText(
                        text = groupDescription,
                        color = primaryText,
                        fontSize = 16.sp,
                        maxLines = 8,
                        overflow = android.text.TextUtils.TruncateAt.END,
                        enableLinks = true
                    )
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = "Description",
                        color = Color(0xFF8E8E93),
                        fontSize = 13.sp
                    )
                }
            }
            Divider(
                color = borderColor,
                thickness = 0.5.dp,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = sectionColor
        ) {
            Column {
                // Add Members — flat Telegram style, edge-to-edge, white text
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable { openInviteSharePicker() }
                        .padding(horizontal = 16.dp, vertical = 13.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(
                        imageVector = Icons.Default.PersonAdd,
                        contentDescription = null,
                        tint = accentColor,
                        modifier = Modifier.size(24.dp)
                    )
                    Spacer(modifier = Modifier.width(28.dp))
                    Text(
                        text = "Add Members",
                        color = primaryText,
                        fontSize = 16.sp,
                        modifier = Modifier.weight(1f)
                    )
                }

                Divider(
                    color = borderColor,
                    thickness = 0.5.dp,
                    modifier = Modifier.fillMaxWidth()
                )

                // Encryption Key — flat Telegram style, edge-to-edge, white text
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable { openEncryptionKey() }
                        .padding(horizontal = 16.dp, vertical = 13.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(
                        imageVector = Icons.Default.Lock,
                        contentDescription = null,
                        tint = accentColor,
                        modifier = Modifier.size(24.dp)
                    )
                    Spacer(modifier = Modifier.width(28.dp))
                    Text(
                        text = "Encryption Key",
                        color = primaryText,
                        fontSize = 16.sp,
                        modifier = Modifier.weight(1f)
                    )
                    if (encryptionKeyLoading) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(groupMenuTrailingIconSize),
                            strokeWidth = 2.dp,
                            color = accentColor
                        )
                    } else {
                        val identiconKey = encryptionKey.ifBlank { dialogPublicKey }
                        Box(
                            modifier = Modifier
                                .size(groupMenuTrailingIconSize)
                                .clip(RoundedCornerShape(3.dp))
                        ) {
                            TelegramStyleIdenticon(
                                keyRender = identiconKey,
                                size = groupMenuTrailingIconSize,
                                isDarkTheme = isDarkTheme
                            )
                        }
                    }
                }
            }
        }
        Spacer(modifier = Modifier.height(8.dp))

        TabRow(
            selectedTabIndex = GroupInfoTab.entries.indexOf(selectedTab),
            containerColor = backgroundColor,
            contentColor = accentColor,
            divider = { Divider(color = borderColor) }
        ) {
            GroupInfoTab.entries.forEach { tab ->
                val tabTitle = when (tab) {
                    GroupInfoTab.MEMBERS -> "Members"
                    GroupInfoTab.MEDIA -> "Media ${sharedStats.mediaCount}"
                    GroupInfoTab.FILES -> "Files ${sharedStats.fileCount}"
                    GroupInfoTab.LINKS -> "Links ${sharedStats.linksCount}"
                }
                Tab(
                    selected = tab == selectedTab,
                    onClick = { selectedTab = tab },
                    text = {
                        Text(
                            text = tabTitle,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis,
                            fontSize = 13.sp,
                            color = if (tab == selectedTab) accentColor else secondaryText
                        )
                    }
                )
            }
        }

        if (showSearch && selectedTab == GroupInfoTab.MEMBERS) {
            OutlinedTextField(
                value = searchQuery,
                onValueChange = { searchQuery = it },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 14.dp, vertical = 8.dp),
                singleLine = true,
                label = { Text("Search members") },
                colors = TextFieldDefaults.colors(
                    focusedContainerColor = sectionColor,
                    unfocusedContainerColor = sectionColor,
                    focusedIndicatorColor = accentColor,
                    unfocusedIndicatorColor = borderColor,
                    focusedTextColor = primaryText,
                    unfocusedTextColor = primaryText,
                    focusedLabelColor = accentColor,
                    unfocusedLabelColor = secondaryText,
                    cursorColor = accentColor
                )
            )
        }

        when (selectedTab) {
            GroupInfoTab.MEMBERS -> {
                if (membersLoading && members.isEmpty()) {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(text = "Loading...", color = secondaryText)
                    }
                } else if (memberItems.isEmpty()) {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(
                            text = if (members.isEmpty()) "No members found" else "No results",
                            color = secondaryText
                        )
                    }
                } else {
                    LazyColumn(
                        modifier = Modifier.fillMaxSize(),
                        contentPadding = PaddingValues(bottom = 20.dp)
                    ) {
                        itemsIndexed(memberItems, key = { _, item -> item.publicKey }) { index, member ->
                            val canKickMember =
                                currentUserIsAdmin &&
                                    !member.publicKey.trim().equals(normalizedCurrentUserKey, ignoreCase = true) &&
                                    !member.isAdmin

                            SwipeKickMemberItem(
                                memberKey = member.publicKey,
                                canKick = canKickMember,
                                isDarkTheme = isDarkTheme,
                                isSwipedOpen = swipedMemberKey == member.publicKey,
                                onSwipeStarted = { swipedMemberKey = member.publicKey },
                                onSwipeClosed = {
                                    if (swipedMemberKey == member.publicKey) {
                                        swipedMemberKey = null
                                    }
                                },
                                onKickRequested = {
                                    memberToKick = member
                                },
                                onClick = {
                                    onMemberClick(member.searchUser)
                                },
                                onLongClick = {
                                    if (canKickMember) {
                                        hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
                                        memberToKick = member
                                    }
                                }
                            ) {
                                GroupMemberRow(
                                    member = member,
                                    primaryText = primaryText,
                                    secondaryText = secondaryText,
                                    rowColor = listRowColor,
                                    dividerColor = memberDividerColor,
                                    accentColor = accentColor,
                                    isDarkTheme = isDarkTheme,
                                    avatarRepository = avatarRepository,
                                    showDivider = index < memberItems.lastIndex
                                )
                            }
                        }
                    }
                }
            }
            GroupInfoTab.MEDIA -> {
                if (groupMediaItems.isEmpty()) {
                    GroupTabPlaceholder(
                        animationAssetPath = "lottie/saved.json",
                        title = if (sharedStats.mediaCount > 0) "${sharedStats.mediaCount} media detected" else "No shared media yet",
                        subtitle = if (sharedStats.mediaCount > 0) "Media preview is preparing for this group." else "Photos from this group chat will appear here.",
                        isDarkTheme = isDarkTheme
                    )
                } else {
                    val mediaColumns = 3
                    val mediaSpacing = 1.dp
                    val mediaRowsCount = remember(groupMediaItems.size) {
                        ceil(groupMediaItems.size / mediaColumns.toFloat()).toInt().coerceAtLeast(1)
                    }
                    val mediaFastScrollVisible by remember(groupMediaItems.size, groupMediaGridState) {
                        derivedStateOf {
                            val visibleItems = groupMediaGridState.layoutInfo.visibleItemsInfo
                            if (visibleItems.isEmpty()) return@derivedStateOf false
                            val cellHeight = visibleItems.first().size.height
                            val viewportHeight = groupMediaGridState.layoutInfo.viewportEndOffset -
                                groupMediaGridState.layoutInfo.viewportStartOffset
                            mediaRowsCount * cellHeight > viewportHeight
                        }
                    }
                    val mediaFastScrollProgress by remember(groupMediaItems.size, groupMediaGridState) {
                        derivedStateOf {
                            val visibleItems = groupMediaGridState.layoutInfo.visibleItemsInfo
                            if (visibleItems.isEmpty()) return@derivedStateOf 0f
                            val cellHeight = visibleItems.first().size.height
                            if (cellHeight <= 0) return@derivedStateOf 0f
                            val viewportHeight = groupMediaGridState.layoutInfo.viewportEndOffset -
                                groupMediaGridState.layoutInfo.viewportStartOffset
                            val totalHeight = mediaRowsCount * cellHeight
                            val maxScroll = (totalHeight - viewportHeight).coerceAtLeast(1)
                            val firstRow = groupMediaGridState.firstVisibleItemIndex / mediaColumns
                            val scrollY = firstRow * cellHeight + groupMediaGridState.firstVisibleItemScrollOffset
                            (scrollY.toFloat() / maxScroll.toFloat()).coerceIn(0f, 1f)
                        }
                    }
                    val mediaFastScrollMonthLabel by remember(groupMediaItems, groupMediaGridState.firstVisibleItemIndex) {
                        derivedStateOf {
                            if (groupMediaItems.isEmpty()) return@derivedStateOf ""
                            val index = groupMediaGridState.firstVisibleItemIndex.coerceIn(0, groupMediaItems.lastIndex)
                            formatMediaMonthLabel(groupMediaItems[index].timestamp)
                        }
                    }

                    Box(modifier = Modifier.fillMaxSize()) {
                        LazyVerticalGrid(
                            columns = GridCells.Fixed(mediaColumns),
                            state = groupMediaGridState,
                            modifier = Modifier.fillMaxSize(),
                            contentPadding = PaddingValues(bottom = 20.dp),
                            horizontalArrangement = Arrangement.spacedBy(mediaSpacing),
                            verticalArrangement = Arrangement.spacedBy(mediaSpacing)
                        ) {
                            gridItemsIndexed(groupMediaItems, key = { _, item -> item.key }) { index, mediaItem ->
                                Box(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .aspectRatio(1f)
                                        .clip(RoundedCornerShape(0.dp))
                                        .background(if (isDarkTheme) Color(0xFF141416) else Color(0xFFE6E7EB))
                                ) {
                                    ImageAttachment(
                                        attachment = mediaItem.attachment,
                                        chachaKey = mediaItem.chachaKey,
                                        privateKey = currentUserPrivateKey,
                                        senderPublicKey = mediaItem.senderPublicKey,
                                        isOutgoing = mediaItem.senderPublicKey == currentUserPublicKey,
                                        isDarkTheme = isDarkTheme,
                                        timestamp = Date(mediaItem.timestamp),
                                        showTimeOverlay = false,
                                        fillMaxSize = true,
                                        onImageClick = { _, bounds ->
                                            imageViewerInitialIndex = index
                                            imageViewerSourceBounds = bounds
                                            showImageViewer = true
                                        }
                                    )
                                }
                            }
                        }

                        SharedMediaFastScrollOverlay(
                            visible = mediaFastScrollVisible,
                            progress = mediaFastScrollProgress,
                            monthLabel = mediaFastScrollMonthLabel,
                            isDarkTheme = isDarkTheme,
                            showHint = mediaFastScrollVisible && !groupMediaFastScrollHintDismissed,
                            onHintDismissed = { groupMediaFastScrollHintDismissed = true },
                            onDragProgressChanged = { fraction ->
                                if (groupMediaItems.isEmpty()) return@SharedMediaFastScrollOverlay
                                val targetRow = ((mediaRowsCount - 1) * fraction).roundToInt()
                                val targetIndex = (targetRow * mediaColumns).coerceIn(0, groupMediaItems.lastIndex)
                                scope.launch {
                                    groupMediaGridState.scrollToItem(targetIndex)
                                }
                            }
                        )
                    }
                }
            }
            GroupInfoTab.FILES -> {
                GroupTabPlaceholder(
                    animationAssetPath = "lottie/folder.json",
                    title = if (sharedStats.fileCount > 0) "${sharedStats.fileCount} files detected" else "No shared files",
                    subtitle = if (sharedStats.fileCount > 0) "File list is preparing for this group." else "Documents from this group chat will appear here.",
                    isDarkTheme = isDarkTheme
                )
            }
            GroupInfoTab.LINKS -> {
                GroupTabPlaceholder(
                    animationAssetPath = "lottie/earth.json",
                    title = if (sharedStats.linksCount > 0) "${sharedStats.linksCount} links detected" else "No shared links",
                    subtitle = if (sharedStats.linksCount > 0) "Links list is preparing for this group." else "Links from group messages will appear here.",
                    isDarkTheme = isDarkTheme
                )
            }
        }
    }

    memberToKick?.let { targetMember ->
        AlertDialog(
            onDismissRequest = {
                if (!isKickingMember) {
                    memberToKick = null
                }
            },
            containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
            titleContentColor = primaryText,
            textContentColor = primaryText,
            title = { Text("Remove member") },
            text = {
                Text("Remove ${targetMember.title} from this group?")
            },
            confirmButton = {
                TextButton(
                    onClick = { kickMember(targetMember) },
                    enabled = !isKickingMember
                ) {
                    if (isKickingMember) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(16.dp),
                            strokeWidth = 2.dp,
                            color = Color(0xFFFF3B30)
                        )
                    } else {
                        Text("Kick", color = Color(0xFFFF3B30))
                    }
                }
            },
            dismissButton = {
                TextButton(
                    onClick = { memberToKick = null },
                    enabled = !isKickingMember
                ) {
                    Text("Cancel", color = accentColor)
                }
            }
        )
    }

    if (showLeaveConfirm) {
        AlertDialog(
            onDismissRequest = { showLeaveConfirm = false },
            title = { Text("Leave group") },
            text = { Text("Are you sure you want to leave this group?") },
            confirmButton = {
                TextButton(
                    onClick = {
                        showLeaveConfirm = false
                        leaveGroup()
                    },
                    enabled = !isLeaving
                ) {
                    Text("Leave", color = Color(0xFFFF3B30))
                }
            },
            dismissButton = {
                TextButton(onClick = { showLeaveConfirm = false }, enabled = !isLeaving) {
                    Text("Cancel")
                }
            }
        )
    }

    SwipeBackContainer(
        isVisible = showEncryptionPage,
        onBack = { showEncryptionPage = false },
        isDarkTheme = isDarkTheme,
        layer = 3,
        propagateBackgroundProgress = false
    ) {
        val displayLines = remember(encryptionKey) { encodeGroupKeyForDisplay(encryptionKey) }
        GroupEncryptionKeyPage(
            encryptionKey = encryptionKey,
            displayLines = displayLines,
            peerTitle = groupTitle,
            isDarkTheme = isDarkTheme,
            topSurfaceColor = topSurfaceColor,
            backgroundColor = backgroundColor,
            onBack = { showEncryptionPage = false }
        )
    }

    if (showImageViewer && mediaViewerImages.isNotEmpty() && currentUserPrivateKey.isNotBlank()) {
        ImageViewerScreen(
            images = mediaViewerImages,
            initialIndex = imageViewerInitialIndex.coerceIn(0, mediaViewerImages.lastIndex),
            privateKey = currentUserPrivateKey,
            onDismiss = {
                showImageViewer = false
                imageViewerSourceBounds = null
            },
            isDarkTheme = isDarkTheme,
            sourceBounds = imageViewerSourceBounds
        )
    }

    if (showAddMembersPicker) {
        val selectableDialogs = remember(forwardDialogs, dialogPublicKey) {
            forwardDialogs.filter { it.opponentKey != dialogPublicKey }
        }
        ForwardChatPickerBottomSheet(
            dialogs = selectableDialogs,
            isDarkTheme = isDarkTheme,
            currentUserPublicKey = currentUserPublicKey,
            avatarRepository = avatarRepository,
            onDismiss = {
                showAddMembersPicker = false
                ForwardManager.clear()
            },
            onChatsSelected = { selectedDialogs ->
                showAddMembersPicker = false
                ForwardManager.clear()

                if (selectedDialogs.isEmpty()) {
                    return@ForwardChatPickerBottomSheet
                }
                val inviteText = pendingInviteText.trim()
                if (inviteText.isBlank()) {
                    Toast.makeText(context, "Invite is empty", Toast.LENGTH_SHORT).show()
                    return@ForwardChatPickerBottomSheet
                }

                scope.launch {
                    var sentCount = 0
                    selectedDialogs.forEach { dialog ->
                        val sent = runCatching {
                            messageRepository.sendMessage(
                                toPublicKey = dialog.opponentKey,
                                text = inviteText
                            )
                        }.isSuccess
                        if (sent) sentCount += 1
                    }

                    if (sentCount > 0) {
                        Toast.makeText(
                            context,
                            "Invite sent to $sentCount chat${if (sentCount == 1) "" else "s"}",
                            Toast.LENGTH_SHORT
                        ).show()
                    } else {
                        Toast.makeText(context, "Failed to send invite", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        )
    }

    ProfilePhotoPicker(
        isVisible = showGroupAvatarPicker,
        onDismiss = { showGroupAvatarPicker = false },
        onPhotoSelected = { uri ->
            showGroupAvatarPicker = false
            val cropIntent = ImageCropHelper.createCropIntent(context, uri, isDarkTheme)
            cropLauncher.launch(cropIntent)
        },
        isDarkTheme = isDarkTheme
    )

    FullScreenAvatarViewer(
        isVisible = showGroupAvatarViewer,
        onDismiss = { showGroupAvatarViewer = false },
        displayName = groupTitle.ifBlank { shortPublicKey(dialogPublicKey) },
        avatarTimestamp = groupAvatarViewerTimestamp,
        avatarBitmap = groupAvatarViewerBitmap,
        publicKey = dialogPublicKey,
        isDarkTheme = isDarkTheme
    )
}

@Composable
private fun TelegramStyleIdenticon(
    keyRender: String,
    size: androidx.compose.ui.unit.Dp,
    isDarkTheme: Boolean
) {
    val palette = IDENTICON_COLORS

    // Convert key string to byte array, then use 2-bit grouping like Telegram's IdenticonDrawable
    val keyBytes = remember(keyRender) {
        val source = keyRender.ifBlank { "rosetta" }
        source.toByteArray(Charsets.UTF_8)
    }

    Canvas(
        modifier = Modifier
            .size(size)
    ) {
        val cells = 12
        val cellSize = this.size.minDimension / cells.toFloat()
        var bitPointer = 0
        for (iy in 0 until cells) {
            for (ix in 0 until cells) {
                val byteIndex = (bitPointer / 8) % keyBytes.size
                val bitOffset = bitPointer % 8
                val value = (keyBytes[byteIndex].toInt() shr bitOffset) and 0x3
                val colorIndex = kotlin.math.abs(value) % 4
                drawRect(
                    color = Color(palette[colorIndex]),
                    topLeft = Offset(ix * cellSize, iy * cellSize),
                    size = Size(cellSize, cellSize)
                )
                bitPointer += 2
            }
        }
    }
}

@Composable
private fun GroupEncryptionKeyPage(
    encryptionKey: String,
    displayLines: List<String>,
    peerTitle: String,
    isDarkTheme: Boolean,
    topSurfaceColor: Color,
    backgroundColor: Color,
    onBack: () -> Unit
) {
    val uriHandler = LocalUriHandler.current
    val safePeerTitle = peerTitle.ifBlank { "this group" }

    // Rosetta theme colors
    val identiconBg = if (isDarkTheme) Color(0xFF111111) else Color(0xFFF2F2F7)
    val bottomBg = if (isDarkTheme) Color(0xFF1A1A1A) else Color.White
    val codeColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF6D7883)
    val descriptionColor = Color(0xFF8E8E93)
    val linkColor = if (isDarkTheme) Color(0xFF5AA5FF) else Color(0xFF228BE6)

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(backgroundColor)
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
        ) {
            // Top bar
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(topSurfaceColor)
                    .statusBarsPadding()
                    .padding(horizontal = 4.dp, vertical = 6.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                IconButton(onClick = onBack) {
                    Icon(
                        imageVector = TablerIcons.ChevronLeft,
                        contentDescription = "Back",
                        tint = Color.White
                    )
                }
                Text(
                    text = "Encryption Key",
                    color = Color.White,
                    fontSize = 20.sp,
                    fontWeight = FontWeight.SemiBold
                )
            }

            // Two-half layout like Telegram
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .weight(1f)
            ) {
                // Top half - Identicon image on gray background
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f)
                        .background(identiconBg)
                        .padding(24.dp),
                    contentAlignment = Alignment.Center
                ) {
                    TelegramStyleIdenticon(
                        keyRender = encryptionKey,
                        size = 280.dp,
                        isDarkTheme = isDarkTheme
                    )
                }

                // Bottom half - Code + Description on white background
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f)
                        .background(bottomBg)
                        .verticalScroll(rememberScrollState())
                        .padding(horizontal = 24.dp, vertical = 20.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    // Hex code display
                    SelectionContainer {
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally
                        ) {
                            displayLines.forEach { line ->
                                Text(
                                    text = line,
                                    color = codeColor,
                                    fontSize = 14.sp,
                                    fontFamily = FontFamily.Monospace,
                                    letterSpacing = 0.5.sp,
                                    textAlign = TextAlign.Center
                                )
                            }
                        }
                    }

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

                    // Description text
                    Text(
                        text = "This image and text were derived from the encryption key for this group with $safePeerTitle.\n\nIf they look the same on $safePeerTitle's device, end-to-end encryption is guaranteed.",
                        color = descriptionColor,
                        fontSize = 14.sp,
                        textAlign = TextAlign.Center,
                        lineHeight = 20.sp
                    )

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

                    Text(
                        text = "Learn more at rosetta.im",
                        color = linkColor,
                        fontSize = 14.sp,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.clickable { uriHandler.openUri("https://rosetta.im/") }
                    )

                    Spacer(modifier = Modifier.height(16.dp))
                }
            }
        }
    }
}

@Composable
private fun GroupInfoMenuItem(
    icon: ImageVector,
    text: String,
    tintColor: Color,
    textColor: Color,
    onClick: () -> Unit
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp)
            .clickable(onClick = onClick)
            .padding(horizontal = 18.dp),
        contentAlignment = Alignment.CenterStart
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Icon(
                imageVector = icon,
                contentDescription = null,
                tint = tintColor,
                modifier = Modifier.size(24.dp)
            )
            Spacer(modifier = Modifier.width(19.dp))
            Text(
                text = text,
                color = textColor,
                fontSize = 16.sp
            )
        }
    }
}

@Composable
private fun GroupActionButton(
    modifier: Modifier = Modifier,
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    label: String,
    backgroundColor: Color,
    contentColor: Color,
    iconColor: Color = contentColor,
    onClick: () -> Unit
) {
    Surface(
        modifier = modifier
            .clip(RoundedCornerShape(14.dp))
            .clickable(onClick = onClick),
        color = backgroundColor,
        shape = RoundedCornerShape(14.dp)
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 12.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Icon(
                imageVector = icon,
                contentDescription = null,
                tint = iconColor,
                modifier = Modifier.size(20.dp)
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = label, color = contentColor, fontSize = 13.sp, fontWeight = FontWeight.Medium)
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SwipeKickMemberItem(
    memberKey: String,
    canKick: Boolean,
    isDarkTheme: Boolean,
    isSwipedOpen: Boolean,
    onSwipeStarted: () -> Unit,
    onSwipeClosed: () -> Unit,
    onKickRequested: () -> Unit,
    onClick: () -> Unit,
    onLongClick: () -> Unit,
    content: @Composable () -> Unit
) {
    val density = LocalDensity.current
    val actionWidth = 80.dp
    val actionWidthPx = with(density) { actionWidth.toPx() }
    var offsetX by remember(memberKey) { mutableFloatStateOf(0f) }
    var itemHeightPx by remember(memberKey) { mutableStateOf(0) }

    LaunchedEffect(isSwipedOpen, canKick, actionWidthPx) {
        if (!canKick) {
            offsetX = 0f
        } else {
            offsetX = if (isSwipedOpen) -actionWidthPx else 0f
        }
    }

    val animatedOffsetX by animateFloatAsState(
        targetValue = offsetX,
        animationSpec = tween(durationMillis = 180),
        label = "groupMemberSwipeOffset"
    )

    val actionBackground = if (isDarkTheme) Color(0xFFFF6B6B) else Color(0xFFFF6B6B)

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .clipToBounds()
    ) {
        if (canKick) {
            val actionHeight =
                if (itemHeightPx > 0) with(density) { itemHeightPx.toDp() } else 72.dp
            Box(
                modifier = Modifier
                    .align(Alignment.CenterEnd)
                    .width(actionWidth)
                    .height(actionHeight)
                    .background(actionBackground)
                    .pointerInput(actionWidthPx) {
                        detectHorizontalDragGestures(
                            onHorizontalDrag = { change, dragAmount ->
                                val updated = (offsetX + dragAmount).coerceIn(-actionWidthPx, 0f)
                                offsetX = updated
                                change.consume()
                            },
                            onDragEnd = {
                                val shouldOpen = abs(offsetX) > actionWidthPx * 0.45f
                                if (shouldOpen) {
                                    offsetX = -actionWidthPx
                                    onSwipeStarted()
                                } else {
                                    offsetX = 0f
                                    onSwipeClosed()
                                }
                            },
                            onDragCancel = {
                                if (abs(offsetX) <= actionWidthPx * 0.45f) {
                                    offsetX = 0f
                                    onSwipeClosed()
                                }
                            }
                        )
                    }
                    .clickable {
                        offsetX = 0f
                        onSwipeClosed()
                        onKickRequested()
                    },
                contentAlignment = Alignment.Center
            ) {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Center
                ) {
                    Icon(
                        painter = TelegramIcons.Leave,
                        contentDescription = "Kick",
                        tint = Color.White,
                        modifier = Modifier.size(22.dp)
                    )
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = "Kick",
                        color = Color.White,
                        fontSize = 12.sp,
                        fontWeight = FontWeight.SemiBold
                    )
                }
            }
        }

        Box(
            modifier = Modifier
                .offset { IntOffset(animatedOffsetX.toInt(), 0) }
                .onSizeChanged { measured -> itemHeightPx = measured.height }
                .pointerInput(canKick, actionWidthPx) {
                    if (!canKick) return@pointerInput
                    detectHorizontalDragGestures(
                        onDragStart = {
                            onSwipeStarted()
                        },
                        onHorizontalDrag = { change, dragAmount ->
                            if (dragAmount < 0f || offsetX < 0f) {
                                val updated = (offsetX + dragAmount).coerceIn(-actionWidthPx, 0f)
                                offsetX = updated
                                change.consume()
                            }
                        },
                        onDragEnd = {
                            val shouldOpen = abs(offsetX) > actionWidthPx * 0.45f
                            if (shouldOpen) {
                                offsetX = -actionWidthPx
                                onSwipeStarted()
                            } else {
                                offsetX = 0f
                                onSwipeClosed()
                            }
                        },
                        onDragCancel = {
                            if (abs(offsetX) <= actionWidthPx * 0.45f) {
                                offsetX = 0f
                                onSwipeClosed()
                            }
                        }
                    )
                }
                .combinedClickable(
                    onClick = {
                        if (offsetX < -6f) {
                            offsetX = 0f
                            onSwipeClosed()
                        } else {
                            onClick()
                        }
                    },
                    onLongClick = {
                        if (canKick) {
                            onLongClick()
                        }
                    }
                )
        ) {
            content()
        }
    }
}

@Composable
private fun GroupMemberRow(
    member: GroupMemberUi,
    primaryText: Color,
    secondaryText: Color,
    rowColor: Color,
    dividerColor: Color,
    accentColor: Color,
    isDarkTheme: Boolean,
    avatarRepository: AvatarRepository?,
    showDivider: Boolean
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = rowColor
    ) {
        Column {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 14.dp, vertical = 10.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                AvatarImage(
                    publicKey = member.publicKey,
                    avatarRepository = avatarRepository,
                    size = 48.dp,
                    isDarkTheme = isDarkTheme,
                    displayName = member.title
                )
                Spacer(modifier = Modifier.size(12.dp))
                Column(modifier = Modifier.weight(1f)) {
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(4.dp)
                    ) {
                        Text(
                            text = member.title,
                            color = primaryText,
                            fontSize = 17.sp,
                            fontWeight = FontWeight.SemiBold,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                        if (member.verified > 0) {
                            VerifiedBadge(
                                verified = member.verified,
                                size = 16,
                                isDarkTheme = isDarkTheme
                            )
                        }
                    }
                    Text(
                        text = member.subtitle,
                        color = if (member.online) accentColor else secondaryText,
                        fontSize = 13.sp,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                }
                if (member.isAdmin) {
                    Text(
                        text = "Admin",
                        color = accentColor,
                        fontSize = 13.sp,
                        fontWeight = FontWeight.Medium
                    )
                }
            }
            if (showDivider) {
                Divider(
                    modifier = Modifier.padding(start = 74.dp),
                    color = dividerColor,
                    thickness = 0.7.dp
                )
            }
        }
    }
}

@Composable
private fun GroupTabPlaceholder(
    animationAssetPath: String,
    title: String,
    subtitle: String,
    isDarkTheme: Boolean
) {
    val composition by rememberLottieComposition(LottieCompositionSpec.Asset(animationAssetPath))
    val progress by animateLottieCompositionAsState(
        composition = composition,
        iterations = LottieConstants.IterateForever,
        speed = 0.9f,
        restartOnPlay = false,
        isPlaying = composition != null
    )
    val titleColor = if (isDarkTheme) Color(0xFFE5E5EA) else Color(0xFF1F2937)
    val subtitleColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF6B7280)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 24.dp, vertical = 24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Box(modifier = Modifier.size(64.dp), contentAlignment = Alignment.Center) {
            if (composition != null) {
                LottieAnimation(
                    composition = composition,
                    progress = { progress },
                    modifier = Modifier.fillMaxSize()
                )
            }
        }
        Spacer(modifier = Modifier.height(10.dp))
        Text(
            text = title,
            color = titleColor,
            fontWeight = FontWeight.SemiBold,
            fontSize = 16.sp,
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(4.dp))
        Text(
            text = subtitle,
            color = subtitleColor,
            fontSize = 13.sp,
            textAlign = TextAlign.Center
        )
    }
}

private fun shortPublicKey(publicKey: String): String {
    val trimmed = publicKey.trim()
    if (trimmed.length <= 12) return trimmed
    return "${trimmed.take(6)}...${trimmed.takeLast(4)}"
}

private fun formatMediaMonthLabel(timestamp: Long): String {
    return runCatching {
        SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(Date(timestamp))
    }.getOrElse { "" }
}

private fun decryptStoredMessageText(encryptedText: String, privateKey: String): String {
    if (encryptedText.isBlank()) return ""
    if (privateKey.isBlank()) return encryptedText
    return try {
        CryptoManager.decryptWithPassword(encryptedText, privateKey) ?: encryptedText
    } catch (_: Exception) {
        encryptedText
    }
}

private fun extractUrls(text: String): List<String> {
    return URL_REGEX.findAll(text).map { match ->
        val rawUrl = match.groupValues[1]
            .trim()
            .trimEnd('.', ',', ';', ':', '!', '?', ')', ']', '}')
        if (rawUrl.startsWith("www.", ignoreCase = true)) {
            "https://$rawUrl"
        } else {
            rawUrl
        }
    }.distinct().toList()
}

private fun countAttachmentTypes(attachmentsJson: String): Pair<Int, Int> {
    if (attachmentsJson.isBlank() || attachmentsJson == "[]") return 0 to 0

    var mediaCount = 0
    var fileCount = 0

    runCatching {
        val attachments = JSONArray(attachmentsJson)
        for (i in 0 until attachments.length()) {
            val attachment = attachments.getJSONObject(i)
            when (AttachmentType.fromInt(attachment.optInt("type", 0))) {
                AttachmentType.IMAGE -> mediaCount += 1
                AttachmentType.FILE -> fileCount += 1
                else -> Unit
            }
        }
    }

    return mediaCount to fileCount
}

private fun buildGroupSharedStats(messages: List<MessageEntity>, privateKey: String): GroupSharedStats {
    if (messages.isEmpty()) return GroupSharedStats()

    var mediaCount = 0
    var fileCount = 0
    val links = linkedSetOf<String>()

    messages.forEach { message ->
        val decryptedText = decryptStoredMessageText(message.plainMessage, privateKey)
        if (decryptedText.isNotBlank()) {
            links.addAll(extractUrls(decryptedText))
        }

        val (mediaInMessage, filesInMessage) = countAttachmentTypes(message.attachments)
        mediaCount += mediaInMessage
        fileCount += filesInMessage
    }

    return GroupSharedStats(
        mediaCount = mediaCount,
        fileCount = fileCount,
        linksCount = links.size
    )
}

private fun parseAttachmentsForGroupInfo(attachmentsJson: String): List<MessageAttachment> {
    if (attachmentsJson.isBlank() || attachmentsJson == "[]") return emptyList()

    return try {
        val attachments = JSONArray(attachmentsJson)
        buildList {
            for (i in 0 until attachments.length()) {
                val attachment = attachments.getJSONObject(i)
                val type = AttachmentType.fromInt(attachment.optInt("type", 0))
                if (type == AttachmentType.MESSAGES) continue
                val transportObj = attachment.optJSONObject("transport")
                val transportTag =
                    attachment.optString(
                        "transportTag",
                        attachment.optString(
                            "transport_tag",
                            transportObj?.optString("transport_tag", "") ?: ""
                        )
                    )
                val transportServer =
                    attachment.optString(
                        "transportServer",
                        attachment.optString(
                            "transport_server",
                            transportObj?.optString("transport_server", "") ?: ""
                        )
                    )

                add(
                    MessageAttachment(
                        id = attachment.optString("id", ""),
                        blob = attachment.optString("blob", ""),
                        type = type,
                        preview = attachment.optString("preview", ""),
                        width = attachment.optInt("width", 0),
                        height = attachment.optInt("height", 0),
                        localUri = attachment.optString("localUri", ""),
                        transportTag = transportTag,
                        transportServer = transportServer
                    )
                )
            }
        }
    } catch (_: Exception) {
        emptyList()
    }
}

private fun resolveGroupSenderName(
    protocolGateway: ProtocolGateway,
    senderPublicKey: String,
    currentUserPublicKey: String,
    memberInfoByKey: Map<String, SearchUser>
): String {
    if (senderPublicKey.isBlank()) return "Unknown"
    if (senderPublicKey == currentUserPublicKey) return "You"

    val info = memberInfoByKey[senderPublicKey] ?: protocolGateway.getCachedUserInfo(senderPublicKey)
    return info?.title?.takeIf { it.isNotBlank() }
        ?: info?.username?.takeIf { it.isNotBlank() }
        ?: shortPublicKey(senderPublicKey)
}

private fun buildGroupMediaItems(
    protocolGateway: ProtocolGateway,
    messages: List<MessageEntity>,
    privateKey: String,
    currentUserPublicKey: String,
    memberInfoByKey: Map<String, SearchUser>
): List<GroupMediaItem> {
    if (messages.isEmpty()) return emptyList()

    val uniqueMedia = hashSetOf<String>()
    val mediaItems = mutableListOf<GroupMediaItem>()

    for (message in messages) {
        val attachments = parseAttachmentsForGroupInfo(message.attachments)
        if (attachments.isEmpty()) continue

        val senderKey = message.fromPublicKey.takeIf { it.isNotBlank() }
            ?: if (message.fromMe == 1) currentUserPublicKey else ""
        val senderCacheKey = senderKey.ifBlank { currentUserPublicKey }
        val senderName = resolveGroupSenderName(
            protocolGateway = protocolGateway,
            senderPublicKey = senderCacheKey,
            currentUserPublicKey = currentUserPublicKey,
            memberInfoByKey = memberInfoByKey
        )
        val caption = decryptStoredMessageText(message.plainMessage, privateKey).trim()

        for (attachment in attachments) {
            if (attachment.type != AttachmentType.IMAGE) continue

            val key = "${message.messageId}:${attachment.id}"
            if (!uniqueMedia.add(key)) continue

            mediaItems.add(
                GroupMediaItem(
                    key = key,
                    attachment = attachment,
                    chachaKey = message.chachaKey,
                    senderPublicKey = senderCacheKey,
                    senderName = senderName,
                    timestamp = message.timestamp,
                    caption = caption
                )
            )
        }
    }

    return mediaItems.sortedByDescending { it.timestamp }
}

private fun encodeGroupKeyForDisplay(encryptKey: String): List<String> {
    val normalized = encryptKey.trim()
    if (normalized.isBlank()) return emptyList()

    // Telegram-style: each char → XOR 27 → 2-char hex, grouped as:
    // "ab cd ef 12  34 56 78 9a" (4 pairs + double space + 4 pairs per line)
    val hexPairs = normalized.map { symbol ->
        (symbol.code xor 27).toString(16).padStart(2, '0')
    }

    val lines = mutableListOf<String>()
    for (lineStart in hexPairs.indices step 8) {
        val lineEnd = minOf(lineStart + 8, hexPairs.size)
        val linePairs = hexPairs.subList(lineStart, lineEnd)
        val sb = StringBuilder()
        for ((i, pair) in linePairs.withIndex()) {
            if (i > 0) {
                sb.append(if (i % 4 == 0) "  " else " ")
            }
            sb.append(pair)
        }
        lines.add(sb.toString())
    }
    return lines
}
