package com.rosetta.messenger.push

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Base64
import android.util.Log
import com.rosetta.messenger.BuildConfig
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.rosetta.messenger.MainActivity
import com.rosetta.messenger.R
import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.di.ProtocolGateway
import com.rosetta.messenger.network.CallForegroundService
import com.rosetta.messenger.network.CallManager
import com.rosetta.messenger.network.CallPhase
import com.rosetta.messenger.network.CallUiState
import com.rosetta.messenger.utils.AvatarFileManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.util.Locale

/**
 * Firebase Cloud Messaging Service для обработки push-уведомлений
 *
 * Обрабатывает:
 * - Получение нового FCM токена
 * - Получение push-уведомлений о новых сообщениях
 * - Отображение уведомлений
 */
@AndroidEntryPoint
class RosettaFirebaseMessagingService : FirebaseMessagingService() {

    @Inject lateinit var accountManager: AccountManager
    @Inject lateinit var preferencesManager: PreferencesManager
    @Inject lateinit var protocolGateway: ProtocolGateway

    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    companion object {
        private const val TAG = "RosettaFCM"
        private const val CHANNEL_ID = "rosetta_messages"
        private const val CHANNEL_NAME = "Messages"
        private const val PUSH_TYPE_PERSONAL_MESSAGE = "personal_message"
        private const val PUSH_TYPE_GROUP_MESSAGE = "group_message"
        private const val PUSH_TYPE_CALL = "call"
        private const val PUSH_TYPE_READ = "read"

        // 🔥 Флаг - приложение в foreground (видимо пользователю)
        @Volatile var isAppInForeground = false

        // Dedup: suppress duplicate pushes from the server within a short window.
        // Key = senderPublicKey (or "__simple__"), Value = timestamp of last shown notification.
        private const val DEDUP_WINDOW_MS = 10_000L
        private val lastNotifTimestamps = java.util.concurrent.ConcurrentHashMap<String, Long>()

        /** Уникальный notification ID для каждого чата (по publicKey) */
        fun getNotificationIdForChat(senderPublicKey: String): Int {
            return senderPublicKey.hashCode() and 0x7FFFFFFF // positive int
        }

        /** Убрать уведомление конкретного чата из шторки */
        fun cancelNotificationForChat(context: Context, senderPublicKey: String) {
            val notificationManager =
                    context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val variants = buildDialogKeyVariants(senderPublicKey)
            for (key in variants) {
                notificationManager.cancel(getNotificationIdForChat(key))
                lastNotifTimestamps.remove(key)
            }
            // Fallback: некоторые серверные payload могут прийти без sender key.
            // Для них используется ID от пустой строки — тоже очищаем при входе в диалог.
            notificationManager.cancel(getNotificationIdForChat(""))
            lastNotifTimestamps.remove("__no_sender__")
            lastNotifTimestamps.remove("__simple__")
        }

        private fun buildDialogKeyVariants(rawKey: String): Set<String> {
            val trimmed = rawKey.trim()
            if (trimmed.isBlank()) return emptySet()

            val variants = linkedSetOf(trimmed)
            val lower = trimmed.lowercase(Locale.ROOT)
            when {
                lower.startsWith("#group:") -> {
                    val groupId = trimmed.substringAfter(':').trim()
                    if (groupId.isNotBlank()) {
                        variants.add("group:$groupId")
                        variants.add(groupId)
                    }
                }
                lower.startsWith("group:") -> {
                    val groupId = trimmed.substringAfter(':').trim()
                    if (groupId.isNotBlank()) {
                        variants.add("#group:$groupId")
                        variants.add(groupId)
                    }
                }
                else -> {
                    variants.add("#group:$trimmed")
                    variants.add("group:$trimmed")
                }
            }
            return variants
        }
    }

