/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.components.metrics

import android.app.Application
import android.content.Context.MODE_PRIVATE
import android.net.Uri
import android.os.StrictMode
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
import com.leanplum.LeanplumActivityHelper
import com.leanplum.annotations.Parser
import com.leanplum.internal.LeanplumInternal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.StrictModeManager
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import java.util.Locale
import java.util.MissingResourceException
import java.util.UUID.randomUUID

private val Event.name: String?
    get() = when (this) {
        is Event.AddBookmark -> "E_Add_Bookmark"
        is Event.RemoveBookmark -> "E_Remove_Bookmark"
        is Event.OpenedBookmark -> "E_Opened_Bookmark"
        is Event.OpenedApp -> "E_Opened_App"
        is Event.OpenedAppFirstRun -> "E_Opened_App_FirstRun"
        is Event.InteractWithSearchURLArea -> "E_Interact_With_Search_URL_Area"
        is Event.CollectionSaved -> "E_Collection_Created"
        is Event.CollectionTabRestored -> "E_Collection_Tab_Opened"
        is Event.SyncAuthSignUp -> "E_FxA_New_Signup"
        is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA"
        is Event.SyncAuthFromSharedCopy, Event.SyncAuthFromSharedReuse -> "E_Sign_In_FxA_Fennec_to_Fenix"
        is Event.SyncAuthSignOut -> "E_Sign_Out_FxA"
        is Event.ClearedPrivateData -> "E_Cleared_Private_Data"
        is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"
        is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated"
        is Event.AddonInstalled -> "E_Addon_Installed"
        is Event.SearchWidgetInstalled -> "E_Search_Widget_Added"
        is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix"
        is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP"

        // Do not track other events in Leanplum
        else -> null
    }

