package com.fabledsword.minstrel

import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import com.fabledsword.minstrel.cache.CacheIndexer
import com.fabledsword.minstrel.cache.mutations.MutationReplayer
import com.fabledsword.minstrel.cache.sync.SyncController
import com.fabledsword.minstrel.di.ApplicationScope
import com.fabledsword.minstrel.diagnostics.DiagnosticsReporter
import com.fabledsword.minstrel.diagnostics.DiagnosticsUploader
import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.events.LiveEventsDispatcher
import com.fabledsword.minstrel.metadata.FreshnessSweeper
import com.fabledsword.minstrel.player.AudioPrefetcher
import com.fabledsword.minstrel.player.CoverPrefetcher
import com.fabledsword.minstrel.player.PlayEventsReporter
import com.fabledsword.minstrel.player.PlaybackErrorReporter
import com.fabledsword.minstrel.player.ResumeController
import com.fabledsword.minstrel.update.data.UpdateBannerController
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject

// Cover-art fade-in. Coil skips the transition for memory-cache hits, so
// already-loaded art still appears instantly — only a genuine fetch fades.
private const val ART_CROSSFADE_MS = 220

@HiltAndroidApp
class MinstrelApplication :
    Application(),
    Configuration.Provider,
    SingletonImageLoader.Factory {

    @Inject lateinit var workerFactory: HiltWorkerFactory

    /**
     * The single shared OkHttp client (with the auth-cookie interceptor)
     * — also handed to Coil so cover-art requests carry the session
     * cookie and reuse the same connection pool + TLS config.
     */
    @Inject lateinit var okHttpClient: OkHttpClient

    /**
     * Injecting ResumeController forces Hilt to construct the singleton
     * so its init block (observe + persist on track change) wires up at
     * app launch. We also fire `restore()` from onCreate so a torn-down
     * player resumes the last queue without requiring UI interaction.
     */
    @Inject lateinit var resumeController: ResumeController

    /**
     * Same construct-the-singleton trick — SyncController's init block
     * subscribes to AuthStore.sessionCookie and fires `/api/library/sync`
     * whenever the cookie transitions to a value (fresh sign-in OR
     * cold start with persisted cookie). Without this @Inject the
     * singleton would never instantiate.
     */
    @Suppress("unused") @Inject lateinit var syncController: SyncController

    /**
     * Same construct-the-singleton trick — MutationReplayer's init
     * block subscribes to AuthStore.sessionCookie and drains the
     * offline write queue on every signed-in transition (sign-in OR
     * cold start with persisted cookie). Without this @Inject the
     * queue would only enqueue, never replay.
     */
    @Suppress("unused") @Inject lateinit var mutationReplayer: MutationReplayer

    /**
     * Same construct-the-singleton trick — PlayEventsReporter's init
     * block subscribes to PlayerController.uiState and reports the
     * play-event lifecycle (started/ended/skipped/offline) to the
     * server. Without this @Inject the singleton would never
     * instantiate and Android plays would never reach the server.
     */
    @Suppress("unused") @Inject lateinit var playEventsReporter: PlayEventsReporter

    /**
     * Same construct-the-singleton trick — PlaybackErrorReporter
     * subscribes to PlayerController.playbackErrorEvents and emits
     * coalesced "Couldn't play X" / "Skipped N tracks" messages on
     * its own Flow, which ShellScaffold surfaces in the shared
     * snackbar. Without this construct ExoPlayer skip-on-error is
     * invisible to the user.
     */
    @Suppress("unused") @Inject lateinit var playbackErrorReporter: PlaybackErrorReporter

    /**
     * Same construct-the-singleton trick — CoverPrefetcher's init
     * block subscribes to PlayerController.uiState and warms Coil's
     * cache with the next track's cover so track changes don't flash
     * a loading state.
     */
    @Suppress("unused") @Inject lateinit var coverPrefetcher: CoverPrefetcher

    /**
     * Same construct-the-singleton trick — CacheIndexer's init block
     * subscribes to PlayerController.uiState and records each played
     * track into `audio_cache_index`, giving the offline shuffle pools
     * and the cached indicator their data source. Without this @Inject
     * the index would stay empty and the offline pools would be inert.
     */
    @Suppress("unused") @Inject lateinit var cacheIndexer: CacheIndexer

    /**
     * Same construct-the-singleton trick — AudioPrefetcher's init
     * block subscribes to PlayerController.uiState +
     * AuthStore.cacheSettings and pins the next-N tracks into
     * SimpleCache via CacheWriter. Without this @Inject the
     * prefetchWindow setting would be inert and skips would re-fetch.
     */
    @Suppress("unused") @Inject lateinit var audioPrefetcher: AudioPrefetcher

    /**
     * Same construct-the-singleton trick — NetworkStatusController owns the
     * /healthz poll loop + the device-link collector + the reachability state
     * machine, and is the single authority on the tri-state ServerHealth
     * signal (plus the VersionTooOld byproduct). It must exist from launch so
     * the poll loop runs and the StateFlow stays warm for every consumer.
     */
    @Suppress("unused") @Inject lateinit var networkStatusController: NetworkStatusController

    /**
     * Same construct-the-singleton trick — UpdateBannerController polls
     * /api/client/version at launch + every 24h and drives the shell's
     * soft "update available" banner. Without this @Inject the poll
     * loop would never start and the banner would never surface.
     */
    @Suppress("unused") @Inject lateinit var updateBannerController: UpdateBannerController

    /**
     * Same construct-the-singleton trick — FreshnessSweeper's init
     * block starts a 30-min sweep loop that re-fetches cached
     * album/artist/track rows whose `fetchedAt` is older than 24h
     * via MetadataProvider, so the cache stays warm without
     * user-driven pull-to-refresh.
     */
    @Suppress("unused") @Inject lateinit var freshnessSweeper: FreshnessSweeper

    /**
     * Same construct-the-singleton trick — EventsStream's init block
     * subscribes to AuthStore.sessionCookie and opens / closes the
     * `/api/events/stream` SSE subscription on auth transitions.
     * Without this @Inject the events SharedFlow would have no
     * upstream and consumers would never see anything.
     */
    @Suppress("unused") @Inject lateinit var eventsStream: EventsStream

    /**
     * Same construct-the-singleton trick — LiveEventsDispatcher's
     * init block subscribes to EventsStream.events + the process
     * lifecycle and routes cross-screen state refreshes.
     */
    @Suppress("unused") @Inject lateinit var liveEventsDispatcher: LiveEventsDispatcher

    // Device diagnostics (M9). The reporter gates itself on the account's
    // debug flag; the uploader drains its buffer on a tick / recovery.
    @Suppress("unused") @Inject lateinit var diagnosticsReporter: DiagnosticsReporter
    @Suppress("unused") @Inject lateinit var diagnosticsUploader: DiagnosticsUploader

    @Inject @ApplicationScope lateinit var appScope: CoroutineScope

    override fun onCreate() {
        super.onCreate()
        // Debug builds get the full DebugTree (verbose). Release builds
        // get a WARN+ tree so operator-driven diagnosis via `adb logcat`
        // still surfaces UPnP / cast failures, OkHttp errors, and our
        // own Timber.w / Timber.e calls — without the chatty DEBUG /
        // INFO traffic flooding the buffer in production.
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(ReleaseTree())
        }
        appScope.launch { resumeController.restore() }
    }

    /**
     * Release-build Timber tree: emits at WARN and above only.
     * `android.util.Log` with the canonical tag so `adb logcat` shows
     * the line under the standard tag column without falling through
     * to the package-stack-trace tag DebugTree produces.
     */
    private class ReleaseTree : Timber.Tree() {
        override fun isLoggable(tag: String?, priority: Int): Boolean =
            priority >= android.util.Log.WARN

        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
            val resolvedTag = tag ?: "Minstrel"
            if (t == null) {
                android.util.Log.println(priority, resolvedTag, message)
            } else {
                android.util.Log.println(
                    priority,
                    resolvedTag,
                    message + '\n' + android.util.Log.getStackTraceString(t),
                )
            }
        }
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()

    /**
     * Builds the process-singleton Coil ImageLoader with our shared
     * OkHttp client as the network fetcher. The `callFactory` lambda
     * is invoked lazily so Hilt has time to inject `okHttpClient`
     * before Coil makes its first request.
     *
     * Crossfade is set here rather than per-call so every cover surface
     * in the app fades its artwork in instead of snapping it. Art
     * landing a beat after its tile was the most visible pop-in on Home
     * (issue #2327); `ServerImage` fades its placeholder out over the
     * same window so the two read as one cross-dissolve.
     */
    override fun newImageLoader(context: android.content.Context): ImageLoader =
        ImageLoader.Builder(context)
            .components {
                add(OkHttpNetworkFetcherFactory(callFactory = { okHttpClient }))
            }
            .crossfade(ART_CROSSFADE_MS)
            .build()
}
