/* Copyright 2022 Tusky contributors
 *
 * This file is a part of Tusky.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Tusky; if not,
 * see <http://www.gnu.org/licenses>. */

@file:JvmName("PushNotificationHelper")

package com.keylesspalace.tusky.components.systemnotifications

import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.util.Log
import android.view.View
import androidx.preference.PreferenceManager
import at.connyduck.calladapter.networkresult.onFailure
import at.connyduck.calladapter.networkresult.onSuccess
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.CryptoUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush

private const val TAG = "PushNotificationHelper"

private const val KEY_MIGRATION_NOTICE_DISMISSED = "migration_notice_dismissed"

private fun anyAccountNeedsMigration(accountManager: AccountManager): Boolean =
    accountManager.accounts.any(::accountNeedsMigration)

private fun accountNeedsMigration(account: AccountEntity): Boolean =
    !account.oauthScopes.contains("push")

fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean =
    accountManager.activeAccount?.let(::accountNeedsMigration) ?: false

fun showMigrationNoticeIfNecessary(
    context: Context,
    parent: View,
    anchorView: View?,
    accountManager: AccountManager
) {
    // No point showing anything if we cannot enable it
    if (!isUnifiedPushAvailable(context)) return
    if (!anyAccountNeedsMigration(accountManager)) return

    val pm = PreferenceManager.getDefaultSharedPreferences(context)
    if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return

    Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE)
        .setAnchorView(anchorView)
        .setAction(
            R.string.action_details
        ) { showMigrationExplanationDialog(context, accountManager) }
        .show()
}

private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) {
    MaterialAlertDialogBuilder(context).apply {
        if (currentAccountNeedsMigration(accountManager)) {
            setMessage(R.string.dialog_push_notification_migration)
            setPositiveButton(R.string.title_migration_relogin) { _, _ ->
                context.startActivity(
                    LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
                )
            }
        } else {
            setMessage(R.string.dialog_push_notification_migration_other_accounts)
        }
        setNegativeButton(R.string.action_dismiss) { dialog, _ ->
            val pm = PreferenceManager.getDefaultSharedPreferences(context)
            pm.edit().putBoolean(KEY_MIGRATION_NOTICE_DISMISSED, true).apply()
            dialog.dismiss()
        }
        show()
    }
}

private suspend fun enableUnifiedPushNotificationsForAccount(
    context: Context,
    api: MastodonApi,
    accountManager: AccountManager,
    account: AccountEntity
) {
    if (isUnifiedPushNotificationEnabledForAccount(account)) {
        // Already registered, update the subscription to match notification settings
        updateUnifiedPushSubscription(context, api, accountManager, account)
    } else {
        UnifiedPush.registerAppWithDialog(
            context,
            account.id.toString(),
            features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)
        )
    }
}

fun disableUnifiedPushNotificationsForAccount(context: Context, account: AccountEntity) {
    if (!isUnifiedPushNotificationEnabledForAccount(account)) {
        // Not registered
        return
    }

    UnifiedPush.unregisterApp(context, account.id.toString())
}

fun isUnifiedPushNotificationEnabledForAccount(account: AccountEntity): Boolean =
    account.unifiedPushUrl.isNotEmpty()

private fun isUnifiedPushAvailable(context: Context): Boolean =
    UnifiedPush.getDistributors(context).isNotEmpty()

fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean =
    isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager)

suspend fun enablePushNotificationsWithFallback(
    context: Context,
    api: MastodonApi,
    accountManager: AccountManager
) {
    if (!canEnablePushNotifications(context, accountManager)) {
        // No UP distributors
        NotificationHelper.enablePullNotifications(context)
        return
    }

    val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    accountManager.accounts.forEach {
        val notificationGroupEnabled = Build.VERSION.SDK_INT < 28 ||
            nm.getNotificationChannelGroup(it.identifier)?.isBlocked == false
        val shouldEnable = it.notificationsEnabled && notificationGroupEnabled

        if (shouldEnable) {
            enableUnifiedPushNotificationsForAccount(context, api, accountManager, it)
        } else {
            disableUnifiedPushNotificationsForAccount(context, it)
        }
    }
}

private fun disablePushNotifications(context: Context, accountManager: AccountManager) {
    accountManager.accounts.forEach {
        disableUnifiedPushNotificationsForAccount(context, it)
    }
}

fun disableAllNotifications(context: Context, accountManager: AccountManager) {
    disablePushNotifications(context, accountManager)
    NotificationHelper.disablePullNotifications(context)
}

private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
    buildMap {
        val notificationManager = context.getSystemService(
            Context.NOTIFICATION_SERVICE
        ) as NotificationManager
        Notification.Type.visibleTypes.forEach {
            put(
                "data[alerts][${it.presentation}]",
                NotificationHelper.filterNotification(notificationManager, account, it)
            )
        }
    }

// Called by UnifiedPush callback
suspend fun registerUnifiedPushEndpoint(
    context: Context,
    api: MastodonApi,
    accountManager: AccountManager,
    account: AccountEntity,
    endpoint: String
) = withContext(Dispatchers.IO) {
    // Generate a prime256v1 key pair for WebPush
    // Decryption is unimplemented for now, since Mastodon uses an old WebPush
    // standard which does not send needed information for decryption in the payload
    // This makes it not directly compatible with UnifiedPush
    // As of now, we use it purely as a way to trigger a pull
    val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
    val auth = CryptoUtil.secureRandomBytesEncoded(16)

    api.subscribePushNotifications(
        "Bearer ${account.accessToken}",
        account.domain,
        endpoint,
        keyPair.pubkey,
        auth,
        buildSubscriptionData(context, account)
    ).onFailure { throwable ->
        Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable)
        disableUnifiedPushNotificationsForAccount(context, account)
    }.onSuccess {
        Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}")

        account.pushPubKey = keyPair.pubkey
        account.pushPrivKey = keyPair.privKey
        account.pushAuth = auth
        account.pushServerKey = it.serverKey
        account.unifiedPushUrl = endpoint
        accountManager.saveAccount(account)
    }
}

// Synchronize the enabled / disabled state of notifications with server-side subscription
suspend fun updateUnifiedPushSubscription(
    context: Context,
    api: MastodonApi,
    accountManager: AccountManager,
    account: AccountEntity
) {
    withContext(Dispatchers.IO) {
        api.updatePushNotificationSubscription(
            "Bearer ${account.accessToken}",
            account.domain,
            buildSubscriptionData(context, account)
        ).onSuccess {
            Log.d(TAG, "UnifiedPush subscription updated for account ${account.id}")

            account.pushServerKey = it.serverKey
            accountManager.saveAccount(account)
        }
    }
}

suspend fun unregisterUnifiedPushEndpoint(
    api: MastodonApi,
    accountManager: AccountManager,
    account: AccountEntity
) {
    withContext(Dispatchers.IO) {
        api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
            .onFailure { throwable ->
                Log.w(TAG, "Error unregistering push endpoint for account " + account.id, throwable)
            }
            .onSuccess {
                Log.d(TAG, "UnifiedPush unregistration succeeded for account " + account.id)
                // Clear the URL in database
                account.unifiedPushUrl = ""
                account.pushServerKey = ""
                account.pushAuth = ""
                account.pushPrivKey = ""
                account.pushPubKey = ""
                accountManager.saveAccount(account)
            }
    }
}
