/* 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.focus

import android.app.Application
import android.content.Context
import android.os.Build
import android.os.StrictMode
import android.util.Log.INFO
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.emoji2.text.DefaultEmojiCompatConfig
import androidx.emoji2.text.EmojiCompat
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration.Builder
import androidx.work.Configuration.Provider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.action.SearchAction
import mozilla.components.support.AppServicesInitializer
import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.locale.LocaleManager
import mozilla.components.support.remotesettings.GlobalRemoteSettingsDependencyProvider
import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.focus.biometrics.LockObserver
import org.mozilla.focus.experiments.finishNimbusInitialization
import org.mozilla.focus.ext.settings
import org.mozilla.focus.navigation.StoreLink
import org.mozilla.focus.nimbus.FocusNimbus
import org.mozilla.focus.session.VisibilityLifeCycleCallback
import org.mozilla.focus.telemetry.FactsProcessor
import org.mozilla.focus.telemetry.ProfilerMarkerFactProcessor
import org.mozilla.focus.utils.AppConstants
import mozilla.components.support.AppServicesInitializer.Config as AppServiceConfig

/**
 * Focus application class.
 */
open class FocusApplication : Application(), Provider {

    protected val applicationScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    protected val ioDispatcher = Dispatchers.IO

    open val components: Components by lazy { Components(this) }

    var visibilityLifeCycleCallback: VisibilityLifeCycleCallback? = null
        private set

    private val storeLink by lazy { StoreLink(components.appStore, components.store) }
    private val lockObserver by lazy { LockObserver(this, components.store, components.appStore) }

    override fun onCreate() {
        super.onCreate()

        Log.addSink(AndroidLogSink("Focus"))
        components.crashReporter.install(this)

        if (isMainProcess()) {
            initializeNimbus()

            setTheme(this)
            components.engine.warmUp()

            initializeTelemetry()

            finishSetupMegazord()

            ProfilerMarkerFactProcessor.create { components.engine.profiler }.register()

            enableStrictMode()

            visibilityLifeCycleCallback = VisibilityLifeCycleCallback(this@FocusApplication)
            registerActivityLifecycleCallbacks(visibilityLifeCycleCallback)
            registerComponentCallbacks(visibilityLifeCycleCallback)

            storeLink.start(applicationScope)

            initializeWebExtensionSupport()

            initializeRemoteSettingsSupport()

            refreshSearchEngineUpdate()

            setupLeakCanary()

            components.appStartReasonProvider.registerInAppOnCreate(this)
            components.startupActivityLog.registerInAppOnCreate(this)

            ProcessLifecycleOwner.get().lifecycle.addObserver(lockObserver)

            applicationScope.launch {
                // Initialize EmojiCompat manually.
                initializeEmojiCompat()

                // Remove stale temporary uploaded files.
                components.fileUploadsDirCleaner.cleanUploadsDirectory()
            }
        }
    }

    private fun refreshSearchEngineUpdate() {
        components.store.dispatch(SearchAction.RefreshSearchEnginesAction)
    }

    override fun onConfigurationChanged(config: android.content.res.Configuration) {
        applicationContext.resources.configuration.uiMode = config.uiMode

        if (isMainProcess()) {
            super.onConfigurationChanged(config)
            // Update locale on main process
            LocaleManager.updateResources(this)
        } else {
            super.onConfigurationChanged(config)
        }
    }

    override fun attachBaseContext(base: Context) {
        // Sets the locale information. Other threads do not have locale aware needs
        if (base.isMainProcess()) {
            val localeAwareContext = LocaleManager.updateResources(base)
            super.attachBaseContext(localeAwareContext)
        } else {
            super.attachBaseContext(base)
        }
    }

    protected open fun setupLeakCanary() {
        // no-op, LeakCanary is disabled by default
    }

    /**
     * Updates the configuration of LeakCanary based on the provided state.
     *
     * @param isEnabled Whether LeakCanary features should be enabled.
     */
    open fun updateLeakCanaryState(isEnabled: Boolean) {
        // no-op, LeakCanary is disabled by default
    }

    protected open fun initializeNimbus() {
        beginSetupMegazord()

        // This lazily constructs the Nimbus object…
        val nimbus = components.experiments
        // … which we then can populate the feature configuration.
        FocusNimbus.initialize { nimbus }
    }

