package com.fabledsword.minstrel.player

import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import com.fabledsword.minstrel.MainActivity
import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.likes.data.LikesRepository.Companion.ENTITY_TRACK
import com.google.common.collect.ImmutableList
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
 * The app's foreground media-playback service. Replaces what the Flutter
 * audio_service plugin wrapped — Media3 owns the foreground lifecycle,
 * the MediaSession, the notification card, lock-screen surface, BT/AVRCP
 * routing, Pixel Watch tile, and Android Auto adapter natively.
 *
 * Manifest declares this with `foregroundServiceType="mediaPlayback"`
 * plus the standard `MediaSessionService` intent-filter — Media3's
 * MediaButtonReceiver is registered automatically by the library, no
 * manual receiver class needed.
 *
 * The session advertises a single custom CommandButton — the like/heart
 * toggle. [LikeMediaCallback] grants the command in `onConnect` and
 * routes taps through [LikesRepository.toggleLike] so they inherit the
 * offline-resilient MutationQueue path. The icon mirrors server state:
 * a service-scoped coroutine collects `currentMediaItem × isLiked` and
 * rebuilds the preferences list on each emission so a cross-device
 * like (web tap) flips the notification heart automatically.
 *
 * Lifecycle:
 *   - onCreate: build the ExoPlayer + MediaSession once, attach the
 *     callback, set initial preferences, launch the like-state job.
 *   - onGetSession: return the live session to any binding controller
 *     (system UI media card, Wear OS companion, MediaController3 clients).
 *   - onTaskRemoved: if the user swipes the app away, keep playing when
 *     audio is active (standard media-app behavior — music shouldn't
 *     die because the app left recents); otherwise stop the service so
 *     the lingering notification clears.
 *   - onDestroy: cancel the service scope, then release the session +
 *     player. Cancellation comes first so the like-state job doesn't
 *     touch a released session.
 */
@AndroidEntryPoint
class MinstrelPlayerService : MediaSessionService() {

    @Inject lateinit var playerFactory: PlayerFactory

    @Inject lateinit var likesRepository: LikesRepository

    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player: Player = playerFactory.build()
        val callback = LikeMediaCallback(likesRepository, serviceScope)
        val session = MediaSession.Builder(this, player)
            .setSessionActivity(buildNowPlayingPendingIntent())
            .setCallback(callback)
            .setBitmapLoader(playerFactory.buildBitmapLoader())
            .setMediaButtonPreferences(ImmutableList.of(buildLikeButton(isLiked = false)))
            .build()
        mediaSession = session
        serviceScope.launch { observeLikeState(session, player) }
    }

    /**
     * Intent that the media notification / lock-screen card / Pixel
     * Watch tile launches when tapped. Carries an extra MainActivity
     * consumes to navigate the NavController straight to the full
     * NowPlaying screen — without it, Media3 defaults to the launcher
     * activity entry point, which lands on whatever shell route
     * MainActivity last rendered.
     *
     * FLAG_ACTIVITY_SINGLE_TOP keeps the existing MainActivity instance
     * alive when one is already foregrounded; onNewIntent fires and
     * picks up the fresh extra. FLAG_IMMUTABLE is required on
     * Android 12+; FLAG_UPDATE_CURRENT keeps the extra in sync if the
     * Builder rebuilds the PendingIntent.
     */
    private fun buildNowPlayingPendingIntent(): PendingIntent {
        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
            putExtra(MainActivity.EXTRA_OPEN_NOW_PLAYING, true)
        }
        return PendingIntent.getActivity(
            this,
            REQUEST_OPEN_NOW_PLAYING,
            intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
        )
    }

    /**
     * Build the like/heart CommandButton. Icon flips between
     * ICON_HEART_FILLED and ICON_HEART_UNFILLED to mirror server-side
     * like state. The session command is the same in both states so
     * the callback's onCustomCommand handles them identically (it
     * inverts the current observed state regardless of which icon was
     * tapped).
     */
    private fun buildLikeButton(isLiked: Boolean): CommandButton {
        val icon = if (isLiked) {
            CommandButton.ICON_HEART_FILLED
        } else {
            CommandButton.ICON_HEART_UNFILLED
        }
        return CommandButton.Builder(icon)
            .setDisplayName(if (isLiked) "Unlike" else "Like")
            .setSessionCommand(SessionCommand(LikeMediaCallback.CMD_TOGGLE_LIKE, Bundle.EMPTY))
            .build()
    }

    /**
     * Collect player.currentMediaItem changes (via a Player.Listener
     * lifted into a Flow) and, for each non-null mediaId, observe its
     * liked state. On every emission, rebuild the session's media
     * button preferences with the icon flipped accordingly.
     * flatMapLatest cancels the previous track's subscription so we
     * never leak Flows across track transitions.
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    private suspend fun observeLikeState(session: MediaSession, player: Player) {
        currentMediaIdFlow(player)
            .flatMapLatest { mediaId ->
                if (mediaId == null) {
                    flowOf(false)
                } else {
                    likesRepository.observeIsLiked(ENTITY_TRACK, mediaId)
                }
            }
            .collect { isLiked ->
                session.setMediaButtonPreferences(
                    ImmutableList.of(buildLikeButton(isLiked = isLiked)),
                )
            }
    }

    /**
     * Lift Player.currentMediaItem changes into a Flow. Emits the
     * current mediaId on subscription so the initial icon state is
     * correct on the first frame the controller renders (no
     * unfilled-then-flips flicker).
     */
    private fun currentMediaIdFlow(player: Player) = callbackFlow<String?> {
        val listener = object : Player.Listener {
            override fun onMediaItemTransition(item: MediaItem?, reason: Int) {
                trySend(item?.mediaId)
            }
        }
        player.addListener(listener)
        awaitClose { player.removeListener(listener) }
    }.onStart { emit(player.currentMediaItem?.mediaId) }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
        mediaSession

    override fun onTaskRemoved(rootIntent: Intent?) {
        val player = mediaSession?.player ?: return super.onTaskRemoved(rootIntent)
        // player.isPlaying is overridden on MinstrelForwardingPlayer to return
        // remoteState.isPlaying while UPnP is active, so a swipe-away with
        // Sonos playing keeps the service (and its UPnP poll loop) alive.
        val activelyPlaying = player.isPlaying ||
            (player.playWhenReady && player.playbackState != Player.STATE_ENDED)
        if (!activelyPlaying) {
            stopSelf()
        }
        super.onTaskRemoved(rootIntent)
    }

    override fun onDestroy() {
        serviceScope.cancel()
        mediaSession?.run {
            player.release()
            release()
        }
        mediaSession = null
        super.onDestroy()
    }

    private companion object {
        // Stable request code for the session-activity PendingIntent.
        // Arbitrary but distinct from any other PendingIntent we ever
        // build against MainActivity.
        const val REQUEST_OPEN_NOW_PLAYING = 0x100
    }
}
