package com.fabledsword.minstrel.player

import android.content.Context
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.util.BitmapLoader
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSourceBitmapLoader
import androidx.media3.datasource.cache.CacheDataSink
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.CacheBitmapLoader
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
import com.fabledsword.minstrel.player.output.ActiveUpnpHolder
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Builds the process-singleton player with our shared OkHttp +
 * SimpleCache chain. The MinstrelPlayerService (Phase 6.2) calls
 * `build()` once during onCreate.
 *
 * `simpleCache` is exposed as a val so the AudioCacheEvictionWorker
 * (Phase 12.3) can call `cache.removeSpan(...)` when evicting tracks
 * to disk-clear their cached spans.
 *
 * The default `LeastRecentlyUsedCacheEvictor` provides the floor LRU
 * (sizeBytes cap = rollingCap); our policy layer in the worker layers
 * the 2-bucket protection on top by feeding `removeSpan` only for
 * unprotected tracks.
 *
 * `build()` returns a [MinstrelForwardingPlayer] wrapping the internal
 * ExoPlayer. When the UPnP route drops (3 consecutive poll failures or
 * a SOAP failure), [dropEvents] emits the route name so the NowPlaying
 * surface can show a snackbar. The MutableSharedFlow uses DROP_OLDEST
 * with capacity=1 so a burst of failures during a single tear-down
 * surfaces as one event rather than queueing N.
 */
@Singleton
class PlayerFactory @Inject constructor(
    @ApplicationContext private val context: Context,
    private val okHttpClient: OkHttpClient,
    private val cacheConfig: CacheConfig,
    private val activeUpnpHolder: ActiveUpnpHolder,
    private val remoteState: RemotePlayerState,
    private val serverHealth: com.fabledsword.minstrel.connectivity.NetworkStatusController,
) {
    private val cacheDir: File = File(context.cacheDir, "audio_cache").apply { mkdirs() }

    val simpleCache: SimpleCache = SimpleCache(
        cacheDir,
        LeastRecentlyUsedCacheEvictor(cacheConfig.rollingCapBytes),
        StandaloneDatabaseProvider(context),
    )

    // MutableSharedFlow with extraBufferCapacity=1 + DROP_OLDEST so a burst
    // of drop events (rapid SOAP failures during a single tear-down) surfaces
    // as one snackbar rather than queueing N.
    private val dropEventsInternal = MutableSharedFlow<String>(
        replay = 0,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST,
    )
    val dropEvents: SharedFlow<String> = dropEventsInternal.asSharedFlow()

    fun build(): Player {
        val exo = buildExoPlayer()
        return MinstrelForwardingPlayer(
            delegate = exo,
            holder = activeUpnpHolder,
            remoteState = remoteState,
            castNetworkLock = CastNetworkLock(context),
            networkStatus = serverHealth,
            onDrop = { name -> emitDrop(name) },
        )
    }

    private fun buildExoPlayer(): ExoPlayer {
        val httpDataSource = OkHttpDataSource.Factory(okHttpClient)
        // Gate network reads on ServerHealth so a cache miss while offline
        // fails fast with an OfflineException instead of hitting an OkHttp
        // timeout. CacheDataSource only consults the upstream factory on a
        // cache miss, so playback of cached audio is unaffected.
        val gatedUpstream = OfflineGatedDataSourceFactory(httpDataSource, serverHealth)
        val cacheDataSource = CacheDataSource.Factory()
            .setCache(simpleCache)
            .setUpstreamDataSourceFactory(gatedUpstream)
            .setCacheWriteDataSinkFactory(
                CacheDataSink.Factory()
                    .setCache(simpleCache)
                    .setFragmentSize(C.LENGTH_UNSET.toLong()),
            )
        val mediaSourceFactory = DefaultMediaSourceFactory(context)
            .setDataSourceFactory(cacheDataSource)

        return ExoPlayer.Builder(context)
            .setMediaSourceFactory(mediaSourceFactory)
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(C.USAGE_MEDIA)
                    .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
                    .build(),
                /* handleAudioFocus = */ true,
            )
            .setHandleAudioBecomingNoisy(true)
            .build()
    }

    /**
     * BitmapLoader for the MediaSession's notification / lock-screen art.
     * Backed by the shared [okHttpClient] so it inherits the same
     * BaseUrlInterceptor placeholder rewrite + auth cookie that Coil uses
     * for in-app covers — without it the default DefaultHttpDataSource
     * loader can't resolve `http://placeholder.invalid/...` and would 401
     * on the cover endpoint. Wrapped in CacheBitmapLoader so a cover the
     * notification already fetched isn't re-loaded on every metadata
     * refresh. Lets the album-cover artworkUri set in
     * [PlayerController.toMediaItem] actually render on the media card.
     */
    fun buildBitmapLoader(): BitmapLoader =
        CacheBitmapLoader(
            DataSourceBitmapLoader.Builder(context)
                .setDataSourceFactory(OkHttpDataSource.Factory(okHttpClient))
                .build(),
        )

    private fun emitDrop(routeName: String) {
        dropEventsInternal.tryEmit(routeName)
    }
}