    /** Вызывается когда получен новый FCM токен Отправляем его на сервер через протокол */
    override fun onNewToken(token: String) {
        super.onNewToken(token)

        // Сохраняем токен локально
        saveFcmToken(token)

        // Best-effort: если соединение уже авторизовано — сразу обновляем подписку на push.
        // Используем единую runtime-точку отправки (с дедупликацией).
        if (protocolGateway.isAuthenticated()) {
            runCatching { protocolGateway.subscribePushTokenIfAvailable(forceToken = token) }
        }
    }

    /** Вызывается когда получено push-уведомление */
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        if (BuildConfig.DEBUG) Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")

        val data = remoteMessage.data
        val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
        val notificationBody = remoteMessage.notification?.body?.trim().orEmpty()

        // Filter out empty/silent pushes (iOS wake-up pushes with mutable-content, empty alerts, etc.)
        val hasDataContent = data.isNotEmpty() && data.any { (key, value) ->
            key !in setOf("google.delivered_priority", "google.sent_time", "google.ttl",
                "google.original_priority", "gcm.notification.e", "gcm.notification.tag",
                "google.c.a.e", "google.c.sender.id", "google.c.fid",
                "mutable-content", "mutable_content", "content-available", "content_available") &&
            value.isNotBlank()
        }
        val hasNotificationContent = notificationTitle.isNotBlank() || notificationBody.isNotBlank()

        if (!hasDataContent && !hasNotificationContent) {
            if (BuildConfig.DEBUG) Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
            // Still trigger reconnect if WebSocket is disconnected
            protocolGateway.reconnectNowIfNeeded("silent_push")
            return
        }

        var handledByData = false

        // Обрабатываем data payload (новый server формат + legacy fallback)
        if (data.isNotEmpty()) {
            val type =
                    firstNonBlank(data, "type", "event", "action")
                            ?.lowercase(Locale.ROOT)
                            .orEmpty()
            val dialogKey =
                    firstNonBlank(
                            data,
                            "dialog",
                            "sender_public_key",
                            "from_public_key",
                            "fromPublicKey",
                            "from",
                            "public_key",
                            "publicKey"
                    )
            val senderPublicKey =
                    firstNonBlank(
                            data,
                            "sender_public_key",
                            "from_public_key",
                            "fromPublicKey",
                            "from",
                            "public_key",
                            "publicKey"
                    )
            val callId = firstNonBlank(data, "callId", "call_id")
            val joinToken = firstNonBlank(data, "joinToken", "join_token")
            val senderName =
                    firstNonBlank(
                                    data,
                                    "sender_name",
                                    "sender_title",
                                    "from_title",
                                    "sender",
                                    "title",
                                    "name"
                            )
                            ?: notificationTitle.takeIf { it.isNotBlank() }
                            ?: dialogKey?.take(10)
                            ?: senderPublicKey?.take(10)
                            ?: "Rosetta"
            val messagePreview =
                    firstNonBlank(data, "message_preview", "message", "text", "body")
                            ?: notificationBody.takeIf { it.isNotBlank() }
                            ?: when (type) {
                                PUSH_TYPE_GROUP_MESSAGE -> "New group message"
                                PUSH_TYPE_CALL -> "Incoming call"
                                else -> "New message"
                            }

            val isReadEvent = type == "message_read" || type == PUSH_TYPE_READ
            val isMessageEvent =
                    type == "new_message" ||
                            type == "message" ||
                            type == "newmessage" ||
                            type == "msg_new" ||
                            type == PUSH_TYPE_PERSONAL_MESSAGE ||
                            type == PUSH_TYPE_GROUP_MESSAGE

            when {
                isReadEvent -> {
                    val keysToClear = collectReadDialogKeys(data, dialogKey, senderPublicKey)
                    if (keysToClear.isEmpty()) {
                        if (BuildConfig.DEBUG) Log.d(TAG, "READ push received but no dialog key in payload: $data")
                    } else {
                        keysToClear.forEach { key ->
                            cancelNotificationForChat(applicationContext, key)
                        }
                        val titleHints = collectReadTitleHints(data, keysToClear)
                        cancelMatchingActiveNotifications(keysToClear, titleHints)
                        if (BuildConfig.DEBUG) Log.d(
                            TAG,
                            "READ push cleared notifications for keys=$keysToClear titles=$titleHints"
                        )
                    }
                    handledByData = true
                }
                type == PUSH_TYPE_CALL -> {
                    handleIncomingCallPush(
                            dialogKey = dialogKey ?: senderPublicKey.orEmpty(),
                            title = senderName,
                            callId = callId.orEmpty(),
                            joinToken = joinToken.orEmpty()
                    )
                    handledByData = true
                }
                isMessageEvent -> {
                    showMessageNotification(dialogKey ?: senderPublicKey, senderName, messagePreview)
                    handledByData = true
                }
                // Fallback for servers sending data-only payload without explicit "type".
                dialogKey != null || senderPublicKey != null ||
                        data.containsKey("message_preview") ||
                        data.containsKey("message") ||
                        data.containsKey("text") -> {
                    showMessageNotification(dialogKey ?: senderPublicKey, senderName, messagePreview)
                    handledByData = true
                }
            }

            if (!handledByData) {
                val looksLikeMessagePayload =
                        type.contains("message") ||
                                data.keys.any { key ->
                                    val lower = key.lowercase(Locale.ROOT)
                                    lower.contains("message") ||
                                            lower.contains("text") ||
                                            lower.contains("body")
                                }
                if (looksLikeMessagePayload) {
                    showSimpleNotification(senderName, messagePreview, dialogKey ?: senderPublicKey)
                    handledByData = true
                }
            }
        }

