package cy.hexor.furumi.data.repository

import cy.hexor.furumi.data.local.AuthSessionStorage
import cy.hexor.furumi.data.local.ConnectedDeviceStorage
import cy.hexor.furumi.data.local.OfflineLibraryStore
import cy.hexor.furumi.data.local.OfflineMediaStore
import cy.hexor.furumi.data.local.OfflineSettingsStorage
import cy.hexor.furumi.data.remote.AppClientInfo
import cy.hexor.furumi.data.remote.AuthApiErrorParser
import cy.hexor.furumi.data.remote.PlayerEndpoints
import cy.hexor.furumi.data.remote.api.PlayerApi
import cy.hexor.furumi.data.remote.model.AddPlaylistTracksRequest
import cy.hexor.furumi.data.remote.model.ConnectedCommandPayloadBody
import cy.hexor.furumi.data.remote.model.CreatePlaylistRequest
import cy.hexor.furumi.data.remote.model.DeviceActiveRequest
import cy.hexor.furumi.data.remote.model.DeviceCommandRequest
import cy.hexor.furumi.data.remote.model.DevicePollRequest
import cy.hexor.furumi.data.remote.model.JamCreateRequest
import cy.hexor.furumi.data.remote.model.JamInviteRequest
import cy.hexor.furumi.data.remote.model.JamJoinRequest
import cy.hexor.furumi.data.remote.model.RecordHistoryRequest
import cy.hexor.furumi.data.remote.model.TracksByIdsRequest
import cy.hexor.furumi.data.remote.model.toDomain
import cy.hexor.furumi.data.remote.model.toBody
import cy.hexor.furumi.data.remote.model.toCommandPayloadBody
import cy.hexor.furumi.data.remote.model.toTrackItemResponse
import cy.hexor.furumi.data.remote.model.withAbsoluteUrls
import cy.hexor.furumi.data.remote.model.withOfflineManifest
import cy.hexor.furumi.domain.model.ArtistDetail
import cy.hexor.furumi.domain.model.ArtistPage
import cy.hexor.furumi.domain.model.AuthException
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.ListeningHistoryPage
import cy.hexor.furumi.domain.model.OfflineState
import cy.hexor.furumi.domain.model.OfflineSyncSummary
import cy.hexor.furumi.domain.model.PlaylistCard
import cy.hexor.furumi.domain.model.PlaylistDetail
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.PlayerRepository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject

