/* 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.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage
import mozilla.components.browser.engine.gecko.util.EngineDownloadDelegate
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.app.links.AppLinksInterceptor
import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuUseCases
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DefaultFileSizeFormatter
import mozilla.components.feature.downloads.DownloadEstimator
import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.downloads.FileSizeFormatter
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.prompts.PromptMiddleware
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
import mozilla.components.feature.prompts.file.FileUploadsDirCleanerMiddleware
import mozilla.components.feature.search.SearchApplicationName
import mozilla.components.feature.search.SearchDeviceType
import mozilla.components.feature.search.SearchUpdateChannel
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.search.middleware.AdsTelemetryMiddleware
import mozilla.components.feature.search.middleware.SearchMiddleware
import mozilla.components.feature.search.region.RegionMiddleware
import mozilla.components.feature.search.storage.SearchEngineSelectorConfig
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
import mozilla.components.feature.webcompat.WebCompatFeature
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.lib.crash.sentry.SentryService
import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.lib.crash.service.GleanCrashReporterService
import mozilla.components.lib.crash.service.MozillaSocorroService
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.service.location.LocationService
import mozilla.components.service.location.MozillaLocationService
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.ktx.android.content.appVersionName
import mozilla.components.support.locale.LocaleManager
import mozilla.components.support.remotesettings.DefaultRemoteSettingsSyncScheduler
import mozilla.components.support.remotesettings.RemoteSettingsServer
import mozilla.components.support.remotesettings.RemoteSettingsService
import mozilla.components.support.remotesettings.into
import mozilla.components.support.utils.DateTimeProvider
import mozilla.components.support.utils.DefaultDateTimeProvider
import mozilla.components.support.utils.DefaultDownloadFileUtils
import org.mozilla.focus.activity.MainActivity
import org.mozilla.focus.browser.BlockedTrackersMiddleware
import org.mozilla.focus.cfr.CfrMiddleware
import org.mozilla.focus.components.EngineProvider
import org.mozilla.focus.downloads.DownloadService
import org.mozilla.focus.engine.AppContentInterceptor
import org.mozilla.focus.engine.ClientWrapper
import org.mozilla.focus.engine.SanityCheckMiddleware
import org.mozilla.focus.experiments.createNimbus
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.isTablet
import org.mozilla.focus.ext.settings
import org.mozilla.focus.media.MediaSessionService
import org.mozilla.focus.nimbus.FocusNimbus
import org.mozilla.focus.search.SearchFilterMiddleware
import org.mozilla.focus.search.SearchMigration
import org.mozilla.focus.state.AppState
import org.mozilla.focus.state.AppStore
import org.mozilla.focus.state.Screen
import org.mozilla.focus.telemetry.GleanMetricsService
import org.mozilla.focus.telemetry.GleanUsageReportingMetricsService
import org.mozilla.focus.telemetry.GleanUsageReportingMetricsService.GleanProfileIdPreferenceStore
import org.mozilla.focus.telemetry.TelemetryMiddleware
import org.mozilla.focus.telemetry.startuptelemetry.AppStartReasonProvider
import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog
import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider
import org.mozilla.focus.topsites.DefaultTopSitesStorage
import org.mozilla.focus.utils.Settings
import java.util.Locale
import java.util.concurrent.TimeUnit

/**
 * Helper object for lazily initializing components.
 */
