package com.rosetta.messenger.ui.chats

import android.graphics.BitmapFactory
import android.net.Uri
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.domain.chats.usecase.CreateAvatarAttachmentCommand
import com.rosetta.messenger.domain.chats.usecase.CreateFileAttachmentCommand
import com.rosetta.messenger.domain.chats.usecase.CreateVideoCircleAttachmentCommand
import com.rosetta.messenger.domain.chats.usecase.EncryptAndUploadAttachmentCommand
import com.rosetta.messenger.domain.chats.usecase.SendMediaMessageCommand
import com.rosetta.messenger.domain.chats.usecase.SendTypingIndicatorCommand
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.DeliveryStatus
import com.rosetta.messenger.network.MessageAttachment
import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.ui.chats.models.ChatMessage
import com.rosetta.messenger.ui.chats.models.MessageStatus
import com.rosetta.messenger.utils.AttachmentFileManager
import com.rosetta.messenger.utils.AvatarFileManager
import com.rosetta.messenger.utils.MediaUtils
import com.rosetta.messenger.utils.RosettaDev1Log
import java.util.Date
import java.util.UUID
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject

class MessagesViewModel internal constructor(
    private val chatViewModel: ChatViewModel
) {
    val messages: StateFlow<List<ChatMessage>> = chatViewModel.messages
    val messagesWithDates: StateFlow<List<Pair<ChatMessage, Boolean>>> = chatViewModel.messagesWithDates
    val isLoading: StateFlow<Boolean> = chatViewModel.isLoading
    val isLoadingMore: StateFlow<Boolean> = chatViewModel.isLoadingMore
    val groupRequiresRejoin: StateFlow<Boolean> = chatViewModel.groupRequiresRejoin
    val inputText: StateFlow<String> = chatViewModel.inputText
    val replyMessages: StateFlow<List<ChatViewModel.ReplyMessage>> = chatViewModel.replyMessages
    val isForwardMode: StateFlow<Boolean> = chatViewModel.isForwardMode
    val pendingDeleteIds: StateFlow<Set<String>> = chatViewModel.pendingDeleteIds
    val pinnedMessages = chatViewModel.pinnedMessages
    val pinnedMessagePreviews = chatViewModel.pinnedMessagePreviews
    val currentPinnedIndex = chatViewModel.currentPinnedIndex
    val chatOpenMetrics = chatViewModel.chatOpenMetrics
    val myPublicKey: String? get() = chatViewModel.myPublicKey

    fun setUserKeys(publicKey: String, privateKey: String) = chatViewModel.setUserKeys(publicKey, privateKey)

    fun ensureSendContext(
        publicKey: String,
        title: String = "",
        username: String = "",
        verified: Int = 0
    ) = chatViewModel.ensureSendContext(publicKey, title, username, verified)

    fun openDialog(
        publicKey: String,
        title: String = "",
        username: String = "",
        verified: Int = 0
    ) = chatViewModel.openDialog(publicKey, title, username, verified)

    fun closeDialog() = chatViewModel.closeDialog()

    fun setDialogActive(active: Boolean) = chatViewModel.setDialogActive(active)

    fun loadMoreMessages() = chatViewModel.loadMoreMessages()

    fun updateInputText(text: String) = chatViewModel.updateInputText(text)

    fun setReplyMessages(messages: List<ChatMessage>) = chatViewModel.setReplyMessages(messages)

    fun setForwardMessages(messages: List<ChatMessage>) = chatViewModel.setForwardMessages(messages)

    fun clearReplyMessages() = chatViewModel.clearReplyMessages()

    suspend fun ensureMessageLoaded(messageId: String): Boolean = chatViewModel.ensureMessageLoaded(messageId)

    fun pinMessage(messageId: String) = chatViewModel.pinMessage(messageId)

    fun unpinMessage(messageId: String) = chatViewModel.unpinMessage(messageId)

    suspend fun isMessagePinned(messageId: String): Boolean = chatViewModel.isMessagePinned(messageId)

    fun navigateToNextPinned(): String? = chatViewModel.navigateToNextPinned()

    fun unpinAllMessages() = chatViewModel.unpinAllMessages()

    fun deleteMessage(messageId: String) = chatViewModel.deleteMessage(messageId)

    suspend fun resolveUserForProfile(publicKey: String): SearchUser? =
        chatViewModel.resolveUserForProfile(publicKey)

    suspend fun resolveUserByUsername(username: String, timeoutMs: Long = 3000): SearchUser? =
        chatViewModel.resolveUserByUsername(username, timeoutMs)

    fun retryMessage(message: ChatMessage) {
        deleteMessage(message.id)
        updateInputText(message.text)
        chatViewModel.launchInViewModel {
            delay(100)
            sendMessage()
        }
    }

    fun sendMessage() {
        val hasPayload = inputText.value.trim().isNotEmpty() || replyMessages.value.isNotEmpty()
        if (!hasPayload) return
        chatViewModel.trySendTextMessage(allowPendingRecovery = true)
    }

    fun sendForwardDirectly(
        targetPublicKey: String,
        forwardedMessages: List<ForwardManager.ForwardMessage>
    ) = chatViewModel.sendForwardDirectly(targetPublicKey, forwardedMessages)

    fun markVisibleMessagesAsRead() = chatViewModel.markVisibleMessagesAsRead()

    fun subscribeToOnlineStatus() = chatViewModel.subscribeToOnlineStatus()

    fun markFirstListLayoutReady() = chatViewModel.markFirstListLayoutReady()

    fun addChatOpenTraceEvent(event: String, details: String = "") =
        chatViewModel.addChatOpenTraceEvent(event, details)
}

