package xyz.cottongin.radio247.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationCompat
import xyz.cottongin.radio247.R
import com.google.common.util.concurrent.SettableFuture
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import xyz.cottongin.radio247.RadioApplication
import xyz.cottongin.radio247.audio.AudioEngine
import xyz.cottongin.radio247.audio.AudioEngineEvent
import xyz.cottongin.radio247.audio.EngineError
import xyz.cottongin.radio247.audio.IcyMetadata
import xyz.cottongin.radio247.audio.StreamInfo
import xyz.cottongin.radio247.data.db.ConnectionSpanDao
import xyz.cottongin.radio247.data.db.ListeningSessionDao
import xyz.cottongin.radio247.data.db.MetadataSnapshotDao
import xyz.cottongin.radio247.data.db.StationDao
import xyz.cottongin.radio247.data.model.ConnectionSpan
import xyz.cottongin.radio247.data.model.ListeningSession
import xyz.cottongin.radio247.data.model.MetadataSnapshot
import xyz.cottongin.radio247.data.model.Playlist
import xyz.cottongin.radio247.data.model.Station
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Job

class ConnectionFailedException(cause: Throwable) : Exception("Connection failed", cause)

class RadioPlaybackService : MediaLibraryService() {
    companion object {
        private const val TAG = "RadioPlayback"
        private const val FOREGROUND_CHANNEL_ID = "default_channel_id"
        private const val FOREGROUND_NOTIFICATION_ID = 1001
        const val ACTION_PLAY = "xyz.cottongin.radio247.PLAY"
        const val ACTION_STOP = "xyz.cottongin.radio247.STOP"
        const val ACTION_PAUSE = "xyz.cottongin.radio247.PAUSE"
        const val ACTION_SEEK_LIVE = "xyz.cottongin.radio247.SEEK_LIVE"
        const val EXTRA_STATION_ID = "station_id"
    }

    private val app: RadioApplication
        get() = application as RadioApplication

    private val controller: RadioController
        get() = app.controller

    private val database get() = app.database
    private val stationDao: StationDao get() = database.stationDao()
    private val listeningSessionDao: ListeningSessionDao get() = database.listeningSessionDao()
    private val connectionSpanDao: ConnectionSpanDao get() = database.connectionSpanDao()
    private val metadataSnapshotDao: MetadataSnapshotDao get() = database.metadataSnapshotDao()

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

    private var mediaSession: MediaLibraryService.MediaLibrarySession? = null
    private var playerAdapter: RadioPlayerAdapter? = null
    private val seekToLiveCommand = SessionCommand("SEEK_TO_LIVE", Bundle.EMPTY)
    private var engine: AudioEngine? = null
    private var wakeLock: PowerManager.WakeLock? = null
    private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null

    @Volatile
    private var stayConnected = false

    @Volatile
    private var sessionStartedAt: Long = 0L

    @Volatile
    private var connectionSpanId: Long = 0L

    @Volatile
    private var listeningSessionId: Long = 0L

    @Volatile
    private var currentMetadata: IcyMetadata? = null

    @Volatile
    private var networkAvailableCallback: ConnectivityManager.NetworkCallback? = null

    @Volatile
    private var retryImmediatelyOnNetwork = false

    private var playJob: Job? = null

    private var audioManager: AudioManager? = null
    private var focusRequest: AudioFocusRequest? = null
    @Volatile
    private var focusLostTransiently = false

