package tk.zwander.lockscreenwidgets.services

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.BadParcelableException
import android.os.Build
import android.os.DeadObjectException
import android.os.IBinder
import android.os.Parcel
import android.os.SystemProperties
import android.provider.Settings
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
import com.bugsnag.android.BreadcrumbType
import dev.zwander.lswinterconnect.BugsnagUtils
import dev.zwander.lswinterconnect.stringify
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import tk.zwander.common.util.Event
import tk.zwander.common.util.EventObserver
import tk.zwander.common.util.eventManager
import tk.zwander.common.util.globalState
import tk.zwander.common.util.logUtils
import tk.zwander.common.util.mainHandler

//Check if the notification listener service is enabled
val Context.isNotificationListenerActive: Boolean
    get() = Settings.Secure.getString(contentResolver, "enabled_notification_listeners")?.run {
        val cmp = ComponentName(this@isNotificationListenerActive, NotificationListener::class.java)
        contains(cmp.flattenToString()) || contains(cmp.flattenToShortString())
    } == true

/**
 * Used to notify the Accessibility service about changes in notification count,
 * along with the actual count. If the number of notifications with
 * [Notification.visibility] == [Notification.VISIBILITY_PUBLIC] or [Notification.VISIBILITY_PRIVATE]
 * AND [Notification.priority] > [Notification.PRIORITY_MIN] (importance for > Nougat),
 * and the user has the option enabled, the widget frame will hide.
 */
@Suppress("unused")
class NotificationListener : NotificationListenerService(), EventObserver, LifecycleOwner {
    private val dispatcher = ServiceLifecycleDispatcher(this)
    private val nm by lazy { getSystemService(NOTIFICATION_SERVICE) as NotificationManager }

    private val isListening = atomic(false)
    private val updateJob = atomic<Job?>(null)

    override val lifecycle: Lifecycle
        get() = dispatcher.lifecycle

    override fun onListenerConnected() {
        logUtils.debugLog("Notification listener connected.", null)
        isListening.value = true
        mainHandler.post {
            logUtils.debugLog("Sending notification update because listener was connected.", null)
            sendUpdate()
        }
        eventManager.addObserver(this)
    }

    override fun onListenerDisconnected() {
        logUtils.debugLog("Notification listener disconnected.", null)
        isListening.value = false
        eventManager.removeObserver(this)
    }

    override fun onCreate() {
        dispatcher.onServicePreSuperOnCreate()
        super.onCreate()
    }

    @Suppress("OVERRIDE_DEPRECATION")
    override fun onStart(intent: Intent?, startId: Int) {
        dispatcher.onServicePreSuperOnStart()
        @Suppress("DEPRECATION")
        super.onStart(intent, startId)
    }

    @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
    override fun onBind(intent: Intent?): IBinder {
        dispatcher.onServicePreSuperOnBind()
        super.onBind(intent)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            mWrapper = NougatListenerWrapper()
        }