class VoiceRecordingViewModel internal constructor(
    private val chatViewModel: ChatViewModel
) {
    fun sendVoiceMessage(voiceHex: String, durationSec: Int, waves: List<Float>) {
        val sendContext = chatViewModel.resolveOutgoingSendContext() ?: return
        if (!chatViewModel.tryAcquireSendSlot()) {
            RosettaDev1Log.w("voice-send slot busy: request dropped")
            return
        }

        val voicePayload = chatViewModel.buildVoicePayload(voiceHex, durationSec, waves)
        if (voicePayload == null || voicePayload.normalizedVoiceHex.isEmpty()) {
            RosettaDev1Log.w("voice-send payload invalid: empty normalized voice")
            chatViewModel.releaseSendSlot()
            return
        }

        val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
        val timestamp = System.currentTimeMillis()
        val attachmentId = "voice_$timestamp"
        val senderShort = sendContext.sender.take(12)
        val recipientShort = sendContext.recipient.take(12)
        RosettaDev1Log.i(
            "voice-send start msg=${messageId.take(8)} att=${attachmentId.take(12)} " +
                "from=$senderShort to=$recipientShort dur=${voicePayload.durationSec}s " +
                "waves=${voicePayload.normalizedWaves.size} blobLen=${voicePayload.normalizedVoiceHex.length}"
        )

        chatViewModel.addOutgoingMessageOptimistic(
            ChatMessage(
                id = messageId,
                text = "",
                isOutgoing = true,
                timestamp = Date(timestamp),
                status = MessageStatus.SENDING,
                attachments =
                    listOf(
                        MessageAttachment(
                            id = attachmentId,
                            type = AttachmentType.VOICE,
                            preview = voicePayload.preview,
                            blob = voicePayload.normalizedVoiceHex
                        )
                    )
            )
        )
        chatViewModel.clearInputText()

        chatViewModel.launchOnIo {
            try {
                val encryptionContext =
                    chatViewModel.buildEncryptionContext(
                        plaintext = "",
                        recipient = sendContext.recipient,
                        privateKey = sendContext.privateKey
                    ) ?: throw IllegalStateException("Cannot resolve chat encryption context")

                val privateKeyHash = CryptoManager.generatePrivateKeyHash(sendContext.privateKey)
                val isSavedMessages = (sendContext.sender == sendContext.recipient)
                RosettaDev1Log.d(
                    "voice-send upload begin msg=${messageId.take(8)} att=${attachmentId.take(12)} " +
                        "saved=$isSavedMessages"
                )
                val uploadResult =
                    chatViewModel.encryptAndUploadAttachment(
                        EncryptAndUploadAttachmentCommand(
                            payload = voicePayload.normalizedVoiceHex,
                            attachmentPassword = encryptionContext.attachmentPassword,
                            attachmentId = attachmentId,
                            isSavedMessages = isSavedMessages
                        )
                    )
                RosettaDev1Log.d(
                    "voice-send upload done msg=${messageId.take(8)} att=${attachmentId.take(12)} " +
                        "tag=${uploadResult.transportTag.take(16)} server=${uploadResult.transportServer}"
                )

                val voiceAttachment =
                    MessageAttachment(
                        id = attachmentId,
                        blob = "",
                        type = AttachmentType.VOICE,
                        preview = voicePayload.preview,
                        transportTag = uploadResult.transportTag,
                        transportServer = uploadResult.transportServer
                    )

                chatViewModel.sendMediaMessage(
                    SendMediaMessageCommand(
                        fromPublicKey = sendContext.sender,
                        toPublicKey = sendContext.recipient,
                        encryptedContent = encryptionContext.encryptedContent,
                        encryptedKey = encryptionContext.encryptedKey,
                        aesChachaKey = encryptionContext.aesChachaKey,
                        privateKeyHash = privateKeyHash,
                        messageId = messageId,
                        timestamp = timestamp,
                        mediaAttachments = listOf(voiceAttachment),
                        isSavedMessages = isSavedMessages
                    )
                )
                RosettaDev1Log.i(
                    "voice-send packet dispatched msg=${messageId.take(8)} " +
                        "saved=$isSavedMessages attachments=1"
                )

                runCatching {
                    AttachmentFileManager.saveAttachment(
                        context = chatViewModel.appContext(),
                        blob = voicePayload.normalizedVoiceHex,
                        attachmentId = attachmentId,
                        publicKey = sendContext.sender,
                        privateKey = sendContext.privateKey
                    )
                }

                val attachmentsJson =
                    JSONArray()
                        .apply {
                            put(
                                JSONObject().apply {
                                    put("id", attachmentId)
                                    put("type", AttachmentType.VOICE.value)
                                    put("preview", voicePayload.preview)
                                    put("blob", "")
                                    put("transportTag", uploadResult.transportTag)
                                    put("transportServer", uploadResult.transportServer)
                                }
                            )
                        }
                        .toString()

                val storedEncryptedKey =
                    if (encryptionContext.isGroup) {
                        chatViewModel.buildStoredGroupEncryptedKey(
                            encryptionContext.attachmentPassword,
                            sendContext.privateKey
                        )
                    } else {
                        encryptionContext.encryptedKey
                    }

                chatViewModel.saveOutgoingMessage(
                    messageId = messageId,
                    text = "",
                    encryptedContent = encryptionContext.encryptedContent,
                    encryptedKey = storedEncryptedKey,
                    timestamp = timestamp,
                    delivered = if (isSavedMessages) 1 else 0,
                    attachmentsJson = attachmentsJson,
                    accountPublicKey = sendContext.sender,
                    accountPrivateKey = sendContext.privateKey,
                    opponentPublicKey = sendContext.recipient
                )

                withContext(Dispatchers.Main) {
                    if (isSavedMessages) {
                        chatViewModel.updateMessageStatusUi(messageId, MessageStatus.SENT)
                    }
                }

                chatViewModel.saveOutgoingDialog(
                    lastMessage = "Voice message",
                    timestamp = timestamp,
                    accountPublicKey = sendContext.sender,
                    accountPrivateKey = sendContext.privateKey,
                    opponentPublicKey = sendContext.recipient
                )
                RosettaDev1Log.i("voice-send done msg=${messageId.take(8)} status=queued")
            } catch (e: Exception) {
                RosettaDev1Log.e(
                    "voice-send failed msg=${messageId.take(8)} " +
                        "from=${sendContext.sender.take(12)} to=${sendContext.recipient.take(12)}",
                    e
                )
                withContext(Dispatchers.Main) {
                    chatViewModel.updateMessageStatusUi(messageId, MessageStatus.ERROR)
                }
                chatViewModel.updateMessageStatusDb(messageId, DeliveryStatus.ERROR.value)
                chatViewModel.saveOutgoingDialog(
                    lastMessage = "Voice message",
                    timestamp = timestamp,
                    accountPublicKey = sendContext.sender,
                    accountPrivateKey = sendContext.privateKey,
                    opponentPublicKey = sendContext.recipient
                )
            } finally {
                RosettaDev1Log.d("voice-send slot released msg=${messageId.take(8)}")
                chatViewModel.releaseSendSlot()
            }
        }
    }
}

