package com.seven.ikar.kotlin.core.push

import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.seven.ikar.kotlin.MainActivity
import com.seven.ikar.kotlin.app.IkarNativeApp
import com.seven.ikar.kotlin.core.contacts.PhoneNumberNormalizer

object PushNotificationDisplayService {
    fun show(context: Context, data: Map<String, String>) {
        val settings = AndroidPushSettings(context)
        if (!settings.areNotificationsEnabled()) {
            return
        }
        if (!AndroidNotificationPermissionHelper.areNotificationsEnabled(context)) {
            return
        }

        AndroidPushBootstrapper.tryInitialize(context)

        val kind = data[PushDataKeys.Kind].orEmpty()
        val app = context.applicationContext as? IkarNativeApp
        when (kind) {
            PushDataKinds.AudioCallInvite,
            PushDataKinds.VideoCallInvite -> {
                val invite = pendingIncomingCallInviteFromMap(data)?.withResolvedContactDisplayName(context) ?: return
                if (app?.container?.sessionStore?.read() == null) {
                    return
                }

                if (AndroidAppVisibilityState.isVisible()) {
                    app.container.callManager.presentIncomingCallInvite(
                        callId = invite.callId,
                        chatId = invite.chatId,
                        callerId = invite.callerId,
                        callerDisplayName = invite.callerDisplayName,
                        callerPhoneNumber = invite.callerPhoneNumber,
                        mediaType = invite.mediaType
                    )
                } else {
                    app.container.incomingCallInviteStore.setPending(invite)
                    IncomingCallNotificationManager.showIncomingCall(context, invite)
                }
                return
            }

            PushDataKinds.AudioCallEnded,
            PushDataKinds.VideoCallEnded -> {
                val callId = endedCallIdFromMap(data)
                app?.container?.incomingCallInviteStore?.clearByCallId(callId)
                app?.container?.callManager?.dismissCallIfMatches(callId)
                IncomingCallNotificationManager.cancelIncomingCall(context, callId)
                return
            }
        }

        val isChatMessage = kind == PushDataKinds.ChatMessage
        val isMessageReaction = kind == PushDataKinds.MessageReaction
        val isChatNotification = isChatMessage || isMessageReaction
        val chatId = data[PushDataKeys.ChatId].orEmpty()
        val chatNotificationSettings = if (isChatNotification && chatId.isNotBlank()) {
            settings.loadChatNotificationSnapshot(chatId)
        } else {
            null
        }
        if (isChatNotification && chatNotificationSettings?.notificationsEnabled == false) {
            return
        }
        val hideMessagePreview = isChatNotification &&
            (app?.container?.privacyRuntimeSettings?.loadSnapshot()?.showMessagePreview == false ||
                chatNotificationSettings?.showMessagePreview == false)
        val isAppVisible = AndroidAppVisibilityState.isVisible()
        val isActiveChatVisible = isChatNotification &&
            isAppVisible &&
            chatId == AndroidActiveChatState.getActiveChatId()

        if (isActiveChatVisible) {
            val soundInActiveChatEnabled = chatNotificationSettings?.soundInActiveChatEnabled
                ?: settings.getSoundInActiveChatEnabled()
            if (soundInActiveChatEnabled) {
                val messageSound = chatNotificationSettings?.messageSound ?: settings.getMessageSound()
                val activeChatSound = when (messageSound) {
                    NotificationSoundOption.Silent -> NotificationSoundOption.Silent
                    else -> NotificationSoundOption.Default
                }
                AndroidMessageNotificationSound.play(
                    context = context,
                    option = activeChatSound,
                    dedupeKey = data[PushDataKeys.MessageId]
                        ?: listOfNotNull(
                            data[PushDataKeys.ChatId],
                            data[PushDataKeys.Title],
                            data[PushDataKeys.Body]
                        ).joinToString(separator = "|").takeIf { it.isNotBlank() }
                )
            }
            return
        }

        val popupWindowsEnabled = chatNotificationSettings?.popupWindowsEnabled ?: settings.getPopupWindowsEnabled()
        if (isChatNotification && isAppVisible && !popupWindowsEnabled) {
            return
        }

        val senderContactDisplayName = if (isChatNotification) {
            resolveContactDisplayName(context, data[PushDataKeys.SenderPhoneNumber])
        } else {
            null
        }
        val fallbackTitle = data[PushDataKeys.Title].orEmpty().ifBlank {
            when (kind) {
                PushDataKinds.AppUpdateAvailable -> "Доступно обновление Икар"
                PushDataKinds.AudioCallInvite -> "Входящий аудиозвонок"
                PushDataKinds.VideoCallInvite -> "Входящий видеозвонок"
                else -> "ИКАР"
            }
        }

        val chatType = data[PushDataKeys.ChatType].orEmpty()
        val title = if (isChatNotification && chatType.equals(ChatTypeDirect, ignoreCase = true)) {
            senderContactDisplayName ?: fallbackTitle
        } else {
            fallbackTitle
        }

        val fallbackBody = data[PushDataKeys.Body].orEmpty().ifBlank {
            when (kind) {
                PushDataKinds.AppUpdateAvailable -> "Откройте приложение, чтобы установить обновление."
                PushDataKinds.AudioCallInvite -> "Откройте приложение, чтобы ответить на звонок."
                PushDataKinds.VideoCallInvite -> "Откройте приложение, чтобы ответить на звонок."
                PushDataKinds.MessageReaction -> GenericNewReactionText
                else -> "Новое сообщение"
            }
        }
        val rawBody = if (isChatNotification && chatType.equals(ChatTypeGroup, ignoreCase = true)) {
            prefixSenderName(
                body = fallbackBody,
                senderDisplayName = senderContactDisplayName ?: data[PushDataKeys.SenderName]
            )
        } else {
            fallbackBody
        }
        val showInAppText = chatNotificationSettings?.showMessagePreview ?: settings.getInAppShowTextEnabled()
        val body = if (isChatNotification && isAppVisible && !showInAppText) {
            if (isMessageReaction) GenericNewReactionText else GenericNewMessageText
        } else {
            rawBody
        }

        val notificationTitle = if (hideMessagePreview) "Икар" else title
        val notificationBody = if (hideMessagePreview) {
            if (isMessageReaction) GenericNewReactionText else GenericNewMessageText
        } else {
            body
        }

        val effectiveMessageSound = when {
            !isChatNotification -> NotificationSoundOption.Default
            isAppVisible && !settings.getInAppSoundEnabled() -> NotificationSoundOption.Silent
            else -> chatNotificationSettings?.messageSound ?: settings.getMessageSound()
        }

        val channelId = when (kind) {
            PushDataKinds.AppUpdateAvailable -> AndroidPushBootstrapper.updatesChannelId()
            PushDataKinds.AudioCallInvite,
            PushDataKinds.VideoCallInvite -> AndroidPushBootstrapper.callsChannelId()
            else -> AndroidPushBootstrapper.messageChannelId(effectiveMessageSound)
        }

        val intent = Intent(context, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
            data.forEach { (key, value) -> putExtra(key, value) }
        }

        val notificationId = ((if (isMessageReaction) "reaction:${data[PushDataKeys.MessageId].orEmpty()}" else data[PushDataKeys.MessageId])
            ?: data[PushDataKeys.CallId]
            ?: data[PushDataKeys.VersionCode]
            ?: System.currentTimeMillis().toString()).hashCode()

        val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }

