package cy.hexor.furumi.ui.player

import android.graphics.Bitmap
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cy.hexor.furumi.data.repository.MediaImageLoader
import cy.hexor.furumi.domain.model.ArtistCard
import cy.hexor.furumi.domain.model.ArtistDetail
import cy.hexor.furumi.domain.model.ConnectedCommand
import cy.hexor.furumi.domain.model.ConnectedDevicesState
import cy.hexor.furumi.domain.model.ConnectedJamUser
import cy.hexor.furumi.domain.model.ConnectedPlaybackState
import cy.hexor.furumi.domain.model.ListeningHistoryItem
import cy.hexor.furumi.domain.model.OfflineState
import cy.hexor.furumi.domain.model.PlaylistCard
import cy.hexor.furumi.domain.model.PlaylistDetail
import cy.hexor.furumi.domain.model.ReleaseCard
import cy.hexor.furumi.domain.model.ReleaseDetail
import cy.hexor.furumi.domain.model.SearchResults
import cy.hexor.furumi.domain.model.TrackCard
import cy.hexor.furumi.domain.repository.AuthRepository
import cy.hexor.furumi.domain.repository.PlayerRepository
import cy.hexor.furumi.playback.AudioPlaybackState
import cy.hexor.furumi.playback.PlaybackController
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject

data class PlayerUiState(
    val userName: String = "Listener",
    val appVersion: String = "",
    val serverUrl: String = "",
    val isLoggingOut: Boolean = false,
    val isLoggedOut: Boolean = false,
    val isListeningHistoryVisible: Boolean = false,
    val isListeningHistoryLoading: Boolean = false,
    val listeningHistory: List<ListeningHistoryItem> = emptyList(),
    val listeningHistoryTotal: Long = 0,
    val listeningHistoryError: String? = null,
    val globalArtists: List<ArtistCard> = emptyList(),
    val globalArtistsTotal: Long = 0,
    val globalArtistsPage: Int = 0,
    val globalArtistsHasMore: Boolean = true,
    val isGlobalArtistsLoading: Boolean = false,
    val isGlobalArtistsLoadingMore: Boolean = false,
    val globalArtistsError: String? = null,
    val libraryArtists: List<ArtistCard> = emptyList(),
    val libraryArtistsTotal: Long = 0,
    val libraryArtistsPage: Int = 0,
    val libraryArtistsHasMore: Boolean = true,
    val isLibraryArtistsLoading: Boolean = false,
    val isLibraryArtistsLoadingMore: Boolean = false,
    val libraryArtistsError: String? = null,
    val searchQuery: String = "",
    val searchResults: SearchResults? = null,
    val isSearchLoading: Boolean = false,
    val searchError: String? = null,
    val selectedArtist: ArtistCard? = null,
    val artistDetail: ArtistDetail? = null,
    val isArtistDetailLoading: Boolean = false,
    val artistDetailError: String? = null,
    val selectedRelease: ReleaseCard? = null,
    val releaseDetail: ReleaseDetail? = null,
    val isReleaseDetailLoading: Boolean = false,
    val releaseDetailError: String? = null,
    val mediaImages: Map<String, Bitmap> = emptyMap(),
    val loadingMediaImageUrls: Set<String> = emptySet(),
    val playback: AudioPlaybackState = AudioPlaybackState(),
    val likedTrackIds: Set<Long> = emptySet(),
    val playlists: List<PlaylistCard> = emptyList(),
    val isPlaylistsLoading: Boolean = false,
    val playlistsError: String? = null,
    val playlistPreviewCoverUrls: Map<Long, List<String>> = emptyMap(),
    val loadingPlaylistPreviewIds: Set<Long> = emptySet(),
    val isCreatingPlaylist: Boolean = false,
    val playlistCreateError: String? = null,
    val isDeletingPlaylist: Boolean = false,
    val playlistDeleteError: String? = null,
    val selectedPlaylist: PlaylistCard? = null,
    val playlistDetail: PlaylistDetail? = null,
    val isPlaylistDetailLoading: Boolean = false,
    val playlistDetailError: String? = null,
    val connectedDevicesState: ConnectedDevicesState? = null,
    val connectedDevicesError: String? = null,
    val jamInviteQuery: String = "",
    val jamInviteUsers: List<ConnectedJamUser> = emptyList(),
    val isJamInviteSearchLoading: Boolean = false,
    val offlineState: OfflineState = OfflineState()
)