class AttachmentsViewModel internal constructor(
    private val chatViewModel: ChatViewModel
) {
    fun cancelOutgoingImageUpload(messageId: String, attachmentId: String) =
        chatViewModel.cancelOutgoingImageUpload(messageId, attachmentId)

    fun sendImageFromUri(imageUri: Uri, caption: String = "") {
        val sendContext = chatViewModel.resolveOutgoingSendContext()
        if (sendContext == null) {
            chatViewModel.addProtocolLog("❌ IMG send aborted: missing keys or dialog")
            return
        }

        val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
        val timestamp = System.currentTimeMillis()
        val text = caption.trim()
        val attachmentId = "img_$timestamp"
        val context = chatViewModel.appContext()

        chatViewModel.logPhotoEvent(
            messageId,
            "start: uri=${imageUri.lastPathSegment ?: "unknown"}, captionLen=${text.length}, attachment=${chatViewModel.shortPhotoLogId(attachmentId)}"
        )

        val (imageWidth, imageHeight) = MediaUtils.getImageDimensions(context, imageUri)
        chatViewModel.logPhotoEvent(messageId, "dimensions: ${imageWidth}x$imageHeight")

        chatViewModel.addOutgoingMessageOptimistic(
            ChatMessage(
                id = messageId,
                text = text,
                isOutgoing = true,
                timestamp = Date(timestamp),
                status = MessageStatus.SENDING,
                attachments =
                    listOf(
                        MessageAttachment(
                            id = attachmentId,
                            blob = "",
                            type = AttachmentType.IMAGE,
                            preview = "",
                            width = imageWidth,
                            height = imageHeight,
                            localUri = imageUri.toString()
                        )
                    )
            )
        )
        chatViewModel.clearInputText()
        chatViewModel.logPhotoEvent(messageId, "optimistic UI added")

        val uploadJob =
            chatViewModel.launchBackgroundUpload imageUpload@{
                try {
                    chatViewModel.logPhotoEvent(messageId, "persist optimistic message in DB")
                    val optimisticAttachmentsJson =
                        JSONArray()
                            .apply {
                                put(
                                    JSONObject().apply {
                                        put("id", attachmentId)
                                        put("type", AttachmentType.IMAGE.value)
                                        put("preview", "")
                                        put("blob", "")
                                        put("width", imageWidth)
                                        put("height", imageHeight)
                                        put("localUri", imageUri.toString())
                                    }
                                )
                            }
                            .toString()

                    chatViewModel.saveOutgoingMessage(
                        messageId = messageId,
                        text = text,
                        encryptedContent = "",
                        encryptedKey = "",
                        timestamp = timestamp,
                        delivered = 0,
                        attachmentsJson = optimisticAttachmentsJson,
                        accountPublicKey = sendContext.sender,
                        accountPrivateKey = sendContext.privateKey,
                        opponentPublicKey = sendContext.recipient
                    )

                    chatViewModel.saveOutgoingDialog(
                        lastMessage = if (text.isNotEmpty()) text else "photo",
                        timestamp = timestamp,
                        accountPublicKey = sendContext.sender,
                        accountPrivateKey = sendContext.privateKey,
                        opponentPublicKey = sendContext.recipient
                    )
                    chatViewModel.logPhotoEvent(messageId, "optimistic dialog updated")
                } catch (e: CancellationException) {
                    throw e
                } catch (_: Exception) {
                    chatViewModel.logPhotoEvent(messageId, "optimistic DB save skipped (non-fatal)")
                }

                try {
                    val convertStartedAt = System.currentTimeMillis()
                    val (width, height) = MediaUtils.getImageDimensions(context, imageUri)
                    val imageBase64 = MediaUtils.uriToBase64Image(context, imageUri)
                    if (imageBase64 == null) {
                        chatViewModel.logPhotoEvent(messageId, "base64 conversion returned null")
                        if (!chatViewModel.isViewModelCleared()) {
                            withContext(Dispatchers.Main) {
                                chatViewModel.updateMessageStatusUi(messageId, MessageStatus.ERROR)
                            }
                        }
                        return@imageUpload
                    }
                    chatViewModel.logPhotoEvent(
                        messageId,
                        "base64 ready: len=${imageBase64.length}, elapsed=${System.currentTimeMillis() - convertStartedAt}ms"
                    )

                    val blurhash = MediaUtils.generateBlurhash(context, imageUri)
                    chatViewModel.logPhotoEvent(messageId, "blurhash ready: len=${blurhash.length}")

                    if (!chatViewModel.isViewModelCleared()) {
                        withContext(Dispatchers.Main) {
                            chatViewModel.updateOptimisticImageMessage(
                                messageId = messageId,
                                base64 = imageBase64,
                                blurhash = blurhash,
                                width = width,
                                height = height
                            )
                        }
                        chatViewModel.logPhotoEvent(messageId, "optimistic payload updated in UI")
                    }

                    chatViewModel.sendImageMessageInternal(
                        messageId = messageId,
                        imageBase64 = imageBase64,
                        blurhash = blurhash,
                        caption = text,
                        width = width,
                        height = height,
                        timestamp = timestamp,
                        recipient = sendContext.recipient,
                        sender = sendContext.sender,
                        privateKey = sendContext.privateKey
                    )
                    chatViewModel.logPhotoEvent(messageId, "pipeline completed")
                } catch (e: CancellationException) {
                    chatViewModel.logPhotoEvent(messageId, "pipeline cancelled by user")
                    throw e
                } catch (e: Exception) {
                    chatViewModel.logPhotoErrorEvent(messageId, "prepare+convert", e)
                    if (!chatViewModel.isViewModelCleared()) {
                        withContext(Dispatchers.Main) {
                            chatViewModel.updateMessageStatusUi(messageId, MessageStatus.ERROR)
                        }
                    }
                }
            }

        chatViewModel.registerOutgoingImageUploadJob(messageId, uploadJob)
    }

    fun sendImageMessage(
        imageBase64: String,
        blurhash: String,
        caption: String = "",
        width: Int = 0,
        height: Int = 0
    ) {
        val sendContext = chatViewModel.resolveOutgoingSendContext() ?: return
        if (!chatViewModel.tryAcquireSendSlot()) {
            return
        }

        val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
        val timestamp = System.currentTimeMillis()
        val text = caption.trim()
        val attachmentId = "img_$timestamp"

        chatViewModel.addOutgoingMessageOptimistic(
            ChatMessage(
                id = messageId,
                text = text,
                isOutgoing = true,
                timestamp = Date(timestamp),
                status = MessageStatus.SENDING,
                attachments =
                    listOf(
                        MessageAttachment(
                            id = attachmentId,
                            type = AttachmentType.IMAGE,
                            preview = blurhash,
                            blob = imageBase64,
                            width = width,
                            height = height
                        )
                    )
            )
        )
        chatViewModel.clearInputText()

        chatViewModel.launchBackgroundUpload prepareGroup@{
            try {
                val encryptionContext =
                    chatViewModel.buildEncryptionContext(
                        plaintext = text,
                        recipient = sendContext.recipient,
                        privateKey = sendContext.privateKey
                    ) ?: throw IllegalStateException("Cannot resolve chat encryption context")

                val privateKeyHash = CryptoManager.generatePrivateKeyHash(sendContext.privateKey)
                val isSavedMessages = sendContext.sender == sendContext.recipient
                val uploadResult =
                    chatViewModel.encryptAndUploadAttachment(
                        EncryptAndUploadAttachmentCommand(
                            payload = imageBase64,
                            attachmentPassword = encryptionContext.attachmentPassword,
                            attachmentId = attachmentId,
                            isSavedMessages = isSavedMessages
                        )
                    )

                val imageAttachment =
                    MessageAttachment(
                        id = attachmentId,
                        blob = "",
                        type = AttachmentType.IMAGE,
                        preview = blurhash,
                        width = width,
                        height = height,
                        transportTag = uploadResult.transportTag,
                        transportServer = uploadResult.transportServer
                    )

                chatViewModel.sendMediaMessage(
                    SendMediaMessageCommand(
                        fromPublicKey = sendContext.sender,
                        toPublicKey = sendContext.recipient,
                        encryptedContent = encryptionContext.encryptedContent,
                        encryptedKey = encryptionContext.encryptedKey,
                        aesChachaKey = encryptionContext.aesChachaKey,
                        privateKeyHash = privateKeyHash,
                        messageId = messageId,
                        timestamp = timestamp,
                        mediaAttachments = listOf(imageAttachment),
                        isSavedMessages = isSavedMessages
                    )
                )

                AttachmentFileManager.saveAttachment(
                    context = chatViewModel.appContext(),
                    blob = imageBase64,
                    attachmentId = attachmentId,
                    publicKey = sendContext.sender,
                    privateKey = sendContext.privateKey
                )

                val attachmentsJson =
                    JSONArray()
                        .apply {
                            put(
                                JSONObject().apply {
                                    put("id", attachmentId)
                                    put("type", AttachmentType.IMAGE.value)
                                    put("preview", blurhash)
                                    put("blob", "")
                                    put("width", width)
                                    put("height", height)
                                    put("transportTag", uploadResult.transportTag)
                                    put("transportServer", uploadResult.transportServer)
                                }
                            )
                        }
                        .toString()

                val storedEncryptedKey =
                    if (encryptionContext.isGroup) {
                        chatViewModel.buildStoredGroupEncryptedKey(
                            encryptionContext.attachmentPassword,
                            sendContext.privateKey
                        )
                    } else {
                        encryptionContext.encryptedKey
                    }

                chatViewModel.saveOutgoingMessage(
                    messageId = messageId,
                    text = text,
                    encryptedContent = encryptionContext.encryptedContent,
                    encryptedKey = storedEncryptedKey,
                    timestamp = timestamp,
                    delivered = if (isSavedMessages) 1 else 0,
                    attachmentsJson = attachmentsJson,
                    accountPublicKey = sendContext.sender,
                    accountPrivateKey = sendContext.privateKey,
                    opponentPublicKey = sendContext.recipient
                )

                withContext(Dispatchers.Main) {
                    chatViewModel.updateMessageStatusUi(messageId, MessageStatus.SENT)
                }

                chatViewModel.saveOutgoingDialog(
                    lastMessage = if (text.isNotEmpty()) text else "photo",
                    timestamp = timestamp,
                    accountPublicKey = sendContext.sender,
                    accountPrivateKey = sendContext.privateKey,
                    opponentPublicKey = sendContext.recipient
                )
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    chatViewModel.updateMessageStatusUi(messageId, MessageStatus.ERROR)
                }
            } finally {
                chatViewModel.releaseSendSlot()
            }
        }
    }

    fun sendImageGroupFromUris(imageUris: List<Uri>, caption: String = "") {
        chatViewModel.attachmentsFeatureCoordinator.sendImageGroupFromUris(
            imageUris = imageUris,
            caption = caption
        )
    }

    fun sendImageGroup(images: List<ChatViewModel.ImageData>, caption: String = "") {
        chatViewModel.attachmentsFeatureCoordinator.sendImageGroup(
            images = images,
            caption = caption
        )
    }

    fun sendFileMessage(
        fileBase64: String,
        fileName: String,
        fileSize: Long,
        caption: String = ""
    ) {
        chatViewModel.attachmentsFeatureCoordinator.sendFileMessage(
            fileBase64 = fileBase64,
            fileName = fileName,
            fileSize = fileSize,
            caption = caption
        )
    }

    fun sendVideoCircleFromUri(videoUri: Uri) {
        chatViewModel.attachmentsFeatureCoordinator.sendVideoCircleFromUri(videoUri)
    }

    fun sendAvatarMessage() {
        chatViewModel.attachmentsFeatureCoordinator.sendAvatarMessage()
    }
}