        // Обрабатываем notification payload (если data-ветка не сработала).
        remoteMessage.notification?.let {
            if (!handledByData) {
                val senderPublicKey =
                        firstNonBlank(
                                data,
                                "dialog",
                                "sender_public_key",
                                "from_public_key",
                                "fromPublicKey",
                                "from",
                                "public_key",
                                "publicKey"
                        )
                showSimpleNotification(
                        it.title ?: "Rosetta",
                        it.body ?: "New message",
                        senderPublicKey
                )
            }
        }
    }

    /** Показать уведомление о новом сообщении */
    private fun showMessageNotification(
            senderPublicKey: String?,
            senderName: String,
            messagePreview: String
    ) {
        // 🔥 Не показываем уведомление если приложение открыто
        if (isAppInForeground || !areNotificationsEnabled()) {
            return
        }
        // Dedup: suppress duplicate pushes from the same sender within DEDUP_WINDOW_MS
        val dedupKey = senderPublicKey?.trim().orEmpty().ifEmpty { "__no_sender__" }
        val now = System.currentTimeMillis()
        val lastTs = lastNotifTimestamps[dedupKey]
        if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
            if (BuildConfig.DEBUG) Log.d(TAG, "\ud83d\udeab Dedup BLOCKED notification for key=$dedupKey, delta=${now - lastTs}ms")
            return  // duplicate push — skip
        }
        lastNotifTimestamps[dedupKey] = now
        if (BuildConfig.DEBUG) Log.d(TAG, "\u2705 Showing notification for key=$dedupKey")
        val senderKey = senderPublicKey?.trim().orEmpty()
        if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) {
            return
        }

        createNotificationChannel()

        // Резолвим имя и аватарку по publicKey
        val resolvedName = resolveNameForKey(senderPublicKey) ?: senderName
        val avatarBitmap = loadAvatarBitmap(senderPublicKey)

        val notifId = getNotificationIdForChat(senderPublicKey ?: "")

        // Intent для открытия чата
        val intent =
                Intent(this, MainActivity::class.java).apply {
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                    putExtra("open_chat", senderPublicKey)
                }

        val pendingIntent =
                PendingIntent.getActivity(
                        this,
                        notifId,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                )

        val notification =
                NotificationCompat.Builder(this, CHANNEL_ID)
                        .setSmallIcon(R.drawable.ic_notification)
                        .setContentTitle(resolvedName)
                        .setContentText(messagePreview)
                        .setPriority(NotificationCompat.PRIORITY_HIGH)
                        .setCategory(NotificationCompat.CATEGORY_MESSAGE)
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .apply {
                            if (avatarBitmap != null) {
                                setLargeIcon(avatarBitmap)
                            }
                        }
                        .build()

        val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(notifId, notification)
    }

    /** Показать простое уведомление */
    private fun showSimpleNotification(title: String, body: String, senderPublicKey: String? = null) {
        // 🔥 Не показываем уведомление если приложение открыто
        if (isAppInForeground || !areNotificationsEnabled()) {
            return
        }
        val senderKey = senderPublicKey?.trim().orEmpty()
        if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) {
            return
        }
        // Dedup: suppress duplicate pushes within DEDUP_WINDOW_MS
        val dedupKey = senderKey.ifEmpty { "__simple__" }
        val now = System.currentTimeMillis()
        val lastTs = lastNotifTimestamps[dedupKey]
        if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
            return  // duplicate push — skip
        }
        lastNotifTimestamps[dedupKey] = now

        createNotificationChannel()

        // Резолвим имя и аватарку по publicKey
        val resolvedTitle = if (senderKey.isNotEmpty()) resolveNameForKey(senderKey) ?: title else title
        val avatarBitmap = if (senderKey.isNotEmpty()) loadAvatarBitmap(senderKey) else null

        // Используем sender-based ID если известен ключ — чтобы cancelNotificationForChat мог убрать уведомление
        val notifId = if (senderKey.isNotEmpty()) {
            getNotificationIdForChat(senderKey)
        } else {
            (title + body).hashCode() and 0x7FFFFFFF
        }

        val intent =
                Intent(this, MainActivity::class.java).apply {
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                }

        val pendingIntent =
                PendingIntent.getActivity(
                        this,
                        notifId,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                )

        val notification =
                NotificationCompat.Builder(this, CHANNEL_ID)
                        .setSmallIcon(R.drawable.ic_notification)
                        .setContentTitle(resolvedTitle)
                        .setContentText(body)
                        .setPriority(NotificationCompat.PRIORITY_HIGH)
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .apply {
                            if (avatarBitmap != null) {
                                setLargeIcon(avatarBitmap)
                            }
                        }
                        .build()

        val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(notifId, notification)
    }

    /** Супер push входящего звонка: пробуждаем протокол и запускаем ForegroundService с incoming call UI */
    private fun handleIncomingCallPush(
        dialogKey: String,
        title: String,
        callId: String,
        joinToken: String
    ) {
        pushCallLog("handleIncomingCallPush dialog=$dialogKey title=$title")
        wakeProtocolFromPush("call")

        if (!areNotificationsEnabled()) {
            pushCallLog("SKIP: notifications disabled")
            return
        }

        val normalizedDialog = dialogKey.trim()
        if (normalizedDialog.isNotEmpty() && isDialogMuted(normalizedDialog)) {
            pushCallLog("SKIP: dialog muted")
            return
        }
        val currentPhase = CallManager.state.value.phase
        if (currentPhase != CallPhase.IDLE) {
            pushCallLog("SKIP: phase=$currentPhase (not IDLE)")
            return
        }

        val normalizedCallId = callId.trim()
        val normalizedJoinToken = joinToken.trim()
        val dedupKey =
            if (normalizedCallId.isNotBlank()) {
                "call:$normalizedCallId"
            } else {
                "call:${normalizedDialog.ifEmpty { "__no_dialog__" }}"
            }
        val now = System.currentTimeMillis()
        val lastTs = lastNotifTimestamps[dedupKey]
        if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
            pushCallLog("SKIP: dedup blocked (delta=${now - lastTs}ms)")
            return
        }
        lastNotifTimestamps[dedupKey] = now

        val resolvedName = resolveNameForKey(normalizedDialog) ?: title
        pushCallLog("resolvedName=$resolvedName, calling setIncomingFromPush")

        // Сразу ставим CallManager в INCOMING — не ждём WebSocket
        CallManager.setIncomingFromPush(
            peerPublicKey = normalizedDialog,
            peerTitle = resolvedName,
            callId = normalizedCallId,
            joinToken = normalizedJoinToken
        )
        pushCallLog("setIncomingFromPush done, phase=${CallManager.state.value.phase}")

        // Пробуем запустить IncomingCallActivity напрямую из FCM
        // На Android 10+ может быть заблокировано — тогда fullScreenIntent на нотификации сработает
        try {
            val activityIntent = android.content.Intent(
                applicationContext,
                com.rosetta.messenger.IncomingCallActivity::class.java
            ).apply {
                flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
                    android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
            }
            applicationContext.startActivity(activityIntent)
            pushCallLog("IncomingCallActivity started from FCM OK")
        } catch (e: Throwable) {
            pushCallLog("IncomingCallActivity start from FCM FAILED: ${e.message} — relying on fullScreenIntent")
        }
    }

    private fun pushCallLog(msg: String) {
        if (BuildConfig.DEBUG) Log.d(TAG, msg)
        try {
            val dir = java.io.File(applicationContext.filesDir, "crash_reports")
            if (!dir.exists()) dir.mkdirs()
            val f = java.io.File(dir, "call_notification_log.txt")
            val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
            f.appendText("$ts [FCM] $msg\n")
        } catch (_: Throwable) {}
    }

    /** Пробуждаем сетевой слой по high-priority push, чтобы быстрее получить сигнальные пакеты */
    private fun wakeProtocolFromPush(reason: String) {
        runCatching {
            val account = accountManager.getLastLoggedPublicKey().orEmpty()
            protocolGateway.initialize(applicationContext)
            CallManager.initialize(applicationContext)
            if (account.isNotBlank()) {
                CallManager.bindAccount(account)
            }
            val restored = protocolGateway.restoreAuthFromStoredCredentials(
                preferredPublicKey = account,
                reason = "push_$reason"
            )
            pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}…")
            protocolGateway.reconnectNowIfNeeded("push_$reason")
        }.onFailure { error ->
            if (BuildConfig.DEBUG) Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
        }
    }

    /** Создать notification channel для Android 8+ */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                    NotificationChannel(
                                    CHANNEL_ID,
                                    CHANNEL_NAME,
                                    NotificationManager.IMPORTANCE_HIGH
                            )
                            .apply {
                                description = "Notifications for new messages"
                                enableVibration(true)
                            }

            val notificationManager =
                    getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    /** Сохранить FCM токен в SharedPreferences */
    private fun saveFcmToken(token: String) {
        val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
        prefs.edit().putString("fcm_token", token).apply()
    }

    private fun areNotificationsEnabled(): Boolean {
        return runCatching {
            runBlocking(Dispatchers.IO) {
                preferencesManager.notificationsEnabled.first()
            }
        }.getOrDefault(true)
    }

    private fun firstNonBlank(data: Map<String, String>, vararg keys: String): String? {
        for (key in keys) {
            val value = data[key]?.trim()
            if (!value.isNullOrEmpty()) return value
        }
        return null
    }

    /**
     * Builds a robust list of dialog keys for silent READ pushes.
     * Server payloads may evolve (dialog/from/to/peer/group_* aliases), so we parse
     * all known aliases and exclude current account public key.
     */
    private fun collectReadDialogKeys(
        data: Map<String, String>,
        parsedDialogKey: String?,
        parsedSenderKey: String?
    ): Set<String> {
        val currentAccount = accountManager.getLastLoggedPublicKey().orEmpty().trim()
        val candidates = linkedSetOf<String>()

        fun addCandidate(raw: String?) {
            val value = raw?.trim().orEmpty()
            if (value.isBlank()) return
            if (currentAccount.isNotBlank() && value == currentAccount) return
            candidates.add(value)
        }

        addCandidate(parsedDialogKey)
        addCandidate(parsedSenderKey)
        addCandidate(firstNonBlank(data, "dialog", "dialog_key", "dialogPublicKey", "dialog_public_key"))
        addCandidate(firstNonBlank(data, "peer", "peer_key", "peerPublicKey", "peer_public_key", "chat", "chat_key"))
        addCandidate(firstNonBlank(data, "to", "toPublicKey", "to_public_key", "dst", "dst_public_key"))
        addCandidate(firstNonBlank(data, "from", "fromPublicKey", "from_public_key", "src", "src_public_key"))

        // Group aliases from some server payloads
        val groupId = firstNonBlank(data, "group", "group_id", "groupId")
        if (!groupId.isNullOrBlank()) {
            addCandidate(groupId)
            addCandidate("group:$groupId")
            addCandidate("#group:$groupId")
        }

        return candidates
    }

    private fun collectReadTitleHints(
        data: Map<String, String>,
        dialogKeys: Set<String>
    ): Set<String> {
        val hints = linkedSetOf<String>()

        fun add(raw: String?) {
            val value = raw?.trim().orEmpty()
            if (value.isNotBlank()) hints.add(value)
        }

        add(firstNonBlank(data, "title", "sender_name", "sender_title", "from_title", "name"))
        dialogKeys.forEach { key ->
            add(resolveNameForKey(key))
        }

        return hints
    }

    /**
     * Fallback for system-shown notifications with unknown IDs (FCM notification payload path).
     * Matches active notifications by:
     * 1) deterministic dialog hash id
     * 2) dialog key/group key in notification extras/text
     * 3) known dialog title hints
     */
    private fun cancelMatchingActiveNotifications(
        dialogKeys: Set<String>,
        titleHints: Set<String>
    ) {
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager ?: return
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return

        val normalizedDialogKeys =
            dialogKeys
                .flatMap { key ->
                    buildDialogKeyVariants(key).toList() + key.trim()
                }
                .map { it.trim() }
                .filter { it.isNotEmpty() }
                .toSet()
        val normalizedHints = titleHints.map { it.trim() }.filter { it.isNotEmpty() }.toSet()
        if (normalizedDialogKeys.isEmpty() && normalizedHints.isEmpty()) return

        runCatching {
            manager.activeNotifications.forEach { sbn ->
                val notification = sbn.notification ?: return@forEach
                if (notification.channelId == "rosetta_calls") return@forEach
                val title =
                    notification.extras
                        ?.getCharSequence(android.app.Notification.EXTRA_TITLE)
                        ?.toString()
                        ?.trim()
                        .orEmpty()
                val text =
                    notification.extras
                        ?.getCharSequence(android.app.Notification.EXTRA_TEXT)
                        ?.toString()
                        ?.trim()
                        .orEmpty()
                val bigText =
                    notification.extras
                        ?.getCharSequence(android.app.Notification.EXTRA_BIG_TEXT)
                        ?.toString()
                        ?.trim()
                        .orEmpty()

                val bag = mutableSetOf<String>()
                if (title.isNotEmpty()) bag.add(title)
                if (text.isNotEmpty()) bag.add(text)
                if (bigText.isNotEmpty()) bag.add(bigText)
                notification.extras?.keySet()?.forEach { extraKey ->
                    bag.add(extraKey)
                    val value = notification.extras?.get(extraKey)
                    when (value) {
                        is CharSequence -> bag.add(value.toString())
                        is String -> bag.add(value)
                        is Array<*> -> value.filterIsInstance<CharSequence>().forEach { bag.add(it.toString()) }
                    }
                }
                val bagLower = bag.map { it.lowercase(Locale.ROOT) }
                val matchesDialogKey =
                    normalizedDialogKeys.any { key ->
                        val lowerKey = key.lowercase(Locale.ROOT)
                        bagLower.any { it.contains(lowerKey) }
                    }
                val matchesHint =
                    normalizedHints.any { hint ->
                        title.equals(hint, ignoreCase = true) ||
                            text.contains(hint, ignoreCase = true) ||
                            bigText.contains(hint, ignoreCase = true)
                    }
                val matchesDeterministicId =
                    normalizedDialogKeys.any { key ->
                        getNotificationIdForChat(key) == sbn.id
                    }

                if (matchesDeterministicId || matchesDialogKey || matchesHint) {
                    manager.cancel(sbn.tag, sbn.id)
                    if (BuildConfig.DEBUG) Log.d(
                        TAG,
                        "READ push fallback cancel id=${sbn.id} tag=${sbn.tag} " +
                            "channel=${notification.channelId} title='$title' " +
                            "matchId=$matchesDeterministicId matchKey=$matchesDialogKey matchHint=$matchesHint"
                    )
                }
            }
        }.onFailure { error ->
            if (BuildConfig.DEBUG) Log.w(TAG, "cancelMatchingActiveNotifications failed: ${error.message}")
        }
    }

    private fun isAvatarInNotificationsEnabled(): Boolean {
        return runCatching {
            runBlocking(Dispatchers.IO) {
                preferencesManager.notificationAvatarEnabled.first()
            }
        }.getOrDefault(true)
    }

    /** Проверка: замьючен ли диалог для текущего аккаунта */
    private fun isDialogMuted(senderPublicKey: String): Boolean {
        if (senderPublicKey.isBlank()) return false
        return runCatching {
            val currentAccount = accountManager.getLastLoggedPublicKey().orEmpty()
            runBlocking(Dispatchers.IO) {
                buildDialogKeyVariants(senderPublicKey).any { key ->
                    preferencesManager.isChatMuted(currentAccount, key)
                }
            }
        }.getOrDefault(false)
    }

    /** Получить имя пользователя по publicKey (runtime-кэш → БД dialogs) */
    private fun resolveNameForKey(publicKey: String?): String? {
        if (publicKey.isNullOrBlank()) return null
        // 1. In-memory cache
        protocolGateway.getCachedUserName(publicKey)?.let { return it }
        // 2. DB dialogs table
        return runCatching {
            val account = accountManager.getLastLoggedPublicKey().orEmpty()
            if (account.isBlank()) return null
            val db = RosettaDatabase.getDatabase(applicationContext)
            val dialog = runBlocking(Dispatchers.IO) {
                db.dialogDao().getDialog(account, publicKey)
            }
            dialog?.opponentTitle?.takeIf { it.isNotBlank() }
                ?: dialog?.opponentUsername?.takeIf { it.isNotBlank() }
        }.getOrNull()
    }

    /** Получить аватарку как круглый Bitmap для notification по publicKey */
    private fun loadAvatarBitmap(publicKey: String?): Bitmap? {
        if (publicKey.isNullOrBlank()) return null
        // Проверяем настройку
        if (!isAvatarInNotificationsEnabled()) return null
        return runCatching {
            val db = RosettaDatabase.getDatabase(applicationContext)
            val entity = runBlocking(Dispatchers.IO) {
                db.avatarDao().getLatestAvatarByKeys(listOf(publicKey))
            } ?: return null
            val rawBase64 = AvatarFileManager.readAvatar(applicationContext, entity.avatar)
                ?: return null
            val base64 = if (rawBase64.contains(",")) rawBase64.substringAfter(",") else rawBase64
            val bytes = Base64.decode(base64, Base64.DEFAULT)
            val original = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return null
            // Делаем круглый bitmap для notification
            toCircleBitmap(original)
        }.getOrNull()
    }

    private fun toCircleBitmap(source: Bitmap): Bitmap {
        val size = minOf(source.width, source.height)
        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        val canvas = android.graphics.Canvas(output)
        val paint = android.graphics.Paint().apply {
            isAntiAlias = true
        }
        val rect = android.graphics.Rect(0, 0, size, size)
        canvas.drawARGB(0, 0, 0, 0)
        canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
        paint.xfermode = android.graphics.PorterDuffXfermode(android.graphics.PorterDuff.Mode.SRC_IN)
        canvas.drawBitmap(source, rect, rect, paint)
        return output
    }
}