class PlayerRepositoryImpl @Inject constructor(
    private val playerApi: PlayerApi,
    private val sessionStorage: AuthSessionStorage,
    private val connectedDeviceStorage: ConnectedDeviceStorage,
    private val offlineSettingsStorage: OfflineSettingsStorage,
    private val offlineLibraryStore: OfflineLibraryStore,
    private val offlineMediaStore: OfflineMediaStore,
    private val appClientInfo: AppClientInfo,
    private val playerEndpoints: PlayerEndpoints,
    private val errorParser: AuthApiErrorParser
) : PlayerRepository {
    override val offlineState: StateFlow<OfflineState> = offlineSettingsStorage.state

    override fun setOfflineModeEnabled(enabled: Boolean) {
        offlineSettingsStorage.setOfflineModeEnabled(enabled)
        if (!enabled) {
            offlineSettingsStorage.setNetworkFallback(false)
        }
    }

    override fun setSaveListenedTracksEnabled(enabled: Boolean) {
        offlineSettingsStorage.setSaveListenedTracksEnabled(enabled)
    }

    override fun setOfflineStorageLimit(limitBytes: Long) {
        offlineSettingsStorage.setStorageLimitBytes(limitBytes)
    }

    override suspend fun clearOfflineCache(): Result<Unit> {
        return try {
            offlineMediaStore.clearCachedMedia()
            Result.success(Unit)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun syncOfflineLibrary(): Result<OfflineSyncSummary> {
        offlineSettingsStorage.setSyncing(true)
        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val manifestResponse = playerApi.offlineManifest(playerEndpoints.offlineManifest(baseUrl))

            if (!manifestResponse.isSuccessful) {
                throw AuthException(errorParser.messageFrom(manifestResponse))
            }

            val manifest = manifestResponse.body()
                ?: throw AuthException("Offline manifest response is empty")
            val manifestTracks = manifest.tracks.map { it.withAbsoluteUrls(baseUrl) }
            val manifestById = manifestTracks.associateBy { it.id }

            offlineLibraryStore.upsertManifestTracks(manifestTracks)
            offlineLibraryStore.replacePlaylists(manifest.playlists)
            offlineLibraryStore.replaceLikedTrackIds(manifest.likedTrackIds.toSet())

            val trackCards = manifestTracks
                .map { it.id }
                .chunked(TRACKS_BY_IDS_CHUNK_SIZE)
                .flatMap { ids ->
                    val tracksResponse = playerApi.tracksByIds(
                        url = playerEndpoints.tracksByIds(baseUrl),
                        body = TracksByIdsRequest(ids)
                    )
                    if (!tracksResponse.isSuccessful) {
                        throw AuthException(errorParser.messageFrom(tracksResponse))
                    }
                    tracksResponse.body().orEmpty().map { track ->
                        val domainTrack = track.toDomain(baseUrl)
                        manifestById[domainTrack.id]?.let(domainTrack::withOfflineManifest) ?: domainTrack
                    }
                }

            offlineLibraryStore.upsertTracks(trackCards)
            offlineMediaStore.pruneStaleMedia()
            offlineMediaStore.publishStorageStats()
            val syncedAtMs = System.currentTimeMillis()
            offlineSettingsStorage.setLastSyncedAt(syncedAtMs)
            offlineSettingsStorage.setNetworkFallback(false)
            Result.success(
                OfflineSyncSummary(
                    trackCount = manifestTracks.size,
                    playlistCount = manifest.playlists.size,
                    syncedAtMs = syncedAtMs
                )
            )
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            offlineSettingsStorage.setLastError(e.message ?: "Unable to sync offline library")
            Result.failure(e)
        } finally {
            offlineSettingsStorage.setSyncing(false)
        }
    }

    override suspend fun getArtists(page: Int, limit: Int, mine: Boolean): Result<ArtistPage> {
        if (isOfflineModeEnabled()) {
            return Result.success(offlineLibraryStore.getArtists(page, limit))
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.artists(
                url = playerEndpoints.artists(baseUrl),
                page = page,
                limit = limit,
                mine = mine
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Artists response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) { offlineLibraryStore.getArtists(page, limit) }
        }
    }

    override suspend fun getArtistDetail(artistId: Long): Result<ArtistDetail> {
        if (isOfflineModeEnabled()) {
            return offlineLibraryStore.getArtistDetail(artistId)
                ?.withOnlyCachedPlayback()
                ?.let { Result.success(it) }
                ?: offlineUnavailable("Artist is not available offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.artistDetail(
                url = playerEndpoints.artistDetail(baseUrl, artistId)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Artist response is empty")
            val detail = body.toDomain(baseUrl).withCachedMedia()
            offlineLibraryStore.upsertTracks(detail.topTracks + detail.featuredTracks)
            markOnlineSuccess()
            Result.success(detail)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) {
                offlineLibraryStore.getArtistDetail(artistId)?.withOnlyCachedPlayback()
            }
        }
    }

    override suspend fun getReleaseDetail(releaseId: Long): Result<ReleaseDetail> {
        if (isOfflineModeEnabled()) {
            return offlineLibraryStore.getReleaseDetail(releaseId)
                ?.withOnlyCachedPlayback()
                ?.let { Result.success(it) }
                ?: offlineUnavailable("Release is not available offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.releaseDetail(
                url = playerEndpoints.releaseDetail(baseUrl, releaseId)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Release response is empty")
            val detail = body.toDomain(baseUrl).withCachedMedia()
            offlineLibraryStore.upsertTracks(detail.tracks)
            markOnlineSuccess()
            Result.success(detail)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) {
                offlineLibraryStore.getReleaseDetail(releaseId)?.withOnlyCachedPlayback()
            }
        }
    }

    override suspend fun search(query: String, limit: Int): Result<SearchResults> {
        if (isOfflineModeEnabled()) {
            return Result.success(offlineLibraryStore.search(query, limit).withOnlyCachedPlayback())
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.search(
                url = playerEndpoints.search(baseUrl),
                query = query,
                limit = limit
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Search response is empty")
            val results = body.toDomain(baseUrl).withCachedMedia()
            offlineLibraryStore.upsertTracks(results.tracks)
            markOnlineSuccess()
            Result.success(results)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) { offlineLibraryStore.search(query, limit).withOnlyCachedPlayback() }
        }
    }

    override suspend fun getListeningHistory(page: Int, limit: Int): Result<ListeningHistoryPage> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Listening history is not available offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.history(
                url = playerEndpoints.history(baseUrl),
                page = page,
                limit = limit
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Listening history response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain())
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun toggleLike(trackId: Long): Result<Boolean> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Likes cannot be changed offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.toggleLike(
                url = playerEndpoints.likesToggle(baseUrl, trackId)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Like toggle response is empty")
            val likedIds = offlineLibraryStore.getLikedTrackIds()
            offlineLibraryStore.replaceLikedTrackIds(
                if (body.liked) likedIds + trackId else likedIds - trackId
            )
            markOnlineSuccess()
            Result.success(body.liked)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun getLikedTrackIds(): Result<Set<Long>> {
        if (isOfflineModeEnabled()) {
            return Result.success(offlineLibraryStore.getLikedTrackIds())
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.likes(
                url = playerEndpoints.likes(baseUrl)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Likes response is empty")
            val likedIds = body.trackIds.toSet()
            offlineLibraryStore.replaceLikedTrackIds(likedIds)
            markOnlineSuccess()
            Result.success(likedIds)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) { offlineLibraryStore.getLikedTrackIds() }
        }
    }

    override suspend fun recordListeningHistory(
        trackId: Long,
        startedAt: Long,
        durationListened: Int,
        completed: Boolean
    ): Result<Unit> {
        if (isOfflineModeEnabled()) {
            return try {
                offlineMediaStore.cacheTrackAfterListening(trackId)
                Result.success(Unit)
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                Result.failure(e)
            }
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.recordHistory(
                url = playerEndpoints.recordHistory(baseUrl),
                body = RecordHistoryRequest(
                    trackId = trackId,
                    startedAt = startedAt,
                    durationListened = durationListened,
                    completed = completed
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            offlineMediaStore.cacheTrackAfterListening(trackId)
            markOnlineSuccess()
            Result.success(Unit)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            runCatching { offlineMediaStore.cacheTrackAfterListening(trackId) }
            offlineSettingsStorage.setNetworkFallback(true, e.message)
            Result.failure(e)
        }
    }

    override suspend fun getPlaylists(): Result<List<PlaylistCard>> {
        if (isOfflineModeEnabled()) {
            return Result.success(offlineLibraryStore.getPlaylists())
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.playlists(
                url = playerEndpoints.playlists(baseUrl)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Playlists response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain())
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) { offlineLibraryStore.getPlaylists() }
        }
    }

    override suspend fun getPlaylistDetail(playlistId: Long): Result<PlaylistDetail> {
        if (isOfflineModeEnabled()) {
            return offlineLibraryStore.getPlaylistDetail(playlistId)
                ?.withOnlyCachedPlayback()
                ?.let { Result.success(it) }
                ?: offlineUnavailable("Playlist is not available offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.playlistDetail(
                url = playerEndpoints.playlistDetail(baseUrl, playlistId)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Playlist response is empty")
            val detail = body.toDomain(baseUrl).withCachedMedia()
            offlineLibraryStore.upsertPlaylistDetail(detail)
            markOnlineSuccess()
            Result.success(detail)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            fallbackToOffline(e) {
                offlineLibraryStore.getPlaylistDetail(playlistId)?.withOnlyCachedPlayback()
            }
        }
    }

    override suspend fun createPlaylist(title: String): Result<PlaylistCard> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Playlists cannot be created offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.createPlaylist(
                url = playerEndpoints.playlists(baseUrl),
                body = CreatePlaylistRequest(title = title)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Create playlist response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain())
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun deletePlaylist(playlistId: Long): Result<Unit> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Playlists cannot be deleted offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.deletePlaylist(
                url = playerEndpoints.playlistDetail(baseUrl, playlistId)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            markOnlineSuccess()
            Result.success(Unit)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun addTracksToPlaylist(playlistId: Long, trackIds: List<Long>): Result<Unit> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Playlists cannot be changed offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.addTracksToPlaylist(
                url = playerEndpoints.addPlaylistTracks(baseUrl, playlistId),
                body = AddPlaylistTracksRequest(trackIds = trackIds)
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            markOnlineSuccess()
            Result.success(Unit)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun shareTrack(trackId: Long, title: String): Result<String> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Tracks cannot be shared offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            markOnlineSuccess()
            Result.success(playerEndpoints.shareTrack(baseUrl, trackId))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun pollConnectedDevice(
        playbackState: ConnectedPlaybackState?,
        currentJamId: String?
    ): Result<Pair<ConnectedDevicesState, List<ConnectedCommand>>> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Connected devices are unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.pollDevice(
                url = playerEndpoints.devicesPoll(baseUrl),
                body = DevicePollRequest(
                    deviceId = connectedDeviceStorage.getOrCreateDeviceId(),
                    userAgent = appClientInfo.userAgent,
                    currentJamId = currentJamId,
                    playbackState = playbackState?.toBody()
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Device poll response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl) to body.commands.map { it.toDomain(baseUrl) })
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun setActiveDevice(deviceId: String): Result<ConnectedDevicesState> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Connected devices are unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.setActiveDevice(
                url = playerEndpoints.devicesActive(baseUrl),
                body = DeviceActiveRequest(
                    deviceId = deviceId,
                    currentDeviceId = connectedDeviceStorage.getOrCreateDeviceId()
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Active device response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun sendConnectedDeviceCommand(
        targetDeviceId: String?,
        jamId: String?,
        command: String,
        payload: ConnectedPlaybackState?,
        tracks: List<TrackCard>,
        index: Int?,
        time: Double?,
        volume: Double?,
        shuffle: Boolean?,
        repeatMode: String?,
        fromIndex: Int?,
        toIndex: Int?
    ): Result<Unit> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Connected devices are unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val commandPayload = payload?.toCommandPayloadBody() ?: ConnectedCommandPayloadBody(
                tracks = tracks.takeIf { it.isNotEmpty() }?.map { it.toTrackItemResponse() },
                index = index,
                time = time,
                volume = volume,
                shuffle = shuffle,
                repeatMode = repeatMode,
                fromIndex = fromIndex,
                toIndex = toIndex
            )
            val response = playerApi.sendDeviceCommand(
                url = playerEndpoints.devicesCommand(baseUrl),
                body = DeviceCommandRequest(
                    targetDeviceId = targetDeviceId,
                    jamId = jamId,
                    command = command,
                    payload = commandPayload
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            markOnlineSuccess()
            Result.success(Unit)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun createJam(): Result<ConnectedDevicesState> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Jam is unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.createJam(
                url = playerEndpoints.jams(baseUrl),
                body = JamCreateRequest(deviceId = connectedDeviceStorage.getOrCreateDeviceId())
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Jam response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun joinJam(jamId: String): Result<ConnectedDevicesState> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Jam is unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.joinJam(
                url = playerEndpoints.jamsJoin(baseUrl),
                body = JamJoinRequest(
                    jamId = jamId,
                    deviceId = connectedDeviceStorage.getOrCreateDeviceId()
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Jam response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun leaveJam(jamId: String): Result<ConnectedDevicesState> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Jam is unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.leaveJam(
                url = playerEndpoints.jamsLeave(baseUrl),
                body = JamJoinRequest(
                    jamId = jamId,
                    deviceId = connectedDeviceStorage.getOrCreateDeviceId()
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Jam response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun searchJamUsers(query: String, limit: Int): Result<List<ConnectedJamUser>> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Jam is unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.searchJamUsers(
                url = playerEndpoints.jamsUsers(baseUrl),
                query = query,
                limit = limit
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Jam users response is empty")
            markOnlineSuccess()
            Result.success(body.map { it.toDomain() })
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun inviteToJam(jamId: String, inviteeUserIds: List<Long>): Result<ConnectedDevicesState> {
        if (isOfflineModeEnabled()) {
            return offlineUnavailable("Jam is unavailable offline")
        }

        return try {
            val baseUrl = sessionStorage.getBaseUrl()
                ?: throw AuthException("Server URL is missing")
            val response = playerApi.inviteToJam(
                url = playerEndpoints.jamsInvite(baseUrl),
                body = JamInviteRequest(
                    jamId = jamId,
                    deviceId = connectedDeviceStorage.getOrCreateDeviceId(),
                    inviteeUserIds = inviteeUserIds
                )
            )

            if (!response.isSuccessful) {
                throw AuthException(errorParser.messageFrom(response))
            }

            val body = response.body() ?: throw AuthException("Jam response is empty")
            markOnlineSuccess()
            Result.success(body.toDomain(baseUrl))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    private fun isOfflineModeEnabled(): Boolean {
        return offlineSettingsStorage.state.value.settings.offlineModeEnabled
    }

    private fun markOnlineSuccess() {
        offlineSettingsStorage.setNetworkFallback(false)
        offlineSettingsStorage.setLastError(null)
    }

    private fun <T> fallbackToOffline(error: Exception, block: () -> T?): Result<T> {
        val localValue = runCatching(block).getOrNull()
        return if (localValue != null) {
            offlineSettingsStorage.setNetworkFallback(true, error.message)
            Result.success(localValue)
        } else {
            Result.failure(error)
        }
    }

    private fun <T> offlineUnavailable(message: String): Result<T> {
        return Result.failure(AuthException(message))
    }

    private fun ArtistDetail.withCachedMedia(): ArtistDetail {
        return copy(
            topTracks = topTracks.map { it.withCachedMedia() },
            featuredTracks = featuredTracks.map { it.withCachedMedia() }
        )
    }

    private fun ReleaseDetail.withCachedMedia(): ReleaseDetail {
        return copy(tracks = tracks.map { it.withCachedMedia() })
    }

    private fun PlaylistDetail.withCachedMedia(): PlaylistDetail {
        return copy(tracks = tracks.map { it.withCachedMedia() })
    }

    private fun SearchResults.withCachedMedia(): SearchResults {
        return copy(tracks = tracks.map { it.withCachedMedia() })
    }

    private fun TrackCard.withCachedMedia(): TrackCard {
        val cached = offlineLibraryStore.getTrack(id, preferLocalMedia = true) ?: return this
        return copy(
            streamUrl = if (cached.isAudioCached) cached.streamUrl else streamUrl,
            coverUrl = if (cached.localCoverPath != null) cached.coverUrl else coverUrl,
            localAudioPath = cached.localAudioPath,
            localCoverPath = cached.localCoverPath,
            isAudioCached = cached.isAudioCached
        )
    }

    private fun ArtistDetail.withOnlyCachedPlayback(): ArtistDetail {
        return copy(
            topTracks = topTracks.map { it.withOnlyCachedPlayback() },
            featuredTracks = featuredTracks.map { it.withOnlyCachedPlayback() }
        )
    }

    private fun ReleaseDetail.withOnlyCachedPlayback(): ReleaseDetail {
        return copy(tracks = tracks.map { it.withOnlyCachedPlayback() })
    }

    private fun PlaylistDetail.withOnlyCachedPlayback(): PlaylistDetail {
        return copy(tracks = tracks.map { it.withOnlyCachedPlayback() })
    }

    private fun SearchResults.withOnlyCachedPlayback(): SearchResults {
        return copy(tracks = tracks.map { it.withOnlyCachedPlayback() })
    }

    private fun TrackCard.withOnlyCachedPlayback(): TrackCard {
        return if (isAudioCached || streamUrl.startsWith("file://", ignoreCase = true)) {
            this
        } else {
            copy(streamUrl = "")
        }
    }

    private companion object {
        const val TRACKS_BY_IDS_CHUNK_SIZE = 500
    }
}
