package dev.privacyllc.period

import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Inject

/**
 * The application, and WorkManager's entry point.
 *
 * `Configuration.Provider` replaces WorkManager's default initializer so the
 * reminder worker can be constructed by Hilt with the repository it needs. The
 * default initializer is removed in the manifest — leaving both in place gives
 * you a WorkManager that was already initialised before this ever runs, and a
 * worker that fails to instantiate with a message about a missing no-arg
 * constructor.
 */
@HiltAndroidApp
class PeriodApplication : Application(), Configuration.Provider {

    @Inject lateinit var workerFactory: HiltWorkerFactory

    @Inject lateinit var reminderCoordinator: dev.privacyllc.period.notifications.ReminderCoordinator

    /**
     * Lives as long as the process, because what it watches does.
     *
     * A forecast can move while no screen is open — the engine re-conditions on
     * a "Not yet" recorded from a notification action — and a reminder queued
     * against the old date is wrong from that moment. Tying this to a ViewModel
     * would mean it only ran while somebody was looking.
     */
    private val applicationScope = applicationScope()

    override fun onCreate() {
        super.onCreate()
        reminderCoordinator.start(applicationScope)
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            // Reminders must never write a cycle date to logcat — §45 applies to
            // background work exactly as much as to the UI, and a worker's log
            // is the kind nobody sees in testing.
            .setMinimumLoggingLevel(android.util.Log.WARN)
            .build()

    internal companion object {

        /**
         * The process-long scope, and the handler that keeps a throw from ending
         * the process.
         *
         * `SupervisorJob` is the trap this exists for. It stops a failing child
         * cancelling its siblings, and it is easy to read that as "failures are
         * contained here". It is not: an uncaught throw inside `launch` still
         * goes to the [CoroutineExceptionHandler] in the context, and with none
         * present it reaches the thread's default handler and takes the whole
         * process down.
         *
         * That matters more here than anywhere else in the app, because this is
         * the one scope that runs with **nobody watching**. `onCreate` runs in
         * every process, including the ones WorkManager starts after a reboot
         * and at the daily reminder — no Activity, no screen, nothing to show an
         * error. The symptom a user gets is reminders that stopped.
         *
         * Both ViewModels already install a handler; `PrivacyViewModel` records
         * the lesson in its own KDoc, learned by tapping a button twice. This is
         * the same lesson applied to the scope where it costs the most.
         *
         * ## Why the handler body is empty
         *
         * It cannot log. `checkNoHealthLogging` forbids logging calls in every
         * source file under `app`, and that rule is right here in particular:
         * a `Prediction` invariant failure is a plausible cause, and an
         * exception message on this path can carry a date derived from
         * someone's cycle. So this is a last-resort backstop whose entire job
         * is that the process survives.
         *
         * The place that degrades *usefully* is `ReminderCoordinator`, which
         * catches per-flow so one failing collection does not silently take the
         * other down with it.
         */
        fun applicationScope(): CoroutineScope = CoroutineScope(
            SupervisorJob() + Dispatchers.Default + CoroutineExceptionHandler { _, _ -> },
        )
    }
}