        val pendingIntent = PendingIntent.getActivity(context, notificationId, intent, flags)
        val replyPendingIntent = PendingIntent.getActivity(
            context,
            notificationId + 1,
            intent,
            flags
        )
        val markReadPendingIntent = PendingIntent.getBroadcast(
            context,
            notificationId + 2,
            Intent(context, NotificationActionReceiver::class.java).apply {
                action = NotificationAction.MarkReadAction
                putExtra(PushDataKeys.ChatId, data[PushDataKeys.ChatId])
                putExtra(PushDataKeys.MessageId, data[PushDataKeys.MessageId])
                putExtra(NotificationAction.NotificationIdExtra, notificationId)
            },
            flags
        )
        val priority = when (kind) {
            PushDataKinds.AudioCallInvite,
            PushDataKinds.VideoCallInvite,
            PushDataKinds.AppUpdateAvailable -> NotificationCompat.PRIORITY_HIGH
            else -> NotificationCompat.PRIORITY_HIGH
        }

        val category = when (kind) {
            PushDataKinds.AudioCallInvite,
            PushDataKinds.VideoCallInvite -> NotificationCompat.CATEGORY_CALL
            else -> NotificationCompat.CATEGORY_MESSAGE
        }

        val notificationBuilder = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(android.R.drawable.sym_def_app_icon)
            .setContentTitle(notificationTitle)
            .setContentText(notificationBody)
            .setStyle(NotificationCompat.BigTextStyle().bigText(notificationBody))
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .setPriority(priority)
            .setCategory(category)