class Components(
    context: Context,
    private val engineOverride: Engine? = null,
    private val clientOverride: Client? = null,
) {
    val appStore: AppStore by lazy {
        AppStore(
            AppState(
                screen = if (context.settings.isFirstRun) Screen.FirstRun else Screen.Home,
                topSites = emptyList(),
                isPinningSupported = null,
            ),
        )
    }

    private val notificationManagerCompat = NotificationManagerCompat.from(context)

    val notificationsDelegate: NotificationsDelegate by lazy {
        NotificationsDelegate(notificationManagerCompat)
    }

    val appStartReasonProvider by lazy { AppStartReasonProvider() }

    val startupActivityLog by lazy { StartupActivityLog() }

    val startupStateProvider by lazy { StartupStateProvider(startupActivityLog, appStartReasonProvider) }

    val settings by lazy { Settings(context) }

    val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
        FileUploadsDirCleaner { context.cacheDir }
    }

    val remoteSettingsSyncScheduler by lazy {
        DefaultRemoteSettingsSyncScheduler(
            context,
            Frequency(2, TimeUnit.HOURS),
        )
    }

    val engineDefaultSettings by lazy {
        DefaultSettings(
            requestInterceptor = AppContentInterceptor(context),
            trackingProtectionPolicy = EngineProvider.createTrackingProtectionPolicy(context),
            javascriptEnabled = !settings.shouldBlockJavaScript(),
            remoteDebuggingEnabled = settings.shouldEnableRemoteDebugging(),
            webFontsEnabled = !settings.shouldBlockWebFonts(),
            httpsOnlyMode = settings.getHttpsOnlyMode(),
            preferredColorScheme = settings.getPreferredColorScheme(),
            cookieBannerHandlingModePrivateBrowsing = settings.getCurrentCookieBannerOptionFromSharePref().mode,
            certificateTransparencyMode = FocusNimbus.features.pki.value().certificateTransparencyMode,
            downloadDelegate = EngineDownloadDelegate(
                context = context,
                downloadLocation = {
                    Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_DOWNLOADS,
                    ).absolutePath
                },
            ),
        )
    }

    val engine: Engine by lazy {
        engineOverride ?: EngineProvider.createEngine(context, engineDefaultSettings).apply {
            EngineProvider.setupSafeBrowsing(this, this@Components.settings.shouldUseSafeBrowsing())
            WebCompatFeature.install(this)
            WebCompatReporterFeature.install(this, "focus-geckoview")
        }
    }

    val client: ClientWrapper by lazy {
        ClientWrapper(clientOverride ?: EngineProvider.createClient(context))
    }

    val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) }

    val settingsUseCases by lazy { SettingsUseCases(engine, store) }

    @Suppress("DEPRECATION")
    private val locationService: LocationService by lazy {
        if (BuildConfig.MLS_TOKEN.isEmpty()) {
            LocationService.default()
        } else {
            MozillaLocationService(context, client.unwrap(), BuildConfig.MLS_TOKEN)
        }
    }

    val store by lazy {
        BrowserStore(
            middleware = listOf(
                TelemetryMiddleware(),
                DownloadMiddleware(
                    applicationContext = context,
                    downloadServiceClass = DownloadService::class.java,
                    deleteFileFromStorage = { false },
                    downloadFileUtils = DefaultDownloadFileUtils(
                        context = context,
                    ),
                ),
                SanityCheckMiddleware(),
                // We are currently using the default location service. We should consider using
                // an actual implementation:
                // https://github.com/mozilla-mobile/focus-android/issues/4781
                RegionMiddleware(context, locationService),
                SearchMiddleware(
                    context,
                    migration = SearchMigration(context),
                    searchEngineSelectorConfig = getSearchEngineSelectorConfig(context),
                ),
                SearchFilterMiddleware(),
                PromptMiddleware(),
                AdsTelemetryMiddleware(adsTelemetry),
                BlockedTrackersMiddleware(context),
                RecordingDevicesMiddleware(context, notificationsDelegate),
                CfrMiddleware(appStore, settings),
                FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
            ) + EngineMiddleware.create(
                engine,
                // We are disabling automatic suspending of engine sessions under memory pressure.
                // Instead we solely rely on GeckoView and the Android system to reclaim memory
                // when needed. For details, see:
                // https://bugzilla.mozilla.org/show_bug.cgi?id=1752594
                // https://github.com/mozilla-mobile/fenix/issues/12731
                // https://github.com/mozilla-mobile/android-components/issues/11300
                // https://github.com/mozilla-mobile/android-components/issues/11653
                trimMemoryAutomatically = false,
            ),
        ).apply {
            MediaSessionFeature(context, MediaSessionService::class.java, this).start()
        }
    }

    /**
     * The [CustomTabsServiceStore] holds global custom tabs related data.
     */
    val customTabsStore by lazy { CustomTabsServiceStore() }

    val sessionUseCases: SessionUseCases by lazy { SessionUseCases(store) }

    val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) }

    val cookieBannerStorage: GeckoCookieBannersStorage by lazy { EngineProvider.createCookieBannerStorage(context) }

    val publicSuffixList by lazy { PublicSuffixList(context) }

    val searchUseCases: SearchUseCases by lazy {
        SearchUseCases(store, tabsUseCases, sessionUseCases)
    }

    val contextMenuUseCases: ContextMenuUseCases by lazy { ContextMenuUseCases(store) }

    val downloadsUseCases: DownloadsUseCases by lazy {
        DownloadsUseCases(
            store = store,
            downloadFileUtils = DefaultDownloadFileUtils(
                context = context.applicationContext,
            ),
        )
    }

    val appLinksUseCases: AppLinksUseCases by lazy { AppLinksUseCases(context.applicationContext) }

    val customTabsUseCases: CustomTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) }

    val crashReporter: CrashReporter by lazy { createCrashReporter(context) }

    val metrics: GleanMetricsService by lazy { GleanMetricsService(context) }

    val usageReportingMetricsService: GleanUsageReportingMetricsService by lazy {
        GleanUsageReportingMetricsService(
            gleanProfileIdStore = GleanProfileIdPreferenceStore(
                context,
            ),
        )
    }

    val remoteSettingsService by lazy {
        RemoteSettingsService(
            context,
            if (context.settings.useProductionRemoteSettingsServer) {
                RemoteSettingsServer.Prod.into()
            } else {
                RemoteSettingsServer.Stage.into()
            },
            channel = BuildConfig.BUILD_TYPE,
        )
    }

    val experiments: NimbusApi by lazy {
        createNimbus(context, BuildConfig.NIMBUS_ENDPOINT, remoteSettingsService.remoteSettingsService)
    }

    val adsTelemetry: AdsTelemetry by lazy { AdsTelemetry() }

    val searchTelemetry: InContentTelemetry by lazy { InContentTelemetry() }

    val icons by lazy { BrowserIcons(context, client) }

    val topSitesStorage by lazy { DefaultTopSitesStorage(PinnedSiteStorage(context)) }

    val topSitesUseCases: TopSitesUseCases by lazy { TopSitesUseCases(topSitesStorage) }

    val appLinksInterceptor by lazy {
        AppLinksInterceptor(
            context,
            launchInApp = {
                context.settings.openLinksInExternalApp
            },
        )
    }

    val fileSizeFormatter: FileSizeFormatter by lazy { DefaultFileSizeFormatter(context.applicationContext) }

    val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() }

    val downloadEstimator: DownloadEstimator by lazy { DownloadEstimator(dateTimeProvider = dateTimeProvider) }
}