class TypingViewModel internal constructor(
    private val chatViewModel: ChatViewModel
) {
    val opponentTyping: StateFlow<Boolean> = chatViewModel.opponentTyping
    val typingDisplayName: StateFlow<String> = chatViewModel.typingDisplayName
    val typingDisplayPublicKey: StateFlow<String> = chatViewModel.typingDisplayPublicKey
    val opponentOnline: StateFlow<Boolean> = chatViewModel.opponentOnline

    fun sendTypingIndicator() {
        val now = System.currentTimeMillis()
        val context = chatViewModel.resolveTypingSendContext() ?: return
        val decision =
            chatViewModel.decideTypingSend(
                SendTypingIndicatorCommand(
                    nowMs = now,
                    lastSentMs = chatViewModel.lastTypingSentTimeMs(),
                    throttleMs = chatViewModel.typingThrottleMs(),
                    opponentPublicKey = context.opponent,
                    senderPublicKey = context.sender,
                    isGroupDialog = context.isGroupDialog,
                    isOpponentOnline = context.isOpponentOnline
                )
            )
        if (!decision.shouldSend) return
        chatViewModel.setLastTypingSentTimeMs(decision.nextLastSentMs)
        chatViewModel.sendTypingPacket(
            privateKey = context.privateKey,
            sender = context.sender,
            opponent = context.opponent
        )
    }
}
