package ca.ksamad.encore.playback

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.IBinder
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.media.VolumeProviderCompat
import androidx.media.app.NotificationCompat.MediaStyle
import androidx.media.session.MediaButtonReceiver
import ca.ksamad.encore.EncoreApplication
import ca.ksamad.encore.MainActivity
import ca.ksamad.encore.R
import ca.ksamad.encore.mpd.MpdConnectionState
import ca.ksamad.encore.mpd.model.MpdSong
import ca.ksamad.encore.mpd.model.MpdStatus
import ca.ksamad.encore.mpd.model.PlayerState
import coil3.SingletonImageLoader
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.toBitmap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

/**
 * Foreground service that mirrors the shared [MpdConnectionManager] into an OS
 * [MediaSessionCompat]: it publishes now-playing metadata + playback state (driving the media
 * notification, lock screen, and Quick Settings player), exposes transport controls, and — via
 * [setPlaybackToRemote] with a [VolumeProviderCompat] — makes the hardware volume keys control the
 * *server's* volume system-wide, cast-style, even when the app is in the background.
 *
 * Started/stopped by [MpdConnectionManager] as the connection comes and goes.
 */
class PlaybackService : Service() {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
    private val manager
        get() = (application as EncoreApplication).manager

    private lateinit var session: MediaSessionCompat
    private lateinit var volumeProvider: VolumeProviderCompat

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()

        session =
            MediaSessionCompat(this, "Encore").apply {
                setCallback(mediaCallback)
                isActive = true
            }

        // Remote (cast-style) volume: absolute 0..100, initialised from the server.
        volumeProvider =
            object :
                VolumeProviderCompat(
                    VOLUME_CONTROL_ABSOLUTE,
                    MAX_VOLUME,
                    manager.status.value?.volume ?: 0,
                ) {
                override fun onSetVolumeTo(volume: Int) {
                    manager.setVolume(volume)
                    currentVolume = volume
                }

                override fun onAdjustVolume(direction: Int) {
                    if (direction != 0) manager.nudgeVolume(up = direction > 0)
                }
            }
        session.setPlaybackToRemote(volumeProvider)

