package com.rosetta.messenger.network.connection

import com.rosetta.messenger.network.PacketTyping
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class PresenceTypingService(
    private val scope: CoroutineScope,
    private val typingIndicatorTimeoutMs: Long
) {
    private val _typingUsers = MutableStateFlow<Set<String>>(emptySet())
    val typingUsers: StateFlow<Set<String>> = _typingUsers.asStateFlow()

    private val _typingUsersByDialogSnapshot =
        MutableStateFlow<Map<String, Set<String>>>(emptyMap())
    val typingUsersByDialogSnapshot: StateFlow<Map<String, Set<String>>> =
        _typingUsersByDialogSnapshot.asStateFlow()

    private val typingStateLock = Any()
    private val typingUsersByDialog = mutableMapOf<String, MutableSet<String>>()
    private val typingTimeoutJobs = ConcurrentHashMap<String, Job>()

    fun getTypingUsersForDialog(dialogKey: String): Set<String> {
        val normalizedDialogKey =
            if (isGroupDialogKey(dialogKey)) {
                normalizeGroupDialogKey(dialogKey)
            } else {
                dialogKey.trim()
            }
        if (normalizedDialogKey.isBlank()) return emptySet()

        synchronized(typingStateLock) {
            return typingUsersByDialog[normalizedDialogKey]?.toSet() ?: emptySet()
        }
    }

    fun handleTypingPacket(
        packet: PacketTyping,
        ownPublicKeyProvider: () -> String
    ) {
        val fromPublicKey = packet.fromPublicKey.trim()
        val toPublicKey = packet.toPublicKey.trim()
        if (fromPublicKey.isBlank() || toPublicKey.isBlank()) return

        val ownPublicKey = ownPublicKeyProvider().trim()
        if (ownPublicKey.isNotBlank() && fromPublicKey.equals(ownPublicKey, ignoreCase = true)) {
            return
        }

        val dialogKey =
            resolveTypingDialogKey(
                fromPublicKey = fromPublicKey,
                toPublicKey = toPublicKey,
                ownPublicKey = ownPublicKey
            ) ?: return

        rememberTypingEvent(dialogKey, fromPublicKey)
    }

    fun clear() {
        typingTimeoutJobs.values.forEach { it.cancel() }
        typingTimeoutJobs.clear()
        synchronized(typingStateLock) {
            typingUsersByDialog.clear()
            _typingUsers.value = emptySet()
            _typingUsersByDialogSnapshot.value = emptyMap()
        }
    }

    private fun isGroupDialogKey(value: String): Boolean {
        val normalized = value.trim().lowercase(Locale.ROOT)
        return normalized.startsWith("#group:") || normalized.startsWith("group:")
    }

    private fun normalizeGroupDialogKey(value: String): String {
        val trimmed = value.trim()
        val normalized = trimmed.lowercase(Locale.ROOT)
        return when {
            normalized.startsWith("#group:") -> "#group:${trimmed.substringAfter(':').trim()}"
            normalized.startsWith("group:") -> "#group:${trimmed.substringAfter(':').trim()}"
            else -> trimmed
        }
    }

    private fun resolveTypingDialogKey(
        fromPublicKey: String,
        toPublicKey: String,
        ownPublicKey: String
    ): String? {
        return when {
            isGroupDialogKey(toPublicKey) -> normalizeGroupDialogKey(toPublicKey)
            ownPublicKey.isNotBlank() && toPublicKey.equals(ownPublicKey, ignoreCase = true) ->
                fromPublicKey.trim()
            else -> null
        }
    }

    private fun makeTypingTimeoutKey(dialogKey: String, fromPublicKey: String): String {
        return "${dialogKey.lowercase(Locale.ROOT)}|${fromPublicKey.lowercase(Locale.ROOT)}"
    }

    private fun rememberTypingEvent(dialogKey: String, fromPublicKey: String) {
        val normalizedDialogKey =
            if (isGroupDialogKey(dialogKey)) normalizeGroupDialogKey(dialogKey) else dialogKey.trim()
        val normalizedFrom = fromPublicKey.trim()
        if (normalizedDialogKey.isBlank() || normalizedFrom.isBlank()) return

        synchronized(typingStateLock) {
            val users = typingUsersByDialog.getOrPut(normalizedDialogKey) { mutableSetOf() }
            users.add(normalizedFrom)
            _typingUsers.value = typingUsersByDialog.keys.toSet()
            _typingUsersByDialogSnapshot.value =
                typingUsersByDialog.mapValues { entry -> entry.value.toSet() }
        }

        val timeoutKey = makeTypingTimeoutKey(normalizedDialogKey, normalizedFrom)
        typingTimeoutJobs.remove(timeoutKey)?.cancel()
        typingTimeoutJobs[timeoutKey] =
            scope.launch {
                delay(typingIndicatorTimeoutMs)
                synchronized(typingStateLock) {
                    val users = typingUsersByDialog[normalizedDialogKey]
                    users?.remove(normalizedFrom)
                    if (users.isNullOrEmpty()) {
                        typingUsersByDialog.remove(normalizedDialogKey)
                    }
                    _typingUsers.value = typingUsersByDialog.keys.toSet()
                    _typingUsersByDialogSnapshot.value =
                        typingUsersByDialog.mapValues { entry -> entry.value.toSet() }
                }
                typingTimeoutJobs.remove(timeoutKey)
            }
    }
}