class LeanplumMetricsService(
    private val application: Application,
    strictMode: StrictModeManager,
    private val deviceIdGenerator: () -> String = { randomUUID().toString() }
) : MetricsService, DeepLinkIntentProcessor.DeepLinkVerifier {
    val scope = CoroutineScope(Dispatchers.IO)
    var leanplumJob: Job? = null

    data class Token(val id: String, val token: String) {
        enum class Type { Development, Production, Invalid }

        val type by lazy {
            when {
                token.take(ProdPrefix.length) == ProdPrefix -> Type.Production
                token.take(DevPrefix.length) == DevPrefix -> Type.Development
                else -> Type.Invalid
            }
        }

        companion object {
            private const val ProdPrefix = "prod"
            private const val DevPrefix = "dev"
        }
    }

    override val type = MetricServiceType.Marketing
    private val token = Token(LeanplumId, LeanplumToken)

    private val preferences = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
        application.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE)
    }

    @VisibleForTesting
    internal val deviceId by lazy {
        var deviceId = preferences.getString(DEVICE_ID_KEY, null)

        if (deviceId == null) {
            deviceId = deviceIdGenerator.invoke()
            preferences.edit().putString(DEVICE_ID_KEY, deviceId).apply()
        }

        deviceId
    }

    @Suppress("ComplexMethod")
    override fun start() {

        if (!application.settings().isMarketingTelemetryEnabled) return

        Leanplum.setIsTestModeEnabled(false)
        Leanplum.setApplicationContext(application)
        Leanplum.setDeviceId(deviceId)
        Parser.parseVariables(application)

        leanplumJob = scope.launch {

            val applicationSetLocale = LocaleManager.getCurrentLocale(application)
            val currentLocale = applicationSetLocale ?: Locale.getDefault()
            val languageCode =
                currentLocale.iso3LanguageOrNull
                    ?: currentLocale.language.let {
                        if (it.isNotBlank()) {
                            it
                        } else {
                            currentLocale.toString()
                        }
                    }

            if (!isLeanplumEnabled(languageCode)) {
                Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode")
                return@launch
            }

            when (token.type) {
                Token.Type.Production -> Leanplum.setAppIdForProductionMode(token.id, token.token)
                Token.Type.Development -> Leanplum.setAppIdForDevelopmentMode(token.id, token.token)
                Token.Type.Invalid -> {
                    Log.i(LOGTAG, "Invalid or missing Leanplum token")
                    return@launch
                }
            }

            LeanplumActivityHelper.enableLifecycleCallbacks(application)

            val installedApps = MozillaProductDetector.getInstalledMozillaProducts(application)

            val trackingProtection = application.settings().run {
                when {
                    !shouldUseTrackingProtection -> "none"
                    useStandardTrackingProtection -> "standard"
                    useStrictTrackingProtection -> "strict"
                    else -> "custom"
                }
            }

            Leanplum.start(
                application, hashMapOf(
                    "default_browser" to MozillaProductDetector.getMozillaBrowserDefault(application)
                        .orEmpty(),
                    "fennec_installed" to installedApps.contains(MozillaProducts.FIREFOX.productName),
                    "focus_installed" to installedApps.contains(MozillaProducts.FOCUS.productName),
                    "klar_installed" to installedApps.contains(MozillaProducts.KLAR.productName),
                    "fxa_signed_in" to application.settings().fxaSignedIn,
                    "fxa_has_synced_items" to application.settings().fxaHasSyncedItems,
                    "search_widget_installed" to application.settings().searchWidgetInstalled,
                    "tracking_protection_enabled" to application.settings().shouldUseTrackingProtection,
                    "tracking_protection_setting" to trackingProtection,
                    "fenix" to true
                )
            )

            withContext(Main) {
                LeanplumInternal.setCalledStart(true)
                LeanplumInternal.setHasStarted(true)
                LeanplumInternal.setStartedInBackground(true)
            }
        }
    }

    /**
     * Verifies a deep link and returns `true` for deep links that should be handled and `false` if
     * a deep link should be rejected.
     *
     * @See DeepLinkIntentProcessor.verifier
     */
    override fun verifyDeepLink(deepLink: Uri): Boolean {
        // We compare the local Leanplum device ID against the "uid" query parameter and only
        // accept deep links where both values match.
        val uid = deepLink.getQueryParameter("uid")
        return uid == deviceId
    }

    override fun stop() {
        if (application.settings().isMarketingTelemetryEnabled) return
        // As written in LeanPlum SDK documentation, "This prevents Leanplum from communicating with the server."
        // as this "isTestMode" flag is checked before LeanPlum SDK does anything.
        // Also has the benefit effect of blocking the display of already downloaded messages.
        // The reverse of this - setIsTestModeEnabled(false) must be called before trying to init
        // LP in the same session.
        Leanplum.setIsTestModeEnabled(true)

        // This is just to allow restarting LP and it's functionality in the same app session
        // as LP stores it's state internally and check against it
        LeanplumInternal.setCalledStart(false)
        LeanplumInternal.setHasStarted(false)
        leanplumJob?.cancel()
    }

    override fun track(event: Event) {
        val leanplumExtras = event.extras
            ?.map { (key, value) -> key.toString() to value }
            ?.toMap()

        event.name?.also {
            Leanplum.track(it, leanplumExtras)
        }
    }

    override fun shouldTrack(event: Event): Boolean {
        return application.settings().isTelemetryEnabled &&
                token.type != Token.Type.Invalid && !event.name.isNullOrEmpty()
    }

    private fun isLeanplumEnabled(locale: String): Boolean {
        return LEANPLUM_ENABLED_LOCALES.contains(locale)
    }

    private val Locale.iso3LanguageOrNull: String?
        get() =
            try {
                this.isO3Language
            } catch (_: MissingResourceException) {
                null
            }

    companion object {
        private const val LOGTAG = "LeanplumMetricsService"

        private val LeanplumId: String
            // Debug builds have a null (nullable) LEANPLUM_ID
            get() = BuildConfig.LEANPLUM_ID.orEmpty()
        private val LeanplumToken: String
            // Debug builds have a null (nullable) LEANPLUM_TOKEN
            get() = BuildConfig.LEANPLUM_TOKEN.orEmpty()

        // Leanplum needs to be enabled for the following locales.
        // Irrespective of the actual device location.
        private val LEANPLUM_ENABLED_LOCALES = setOf(
            "eng", // English
            "zho", // Chinese
            "deu", // German
            "fra", // French
            "ita", // Italian
            "ind", // Indonesian
            "por", // Portuguese
            "spa", // Spanish; Castilian
            "pol", // Polish
            "rus", // Russian
            "hin", // Hindi
            "per", // Persian
            "fas", // Persian
            "ara", // Arabic
            "jpn" // Japanese
        )

        private const val PREFERENCE_NAME = "LEANPLUM_PREFERENCES"
        private const val DEVICE_ID_KEY = "LP_DEVICE_ID"
    }
}