        createChannel()
        observeState()
    }

    override fun onStartCommand(
        intent: Intent?,
        flags: Int,
        startId: Int,
    ): Int {
        // Deliver hardware / notification media-button presses to the session.
        MediaButtonReceiver.handleIntent(session, intent)
        startForeground(NOTIFICATION_ID, buildNotification())
        return START_STICKY
    }

    override fun onDestroy() {
        scope.cancel()
        session.isActive = false
        session.release()
        super.onDestroy()
    }

    private val mediaCallback =
        object : MediaSessionCompat.Callback() {
            override fun onPlay() = manager.resume()

            override fun onPause() = manager.pause()

            override fun onStop() = manager.stop()

            override fun onSkipToNext() = manager.next()

            override fun onSkipToPrevious() = manager.previous()

            override fun onSeekTo(pos: Long) = manager.seekTo(pos / 1000.0)
        }

    // Album art for the session/notification, cached against the song it's for.
    private var artUri: String? = null
    private var artBitmap: Bitmap? = null

    private fun observeState() {
        scope.launch {
            combine(manager.status, manager.currentSong) { s, song -> s to song }
                .collect { (status, song) ->
                    session.setMetadata(buildMetadata(song, status))
                    session.setPlaybackState(buildPlaybackState(status))
                    volumeProvider.currentVolume = status?.volume ?: 0
                    postNotification()
                }
        }
        // Fetch cover art on song change (via Coil, so it shares the app's cache)
        // and re-publish the metadata/notification once it arrives.
        scope.launch {
            manager.currentSong
                .map { it?.uri }
                .distinctUntilChanged()
                .collectLatest { uri ->
                    artUri = uri
                    val version = manager.currentSong.value?.takeIf { it.uri == uri }?.lastModified
                    artBitmap = if (uri != null) loadArtBitmap(uri, version) else null
                    if (artUri == uri) {
                        session.setMetadata(
                            buildMetadata(manager.currentSong.value, manager.status.value)
                        )
                        postNotification()
                    }
                }
        }
        scope.launch {
            manager.connectionState.collect { st ->
                if (st is MpdConnectionState.Disconnected || st is MpdConnectionState.Error) {
                    stopForegroundCompat()
                    stopSelf()
                }
            }
        }
    }

    private suspend fun loadArtBitmap(
        uri: String,
        version: String?,
    ): Bitmap? {
        val result =
            SingletonImageLoader.get(applicationContext)
                .execute(
                    ImageRequest.Builder(applicationContext).data(SongArt(uri, version)).build()
                )
        return (result as? SuccessResult)?.image?.toBitmap()
    }

    private fun buildMetadata(
        song: MpdSong?,
        status: MpdStatus?,
    ): MediaMetadataCompat =
        MediaMetadataCompat.Builder()
            .apply {
                putString(MediaMetadataCompat.METADATA_KEY_TITLE, song?.title ?: song?.uri ?: "")
                putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song?.artist ?: "")
                putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song?.album ?: "")
                val durationMs = ((song?.duration ?: status?.duration ?: 0.0) * 1000).toLong()
                putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs)
                if (artBitmap != null && artUri == song?.uri) {
                    putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, artBitmap)
                }
            }
            .build()

    private fun buildPlaybackState(status: MpdStatus?): PlaybackStateCompat {
        val state =
            when (status?.state) {
                PlayerState.PLAY -> PlaybackStateCompat.STATE_PLAYING
                PlayerState.PAUSE -> PlaybackStateCompat.STATE_PAUSED
                else -> PlaybackStateCompat.STATE_STOPPED
            }
        val positionMs = ((status?.elapsed ?: 0.0) * 1000).toLong()
        val speed = if (status?.state == PlayerState.PLAY) 1f else 0f
        return PlaybackStateCompat.Builder()
            .setActions(
                PlaybackStateCompat.ACTION_PLAY or
                    PlaybackStateCompat.ACTION_PAUSE or
                    PlaybackStateCompat.ACTION_PLAY_PAUSE or
                    PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
                    PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
                    PlaybackStateCompat.ACTION_SEEK_TO or
                    PlaybackStateCompat.ACTION_STOP
            )
            .setState(state, positionMs, speed)
            .build()
    }

    private fun buildNotification(): android.app.Notification {
        val song = manager.currentSong.value
        val playing = manager.status.value?.state == PlayerState.PLAY

        val contentIntent =
            PendingIntent.getActivity(
                this,
                0,
                Intent(this, MainActivity::class.java),
                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
            )

        val playPause =
            if (playing) {
                NotificationCompat.Action(
                    android.R.drawable.ic_media_pause,
                    "Pause",
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                        this,
                        PlaybackStateCompat.ACTION_PLAY_PAUSE,
                    ),
                )
            } else {
                NotificationCompat.Action(
                    android.R.drawable.ic_media_play,
                    "Play",
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                        this,
                        PlaybackStateCompat.ACTION_PLAY_PAUSE,
                    ),
                )
            }

        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_music_note)
            .setContentTitle(song?.title ?: song?.uri ?: "Encore")
            .setContentText(song?.artist ?: "")
            .setSubText(song?.album)
            .setContentIntent(contentIntent)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            .setOngoing(playing)
            .addAction(
                android.R.drawable.ic_media_previous,
                "Previous",
                MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
                ),
            )
            .addAction(playPause)
            .addAction(
                android.R.drawable.ic_media_next,
                "Next",
                MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
                ),
            )
            .setStyle(
                MediaStyle()
                    .setMediaSession(session.sessionToken)
                    .setShowActionsInCompactView(0, 1, 2)
            )
            .build()
    }

    private fun postNotification() {
        // No-ops silently if POST_NOTIFICATIONS isn't granted; the service still runs.
        NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, buildNotification())
    }

    private fun createChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel(
                        CHANNEL_ID,
                        "Playback",
                        NotificationManager.IMPORTANCE_LOW,
                    )
                    .apply {
                        description = "Now-playing controls"
                        setShowBadge(false)
                    }
            getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }
    }

    @Suppress("DEPRECATION")
    private fun stopForegroundCompat() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(STOP_FOREGROUND_REMOVE)
        } else {
            stopForeground(true)
        }
    }

    companion object {
        private const val CHANNEL_ID = "playback"
        private const val NOTIFICATION_ID = 1
        private const val MAX_VOLUME = 100

        fun start(context: Context) {
            ContextCompat.startForegroundService(
                context,
                Intent(context, PlaybackService::class.java),
            )
        }

        fun stop(context: Context) {
            context.stopService(Intent(context, PlaybackService::class.java))
        }
    }
}