        return NotificationListenerService::class.java.getDeclaredField("mWrapper")
            .apply { isAccessible = true }
            .get(this) as IBinder
    }

    override fun onDestroy() {
        dispatcher.onServicePreSuperOnDestroy()
        super.onDestroy()
    }

    override suspend fun onEvent(event: Event) {
        when (event) {
            Event.RequestNotificationCount -> {
                if (isListening.value) {
                    logUtils.debugLog(
                        "Sending notification update because update was requested.",
                        null
                    )
                    mainHandler.post {
                        sendUpdate()
                    }
                }
            }

            else -> {}
        }
    }

    override fun onNotificationPosted(sbn: StatusBarNotification?) {
        logUtils.debugLog("Sending notification update because notification was posted.", null)
        sendUpdate()
    }

    override fun onNotificationRemoved(sbn: StatusBarNotification?) {
        logUtils.debugLog("Sending notification update because notification was removed.", null)
        sendUpdate()
    }

    override fun onNotificationRankingUpdate(rankingMap: RankingMap?) {
        logUtils.debugLog("Sending notification update because notification was updated.", null)
        sendUpdate()
    }

    private fun sendUpdate() {
        logUtils.debugLog("Sending notification update.", null)

        updateJob.value?.cancel()
        updateJob.value = lifecycleScope.launch(Dispatchers.IO) {
            if (isListening.value) {
                //Even with the check to make sure the notification listener is connected, some devices still
                //crash with an "unknown listener" error when trying to retrieve the notification lists.
                //This shouldn't happen, but it does, so catch the Exception.
                try {
                    // This seems to cause ANRs on a bunch of devices, so run on a background thread.
                    val activeNotifications = try {
                        getActiveNotifications(null, TRIM_LIGHT)
                    } catch (e: Throwable) {
                        logUtils.debugLog("Error getting notifications with TRIM_LIGHT", e)
                        activeNotifications
                    } ?: arrayOf()
                    logUtils.debugLog("Filtering notifications ${activeNotifications.size}", null)
                    globalState.notificationCount.value =
                        activeNotifications.count { it.shouldCount }
                } catch (e: BadParcelableException) {
                    logUtils.normalLog("Error sending notification count update", e)

                    if (e.cause !is DeadObjectException) {
                        BugsnagUtils.notify(e)
                    }
                } catch (e: OutOfMemoryError) {
                    logUtils.normalLog("Error sending notification count update", e)
                } catch (e: Throwable) {
                    logUtils.normalLog("Error sending notification count update", e)
                    BugsnagUtils.leaveBreadcrumb(
                        "Error sending notification count update",
                        mapOf("error" to e.stringify()),
                        BreadcrumbType.ERROR
                    )
                }
            }
        }
    }

    private val StatusBarNotification.shouldCount: Boolean
        get() {
            logUtils.debugLog("Checking if notification $this should count", null)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                if (notification.flags and Notification.FLAG_BUBBLE != 0 &&
                    notification.bubbleMetadata?.isNotificationSuppressed == true
                ) {
                    logUtils.debugLog("Bubble and suppressed ${this.notification.channelId}", null)
                    return false
                }
            }

            if (notification.visibility == Notification.VISIBILITY_SECRET) {
                logUtils.debugLog("Secret visibility ${this.packageName}", null)
                return false
            }

            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
                val ranking = Ranking()
                val rankingResult = currentRanking.getRanking(key, ranking)

                if (rankingResult && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
                    (ranking.lockscreenVisibilityOverride == Notification.VISIBILITY_SECRET ||
                            ranking.channel.lockscreenVisibility == Notification.VISIBILITY_SECRET)
                ) {
                    logUtils.debugLog("Secret ranking ${this.notification.channelId}", null)
                    return false
                }

                val importance = ranking.importance

                if (importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
                    if (importance <= NotificationManager.IMPORTANCE_MIN) {
                        logUtils.debugLog("Min important ${this.notification.channelId}", null)
                        return false
                    }

                    val shouldCheckHideSilentStatusBarIcons =
                        importance <= NotificationManager.IMPORTANCE_LOW &&
                                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q

                    if (shouldCheckHideSilentStatusBarIcons) {
                        logUtils.debugLog(
                            "Checking shouldHideSilentStatusBarIcons with context package name $opPackageName",
                            null
                        )
                    }

                    if (shouldCheckHideSilentStatusBarIcons &&
                        Settings.Secure.getInt(
                            context.contentResolver,
                            Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
                            0,
                        ) == 0
                    ) {
                        logUtils.debugLog(
                            "Low importance and silent hidden ${this.notification.channelId}",
                            null
                        )
                        return false
                    }

                    // There's a bug in Pixel UI 13 where silent notifications don't
                    // show on the lock screen even if they're set to do so.
                    if (importance <= NotificationManager.IMPORTANCE_LOW &&
                        Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU &&
                        SystemProperties.get("ro.vendor.camera.extensions.service")
                            .contains("com.google.android.apps.camera.services.extensions.service.PixelExtensions")
                    ) {
                        logUtils.debugLog(
                            "Pixel workaround on ${this.packageName} ${this.notification.channelId}",
                            null
                        )
                        return false
                    }
                }
            } else {
                @Suppress("DEPRECATION")
                if (notification.priority <= Notification.PRIORITY_MIN) {
                    logUtils.debugLog("Priority min ${this.packageName}", null)
                    return false
                }
            }

            return true
        }

    private inner class NougatListenerWrapper : NotificationListenerWrapper() {
        override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
            return try {
                super.onTransact(code, data, reply, flags)
            } catch (e: Throwable) {
                BugsnagUtils.leaveBreadcrumb(
                    "Unable to receive notification update",
                    mapOf("error" to e.stringify()),
                    BreadcrumbType.ERROR
                )
                false
            }
        }
    }
}