@HiltViewModel
class PlayerViewModel @Inject constructor(
    private val authRepository: AuthRepository,
    private val playerRepository: PlayerRepository,
    private val mediaImageLoader: MediaImageLoader,
    private val playbackController: PlaybackController,
    appClientInfo: cy.hexor.furumi.data.remote.AppClientInfo
) : ViewModel() {

    private val artistDetailCache = mutableMapOf<Long, ArtistDetail>()
    private val releaseDetailCache = mutableMapOf<Long, ReleaseDetail>()
    private val playlistDetailCache = mutableMapOf<Long, PlaylistDetail>()
    private var searchJob: Job? = null
    private var jamInviteSearchJob: Job? = null
    private var offlineModeRefreshJob: Job? = null
    private val handledConnectedCommandIds = ArrayDeque<String>()
    private val handledConnectedCommandIdSet = mutableSetOf<String>()
    private var wasCurrentDeviceActive = false
    private var wasControllingRemoteJam = false
    private var hasResolvedStartupActiveDevice = false
    private var shouldClaimActiveDeviceOnReconnect = false
    private val _shareEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
    val shareEvent: SharedFlow<String> = _shareEvent.asSharedFlow()

    private val _uiState = MutableStateFlow(
        authRepository.getCurrentSession()?.let { session ->
            PlayerUiState(
                userName = session.user.name,
                serverUrl = session.serverBaseUrl,
                appVersion = appClientInfo.version
            )
        } ?: PlayerUiState(
            serverUrl = authRepository.getSavedServerUrl().orEmpty(),
            appVersion = appClientInfo.version
        )
    )
    val uiState: StateFlow<PlayerUiState> = _uiState.asStateFlow()

    init {
        loadGlobalArtistsIfNeeded()
        observeOfflineState()
        observePlayback()
        loadLikedTrackIds()
        startConnectedDevicesPolling()
        playbackController.onTrackPlayReported = { trackId, startedAt, durationListened, completed ->
            reportListeningHistory(trackId, startedAt, durationListened, completed)
        }
    }

    fun loadGlobalArtistsIfNeeded() {
        val state = _uiState.value
        if (state.globalArtists.isNotEmpty() ||
            state.isGlobalArtistsLoading ||
            state.isGlobalArtistsLoadingMore
        ) {
            return
        }

        loadGlobalArtists(page = 1, append = false)
    }

    fun retryGlobalArtists() {
        loadGlobalArtists(page = 1, append = false)
    }

    fun loadMoreGlobalArtists() {
        val state = _uiState.value
        if (!state.globalArtistsHasMore ||
            state.isGlobalArtistsLoading ||
            state.isGlobalArtistsLoadingMore
        ) {
            return
        }

        loadGlobalArtists(page = state.globalArtistsPage + 1, append = true)
    }

    fun loadLibraryArtistsIfNeeded() {
        val state = _uiState.value
        if (state.libraryArtists.isNotEmpty() ||
            state.isLibraryArtistsLoading ||
            state.isLibraryArtistsLoadingMore
        ) {
            return
        }

        loadLibraryArtists(page = 1, append = false)
    }

    fun retryLibraryArtists() {
        loadLibraryArtists(page = 1, append = false)
    }

    fun loadMoreLibraryArtists() {
        val state = _uiState.value
        if (!state.libraryArtistsHasMore ||
            state.isLibraryArtistsLoading ||
            state.isLibraryArtistsLoadingMore
        ) {
            return
        }

        loadLibraryArtists(page = state.libraryArtistsPage + 1, append = true)
    }

    fun loadLibraryIfNeeded() {
        loadPlaylistsIfNeeded()
        loadLibraryArtistsIfNeeded()
    }

    fun onSearchQueryChange(query: String) {
        val normalizedQuery = query.withoutLineBreaks()
        _uiState.value = _uiState.value.copy(
            searchQuery = normalizedQuery,
            searchError = null
        )

        searchJob?.cancel()
        if (normalizedQuery.isBlank()) {
            _uiState.value = _uiState.value.copy(
                searchResults = null,
                isSearchLoading = false,
                searchError = null
            )
            return
        }

        searchJob = viewModelScope.launch {
            delay(SEARCH_DEBOUNCE_MS)
            search(normalizedQuery)
        }
    }

    fun retrySearch() {
        val query = _uiState.value.searchQuery
        if (query.isBlank()) return
        searchJob?.cancel()
        searchJob = viewModelScope.launch {
            search(query)
        }
    }

    fun openArtist(artist: ArtistCard) {
        val cachedDetail = artistDetailCache[artist.id]

        _uiState.value = _uiState.value.copy(
            selectedArtist = cachedDetail?.artist ?: artist,
            artistDetail = cachedDetail,
            isArtistDetailLoading = cachedDetail == null,
            artistDetailError = null
        )

        if (cachedDetail == null) {
            loadArtistDetail(artist.id)
        }
    }

    fun closeArtistDetail() {
        _uiState.value = _uiState.value.copy(
            selectedArtist = null,
            artistDetail = null,
            isArtistDetailLoading = false,
            artistDetailError = null,
            selectedRelease = null,
            releaseDetail = null,
            isReleaseDetailLoading = false,
            releaseDetailError = null
        )
    }

    fun retryArtistDetail() {
        val artist = _uiState.value.selectedArtist ?: return
        loadArtistDetail(artist.id)
    }

    fun openRelease(release: ReleaseCard) {
        val cachedDetail = releaseDetailCache[release.id]
            ?.withPreferredReleaseArtwork(release)

        _uiState.value = _uiState.value.copy(
            selectedRelease = cachedDetail?.release ?: release,
            releaseDetail = cachedDetail,
            isReleaseDetailLoading = cachedDetail == null,
            releaseDetailError = null
        )

        if (cachedDetail == null) {
            loadReleaseDetail(release.id)
        }
    }

    fun closeReleaseDetail() {
        _uiState.value = _uiState.value.copy(
            selectedRelease = null,
            releaseDetail = null,
            isReleaseDetailLoading = false,
            releaseDetailError = null
        )
    }

    fun openPlaylist(playlist: PlaylistCard) {
        val cachedDetail = playlistDetailCache[playlist.id]

        _uiState.value = _uiState.value.copy(
            selectedPlaylist = cachedDetail?.playlist ?: playlist,
            playlistDetail = cachedDetail,
            isPlaylistDetailLoading = cachedDetail == null,
            playlistDetailError = null
        )

        if (cachedDetail == null) {
            loadPlaylistDetail(playlist.id)
        }
    }

    fun closePlaylistDetail() {
        _uiState.value = _uiState.value.copy(
            selectedPlaylist = null,
            playlistDetail = null,
            isPlaylistDetailLoading = false,
            playlistDetailError = null
        )
    }

    fun retryPlaylistDetail() {
        val playlist = _uiState.value.selectedPlaylist ?: return
        loadPlaylistDetail(playlist.id)
    }

    fun retryReleaseDetail() {
        val release = _uiState.value.selectedRelease ?: return
        loadReleaseDetail(release.id)
    }

    fun playTrack(track: TrackCard, queue: List<TrackCard>, index: Int? = null): Boolean {
        if (track.streamUrl.isBlank() && index == null) return false

        activeRemoteDeviceId()?.let { targetDeviceId ->
            val playableQueue = queue
                .ifEmpty { listOf(track) }
                .filter { it.streamUrl.isNotBlank() }
            
            val targetIndex = index ?: playableQueue.indexOfFirst { it.id == track.id }
                .takeIf { it >= 0 }
                ?: 0

            val remoteState = ConnectedPlaybackState(
                track = playableQueue.getOrNull(targetIndex) ?: track,
                tracks = playableQueue,
                index = targetIndex,
                positionSeconds = 0.0,
                durationSeconds = (playableQueue.getOrNull(targetIndex) ?: track).durationSeconds?.toDouble() ?: 0.0,
                paused = false,
                shuffle = _uiState.value.connectedDevicesState?.remotePlaybackState?.shuffle ?: false,
                repeatMode = _uiState.value.connectedDevicesState?.remotePlaybackState?.repeatMode ?: "off",
                volume = _uiState.value.connectedDevicesState?.remotePlaybackState?.volume ?: 1.0
            )
            sendConnectedCommand(targetDeviceId, "play_from_index", payload = remoteState)
            return true
        }

        if (index != null) {
            playbackController.playQueue(queue, index)
        } else {
            playbackController.playTrack(track, queue)
        }
        return true
    }

    fun playArtistTracks(shuffle: Boolean = false): Boolean {
        val detail = _uiState.value.artistDetail ?: return false
        val tracks = detail.topTracks
            .ifEmpty { detail.featuredTracks }
            .filter { it.streamUrl.isNotBlank() }
            .let { if (shuffle) it.shuffled() else it }

        return playTracks(tracks, shuffle)
    }

    fun playReleaseTracks(shuffle: Boolean = false): Boolean {
        val tracks = _uiState.value.releaseDetail
            ?.tracks
            .orEmpty()
            .filter { it.streamUrl.isNotBlank() }
            .let { if (shuffle) it.shuffled() else it }

        return playTracks(tracks, shuffle)
    }

    fun playPlaylistTracks(shuffle: Boolean = false): Boolean {
        val tracks = _uiState.value.playlistDetail
            ?.tracks
            .orEmpty()
            .filter { it.streamUrl.isNotBlank() }
            .let { if (shuffle) it.shuffled() else it }

        return playTracks(tracks, shuffle)
    }

    private fun playTracks(tracks: List<TrackCard>, shuffle: Boolean = false): Boolean {
        if (tracks.isEmpty()) return false

        activeRemoteDeviceId()?.let { targetDeviceId ->
            val firstTrack = tracks.first()
            val remoteState = ConnectedPlaybackState(
                track = firstTrack,
                tracks = tracks,
                index = 0,
                positionSeconds = 0.0,
                durationSeconds = firstTrack.durationSeconds?.toDouble() ?: 0.0,
                paused = false,
                shuffle = shuffle,
                repeatMode = _uiState.value.connectedDevicesState?.remotePlaybackState?.repeatMode ?: "off",
                volume = _uiState.value.connectedDevicesState?.remotePlaybackState?.volume ?: 1.0
            )
            sendConnectedCommand(targetDeviceId, "play_track", payload = remoteState)
            return true
        }

        playbackController.playQueue(tracks)
        return true
    }

    fun togglePlayPause() {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            val remoteState = _uiState.value.connectedDevicesState?.remotePlaybackState
            sendConnectedCommand(targetDeviceId, if (remoteState?.paused == true) "resume" else "pause")
            return
        }

        playbackController.togglePlayPause()
    }

    fun toggleShuffle() {
        val (currentShuffle, currentRepeatMode) = playbackOptionsForActiveTarget()
        activeRemoteDeviceId()?.let { targetDeviceId ->
            sendConnectedCommand(
                targetDeviceId = targetDeviceId,
                command = "set_options",
                shuffle = !currentShuffle,
                repeatMode = currentRepeatMode
            )
            return
        }

        playbackController.setOptions(shuffle = !currentShuffle, repeatMode = null)
    }

    fun cycleRepeatMode() {
        val (currentShuffle, currentRepeatMode) = playbackOptionsForActiveTarget()
        val nextRepeatMode = currentRepeatMode.nextPlaybackRepeatMode()
        activeRemoteDeviceId()?.let { targetDeviceId ->
            sendConnectedCommand(
                targetDeviceId = targetDeviceId,
                command = "set_options",
                shuffle = currentShuffle,
                repeatMode = nextRepeatMode
            )
            return
        }

        playbackController.setOptions(shuffle = null, repeatMode = nextRepeatMode)
    }

    fun nextTrack() {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            val remoteState = _uiState.value.connectedDevicesState?.remotePlaybackState
            sendConnectedCommand(
                targetDeviceId = targetDeviceId,
                command = "next",
                shuffle = remoteState?.shuffle,
                repeatMode = remoteState?.repeatMode
            )
            return
        }

        playbackController.next()
    }

    fun previousTrack() {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            sendConnectedCommand(targetDeviceId, "prev")
            return
        }

        playbackController.previous()
    }

    fun setActiveDevice(deviceId: String) {
        if (_uiState.value.offlineState.isOfflineActive) return
        viewModelScope.launch {
            playerRepository.setActiveDevice(deviceId)
                .onSuccess { devicesState ->
                    applyConnectedDevicesState(devicesState)
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to switch active device"
                    )
                }
        }
    }

    fun setOfflineModeEnabled(enabled: Boolean) {
        playerRepository.setOfflineModeEnabled(enabled)
        if (enabled) {
            _uiState.value = _uiState.value.copy(
                connectedDevicesState = null,
                connectedDevicesError = null
            )
            syncOfflineLibrary()
        } else {
            shouldClaimActiveDeviceOnReconnect = true
        }
    }

    fun setSaveListenedTracksEnabled(enabled: Boolean) {
        playerRepository.setSaveListenedTracksEnabled(enabled)
        if (enabled) {
            syncOfflineLibrary()
        }
    }

    fun setOfflineStorageLimit(limitBytes: Long) {
        playerRepository.setOfflineStorageLimit(limitBytes)
    }

    fun syncOfflineLibrary() {
        if (_uiState.value.offlineState.isSyncing) return
        viewModelScope.launch {
            playerRepository.syncOfflineLibrary()
                .onSuccess {
                    refreshAfterOfflineLibraryChange()
                }
        }
    }

    fun clearOfflineCache() {
        viewModelScope.launch {
            playerRepository.clearOfflineCache()
                .onSuccess {
                    refreshAfterOfflineLibraryChange()
                }
        }
    }

    fun createJam() {
        viewModelScope.launch {
            playerRepository.createJam()
                .onSuccess(::applyConnectedDevicesState)
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to create jam"
                    )
                }
        }
    }

    fun joinJam(jamId: String) {
        viewModelScope.launch {
            playerRepository.joinJam(jamId)
                .onSuccess(::applyConnectedDevicesState)
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to join jam"
                    )
                }
        }
    }

    fun leaveJam(jamId: String) {
        viewModelScope.launch {
            playerRepository.leaveJam(jamId)
                .onSuccess(::applyConnectedDevicesState)
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to leave jam"
                    )
                }
        }
    }

    fun onJamInviteQueryChange(query: String) {
        val normalizedQuery = query.withoutLineBreaks()
        _uiState.value = _uiState.value.copy(
            jamInviteQuery = normalizedQuery,
            connectedDevicesError = null
        )

        jamInviteSearchJob?.cancel()
        if (normalizedQuery.isBlank()) {
            _uiState.value = _uiState.value.copy(
                jamInviteUsers = emptyList(),
                isJamInviteSearchLoading = false
            )
            return
        }

        jamInviteSearchJob = viewModelScope.launch {
            delay(JAM_INVITE_SEARCH_DEBOUNCE_MS)
            searchJamInviteUsers(normalizedQuery)
        }
    }

    fun inviteToJam(jamId: String, userId: Long) {
        viewModelScope.launch {
            playerRepository.inviteToJam(jamId, listOf(userId))
                .onSuccess { devicesState ->
                    applyConnectedDevicesState(devicesState)
                    _uiState.value = _uiState.value.copy(
                        jamInviteQuery = "",
                        jamInviteUsers = emptyList(),
                        isJamInviteSearchLoading = false
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to invite user"
                    )
                }
        }
    }

    fun seekToPlaybackProgress(progress: Float) {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            val durationSeconds = _uiState.value.connectedDevicesState
                ?.remotePlaybackState
                ?.durationSeconds
                ?.takeIf { it > 0.0 }
                ?: return
            sendConnectedCommand(
                targetDeviceId = targetDeviceId,
                command = "seek",
                time = durationSeconds * progress.coerceIn(0f, 1f)
            )
            return
        }

        playbackController.seekToProgress(progress)
    }

    fun loadMediaImage(url: String?) {
        val imageUrl = url?.takeIf { it.isNotBlank() } ?: return
        val state = _uiState.value

        if (state.mediaImages.containsKey(imageUrl) ||
            state.loadingMediaImageUrls.contains(imageUrl)
        ) {
            return
        }

        val equivalentBitmap = state.mediaImages.findEquivalentMediaImage(imageUrl)
        if (equivalentBitmap != null) {
            _uiState.value = state.copy(
                mediaImages = state.mediaImages + (imageUrl to equivalentBitmap)
            )
            return
        }

        _uiState.value = state.copy(
            loadingMediaImageUrls = state.loadingMediaImageUrls + imageUrl
        )

        viewModelScope.launch {
            mediaImageLoader.load(imageUrl)
                .onSuccess { bitmap ->
                    val currentState = _uiState.value
                    _uiState.value = currentState.copy(
                        mediaImages = currentState.mediaImages + (imageUrl to bitmap)
                    )
                }

            val currentState = _uiState.value
            _uiState.value = currentState.copy(
                loadingMediaImageUrls = currentState.loadingMediaImageUrls - imageUrl
            )
        }
    }

    fun openListeningHistory() {
        _uiState.value = _uiState.value.copy(isListeningHistoryVisible = true)

        if (_uiState.value.listeningHistory.isEmpty() && !_uiState.value.isListeningHistoryLoading) {
            loadListeningHistory()
        }
    }

    fun closeListeningHistory() {
        _uiState.value = _uiState.value.copy(isListeningHistoryVisible = false)
    }

    fun retryListeningHistory() {
        loadListeningHistory()
    }

    fun logout() {
        if (_uiState.value.isLoggingOut) return

        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoggingOut = true)
            authRepository.logout()
            _uiState.value = _uiState.value.copy(
                isLoggingOut = false,
                isLoggedOut = true
            )
        }
    }

    fun toggleLike(trackId: Long) {
        val current = _uiState.value.likedTrackIds
        val optimistic = if (current.contains(trackId)) current - trackId else current + trackId
        _uiState.value = _uiState.value.copy(likedTrackIds = optimistic)

        viewModelScope.launch {
            playerRepository.toggleLike(trackId)
                .onSuccess { liked ->
                    val updated = if (liked) {
                        _uiState.value.likedTrackIds + trackId
                    } else {
                        _uiState.value.likedTrackIds - trackId
                    }
                    _uiState.value = _uiState.value.copy(likedTrackIds = updated)
                }
                .onFailure {
                    _uiState.value = _uiState.value.copy(likedTrackIds = current)
                }
        }
    }

    fun isTrackLiked(trackId: Long): Boolean = _uiState.value.likedTrackIds.contains(trackId)

    fun addToPlayNext(track: TrackCard) {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            sendConnectedCommand(targetDeviceId, "queue_add_next", tracks = listOf(track))
            return
        }

        playbackController.addNext(track)
    }

    fun addToQueueEnd(track: TrackCard) {
        activeRemoteDeviceId()?.let { targetDeviceId ->
            sendConnectedCommand(targetDeviceId, "queue_add_end", tracks = listOf(track))
            return
        }

        playbackController.addToEnd(track)
    }

    fun shareTrack(track: TrackCard) {
        viewModelScope.launch {
            playerRepository.shareTrack(track.id, track.title)
                .onSuccess { url ->
                    _shareEvent.tryEmit(url)
                }
        }
    }

    fun loadPlaylists() {
        if (_uiState.value.isPlaylistsLoading) return
        _uiState.value = _uiState.value.copy(
            isPlaylistsLoading = true,
            playlistsError = null
        )
        viewModelScope.launch {
            playerRepository.getPlaylists()
                .onSuccess { playlists ->
                    _uiState.value = _uiState.value.copy(
                        playlists = playlists,
                        isPlaylistsLoading = false,
                        playlistsError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isPlaylistsLoading = false,
                        playlistsError = error.message ?: "Unable to load playlists"
                    )
                }
        }
    }

    fun loadPlaylistsIfNeeded() {
        val state = _uiState.value
        if (state.playlists.isNotEmpty() || state.isPlaylistsLoading) return
        loadPlaylists()
    }

    fun loadPlaylistPreview(playlist: PlaylistCard) {
        if (playlist.isLikesPlaylistPreview()) return

        val state = _uiState.value
        if (state.playlistPreviewCoverUrls.containsKey(playlist.id) ||
            state.loadingPlaylistPreviewIds.contains(playlist.id)
        ) {
            return
        }

        playlistDetailCache[playlist.id]?.let { detail ->
            setPlaylistPreviewCoverUrls(playlist.id, detail.previewCoverUrls())
            return
        }

        _uiState.value = state.copy(
            loadingPlaylistPreviewIds = state.loadingPlaylistPreviewIds + playlist.id
        )

        viewModelScope.launch {
            playerRepository.getPlaylistDetail(playlist.id)
                .onSuccess { detail ->
                    playlistDetailCache[playlist.id] = detail
                    setPlaylistPreviewCoverUrls(playlist.id, detail.previewCoverUrls())
                }
                .onFailure {
                    setPlaylistPreviewCoverUrls(playlist.id, emptyList())
                }
        }
    }

    fun clearPlaylistCreateError() {
        if (_uiState.value.playlistCreateError == null) return
        _uiState.value = _uiState.value.copy(playlistCreateError = null)
    }

    fun clearPlaylistDeleteError() {
        if (_uiState.value.playlistDeleteError == null) return
        _uiState.value = _uiState.value.copy(playlistDeleteError = null)
    }

    fun createPlaylist(
        title: String,
        trackIdToAdd: Long? = null,
        onCreated: () -> Unit = {}
    ) {
        val playlistTitle = title.trim()
        if (playlistTitle.isBlank()) {
            _uiState.value = _uiState.value.copy(playlistCreateError = "Playlist name is required")
            return
        }
        if (_uiState.value.isCreatingPlaylist) return

        _uiState.value = _uiState.value.copy(
            isCreatingPlaylist = true,
            playlistCreateError = null
        )

        viewModelScope.launch {
            playerRepository.createPlaylist(playlistTitle)
                .onSuccess { playlist ->
                    if (trackIdToAdd == null) {
                        addPlaylistToState(playlist)
                        _uiState.value = _uiState.value.copy(
                            isCreatingPlaylist = false,
                            playlistCreateError = null
                        )
                        onCreated()
                        return@launch
                    }

                    playerRepository.addTracksToPlaylist(playlist.id, listOf(trackIdToAdd))
                        .onSuccess {
                            addPlaylistToState(playlist.copy(trackCount = playlist.trackCount + 1))
                            playlistDetailCache.remove(playlist.id)
                            _uiState.value = _uiState.value.copy(
                                isCreatingPlaylist = false,
                                playlistCreateError = null
                            )
                            onCreated()
                        }
                        .onFailure { error ->
                            addPlaylistToState(playlist)
                            _uiState.value = _uiState.value.copy(
                                isCreatingPlaylist = false,
                                playlistCreateError = error.message
                                    ?: "Playlist was created, but the track could not be added"
                            )
                        }
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isCreatingPlaylist = false,
                        playlistCreateError = error.message ?: "Unable to create playlist"
                    )
                }
        }
    }

    fun deletePlaylist(
        playlist: PlaylistCard,
        onDeleted: () -> Unit = {}
    ) {
        if (!playlist.canDeleteUserPlaylist()) return
        if (_uiState.value.isDeletingPlaylist) return

        _uiState.value = _uiState.value.copy(
            isDeletingPlaylist = true,
            playlistDeleteError = null
        )

        viewModelScope.launch {
            playerRepository.deletePlaylist(playlist.id)
                .onSuccess {
                    playlistDetailCache.remove(playlist.id)
                    _uiState.value = _uiState.value.copy(
                        playlists = _uiState.value.playlists.filterNot { it.id == playlist.id },
                        playlistPreviewCoverUrls = _uiState.value.playlistPreviewCoverUrls - playlist.id,
                        loadingPlaylistPreviewIds = _uiState.value.loadingPlaylistPreviewIds - playlist.id,
                        selectedPlaylist = null,
                        playlistDetail = null,
                        isPlaylistDetailLoading = false,
                        playlistDetailError = null,
                        isDeletingPlaylist = false,
                        playlistDeleteError = null
                    )
                    onDeleted()
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isDeletingPlaylist = false,
                        playlistDeleteError = error.message ?: "Unable to delete playlist"
                    )
                }
        }
    }

    fun addTrackToPlaylist(trackId: Long, playlistId: Long) {
        viewModelScope.launch {
            playerRepository.addTracksToPlaylist(playlistId, listOf(trackId))
                .onSuccess {
                    incrementPlaylistTrackCount(playlistId)
                    playlistDetailCache.remove(playlistId)
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        playlistsError = error.message ?: "Unable to add track to playlist"
                    )
                }
        }
    }

    private fun addPlaylistToState(playlist: PlaylistCard) {
        val currentPlaylists = _uiState.value.playlists
        _uiState.value = _uiState.value.copy(
            playlists = listOf(playlist) + currentPlaylists.filterNot { it.id == playlist.id }
        )
    }

    private fun incrementPlaylistTrackCount(playlistId: Long) {
        _uiState.value = _uiState.value.copy(
            playlists = _uiState.value.playlists.map { playlist ->
                if (playlist.id == playlistId) {
                    playlist.copy(trackCount = playlist.trackCount + 1)
                } else {
                    playlist
                }
            }
        )
    }

    private fun setPlaylistPreviewCoverUrls(playlistId: Long, coverUrls: List<String>) {
        val distinctCoverUrls = coverUrls.distinct().take(PLAYLIST_PREVIEW_IMAGE_LIMIT)
        val state = _uiState.value
        _uiState.value = state.copy(
            playlistPreviewCoverUrls = state.playlistPreviewCoverUrls + (playlistId to distinctCoverUrls),
            loadingPlaylistPreviewIds = state.loadingPlaylistPreviewIds - playlistId
        )
        distinctCoverUrls.forEach(::loadMediaImage)
    }

    private fun reportListeningHistory(
        trackId: Long,
        startedAt: Long,
        durationListened: Int,
        completed: Boolean
    ) {
        viewModelScope.launch {
            playerRepository.recordListeningHistory(trackId, startedAt, durationListened, completed)
                .onSuccess {
                    Log.d(
                        PLAYER_VIEW_MODEL_TAG,
                        "Recorded play history: trackId=$trackId duration=$durationListened completed=$completed"
                    )
                }
                .onFailure { error ->
                    Log.w(
                        PLAYER_VIEW_MODEL_TAG,
                        "Failed to record play history: trackId=$trackId duration=$durationListened completed=$completed",
                        error
                    )
                }
        }
    }

    private fun loadLikedTrackIds() {
        viewModelScope.launch {
            playerRepository.getLikedTrackIds()
                .onSuccess { ids ->
                    _uiState.value = _uiState.value.copy(likedTrackIds = ids)
                }
        }
    }

    private fun loadListeningHistory() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                isListeningHistoryLoading = true,
                listeningHistoryError = null
            )

            playerRepository.getListeningHistory(page = 1, limit = 20)
                .onSuccess { page ->
                    _uiState.value = _uiState.value.copy(
                        isListeningHistoryLoading = false,
                        listeningHistory = page.items,
                        listeningHistoryTotal = page.total,
                        listeningHistoryError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isListeningHistoryLoading = false,
                        listeningHistoryError = error.message ?: "Unable to load listening history"
                    )
                }
        }
    }

    private fun observePlayback() {
        viewModelScope.launch {
            playbackController.state.collect { playbackState ->
                _uiState.value = _uiState.value.copy(playback = playbackState)
            }
        }
    }

    private fun observeOfflineState() {
        viewModelScope.launch {
            playerRepository.offlineState.collect { offlineState ->
                val current = _uiState.value
                val offlineModeChanged = current.offlineState.isOfflineActive != offlineState.isOfflineActive
                if (current.offlineState.isOfflineActive && !offlineState.isOfflineActive) {
                    shouldClaimActiveDeviceOnReconnect = true
                }
                _uiState.value = current.copy(
                    offlineState = offlineState,
                    connectedDevicesState = if (offlineState.settings.offlineModeEnabled) {
                        null
                    } else {
                        current.connectedDevicesState
                    },
                    connectedDevicesError = if (offlineState.settings.offlineModeEnabled) {
                        null
                    } else {
                        current.connectedDevicesError
                    }
                )
                if (offlineModeChanged) {
                    clearModeSensitiveCaches()
                    refreshVisibleContentForOfflineMode()
                }
            }
        }
    }

    private fun startConnectedDevicesPolling() {
        viewModelScope.launch {
            while (true) {
                pollConnectedDevices()
                delay(CONNECTED_DEVICES_POLL_MS)
            }
        }
    }

    private suspend fun pollConnectedDevices() {
        val state = _uiState.value
        if (state.offlineState.settings.offlineModeEnabled) return
        val shouldClaimActiveDevice = shouldClaimActiveDeviceOnReconnect ||
            state.offlineState.isUsingNetworkFallback
        val isCurrentDeviceActive = state.connectedDevicesState?.isCurrentDeviceActive == true
        
        // Android follows the spec: only the active device sends its playback state.
        // If current device is active and not controlling a remote Jam as a member, it sends its state.
        val playbackState = if (isCurrentDeviceActive && !state.connectedDevicesState.isControllingRemoteJam()) {
            state.playback.toConnectedPlaybackState()
        } else {
            null
        }

        playerRepository.pollConnectedDevice(
            playbackState = playbackState,
            currentJamId = state.connectedDevicesState?.currentJamId
        ).onSuccess { (devicesState, commands) ->
            val resolvedDevicesState = if (shouldClaimActiveDevice) {
                claimCurrentDeviceActive(devicesState)
            } else {
                resolveStartupActiveDevice(devicesState)
            }
            applyConnectedDevicesState(
                devicesState = resolvedDevicesState,
                pauseOnLostControl = !shouldClaimActiveDevice
            )

            if (!shouldClaimActiveDevice) {
                commands.forEach { command ->
                    if (shouldHandleConnectedCommand(command)) {
                        handleConnectedCommand(command)
                    }
                }
            }
        }.onFailure { error ->
            _uiState.value = _uiState.value.copy(
                connectedDevicesError = error.message ?: "Unable to poll connected devices"
            )
        }
    }

    private suspend fun resolveStartupActiveDevice(devicesState: ConnectedDevicesState): ConnectedDevicesState {
        if (hasResolvedStartupActiveDevice) return devicesState
        hasResolvedStartupActiveDevice = true
        if (!devicesState.shouldCurrentDeviceBecomeActiveOnStartup()) return devicesState

        return playerRepository.setActiveDevice(devicesState.deviceId)
            .onFailure { error ->
                _uiState.value = _uiState.value.copy(
                    connectedDevicesError = error.message ?: "Unable to switch active device"
                )
            }
            .getOrElse { devicesState }
    }

    private suspend fun claimCurrentDeviceActive(devicesState: ConnectedDevicesState): ConnectedDevicesState {
        val activeDevicesState = if (devicesState.isCurrentDeviceActive) {
            devicesState
        } else {
            playerRepository.setActiveDevice(devicesState.deviceId)
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        connectedDevicesError = error.message ?: "Unable to switch active device"
                    )
                }
                .getOrElse { return devicesState }
        }

        val (syncedDevicesState, didSync) = pushLocalPlaybackStateAfterReconnect(activeDevicesState)
        if (didSync) {
            shouldClaimActiveDeviceOnReconnect = false
        }
        return syncedDevicesState
    }

    private suspend fun pushLocalPlaybackStateAfterReconnect(
        devicesState: ConnectedDevicesState
    ): Pair<ConnectedDevicesState, Boolean> {
        val playbackState = _uiState.value.playback.toConnectedPlaybackState()
            ?: return devicesState to true

        return playerRepository.pollConnectedDevice(
            playbackState = playbackState,
            currentJamId = devicesState.currentJamId
        )
            .onFailure { error ->
                _uiState.value = _uiState.value.copy(
                    connectedDevicesError = error.message ?: "Unable to sync playback state"
                )
            }
            .fold(
                onSuccess = { (updatedDevicesState, _) -> updatedDevicesState to true },
                onFailure = { devicesState to false }
            )
    }

    private fun applyConnectedDevicesState(
        devicesState: ConnectedDevicesState,
        pauseOnLostControl: Boolean = true
    ) {
        val becameInactive = wasCurrentDeviceActive && !devicesState.isCurrentDeviceActive
        val isControllingRemoteJam = devicesState.isControllingRemoteJam()
        val startedControllingRemoteJam = !wasControllingRemoteJam && isControllingRemoteJam
        _uiState.value = _uiState.value.copy(
            connectedDevicesState = devicesState,
            connectedDevicesError = null
        )
        wasCurrentDeviceActive = devicesState.isCurrentDeviceActive
        wasControllingRemoteJam = isControllingRemoteJam
        if (pauseOnLostControl &&
            (becameInactive || startedControllingRemoteJam) &&
            _uiState.value.playback.currentTrack != null
        ) {
            playbackController.pause()
        }
    }

    private fun searchJamInviteUsers(query: String) {
        viewModelScope.launch {
            if (query != _uiState.value.jamInviteQuery || query.isBlank()) return@launch
            _uiState.value = _uiState.value.copy(isJamInviteSearchLoading = true)
            playerRepository.searchJamUsers(query)
                .onSuccess { users ->
                    if (query == _uiState.value.jamInviteQuery) {
                        val existingMemberIds = _uiState.value.connectedDevicesState
                            ?.jams
                            ?.firstOrNull { it.id == _uiState.value.connectedDevicesState?.currentJamId || it.isActive || it.isOwner }
                            ?.members
                            ?.map { it.userId }
                            ?.toSet()
                            .orEmpty()
                        _uiState.value = _uiState.value.copy(
                            jamInviteUsers = users.filter { it.id !in existingMemberIds },
                            isJamInviteSearchLoading = false
                        )
                    }
                }
                .onFailure { error ->
                    if (query == _uiState.value.jamInviteQuery) {
                        _uiState.value = _uiState.value.copy(
                            isJamInviteSearchLoading = false,
                            connectedDevicesError = error.message ?: "Unable to search users"
                        )
                    }
                }
        }
    }

    private fun shouldHandleConnectedCommand(command: ConnectedCommand): Boolean {
        val id = command.id ?: return true
        if (!handledConnectedCommandIdSet.add(id)) return false
        handledConnectedCommandIds.addLast(id)
        while (handledConnectedCommandIds.size > CONNECTED_COMMAND_ID_CACHE_SIZE) {
            handledConnectedCommandIdSet.remove(handledConnectedCommandIds.removeFirst())
        }
        return true
    }

    private fun handleConnectedCommand(command: ConnectedCommand) {
        val payload = command.payload
        when (command.command) {
            "transfer_state",
            "play_track",
            "play_from_index" -> payload.playbackState?.let(playbackController::playConnectedState)
            "pause" -> playbackController.pause()
            "resume" -> playbackController.resume()
            "seek" -> payload.time?.let(playbackController::seekToSeconds)
            "next" -> {
                playbackController.setOptions(payload.shuffle, payload.repeatMode)
                playbackController.next()
            }
            "prev" -> playbackController.previous()
            "set_volume" -> Unit // Android keeps loudness on the system media stream.
            "set_options" -> playbackController.setOptions(payload.shuffle, payload.repeatMode)
            "queue_add_end" -> playbackController.addToEnd(payload.tracks)
            "queue_add_next" -> playbackController.addNext(payload.tracks)
            "queue_remove" -> payload.index?.let(playbackController::removeQueueIndex)
            "queue_move" -> {
                val fromIndex = payload.fromIndex ?: return
                val toIndex = payload.toIndex ?: return
                playbackController.moveQueueItem(fromIndex, toIndex)
            }
            "queue_clear" -> playbackController.clearQueue()
        }
    }

    private fun activeRemoteDeviceId(): String? {
        if (_uiState.value.offlineState.isOfflineActive) return null
        if (shouldClaimActiveDeviceOnReconnect) return null
        val devicesState = _uiState.value.connectedDevicesState ?: return null
        if (devicesState.isControllingRemoteJam()) {
            return devicesState.activeDeviceId ?: devicesState.deviceId
        }
        val activeDeviceId = devicesState.activeDeviceId ?: return null
        return activeDeviceId.takeIf { it != devicesState.deviceId }
    }

    private fun playbackOptionsForActiveTarget(): Pair<Boolean, String> {
        val state = _uiState.value
        val remotePlayback = state.connectedDevicesState?.remotePlaybackState
        if (activeRemoteDeviceId() != null && remotePlayback != null) {
            return remotePlayback.shuffle to remotePlayback.repeatMode
        }
        return state.playback.shuffle to state.playback.repeatMode
    }

    private fun sendConnectedCommand(
        targetDeviceId: String,
        command: String,
        payload: ConnectedPlaybackState? = null,
        tracks: List<TrackCard> = emptyList(),
        time: Double? = null,
        volume: Double? = null,
        shuffle: Boolean? = null,
        repeatMode: String? = null
    ) {
        viewModelScope.launch {
            val jamId = _uiState.value.connectedDevicesState?.currentJamId
            playerRepository.sendConnectedDeviceCommand(
                targetDeviceId = if (jamId == null) targetDeviceId else null,
                jamId = jamId,
                command = command,
                payload = payload,
                tracks = tracks,
                time = time,
                volume = volume,
                shuffle = shuffle,
                repeatMode = repeatMode
            ).onFailure { error ->
                _uiState.value = _uiState.value.copy(
                    connectedDevicesError = error.message ?: "Unable to send device command"
                )
            }
        }
    }

    private fun loadGlobalArtists(page: Int, append: Boolean) {
        viewModelScope.launch {
            val state = _uiState.value
            _uiState.value = if (append) {
                state.copy(
                    isGlobalArtistsLoadingMore = true,
                    globalArtistsError = null
                )
            } else {
                state.copy(
                    isGlobalArtistsLoading = true,
                    isGlobalArtistsLoadingMore = false,
                    globalArtistsError = null,
                    globalArtistsHasMore = true
                )
            }

            playerRepository.getArtists(page = page, limit = ARTIST_PAGE_SIZE, mine = false)
                .onSuccess { artistPage ->
                    val currentArtists = if (append) _uiState.value.globalArtists else emptyList()
                    val mergedArtists = (currentArtists + artistPage.items).distinctBy { it.id }

                    _uiState.value = _uiState.value.copy(
                        globalArtists = mergedArtists,
                        globalArtistsTotal = artistPage.total,
                        globalArtistsPage = artistPage.page,
                        globalArtistsHasMore = artistPage.hasMore,
                        isGlobalArtistsLoading = false,
                        isGlobalArtistsLoadingMore = false,
                        globalArtistsError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isGlobalArtistsLoading = false,
                        isGlobalArtistsLoadingMore = false,
                        globalArtistsError = error.message ?: "Unable to load artists"
                    )
                }
        }
    }

    private fun loadLibraryArtists(page: Int, append: Boolean) {
        viewModelScope.launch {
            val state = _uiState.value
            _uiState.value = if (append) {
                state.copy(
                    isLibraryArtistsLoadingMore = true,
                    libraryArtistsError = null
                )
            } else {
                state.copy(
                    isLibraryArtistsLoading = true,
                    isLibraryArtistsLoadingMore = false,
                    libraryArtistsError = null,
                    libraryArtistsHasMore = true
                )
            }

            playerRepository.getArtists(page = page, limit = ARTIST_PAGE_SIZE, mine = true)
                .onSuccess { artistPage ->
                    val currentArtists = if (append) _uiState.value.libraryArtists else emptyList()
                    val mergedArtists = (currentArtists + artistPage.items).distinctBy { it.id }

                    _uiState.value = _uiState.value.copy(
                        libraryArtists = mergedArtists,
                        libraryArtistsTotal = artistPage.total,
                        libraryArtistsPage = artistPage.page,
                        libraryArtistsHasMore = artistPage.hasMore,
                        isLibraryArtistsLoading = false,
                        isLibraryArtistsLoadingMore = false,
                        libraryArtistsError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isLibraryArtistsLoading = false,
                        isLibraryArtistsLoadingMore = false,
                        libraryArtistsError = error.message ?: "Unable to load library artists"
                    )
                }
        }
    }

    private suspend fun search(query: String) {
        if (query != _uiState.value.searchQuery || query.isBlank()) return

        _uiState.value = _uiState.value.copy(
            isSearchLoading = true,
            searchError = null
        )

        playerRepository.search(query = query, limit = SEARCH_LIMIT)
            .onSuccess { results ->
                if (query == _uiState.value.searchQuery) {
                    val displayResults = results.withArtistCards(cachedSearchArtistCards())
                    _uiState.value = _uiState.value.copy(
                        searchResults = displayResults,
                        isSearchLoading = false,
                        searchError = null
                    )
                    loadMissingSearchArtistImages(query, displayResults)
                }
            }
            .onFailure { error ->
                if (query == _uiState.value.searchQuery) {
                    _uiState.value = _uiState.value.copy(
                        isSearchLoading = false,
                        searchError = error.message ?: "Unable to search"
                    )
                }
            }
    }

    private fun loadArtistDetail(artistId: Long) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                isArtistDetailLoading = true,
                artistDetailError = null
            )

            playerRepository.getArtistDetail(artistId)
                .onSuccess { detail ->
                    artistDetailCache[artistId] = detail
                    _uiState.value = _uiState.value.copy(
                        selectedArtist = detail.artist,
                        artistDetail = detail,
                        isArtistDetailLoading = false,
                        artistDetailError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isArtistDetailLoading = false,
                        artistDetailError = error.message ?: "Unable to load artist"
                    )
                }
        }
    }

    private fun loadMissingSearchArtistImages(query: String, results: SearchResults) {
        val cachedArtistIds = artistDetailCache.keys + _uiState.value.globalArtists.map { it.id }
        val missingArtists = (results.artistMatches + results.trackArtists)
            .asSequence()
            .filter { it.imageUrl.isNullOrBlank() }
            .distinctBy { it.id }
            .filter { it.id !in cachedArtistIds }
            .take(SEARCH_ARTIST_IMAGE_LIMIT)
            .toList()

        missingArtists.forEach { artist ->
            viewModelScope.launch {
                playerRepository.getArtistDetail(artist.id)
                    .onSuccess { detail ->
                        artistDetailCache[artist.id] = detail
                        if (query == _uiState.value.searchQuery) {
                            val currentResults = _uiState.value.searchResults ?: return@onSuccess
                            _uiState.value = _uiState.value.copy(
                                searchResults = currentResults.withArtistCards(
                                    mapOf(detail.artist.id to detail.artist)
                                )
                            )
                        }
                    }
            }
        }
    }

    private fun cachedSearchArtistCards(): Map<Long, ArtistCard> {
        val globalArtistCards = _uiState.value.globalArtists.associateBy { it.id }
        val libraryArtistCards = _uiState.value.libraryArtists.associateBy { it.id }
        val detailArtistCards = artistDetailCache.values
            .map { it.artist }
            .associateBy { it.id }
        return globalArtistCards + libraryArtistCards + detailArtistCards
    }

    private fun loadReleaseDetail(releaseId: Long) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                isReleaseDetailLoading = true,
                releaseDetailError = null
            )

            playerRepository.getReleaseDetail(releaseId)
                .onSuccess { detail ->
                    val preferredRelease = _uiState.value.selectedRelease
                    val displayDetail = if (preferredRelease != null) {
                        detail.withPreferredReleaseArtwork(preferredRelease)
                    } else {
                        detail
                    }
                    releaseDetailCache[releaseId] = displayDetail
                    _uiState.value = _uiState.value.copy(
                        selectedRelease = displayDetail.release,
                        releaseDetail = displayDetail,
                        isReleaseDetailLoading = false,
                        releaseDetailError = null
                    )
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isReleaseDetailLoading = false,
                        releaseDetailError = error.message ?: "Unable to load release"
                    )
                }
        }
    }

    private fun loadPlaylistDetail(playlistId: Long) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                isPlaylistDetailLoading = true,
                playlistDetailError = null
            )

            playerRepository.getPlaylistDetail(playlistId)
                .onSuccess { detail ->
                    playlistDetailCache[playlistId] = detail
                    _uiState.value = _uiState.value.copy(
                        selectedPlaylist = detail.playlist,
                        playlistDetail = detail,
                        isPlaylistDetailLoading = false,
                        playlistDetailError = null
                    )
                    setPlaylistPreviewCoverUrls(playlistId, detail.previewCoverUrls())
                }
                .onFailure { error ->
                    _uiState.value = _uiState.value.copy(
                        isPlaylistDetailLoading = false,
                        playlistDetailError = error.message ?: "Unable to load playlist"
                    )
                }
        }
    }

    private fun clearModeSensitiveCaches() {
        artistDetailCache.clear()
        releaseDetailCache.clear()
        playlistDetailCache.clear()
    }

    private fun refreshVisibleContentForOfflineMode() {
        offlineModeRefreshJob?.cancel()
        offlineModeRefreshJob = viewModelScope.launch {
            val state = _uiState.value
            when {
                state.selectedPlaylist != null -> {
                    loadPlaylistDetail(state.selectedPlaylist.id)
                }
                state.selectedRelease != null -> {
                    loadReleaseDetail(state.selectedRelease.id)
                }
                state.selectedArtist != null -> {
                    loadArtistDetail(state.selectedArtist.id)
                }
                state.searchQuery.isNotBlank() && state.searchResults != null -> {
                    searchJob?.cancel()
                    search(state.searchQuery)
                }
                state.playlists.isNotEmpty() -> {
                    loadPlaylists()
                }
            }
        }
    }

    private fun refreshAfterOfflineLibraryChange() {
        clearModeSensitiveCaches()
        _uiState.value = _uiState.value.copy(
            playlists = emptyList(),
            playlistPreviewCoverUrls = emptyMap(),
            loadingPlaylistPreviewIds = emptySet()
        )
        loadLikedTrackIds()
        if (_uiState.value.selectedPlaylist != null) {
            _uiState.value.selectedPlaylist?.id?.let(::loadPlaylistDetail)
        } else {
            loadPlaylists()
        }
        _uiState.value.selectedArtist?.id?.let(::loadArtistDetail)
        _uiState.value.selectedRelease?.id?.let(::loadReleaseDetail)
    }

    private companion object {
        const val ARTIST_PAGE_SIZE = 60
        const val SEARCH_DEBOUNCE_MS = 300L
        const val SEARCH_LIMIT = 30
        const val SEARCH_ARTIST_IMAGE_LIMIT = 12
        const val JAM_INVITE_SEARCH_DEBOUNCE_MS = 250L
        const val CONNECTED_DEVICES_POLL_MS = 750L
        const val CONNECTED_COMMAND_ID_CACHE_SIZE = 128
        const val PLAYER_VIEW_MODEL_TAG = "PlayerViewModel"
    }

}