        if (hideMessagePreview) {
            val publicNotificationBody = if (isMessageReaction) GenericNewReactionText else GenericNewMessageText
            val publicVersion = NotificationCompat.Builder(context, channelId)
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setContentTitle("Икар")
                .setContentText(publicNotificationBody)
                .setPriority(priority)
                .setCategory(category)
                .build()
            notificationBuilder
                .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
                .setPublicVersion(publicVersion)
        }

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            val soundUri = AndroidMessageNotificationSound.resolveUri(effectiveMessageSound)
            if (soundUri != null) {
                notificationBuilder.setSound(soundUri)
            } else {
                notificationBuilder.setSilent(true)
            }
        } else if (effectiveMessageSound == NotificationSoundOption.Silent) {
            notificationBuilder.setSilent(true)
        }

        if (isChatMessage &&
            !data[PushDataKeys.ChatId].isNullOrBlank() &&
            !data[PushDataKeys.MessageId].isNullOrBlank()
        ) {
            notificationBuilder
                .addAction(android.R.drawable.ic_menu_send, "Ответить", replyPendingIntent)
                .addAction(android.R.drawable.ic_menu_view, "Отметить как прочитанное", markReadPendingIntent)
        }

        NotificationManagerCompat.from(context).notify(notificationId, notificationBuilder.build())
    }

    private const val GenericNewMessageText = "\u041d\u043e\u0432\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435"
    private const val GenericNewReactionText = "\u041d\u043e\u0432\u0430\u044f \u0440\u0435\u0430\u043a\u0446\u0438\u044f"
    private const val ChatTypeDirect = "Direct"
    private const val ChatTypeGroup = "Group"
    private const val ContactNameCacheTtlMillis = 5 * 60 * 1000L
    private val contactNameCacheLock = Any()

    @Volatile
    private var contactNameCache: ContactNameCache? = null

    private fun PendingIncomingCallInvite.withResolvedContactDisplayName(context: Context): PendingIncomingCallInvite {
        val contactName = resolveContactDisplayName(context, callerPhoneNumber) ?: return this
        return copy(callerDisplayName = contactName)
    }

    private fun resolveContactDisplayName(context: Context, phoneNumber: String?): String? {
        val normalizedPhoneNumber = PhoneNumberNormalizer.normalize(phoneNumber) ?: return null
        return loadContactNamesByPhone(context)[normalizedPhoneNumber]?.takeIf { it.isNotBlank() }
    }

    private fun loadContactNamesByPhone(context: Context): Map<String, String> {
        if (!hasContactsPermission(context)) {
            return emptyMap()
        }

        val now = System.currentTimeMillis()
        contactNameCache
            ?.takeIf { now - it.loadedAtMillis < ContactNameCacheTtlMillis }
            ?.let { return it.namesByPhone }

        return synchronized(contactNameCacheLock) {
            contactNameCache
                ?.takeIf { now - it.loadedAtMillis < ContactNameCacheTtlMillis }
                ?.let { return@synchronized it.namesByPhone }

            val app = context.applicationContext as? IkarNativeApp ?: return@synchronized emptyMap()
            val namesByPhone = runCatching {
                app.container.contactDiscoveryService
                    .getContacts()
                    .associate { contact -> contact.normalizedPhoneNumber to contact.displayName }
            }.getOrDefault(emptyMap())

            contactNameCache = ContactNameCache(now, namesByPhone)
            namesByPhone
        }
    }

    private fun hasContactsPermission(context: Context): Boolean =
        ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.READ_CONTACTS
        ) == PackageManager.PERMISSION_GRANTED

    private fun prefixSenderName(body: String, senderDisplayName: String?): String {
        val name = senderDisplayName?.trim().takeUnless { it.isNullOrBlank() } ?: return body
        return if (body.startsWith("$name:")) body else "$name: $body"
    }

    private data class ContactNameCache(
        val loadedAtMillis: Long,
        val namesByPhone: Map<String, String>
    )
}