    private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
        when (focusChange) {
            AudioManager.AUDIOFOCUS_LOSS -> {
                Log.i(TAG, "Audio focus LOSS — stopping")
                handleStop()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                Log.i(TAG, "Audio focus LOSS_TRANSIENT — pausing")
                focusLostTransiently = true
                engine?.setVolume(0f)
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                Log.i(TAG, "Audio focus LOSS_TRANSIENT_CAN_DUCK — ducking")
                engine?.setVolume(0.2f)
            }
            AudioManager.AUDIOFOCUS_GAIN -> {
                Log.i(TAG, "Audio focus GAIN — restoring")
                focusLostTransiently = false
                engine?.setVolume(1f)
            }
        }
    }

    private fun transition(newState: PlaybackState) {
        Log.i(TAG, "transition → $newState")
        controller.updateState(newState)
    }

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



    override fun onCreate() {
        super.onCreate()
        audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
        ensureForegroundChannel()
        ensureMediaSession()
    }

    private fun ensureForegroundChannel() {
        val channel = NotificationChannel(
            FOREGROUND_CHANNEL_ID, "Now playing", NotificationManager.IMPORTANCE_LOW
        )
        getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        if (!stayConnected) {
            Log.i(TAG, "onTaskRemoved — stopping (stayConnected=false)")
            handleStop()
            cleanupResources()
            stopSelf()
        }
        super.onTaskRemoved(rootIntent)
    }

    private fun requestAudioFocus(): Boolean {
        val attrs = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .build()
        val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
            .setAudioAttributes(attrs)
            .setOnAudioFocusChangeListener(focusChangeListener)
            .setWillPauseWhenDucked(false)
            .build()
        focusRequest = request
        val result = audioManager?.requestAudioFocus(request)
        val granted = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
        Log.i(TAG, "requestAudioFocus: granted=$granted")
        return granted
    }

    private fun abandonAudioFocus() {
        focusRequest?.let {
            audioManager?.abandonAudioFocusRequest(it)
            focusRequest = null
        }
        focusLostTransiently = false
    }

    private fun postPlaceholderForeground() {
        val notification = NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle("Connecting…")
            .setSilent(true)
            .build()
        startForeground(FOREGROUND_NOTIFICATION_ID, notification)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        when (intent?.action) {
            ACTION_PLAY -> {
                val stationId = intent.getLongExtra(EXTRA_STATION_ID, -1L)
                if (stationId >= 0) {
                    postPlaceholderForeground()
                    launchPlay(stationId)
                } else {
                    stopSelf()
                }
            }
            ACTION_PAUSE -> handlePause()
            ACTION_SEEK_LIVE -> handleSeekLive()
            ACTION_STOP -> handleStop()
        }
        return START_NOT_STICKY
    }

    private fun launchPlay(stationId: Long) {
        val oldJob = playJob
        val currentState = controller.state.value
        val isResume = currentState is PlaybackState.Paused && currentState.station.id == stationId
        Log.i(TAG, "launchPlay stationId=$stationId isResume=$isResume hasOldJob=${oldJob != null}")
        playJob = serviceScope.launch {
            oldJob?.let {
                Log.i(TAG, "Stopping old engine and joining old job…")
                if (!isResume) {
                    stayConnected = false
                }
                engine?.stop()
                it.join()
                Log.i(TAG, "Old job joined")
            }
            stayConnected = app.preferences.stayConnected.first()
            val station = stationDao.getStationById(stationId)
            if (station != null) {
                handlePlay(station, reuseSession = isResume)
            } else {
                transition(PlaybackState.Idle)
                stopSelf()
            }
        }
    }

    override fun onDestroy() {
        cleanupResources()
        serviceScope.cancel()
        super.onDestroy()
    }

    private fun cleanupResources() {
        stopForeground(STOP_FOREGROUND_REMOVE)
        abandonAudioFocus()
        engine?.stop()
        engine = null
        releaseLocks()
        mediaSession?.release()
        mediaSession = null
        playerAdapter?.clearState()
        playerAdapter = null
        unregisterNetworkCallback()
        controller.updateLatency(0)
    }

    private suspend fun handlePlay(station: Station, reuseSession: Boolean = false) {
        if (!reuseSession) {
            sessionStartedAt = System.currentTimeMillis()
            listeningSessionId = listeningSessionDao.insert(
                ListeningSession(
                    stationId = station.id,
                    startedAt = sessionStartedAt
                )
            )
        }

        acquireLocks()
        playerAdapter?.updateStation(station)
        playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station))

        if (!requestAudioFocus()) {
            Log.w(TAG, "Audio focus denied — not starting playback")
            transition(PlaybackState.Idle)
            cleanupResources()
            stopSelf()
            return
        }

        try {
            val urls = app.streamResolver.resolveUrls(station)
            Log.i(TAG, "handlePlay resolved ${urls.size} URLs for '${station.name}': ${urls.take(3)}")
            startEngine(station, urls)
            if (stayConnected) {
                reconnectLoop(station)
            }
        } catch (e: Exception) {
            Log.w(TAG, "handlePlay caught: ${e.message}", e)
            if (stayConnected) {
                reconnectLoop(station)
            }
        } finally {
            endConnectionSpan()
            val currentState = controller.state.value
            when {
                currentState is PlaybackState.Paused -> {
                    playerAdapter?.updatePlaybackState(PlaybackState.Paused(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt))
                }
                else -> {
                    val isActiveJob = playJob == coroutineContext[Job]
                    if (isActiveJob) {
                        transition(PlaybackState.Idle)
                        playerAdapter?.updatePlaybackState(PlaybackState.Idle)
                        endListeningSession()
                        cleanupResources()
                        stopSelf()
                    }
                }
            }
        }
    }

    private fun handlePause() {
        stayConnected = false
        retryImmediatelyOnNetwork = false
        engine?.stop()
    }

    private fun handleSeekLive() {
        engine?.skipAhead()
    }

    private fun handleStop() {
        Log.i(TAG, "handleStop()")
        stayConnected = false
        retryImmediatelyOnNetwork = false
        engine?.stop()
    }

    private fun acquireLocks() {
        releaseLocks()
        val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Radio247:Playback").apply {
            acquire(10 * 60 * 1000L)
        }
        val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? android.net.wifi.WifiManager
        wifiLock = wifiManager?.createWifiLock(android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Radio247:Playback")?.apply {
            acquire()
        }
    }

    private fun releaseLocks() {
        wakeLock?.let {
            if (it.isHeld) it.release()
            wakeLock = null
        }
        wifiLock?.let {
            if (it.isHeld) it.release()
            wifiLock = null
        }
    }

    private fun ensureMediaSession() {
        if (mediaSession != null) return

        val adapter = RadioPlayerAdapter(
            onPlay = {
                val state = controller.state.value
                val station = when (state) {
                    is PlaybackState.Playing -> state.station
                    is PlaybackState.Paused -> state.station
                    is PlaybackState.Connecting -> state.station
                    is PlaybackState.Reconnecting -> state.station
                    is PlaybackState.Idle -> null
                }
                station?.let { controller.play(it) }
            },
            onStop = { controller.stop() }
        )
        playerAdapter = adapter

        val sessionActivityIntent = packageManager
            .getLaunchIntentForPackage(packageName)
            ?.let { intent ->
                PendingIntent.getActivity(
                    this, 0, intent,
                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                )
            }

        val seekToLiveButton = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD)
            .setDisplayName("Live")
            .setSessionCommand(seekToLiveCommand)
            .build()

        mediaSession = MediaLibrarySession.Builder(this, adapter, LibrarySessionCallback())
            .also { builder ->
                sessionActivityIntent?.let { builder.setSessionActivity(it) }
            }
            .setCustomLayout(listOf(seekToLiveButton))
            .build()

        addSession(mediaSession!!)
    }

    private inner class LibrarySessionCallback : MediaLibraryService.MediaLibrarySession.Callback {

        override fun onGetLibraryRoot(
            session: MediaLibraryService.MediaLibrarySession,
            browser: MediaSession.ControllerInfo,
            params: MediaLibraryService.LibraryParams?
        ): ListenableFuture<LibraryResult<MediaItem>> {
            val root = MediaItem.Builder()
                .setMediaId("root")
                .setMediaMetadata(
                    MediaMetadata.Builder()
                        .setIsBrowsable(true)
                        .setIsPlayable(false)
                        .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
                        .build()
                )
                .build()
            return Futures.immediateFuture(LibraryResult.ofItem(root, params))
        }

        override fun onGetChildren(
            session: MediaLibraryService.MediaLibrarySession,
            browser: MediaSession.ControllerInfo,
            parentId: String,
            page: Int,
            pageSize: Int,
            params: MediaLibraryService.LibraryParams?
        ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
            val future = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
            serviceScope.launch {
                try {
                    val items = when {
                        parentId == "root" -> buildRootChildren()
                        parentId == "unsorted" -> buildUnsortedStations()
                        parentId.startsWith("playlist:") -> {
                            val playlistId = parentId.removePrefix("playlist:").toLong()
                            buildPlaylistStations(playlistId)
                        }
                        else -> emptyList()
                    }
                    future.set(LibraryResult.ofItemList(ImmutableList.copyOf(items), params))
                } catch (_: Exception) {
                    future.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN))
                }
            }
            return future
        }

        override fun onCustomCommand(
            session: MediaSession,
            controller: MediaSession.ControllerInfo,
            customCommand: SessionCommand,
            args: Bundle
        ): ListenableFuture<SessionResult> {
            if (customCommand.customAction == "SEEK_TO_LIVE") {
                handleSeekLive()
                return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
            }
            return super.onCustomCommand(session, controller, customCommand, args)
        }

        override fun onConnect(
            session: MediaSession,
            controller: MediaSession.ControllerInfo
        ): MediaSession.ConnectionResult {
            val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS
                .buildUpon()
                .add(seekToLiveCommand)
                .build()
            return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
                .setAvailableSessionCommands(sessionCommands)
                .build()
        }

        override fun onAddMediaItems(
            mediaSession: MediaSession,
            controller: MediaSession.ControllerInfo,
            mediaItems: MutableList<MediaItem>
        ): ListenableFuture<List<MediaItem>> {
            val item = mediaItems.firstOrNull()
                ?: return Futures.immediateFuture(emptyList())
            val mediaId = item.mediaId
            if (mediaId.startsWith("station:")) {
                val stationId = mediaId.removePrefix("station:").toLongOrNull()
                if (stationId != null) {
                    serviceScope.launch {
                        val station = stationDao.getStationById(stationId)
                        station?.let { this@RadioPlaybackService.controller.play(it) }
                    }
                }
            }
            return Futures.immediateFuture(mediaItems)
        }
    }

    private suspend fun buildRootChildren(): List<MediaItem> {
        val items = mutableListOf<MediaItem>()
        val playlists: List<Playlist> = database.playlistDao().getAllPlaylists().first()
        for (playlist in playlists) {
            items.add(
                MediaItem.Builder()
                    .setMediaId("playlist:${playlist.id}")
                    .setMediaMetadata(
                        MediaMetadata.Builder()
                            .setTitle(playlist.name)
                            .setIsBrowsable(true)
                            .setIsPlayable(false)
                            .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS)
                            .build()
                    )
                    .build()
            )
        }
        val unsorted = stationDao.getUnsortedStations().first()
        if (unsorted.isNotEmpty()) {
            items.add(
                MediaItem.Builder()
                    .setMediaId("unsorted")
                    .setMediaMetadata(
                        MediaMetadata.Builder()
                            .setTitle("Unsorted")
                            .setIsBrowsable(true)
                            .setIsPlayable(false)
                            .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS)
                            .build()
                    )
                    .build()
            )
        }
        return items
    }

    private suspend fun buildPlaylistStations(playlistId: Long): List<MediaItem> {
        return stationDao.getStationsByPlaylist(playlistId).first().map { it.toMediaItem() }
    }

    private suspend fun buildUnsortedStations(): List<MediaItem> {
        return stationDao.getUnsortedStations().first().map { it.toMediaItem() }
    }

    private fun Station.toMediaItem(): MediaItem {
        return MediaItem.Builder()
            .setMediaId("station:${id}")
            .setMediaMetadata(
                MediaMetadata.Builder()
                    .setTitle(name)
                    .setArtworkUri(defaultArtworkUrl?.let { android.net.Uri.parse(it) })
                    .setIsPlayable(true)
                    .setIsBrowsable(false)
                    .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
                    .build()
            )
            .build()
    }

    private suspend fun startEngine(station: Station, urls: List<String>) {
        for ((index, url) in urls.withIndex()) {
            reconnectionMutex.withLock {
                engine?.stop()
                transition(
                    PlaybackState.Connecting(
                        station = station,
                        urls = urls,
                        currentUrlIndex = index,
                        sessionStartedAt = sessionStartedAt
                    )
                )
                playerAdapter?.updatePlaybackState(PlaybackState.Connecting(station = station))

                val bufferMs = app.preferences.bufferMs.first()
                engine = AudioEngine(url, bufferMs)
                connectionSpanId = connectionSpanDao.insert(
                    ConnectionSpan(
                        sessionId = listeningSessionId,
                        startedAt = System.currentTimeMillis()
                    )
                )
                currentMetadata = null
                engine!!.start()
            }

            try {
                awaitEngine(station)
                return
            } catch (e: Exception) {
                endConnectionSpan()
                if (e is ConnectionFailedException && index < urls.size - 1) {
                    continue
                }
                throw e
            }
        }
        throw Exception("All URLs exhausted")
    }

    private suspend fun awaitEngine(station: Station) {
        val deferred = CompletableDeferred<Unit>()
        val connectionStartedAt = System.currentTimeMillis()

        val collectJob = serviceScope.launch {
            engine!!.events.collect { event ->
                when (event) {
                    is AudioEngineEvent.Started -> {
                        transition(
                            PlaybackState.Playing(
                                station = station,
                                metadata = null,
                                sessionStartedAt = sessionStartedAt,
                                connectionStartedAt = connectionStartedAt
                            )
                        )
                        playerAdapter?.updatePlaybackState(PlaybackState.Playing(station = station))
                        controller.updateLatency(engine!!.estimatedLatencyMs)
                    }
                    is AudioEngineEvent.MetadataChanged -> {
                        currentMetadata = event.metadata
                        val playingState = controller.state.value
                        if (playingState is PlaybackState.Playing) {
                            transition(playingState.copy(metadata = event.metadata))
                        }
                        serviceScope.launch {
                            val artUrl = app.albumArtResolver.resolve(
                                artist = event.metadata.artist,
                                title = event.metadata.title,
                                icyStreamUrl = event.metadata.streamUrl,
                                stationArtworkUrl = station.defaultArtworkUrl
                            )
                            val artUri = artUrl?.let { android.net.Uri.parse(it) }
                            playerAdapter?.updateMetadata(station, event.metadata, artUri)
                        }
                        persistMetadataSnapshot(station.id, event.metadata)
                    }
                    is AudioEngineEvent.StreamInfoReceived -> {
                        val playingState = controller.state.value
                        if (playingState is PlaybackState.Playing) {
                            transition(playingState.copy(streamInfo = event.streamInfo))
                        }
                    }
                    is AudioEngineEvent.AudioFormatDetected -> {
                        val playingState = controller.state.value
                        if (playingState is PlaybackState.Playing) {
                            val updated = (playingState.streamInfo ?: StreamInfo(
                                bitrate = null, ssl = false, contentType = null
                            )).copy(
                                sampleRate = event.sampleRate,
                                channelCount = event.channelCount
                            )
                            transition(playingState.copy(streamInfo = updated))
                        }
                    }
                    is AudioEngineEvent.Error -> {
                        engine?.stop()
                        engine = null
                        val throwable = when (val cause = event.cause) {
                            is EngineError.ConnectionFailed ->
                                ConnectionFailedException(cause.cause)
                            is EngineError.StreamEnded ->
                                Exception("Stream ended")
                            is EngineError.DecoderError ->
                                cause.cause
                            is EngineError.AudioOutputError ->
                                cause.cause
                        }
                        deferred.completeExceptionally(throwable)
                        return@collect
                    }
                    is AudioEngineEvent.Stopped -> {
                        deferred.complete(Unit)
                        return@collect
                    }
                }
            }
            if (!deferred.isCompleted) {
                deferred.completeExceptionally(Exception("Event flow completed unexpectedly"))
            }
        }
        try {
            deferred.await()
        } finally {
            collectJob.cancel()
        }
    }

    private suspend fun reconnectLoop(station: Station) {
        var attempt = 0
        registerNetworkCallback()
        while (stayConnected) {
            if (retryImmediatelyOnNetwork) {
                retryImmediatelyOnNetwork = false
            } else {
                attempt++
                transition(
                    PlaybackState.Reconnecting(
                        station = station,
                        metadata = currentMetadata,
                        sessionStartedAt = sessionStartedAt,
                        attempt = attempt
                    )
                )
                playerAdapter?.updatePlaybackState(PlaybackState.Reconnecting(station = station, metadata = currentMetadata, sessionStartedAt = sessionStartedAt, attempt = attempt))
                val delayMs = minOf(1000L * (1 shl (attempt - 1)), 30_000L)
                val chunk = 500L
                var remaining = delayMs
                while (remaining > 0 && !retryImmediatelyOnNetwork && stayConnected) {
                    delay(minOf(chunk, remaining))
                    remaining -= chunk
                }
            }
            if (!stayConnected) break
            try {
                val urls = app.streamResolver.resolveUrls(station)
                startEngine(station, urls)
                return
            } catch (_: Exception) {
                // Continue loop
            }
        }
    }

    private fun registerNetworkCallback() {
        if (networkAvailableCallback != null) return
        val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        networkAvailableCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                retryImmediatelyOnNetwork = true
            }
        }
        cm.registerNetworkCallback(request, networkAvailableCallback!!)
    }

    private fun unregisterNetworkCallback() {
        networkAvailableCallback?.let { callback ->
            try {
                (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager)
                    .unregisterNetworkCallback(callback)
            } catch (_: Exception) {}
            networkAvailableCallback = null
        }
    }

    private suspend fun persistMetadataSnapshot(stationId: Long, metadata: IcyMetadata) {
        val now = System.currentTimeMillis()
        withContext(Dispatchers.IO) {
            metadataSnapshotDao.insert(
                MetadataSnapshot(
                    stationId = stationId,
                    title = metadata.title,
                    artist = metadata.artist,
                    artworkUrl = null,
                    timestamp = now
                )
            )
            val station = stationDao.getStationById(stationId)
            app.historyWriter.append(
                station = station?.name ?: "Unknown",
                artist = metadata.artist,
                title = metadata.title,
                timestamp = now
            )
        }
    }

    private fun endConnectionSpan() {
        val spanId = connectionSpanId
        if (spanId != 0L) {
            connectionSpanId = 0L
            serviceScope.launch(Dispatchers.IO) {
                connectionSpanDao.updateEndedAt(spanId, System.currentTimeMillis())
            }
        }
    }

    private fun endListeningSession() {
        val sessionId = listeningSessionId
        if (sessionId != 0L) {
            listeningSessionId = 0L
            serviceScope.launch(Dispatchers.IO) {
                listeningSessionDao.updateEndedAt(sessionId, System.currentTimeMillis())
            }
        }
    }
}