private fun AudioPlaybackState.toConnectedPlaybackState(): ConnectedPlaybackState? {
    if (currentTrack == null || queue.isEmpty()) return null
    return ConnectedPlaybackState(
        track = currentTrack,
        tracks = queue,
        index = currentIndex.coerceAtLeast(0),
        positionSeconds = positionMs / 1_000.0,
        durationSeconds = durationMs / 1_000.0,
        paused = !isPlaying,
        shuffle = shuffle,
        repeatMode = repeatMode,
        volume = ANDROID_CONNECTED_VOLUME
    )
}

private fun ConnectedDevicesState.isControllingRemoteJam(): Boolean {
    val currentJam = jams.firstOrNull { it.id == currentJamId }
        ?: jams.firstOrNull { it.isActive }
    return currentJam != null && currentJam.isMember && !currentJam.isOwner
}

private fun ConnectedDevicesState.shouldCurrentDeviceBecomeActiveOnStartup(): Boolean {
    if (isCurrentDeviceActive || isControllingRemoteJam()) return false
    return remotePlaybackState?.isActivelyPlaying() != true
}

private fun ConnectedPlaybackState.isActivelyPlaying(): Boolean {
    return !paused && (track != null || tracks.isNotEmpty())
}

private fun PlaylistCard.isLikesPlaylistPreview(): Boolean {
    return id == -1L || kind.equals("likes", ignoreCase = true)
}