private fun createCrashReporter(context: Context): CrashReporter {
    val services = mutableListOf<CrashReporterService>()

    if (BuildConfig.SENTRY_TOKEN.isNotEmpty()) {
        val sentryService = SentryService(
            context,
            BuildConfig.SENTRY_TOKEN,
            tags = mapOf(
                "build_flavor" to BuildConfig.FLAVOR,
                "build_type" to BuildConfig.BUILD_TYPE,
                "locale_lang_tag" to getLocaleTag(context),
            ),
            environment = BuildConfig.BUILD_TYPE,
            sendEventForNativeCrashes = false, // Do not send native crashes to Sentry
        )

        services.add(sentryService)
    }

    val socorroService = MozillaSocorroService(
        context,
        appName = "Focus",
        version = org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION,
        buildId = org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID,
        vendor = org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR,
        releaseChannel = org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL,
    )
    services.add(socorroService)

    val intent = Intent(context, MainActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
    }

    val crashReportingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        PendingIntent.FLAG_MUTABLE
    } else {
        0 // No flags. Default behavior.
    }

    val pendingIntent = PendingIntent.getActivity(
        context,
        0,
        intent,
        crashReportingIntentFlags,
    )

    return CrashReporter(
        context = context,
        services = services,
        telemetryServices = listOf(
            GleanCrashReporterService(
                context,
                appChannel = org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL,
                appVersion = org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION,
                appBuildId = org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID,
                // There's no need to call `CrashReporter.setTelemetryEnabled`: there's no path for the telemetry
                // setting to change (it is always false).
                isUploadEnabled = GleanMetricsService.isTelemetryEnabled(context),
            ),
        ),
        promptConfiguration = CrashReporter.PromptConfiguration(
            appName = context.resources.getString(R.string.app_name),
        ),
        shouldPrompt = CrashReporter.Prompt.ALWAYS,
        enabled = true,
        nonFatalCrashIntent = pendingIntent,
    )
}

private fun getLocaleTag(context: Context): String {
    val currentLocale = LocaleManager.getCurrentLocale(context)
    return if (currentLocale != null) {
        currentLocale.toLanguageTag()
    } else {
        Locale.getDefault().toLanguageTag()
    }
}

/**
 * Gets a [SearchEngineSelectorConfig] for the app and device.
 */
private fun getSearchEngineSelectorConfig(context: Context): SearchEngineSelectorConfig? {
    if (!context.settings.useRemoteSearchConfiguration) {
        return null
    }

    val updateChannel = when (BuildConfig.BUILD_TYPE) {
        "debug" -> SearchUpdateChannel.DEFAULT
        "nightly", "benchmark" -> SearchUpdateChannel.NIGHTLY
        "beta" -> SearchUpdateChannel.BETA
        "release" -> SearchUpdateChannel.RELEASE
        else -> {
            throw IllegalStateException("Unknown build type: ${BuildConfig.BUILD_TYPE}")
        }
    }

    val deviceType = if (context.isTablet()) {
        SearchDeviceType.TABLET
    } else {
        SearchDeviceType.SMARTPHONE
    }

    return SearchEngineSelectorConfig(
        appName = SearchApplicationName.FOCUS_ANDROID,
        appVersion = context.appVersionName,
        deviceType = deviceType,
        experiment = "",
        updateChannel = updateChannel,
        service = context.components.remoteSettingsService,
    )
}

/**
 * Returns the [Components] object from within a [Composable].
 */
val components: Components
    @Composable
    get() = LocalContext.current.components