    /**
     * Initializes EmojiCompat manually on a background thread.
     *
     * By initializing manually, we avoid the startup penalty associated with the default
     * EmojiCompat initializer's ContentProvider. [DefaultEmojiCompatConfig] is used to
     * automatically find a compatible font provider (such as Google Play Services).
     *
     * @param dispatcher The [CoroutineDispatcher] on which the initialization will occur.
     * Defaults to [ioDispatcher].
     */
    private suspend fun initializeEmojiCompat(dispatcher: CoroutineDispatcher = ioDispatcher) {
        withContext(dispatcher) {
            // If the device has no compatible provider (e.g. no Play Services), config will be null.
            val config = DefaultEmojiCompatConfig.create(applicationContext) ?: return@withContext

            config.setReplaceAll(true)

            config.registerInitCallback(
                object : EmojiCompat.InitCallback() {
                    override fun onInitialized() {
                        Log.log(
                            tag = "EmojiCompat",
                            message = "EmojiCompat initialization completed",
                        )
                    }

                    override fun onFailed(throwable: Throwable?) {
                        Log.log(
                            tag = "EmojiCompat",
                            throwable = throwable,
                            message = "EmojiCompat initialization failed",
                        )
                    }
                },
            )

            EmojiCompat.init(config)
        }
    }

    protected open fun initializeTelemetry() {
        components.metrics.initialize(this)
        FactsProcessor.initialize()
        if (components.settings.isDailyUsagePingEnabled) {
            components.usageReportingMetricsService.start()
        }
    }

    /**
     * Initiate Megazord sequence! Megazord Battle Mode!
     *
     * The application-services combined libraries are known as the "megazord". We use the default `full`
     * megazord - it contains everything that fenix needs, and (currently) nothing more.
     *
     * Documentation on what megazords are, and why they're needed:
     * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
     * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
     *
     * This is the initialization of the megazord without setting up networking, i.e. needing the
     * engine for networking. This should do the minimum work necessary as it is done on the main
     * thread, early in the app startup sequence.
     */
    private fun beginSetupMegazord() {
        AppServicesInitializer.init(
            AppServiceConfig(components.crashReporter),
        )
    }

    /**
     * Finish Megazord setup sequence.
     */
    @OpenForTesting
    open fun finishSetupMegazord(dispatcher: CoroutineDispatcher = ioDispatcher) {
        // We need to use an unwrapped client because native components do not support private
        // requests.
        @Suppress("Deprecation")
        RustHttpConfig.setClient(lazy { components.client.unwrap() })

        applicationScope.launch(dispatcher) {
            // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
            // experiments recipes from the server.
            finishNimbusInitialization(components.experiments)
        }
    }

    private fun initializeRemoteSettingsSupport() {
        GlobalRemoteSettingsDependencyProvider.initialize(components.remoteSettingsService)
        components.remoteSettingsSyncScheduler.registerForSync()
    }

    private fun setTheme(context: Context) {
        val settings = context.settings
        when {
            settings.lightThemeSelected -> {
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_NO,
                )
            }

            settings.darkThemeSelected -> {
                AppCompatDelegate.setDefaultNightMode(
                    AppCompatDelegate.MODE_NIGHT_YES,
                )
            }

            settings.useDefaultThemeSelected -> {
                setDefaultTheme()
            }

            // No theme setting selected, select the default value, follow device theme.
            else -> {
                setDefaultTheme()
                settings.useDefaultThemeSelected = true
            }
        }
    }

    private fun setDefaultTheme() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            AppCompatDelegate.setDefaultNightMode(
                AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
            )
        } else {
            AppCompatDelegate.setDefaultNightMode(
                AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY,
            )
        }
    }

    private fun enableStrictMode() {
        // Only enable StrictMode in debug builds
        if (AppConstants.isDevBuild) {
            val threadPolicyBuilder = StrictMode.ThreadPolicy.Builder().detectAll()
            val vmPolicyBuilder = StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()
                .detectFileUriExposure()
                .detectLeakedClosableObjects()
                .detectLeakedRegistrationObjects()
                .detectLeakedSqlLiteObjects()

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                vmPolicyBuilder.detectNonSdkApiUsage()
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                vmPolicyBuilder.detectUnsafeIntentLaunch()
            }

            threadPolicyBuilder.penaltyLog()
            vmPolicyBuilder.penaltyLog()

            StrictMode.setThreadPolicy(threadPolicyBuilder.build())
            StrictMode.setVmPolicy(vmPolicyBuilder.build())
        }
    }

    @VisibleForTesting
    @OpenForTesting
    internal open fun initializeWebExtensionSupport() {
        WebExtensionSupport.initialize(
            components.engine,
            components.store,
            onNewTabOverride = { _, engineSession, url, selected ->
                components.tabsUseCases.addTab(
                    url = url,
                    selectTab = selected,
                    engineSession = engineSession,
                    private = true,
                )
            },
        )
    }

    override val workManagerConfiguration
        get() = Builder().setMinimumLoggingLevel(INFO).build()
}
