package com.peterino.blastoise

import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import kotlin.math.abs
import kotlin.random.Random

@UnstableApi
class PlaybackService : MediaSessionService(), MusicRoomClient.Listener {
    private val mainHandler = Handler(Looper.getMainLooper())
    private lateinit var client: MusicRoomClient
    private lateinit var player: ExoPlayer
    private var mediaSession: MediaSession? = null

    private var sourceMode = PlaybackSourceMode.RADIO
    private var authState = AuthState.CHECKING
    private var currentUser: UserSession? = null
    private var lastStatus = "Starting"
    private var lastChannels: List<ChannelInfo> = emptyList()
    private var libraryTracks: List<Track> = emptyList()
    private var libraryLoaded = false
    private var myPlaylists: List<Playlist> = emptyList()
    private var sharedPlaylists: List<Playlist> = emptyList()
    private var playlistsLoaded = false
    private var selectedPlaylistId: String? = null
    private var selectedPlaylist: Playlist? = null
    private var currentChannelId: String? = null
    private var currentTrackId: String? = null
    private var localLibraryIndex = -1
    private var lastPaused = true
    private var lastQueue: List<Track> = emptyList()
    private var lastQueueLoaded = false
    private var lastCurrentIndex = 0
    private var playbackMode = "repeat-all"
    private var lastChannelName = "No channel"
    private var lastTrackTitle = "No track"
    private var lastTrackDuration = 0.0
    private var lastServerTimestampMs = 0L
    private var lastStateRealtimeMs = 0L
    private var intentionalDisconnect = false
    private var reconnectAttempts = 0
    private val debugEvents = mutableListOf<String>()
    private val playbackModes = listOf("once", "repeat-all", "repeat-one", "shuffle")

    private val ticker = object : Runnable {
        override fun run() {
            tickPlayback()
            mainHandler.postDelayed(this, 500)
        }
    }

    private val reconnectRunnable = Runnable {
        val channelId = currentChannelId
        if (sourceMode == PlaybackSourceMode.RADIO && channelId != null && !intentionalDisconnect) {
            addDebugEvent("reconnect channel=$channelId attempt=${reconnectAttempts + 1}")
            reconnectAttempts += 1
            client.connectChannel(channelId)
            publishSnapshot()
        }
    }