private fun PlaylistCard.canDeleteUserPlaylist(): Boolean {
    return isOwn && !isLikesPlaylistPreview()
}

private fun PlaylistDetail.previewCoverUrls(): List<String> {
    return tracks.mapNotNull { track ->
        track.coverUrl?.takeIf { it.isNotBlank() }
    }.take(PLAYLIST_PREVIEW_IMAGE_LIMIT)
}

private fun SearchResults.withArtistCards(artistCards: Map<Long, ArtistCard>): SearchResults {
    if (artistCards.isEmpty()) return this
    return copy(
        artistMatches = artistMatches.map { artist -> artistCards[artist.id] ?: artist },
        trackArtists = trackArtists.map { artist -> artistCards[artist.id] ?: artist }
    )
}

private const val ANDROID_CONNECTED_VOLUME = 1.0
private const val PLAYLIST_PREVIEW_IMAGE_LIMIT = 4

private fun String.nextPlaybackRepeatMode(): String {
    return when (lowercase()) {
        "off" -> "all"
        "all" -> "one"
        else -> "off"
    }
}

private fun String.withoutLineBreaks(): String {
    return replace("\r", "").replace("\n", "")
}

private fun ReleaseDetail.withPreferredReleaseArtwork(preferred: ReleaseCard): ReleaseDetail {
    val preferredCoverUrl = preferred.coverUrl?.takeIf { it.isNotBlank() } ?: return this
    if (release.id != preferred.id) return this

    val originalCoverUrl = release.coverUrl
    val updatedRelease = release.copy(coverUrl = preferredCoverUrl)
    val updatedTracks = tracks.map { track ->
        if (track.coverUrl.isNullOrBlank() || sameMediaImageResource(track.coverUrl, originalCoverUrl)) {
            track.copy(coverUrl = preferredCoverUrl)
        } else {
            track
        }
    }

    return copy(
        release = updatedRelease,
        tracks = updatedTracks
    )
}

private fun Map<String, Bitmap>.findEquivalentMediaImage(url: String): Bitmap? {
    val requestedResource = canonicalMediaImageResource(url)
    return entries.firstOrNull { (cachedUrl, _) ->
        canonicalMediaImageResource(cachedUrl) == requestedResource
    }?.value
}

private fun sameMediaImageResource(first: String?, second: String?): Boolean {
    if (first.isNullOrBlank() || second.isNullOrBlank()) return false
    return canonicalMediaImageResource(first) == canonicalMediaImageResource(second)
}