    override fun onCreate() {
        super.onCreate()
        SessionStore.load(this)
        client = MusicRoomClient(this, this)

        val httpDataSourceFactory = DataSource.Factory {
            val requestProperties = mutableMapOf<String, String>()
            if (SessionStore.cookieHeader.isNotBlank()) {
                requestProperties["Cookie"] = SessionStore.cookieHeader
            }

            DefaultHttpDataSource.Factory()
                .setUserAgent(SessionStore.userAgent)
                .setAllowCrossProtocolRedirects(true)
                .setDefaultRequestProperties(requestProperties)
                .createDataSource()
        }

        val mediaSourceFactory = DefaultMediaSourceFactory(this)
            .setDataSourceFactory(httpDataSourceFactory)

        val audioAttributes = AudioAttributes.Builder()
            .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
            .setUsage(C.USAGE_MEDIA)
            .build()

        player = ExoPlayer.Builder(this)
            .setMediaSourceFactory(mediaSourceFactory)
            .setWakeMode(C.WAKE_MODE_LOCAL)
            .setAudioAttributes(audioAttributes, true)
            .setHandleAudioBecomingNoisy(true)
            .build()
            .apply {
                addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        if (playbackState == Player.STATE_ENDED) {
                            handlePlaybackEnded()
                        }
                        publishSnapshot()
                    }

                    override fun onIsPlayingChanged(isPlaying: Boolean) {
                        if (sourceMode == PlaybackSourceMode.LIBRARY) {
                            lastPaused = !isPlaying
                        }
                        publishSnapshot()
                    }
                })
            }

        mediaSession = MediaSession.Builder(this, player).build()
        PlaybackBridge.service = this
        addDebugEvent("service created")
        publishSnapshot()
        validateSession()
        mainHandler.post(ticker)
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        if (!player.isPlaying) {
            pauseAllPlayersAndStopSelf()
        }
    }

    override fun onDestroy() {
        mainHandler.removeCallbacks(ticker)
        mainHandler.removeCallbacks(reconnectRunnable)
        intentionalDisconnect = true
        client.close()
        mediaSession?.run {
            player.release()
            release()
        }
        mediaSession = null
        PlaybackBridge.service = null
        super.onDestroy()
    }

    fun validateSession() = onMain {
        if (SessionStore.cookieHeader.isBlank()) {
            authState = AuthState.SIGNED_OUT
            currentUser = null
            lastStatus = "Sign in required"
            publishSnapshot()
            return@onMain
        }
        authState = AuthState.CHECKING
        lastStatus = "Checking session"
        publishSnapshot()
        client.loadMe()
    }

    fun signIn(serverUrl: String, username: String, password: String) = onMain {
        SessionStore.setBaseUrl(serverUrl)
        SessionStore.save(this)
        authState = AuthState.CHECKING
        lastStatus = "Signing in"
        publishSnapshot()
        client.login(username.trim(), password)
    }

    fun signUp(serverUrl: String, username: String, password: String) = onMain {
        SessionStore.setBaseUrl(serverUrl)
        SessionStore.save(this)
        authState = AuthState.CHECKING
        lastStatus = "Signing up"
        publishSnapshot()
        client.signup(username.trim(), password)
    }

    fun signOut() = onMain {
        authState = AuthState.SIGNED_OUT
        currentUser = null
        intentionalDisconnect = true
        client.close()
        player.pause()
        player.clearMediaItems()
        currentTrackId = null
        myPlaylists = emptyList()
        sharedPlaylists = emptyList()
        playlistsLoaded = false
        selectedPlaylistId = null
        selectedPlaylist = null
        lastTrackTitle = "Sign in to join a room"
        lastTrackDuration = 0.0
        lastPaused = true
        lastStatus = "Signing out"
        publishSnapshot()
        client.logout()
    }

    fun connectToServer(input: String = SessionStore.serverBaseUrl) = onMain {
        SessionStore.setBaseUrl(input)
        SessionStore.save(this)
        if (!hasSignedInUser()) {
            validateSession()
            return@onMain
        }
        intentionalDisconnect = true
        client.close()
        intentionalDisconnect = false
        reconnectAttempts = 0
        sourceMode = PlaybackSourceMode.RADIO
        currentChannelId = null
        currentTrackId = null
        localLibraryIndex = -1
        lastQueue = emptyList()
        lastQueueLoaded = false
        myPlaylists = emptyList()
        sharedPlaylists = emptyList()
        playlistsLoaded = false
        selectedPlaylistId = null
        selectedPlaylist = null
        lastCurrentIndex = 0
        playbackMode = "repeat-all"
        lastChannelName = "No channel"
        lastTrackTitle = "No track"
        lastTrackDuration = 0.0
        lastServerTimestampMs = 0L
        lastStateRealtimeMs = 0L
        lastStatus = "Loading"
        addDebugEvent("connect ${SessionStore.serverBaseUrl}")
        publishSnapshot()
        client.loadChannels()
    }

    fun loadLibrary() = onMain {
        if (!requireSignedIn("Sign in to browse the library")) return@onMain
        client.loadLibrary()
    }

    fun loadPlaylists() = onMain {
        if (!requireSignedIn("Sign in to load playlists")) return@onMain
        client.loadPlaylists()
    }

    fun loadPlaylist(playlistId: String) = onMain {
        if (!requireSignedIn("Sign in to load playlists")) return@onMain
        selectedPlaylistId = playlistId
        selectedPlaylist = null
        publishSnapshot()
        client.loadPlaylist(playlistId)
    }

    fun ensureRadioMode() = onMain {
        if (!requireSignedIn("Sign in to join rooms")) return@onMain
        sourceMode = PlaybackSourceMode.RADIO
        intentionalDisconnect = false
        val target = currentChannelId
            ?: lastChannels.firstOrNull { it.isDefault }?.id
            ?: lastChannels.firstOrNull()?.id
        if (target != null && currentTrackId == null) {
            joinChannel(target)
        } else {
            publishSnapshot()
        }
    }

    fun enterLibraryMode() = onMain {
        if (!requireSignedIn("Sign in to browse the library")) return@onMain
        lastStatus = if (libraryLoaded) "Library search" else "Loading library"
        publishSnapshot()
        if (!libraryLoaded) client.loadLibrary()
    }

    fun joinChannel(channelId: String) = onMain {
        if (!requireSignedIn("Sign in to join rooms")) return@onMain
        sourceMode = PlaybackSourceMode.RADIO
        intentionalDisconnect = false
        mainHandler.removeCallbacks(reconnectRunnable)
        currentChannelId = channelId
        localLibraryIndex = -1
        lastStatus = "Joining"
        addDebugEvent("join channel=$channelId")
        publishSnapshot()
        client.connectChannel(channelId)
    }

    fun playLibraryTrack(index: Int) = onMain {
        if (index !in libraryTracks.indices) return@onMain
        sourceMode = PlaybackSourceMode.LIBRARY
        intentionalDisconnect = true
        client.close()
        val track = libraryTracks[index]
        localLibraryIndex = index
        currentTrackId = track.id
        lastTrackTitle = track.title
        lastTrackDuration = track.duration
        lastChannelName = "Library"
        lastPaused = false
        lastStatus = "Local"
        lastServerTimestampMs = 0L
        lastStateRealtimeMs = 0L
        player.setMediaItem(buildMediaItem(track), 0L)
        applyLocalPlaybackMode()
        player.prepare()
        player.play()
        addDebugEvent("local play ${track.title.take(40)}")
        publishSnapshot()
    }

    fun togglePlay() = onMain {
        if (sourceMode == PlaybackSourceMode.RADIO) {
            client.sendAction(if (lastPaused) "unpause" else "pause")
            return@onMain
        }
        if (player.isPlaying) {
            player.pause()
            lastPaused = true
        } else {
            if (localLibraryIndex < 0 && libraryTracks.isNotEmpty()) {
                playLibraryTrack(0)
                return@onMain
            }
            player.play()
            lastPaused = false
        }
        publishSnapshot()
    }

    fun previous() = onMain {
        if (sourceMode == PlaybackSourceMode.RADIO) {
            jumpRadio(-1)
        } else if (libraryTracks.isNotEmpty()) {
            val nextIndex = if (playbackMode == "shuffle" && libraryTracks.size > 1) {
                randomLibraryIndex()
            } else if (localLibraryIndex <= 0) {
                libraryTracks.lastIndex
            } else {
                localLibraryIndex - 1
            }
            playLibraryTrack(nextIndex)
        }
    }

    fun next() = onMain {
        if (sourceMode == PlaybackSourceMode.RADIO) {
            jumpRadio(1)
        } else if (libraryTracks.isNotEmpty()) {
            playLibraryTrack(nextLibraryIndex())
        }
    }

    fun seekBy(seconds: Int) = onMain {
        val durationMs = when {
            player.duration > 0 -> player.duration
            lastTrackDuration > 0.0 -> (lastTrackDuration * 1000.0).toLong()
            else -> Long.MAX_VALUE
        }
        val targetMs = (player.currentPosition + seconds * 1000L)
            .coerceAtLeast(0L)
            .coerceAtMost(durationMs)
        seekTo(targetMs / 1000.0)
    }

    fun seekTo(seconds: Double) = onMain {
        val targetMs = (seconds * 1000.0).toLong().coerceAtLeast(0L)
        if (sourceMode == PlaybackSourceMode.RADIO) {
            client.sendSeek(seconds)
        } else {
            player.seekTo(targetMs)
        }
        addDebugEvent("seek ${"%.1f".format(seconds)}s")
        publishSnapshot()
    }

    fun stopPlayback() = onMain {
        player.pause()
        player.seekTo(0L)
        lastPaused = true
        if (sourceMode == PlaybackSourceMode.RADIO) {
            client.sendAction("pause")
            client.sendSeek(0.0)
        }
        addDebugEvent("stop")
        publishSnapshot()
    }

    fun stopPlaybackAndExit() = onMain {
        intentionalDisconnect = true
        mainHandler.removeCallbacks(reconnectRunnable)
        client.close()
        player.pause()
        player.stop()
        player.clearMediaItems()
        currentTrackId = null
        localLibraryIndex = -1
        lastPaused = true
        lastTrackTitle = "Stopped"
        lastTrackDuration = 0.0
        lastServerTimestampMs = 0L
        lastStateRealtimeMs = 0L
        lastStatus = "Stopped"
        addDebugEvent("stop and exit")
        publishSnapshot()
        pauseAllPlayersAndStopSelf()
        stopSelf()
    }

    fun cyclePlaybackMode() = onMain {
        if (!requireSignedIn("Sign in to control rooms")) return@onMain
        val currentIndex = playbackModes.indexOf(playbackMode).coerceAtLeast(0)
        val nextMode = playbackModes[(currentIndex + 1) % playbackModes.size]
        playbackMode = nextMode
        if (sourceMode == PlaybackSourceMode.RADIO) {
            val channelId = currentChannelId ?: run {
                lastStatus = "No channel"
                publishSnapshot()
                return@onMain
            }
            client.setPlaybackMode(channelId, nextMode)
        } else {
            applyLocalPlaybackMode()
            lastStatus = "Mode ${modeLabel(nextMode)}"
        }
        publishSnapshot()
    }

    fun queueCurrentTrack(playNext: Boolean) = onMain {
        val trackId = currentTrackId ?: libraryTracks.getOrNull(localLibraryIndex)?.id
        if (trackId == null) {
            lastStatus = "No track"
            publishSnapshot()
            return@onMain
        }
        queueTrackIds(listOf(trackId), playNext)
    }

    fun queueTrack(track: Track, playNext: Boolean) = onMain {
        queueTrackIds(listOf(track.id), playNext)
    }

    fun addPlaylistToQueue(playlist: Playlist, playNext: Boolean) = onMain {
        if (playlist.trackIds.isEmpty()) {
            lastStatus = "Empty playlist"
            publishSnapshot()
            return@onMain
        }
        queueTrackIds(playlist.trackIds, playNext)
    }

    fun queueTrackIds(trackIds: List<String>, playNext: Boolean) = onMain {
        if (!requireSignedIn("Sign in to change the room queue")) return@onMain
        val channelId = currentChannelId
            ?: lastChannels.firstOrNull { it.isDefault }?.id
            ?: lastChannels.firstOrNull()?.id
        if (channelId == null) {
            lastStatus = "No channel"
            publishSnapshot()
            return@onMain
        }
        currentChannelId = channelId
        val insertAt = if (playNext) (lastCurrentIndex + 1).coerceAtLeast(0) else null
        client.addTracksToQueue(channelId, trackIds, insertAt)
        publishSnapshot()
    }

    fun jumpToQueueIndex(index: Int) = onMain {
        if (!requireSignedIn("Sign in to control rooms")) return@onMain
        if (index !in lastQueue.indices) return@onMain
        client.sendJump(index)
        lastStatus = "Jumping"
        publishSnapshot()
    }

    fun removeQueueIndex(index: Int) = onMain {
        if (!requireSignedIn("Sign in to change the room queue")) return@onMain
        val channelId = currentChannelId ?: run {
            lastStatus = "No room"
            publishSnapshot()
            return@onMain
        }
        if (index !in lastQueue.indices) return@onMain
        client.removeTracksFromQueue(channelId, listOf(index))
        publishSnapshot()
    }

    override fun onStatus(message: String) {
        onMain {
            if (sourceMode == PlaybackSourceMode.LIBRARY && message == "Disconnected") {
                lastStatus = if (libraryLoaded) "Library" else "Loading library"
                publishSnapshot()
                return@onMain
            }
            lastStatus = message
            if (message == "Disconnected" && sourceMode == PlaybackSourceMode.RADIO && !intentionalDisconnect) {
                scheduleReconnect()
            }
            publishSnapshot()
        }
    }

    override fun onDebug(message: String) {
        onMain {
            addDebugEvent(message)
            publishSnapshot()
        }
    }

    override fun onError(message: String) {
        onMain {
            if (sourceMode == PlaybackSourceMode.LIBRARY && intentionalDisconnect) {
                lastStatus = if (libraryLoaded) "Library" else "Loading library"
                addDebugEvent("local mode ignored websocket error: $message")
                publishSnapshot()
                return@onMain
            }
            lastStatus = if (
                !hasSignedInUser() &&
                (message.contains("401") || message.contains("403") || message.contains("denied", ignoreCase = true))
            ) {
                "Sign in required"
            } else {
                message
            }
            addDebugEvent("ERROR $message")
            if (sourceMode == PlaybackSourceMode.RADIO && !intentionalDisconnect) {
                scheduleReconnect()
            }
            publishSnapshot()
        }
    }

    override fun onChannels(channels: List<ChannelInfo>) {
        onMain {
            if (!hasSignedInUser()) {
                lastStatus = "Sign in required"
                publishSnapshot()
                return@onMain
            }
            lastChannels = channels
            val target = currentChannelId
                ?: channels.firstOrNull { it.isDefault }?.id
                ?: channels.firstOrNull()?.id
            if (target != null && currentChannelId == null && sourceMode == PlaybackSourceMode.RADIO) {
                joinChannel(target)
            } else {
                publishSnapshot()
            }
        }
    }

    override fun onLibrary(tracks: List<Track>) {
        onMain {
            libraryTracks = tracks
            libraryLoaded = true
            if (sourceMode == PlaybackSourceMode.LIBRARY && lastTrackTitle == "No track") {
                lastStatus = "Library ready"
            }
            publishSnapshot()
        }
    }

    override fun onPlaylists(playlists: PlaylistBundle) {
        onMain {
            myPlaylists = playlists.mine
            sharedPlaylists = playlists.shared
            playlistsLoaded = true
            val all = allPlaylists()
            if (selectedPlaylistId == null || all.none { it.id == selectedPlaylistId }) {
                selectedPlaylistId = all.firstOrNull()?.id
                selectedPlaylist = null
            }
            selectedPlaylistId?.let { client.loadPlaylist(it) }
            publishSnapshot()
        }
    }

    override fun onPlaylistDetail(playlist: Playlist) {
        onMain {
            selectedPlaylist = playlist
            selectedPlaylistId = playlist.id
            publishSnapshot()
        }
    }

    override fun onSwitched(channelId: String) {
        onMain {
            currentChannelId = channelId
            lastStatus = "Tuned"
            publishSnapshot()
        }
    }

    override fun onAuth(user: UserSession?) {
        onMain {
            currentUser = user
            authState = when {
                user == null -> AuthState.SIGNED_OUT
                user.isGuest -> AuthState.GUEST
                else -> AuthState.SIGNED_IN
            }
            if (!hasSignedInUser()) {
                intentionalDisconnect = true
                client.close()
                player.pause()
                lastPaused = true
                lastStatus = if (authState == AuthState.GUEST) "Sign in required" else "Signed out"
                addDebugEvent("auth blocked guest=${user?.isGuest == true}")
                publishSnapshot()
                return@onMain
            }

            val signedInUser = currentUser ?: return@onMain
            intentionalDisconnect = false
            reconnectAttempts = 0
            lastStatus = "Signed in as ${signedInUser.username}"
            addDebugEvent("auth user=${signedInUser.username}")
            publishSnapshot()
            client.loadChannels()
            if (!libraryLoaded) client.loadLibrary()
            if (!playlistsLoaded) client.loadPlaylists()
        }
    }

    override fun onChannelState(state: ChannelState) {
        onMain {
            if (sourceMode != PlaybackSourceMode.RADIO) return@onMain
            reconnectAttempts = 0
            mainHandler.removeCallbacks(reconnectRunnable)
            currentChannelId = state.channelId.ifBlank { currentChannelId }
            lastChannelName = state.channelName.ifBlank { "Channel" }
            lastPaused = state.paused
            playbackMode = state.playbackMode.ifBlank { playbackMode }
            if (state.queue != null) {
                lastQueue = state.queue
                lastQueueLoaded = true
            }
            lastCurrentIndex = state.currentIndex
            lastServerTimestampMs = (state.currentTimestamp * 1000.0).toLong().coerceAtLeast(0L)
            lastStateRealtimeMs = SystemClock.elapsedRealtime()

            val track = state.track
            if (track == null) {
                currentTrackId = null
                lastTrackTitle = "No track"
                lastTrackDuration = 0.0
                player.pause()
                publishSnapshot()
                return@onMain
            }

            lastTrackTitle = track.title
            lastTrackDuration = track.duration

            val targetPositionMs = lastServerTimestampMs
            if (currentTrackId != track.id) {
                currentTrackId = track.id
                player.setMediaItem(buildMediaItem(track), targetPositionMs)
                player.prepare()
                if (state.paused) player.pause() else player.play()
                publishSnapshot()
                return@onMain
            }

            val driftMs = abs(player.currentPosition - targetPositionMs)
            if (driftMs >= 2000L) {
                player.seekTo(targetPositionMs)
            }

            if (state.paused && player.isPlaying) {
                player.pause()
            } else if (!state.paused && !player.isPlaying) {
                player.play()
            }
            publishSnapshot()
        }
    }

    private fun tickPlayback() {
        if (sourceMode == PlaybackSourceMode.RADIO &&
            currentTrackId != null &&
            !lastPaused &&
            player.playbackState == Player.STATE_READY
        ) {
            val driftMs = player.currentPosition - expectedPositionMs()
            if (abs(driftMs) >= 2000L) {
                player.seekTo(expectedPositionMs())
                addDebugEvent("service drift correction ${driftMs}ms")
            }
        }

        if (sourceMode == PlaybackSourceMode.LIBRARY) {
            lastPaused = !player.isPlaying
            if (player.duration > 0 && lastTrackDuration <= 0.0) {
                lastTrackDuration = player.duration / 1000.0
            }
        }

        publishSnapshot()
    }

    private fun handlePlaybackEnded() {
        if (sourceMode != PlaybackSourceMode.LIBRARY) return
        when (playbackMode) {
            "repeat-one" -> {
                player.seekTo(0L)
                player.play()
            }
            "repeat-all", "shuffle" -> {
                if (libraryTracks.isNotEmpty()) playLibraryTrack(nextLibraryIndex())
            }
            else -> {
                lastPaused = true
                publishSnapshot()
            }
        }
    }

    private fun scheduleReconnect() {
        if (currentChannelId == null) return
        mainHandler.removeCallbacks(reconnectRunnable)
        val delayMs = (3000L * (reconnectAttempts + 1)).coerceAtMost(30000L)
        addDebugEvent("schedule reconnect in ${delayMs}ms")
        mainHandler.postDelayed(reconnectRunnable, delayMs)
    }

    private fun jumpRadio(delta: Int) {
        if (lastQueue.isEmpty()) return
        val target = (lastCurrentIndex + delta + lastQueue.size) % lastQueue.size
        client.sendJump(target)
    }

    private fun applyLocalPlaybackMode() {
        player.repeatMode = if (playbackMode == "repeat-one") {
            Player.REPEAT_MODE_ONE
        } else {
            Player.REPEAT_MODE_OFF
        }
        player.shuffleModeEnabled = playbackMode == "shuffle"
    }

    private fun nextLibraryIndex(): Int {
        if (libraryTracks.isEmpty()) return -1
        if (playbackMode == "shuffle" && libraryTracks.size > 1) return randomLibraryIndex()
        return if (localLibraryIndex < 0) 0 else (localLibraryIndex + 1) % libraryTracks.size
    }

    private fun randomLibraryIndex(): Int {
        if (libraryTracks.size <= 1) return 0
        var next = Random.nextInt(libraryTracks.size)
        while (next == localLibraryIndex) {
            next = Random.nextInt(libraryTracks.size)
        }
        return next
    }

    private fun modeLabel(mode: String): String {
        return when (mode) {
            "once" -> "LOOP OFF"
            "repeat-all" -> "LOOP ALL"
            "repeat-one" -> "LOOP ONE"
            "shuffle" -> "SHUFFLE"
            else -> mode.uppercase()
        }
    }

    private fun allPlaylists(): List<Playlist> {
        return myPlaylists + sharedPlaylists
    }

    private fun hasSignedInUser(): Boolean {
        return authState == AuthState.SIGNED_IN && currentUser?.isSignedIn == true
    }

    private fun requireSignedIn(message: String): Boolean {
        if (hasSignedInUser()) return true
        lastStatus = message
        addDebugEvent("auth required")
        publishSnapshot()
        return false
    }

    private fun buildMediaItem(track: Track): MediaItem {
        val metadata = MediaMetadata.Builder()
            .setTitle(track.title)
            .setArtist(if (sourceMode == PlaybackSourceMode.RADIO) lastChannelName else "Blastoise Library")
            .build()
        return MediaItem.Builder()
            .setMediaId(track.id)
            .setUri(SessionStore.trackUrl(track.id))
            .setMediaMetadata(metadata)
            .build()
    }

    private fun expectedPositionMs(): Long {
        if (sourceMode == PlaybackSourceMode.LIBRARY) return player.currentPosition
        if (lastStateRealtimeMs == 0L) return 0L
        return if (lastPaused) {
            lastServerTimestampMs
        } else {
            lastServerTimestampMs + (SystemClock.elapsedRealtime() - lastStateRealtimeMs)
        }
    }

    private fun publishSnapshot() {
        val expectedMs = expectedPositionMs()
        val playerMs = player.currentPosition
        val driftMs = if (sourceMode == PlaybackSourceMode.RADIO && currentTrackId != null) playerMs - expectedMs else 0L
        PlaybackBridge.publish(
            PlaybackSnapshot(
                sourceMode = sourceMode,
                authState = authState,
                currentUser = currentUser,
                status = lastStatus,
                channels = lastChannels,
                libraryTracks = libraryTracks,
                libraryLoaded = libraryLoaded,
                myPlaylists = myPlaylists,
                sharedPlaylists = sharedPlaylists,
                playlistsLoaded = playlistsLoaded,
                selectedPlaylistId = selectedPlaylistId,
                selectedPlaylist = selectedPlaylist,
                currentChannelId = currentChannelId,
                currentTrackId = currentTrackId,
                localLibraryIndex = localLibraryIndex,
                currentRoomListeners = lastChannels.firstOrNull { it.id == currentChannelId }?.listeners.orEmpty(),
                paused = lastPaused,
                queue = lastQueue,
                queueLoaded = lastQueueLoaded,
                currentIndex = lastCurrentIndex,
                playbackMode = playbackMode,
                channelName = lastChannelName,
                trackTitle = lastTrackTitle,
                trackDuration = lastTrackDuration,
                serverTimestampMs = lastServerTimestampMs,
                stateRealtimeMs = lastStateRealtimeMs,
                expectedPositionMs = expectedMs,
                playerPositionMs = playerMs,
                driftMs = driftMs,
                playbackState = playbackStateLabel(),
                isPlaying = player.isPlaying,
                debugEvents = debugEvents.toList(),
            )
        )
    }

    private fun playbackStateLabel(): String {
        return when (player.playbackState) {
            Player.STATE_IDLE -> "idle"
            Player.STATE_BUFFERING -> "buffering"
            Player.STATE_READY -> "ready"
            Player.STATE_ENDED -> "ended"
            else -> "none"
        }
    }

    private fun addDebugEvent(message: String) {
        val stamp = android.text.format.DateFormat.format("HH:mm:ss", System.currentTimeMillis()).toString()
        debugEvents.add("$stamp $message")
        while (debugEvents.size > 8) debugEvents.removeAt(0)
    }

    private fun onMain(block: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            block()
        } else {
            mainHandler.post(block)
        }
    }
}
