package com.dtourolle.jellytau.player

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat
import androidx.media.VolumeProviderCompat
import androidx.media3.common.ForwardingPlayer
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture

/**
 * MediaSessionService for lockscreen controls and media notifications.
 *
 * This service creates a MediaSession that integrates with the system's
 * media controls (lockscreen, notification shade, Bluetooth devices).
 *
 * Media commands are routed back to Rust via JNI to ensure proper
 * queue management for next/previous track operations.
 */
@OptIn(UnstableApi::class)
class JellyTauPlaybackService : MediaSessionService() {

    private var mediaSession: MediaSession? = null
    private var mediaSessionCompat: MediaSessionCompat? = null
    private var wrappedPlayer: androidx.media3.common.ForwardingPlayer? = null
    private var volumeProvider: VolumeProviderCompat? = null
    private var isRemoteVolumeEnabled = false
    private var remoteVolumeLevel = 50 // 0-100

    companion object {
        private const val NOTIFICATION_ID = 1
        private const val NOTIFICATION_CHANNEL_ID = "playback_channel"
        private const val NOTIFICATION_CHANNEL_NAME = "Playback"

        @Volatile
        private var instance: JellyTauPlaybackService? = null

        /**
         * Get the service instance if running.
         */
        @JvmStatic
        fun getInstance(): JellyTauPlaybackService? = instance

        init {
            // Ensure native library is loaded for JNI callbacks
            System.loadLibrary("jellytau_lib")
        }
    }

    override fun onCreate() {
        super.onCreate()
        instance = this

        // Create notification channel for Android O+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_NAME,
                NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "Media playback controls"
                setShowBadge(false)
            }
            val notificationManager = getSystemService(NotificationManager::class.java)
            notificationManager.createNotificationChannel(channel)
        }

        // Check if JellyTauPlayer is initialized
        if (!JellyTauPlayer.isInitialized()) {
            android.util.Log.w("JellyTauPlaybackService", "JellyTauPlayer not initialized, initializing now")
            // Initialize the player with application context
            JellyTauPlayer.initialize(applicationContext)
        }

        // Get the existing JellyTauPlayer instance with its ExoPlayer
        val jellyTauPlayer = JellyTauPlayer.getInstance()
        val exoPlayer = jellyTauPlayer.getExoPlayer()

        // Wrap the ExoPlayer to intercept commands from Media3 controllers
        // (e.g. Android Auto / Wear / system surfaces that bind to the Media3
        // session rather than the MediaSessionCompat).
        //
        // We do NOT execute on ExoPlayer directly here. Every transport command
        // is routed to Rust via nativeOnMediaCommand, which is the single decision
        // point: in local mode Rust drives ExoPlayer, in remote (cast) mode Rust
        // forwards to the remote Jellyfin session. Executing on ExoPlayer here too
        // would double-handle local commands and incorrectly drive the local
        // player while casting.
        wrappedPlayer = object : ForwardingPlayer(exoPlayer) {
            override fun play() {
                nativeOnMediaCommand("play")
            }

            override fun pause() {
                nativeOnMediaCommand("pause")
            }

            override fun seekToNext() {
                nativeOnMediaCommand("next")
            }

            override fun seekToPrevious() {
                nativeOnMediaCommand("previous")
            }

            override fun seekTo(positionMs: Long) {
                val positionSeconds = positionMs / 1000.0
                nativeOnMediaCommand("seek:$positionSeconds")
            }

            override fun stop() {
                nativeOnMediaCommand("stop")
            }
        }

        // Create MediaSession with the wrapped player and callback for command handling
        mediaSession = MediaSession.Builder(this, wrappedPlayer!!)
            .setCallback(object : MediaSession.Callback {
                override fun onSetMediaItems(
                    mediaSession: MediaSession,
                    controller: MediaSession.ControllerInfo,
                    mediaItems: MutableList<androidx.media3.common.MediaItem>,
                    startIndex: Int,
                    startPositionMs: Long
                ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
                    return Futures.immediateFuture(
                        MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)
                    )
                }
            })
            .build()

        // Create MediaSessionCompat for volume control and lock screen button handling
        // We need this alongside Media3's MediaSession because MediaSessionCompat provides
        // VolumeProviderCompat support for remote volume control (routing hardware button presses)
        mediaSessionCompat = MediaSessionCompat(this, "JellyTauMediaSession").apply {
            setFlags(
                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
                MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
            )
            isActive = true

            // Set callback to handle lock screen button presses.
            //
            // All transport commands are routed through Rust via nativeOnMediaCommand
            // rather than directly to ExoPlayer. Rust is the single decision point:
            // in local mode it drives ExoPlayer, in remote (cast) mode it forwards
            // the command to the remote Jellyfin session. This keeps the lockscreen
            // working identically for both, and avoids the ExoPlayer-only behaviour
            // that left remote playback uncontrollable from the lockscreen.
            setCallback(object : MediaSessionCompat.Callback() {
                override fun onPlay() {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Play pressed")
                    nativeOnMediaCommand("play")
                }

                override fun onPause() {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Pause pressed")
                    nativeOnMediaCommand("pause")
                }

                override fun onSkipToNext() {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Next pressed")
                    nativeOnMediaCommand("next")
                }

                override fun onSkipToPrevious() {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Previous pressed")
                    nativeOnMediaCommand("previous")
                }

                override fun onStop() {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Stop pressed")
                    nativeOnMediaCommand("stop")
                }

                override fun onSeekTo(position: Long) {
                    android.util.Log.d("JellyTauPlaybackService", "Lock screen: Seek to $position")
                    // The scrubber is absolute; Rust owns the seek in absolute terms
                    // (in a background-audio handoff it rebuilds the stream at this
                    // StartTimeTicks). Send the absolute position as-is.
                    val positionSeconds = position / 1000.0
                    nativeOnMediaCommand("seek:$positionSeconds")
                }
            })
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // Start as foreground service immediately to avoid crash
        // Media3 will replace this with its own notification
        val notification = createBasicNotification()
        startForeground(NOTIFICATION_ID, notification)
        return super.onStartCommand(intent, flags, startId)
    }

    private fun createBasicNotification(): Notification {
        // Create a media-style notification with lockscreen controls
        val intent = packageManager.getLaunchIntentForPackage(packageName)
        val pendingIntent = PendingIntent.getActivity(
            this,
            0,
            intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )

        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle("JellyTau")
            .setContentText("Playing")
            .setSmallIcon(android.R.drawable.ic_media_play)
            .setContentIntent(pendingIntent)
            .setStyle(
                androidx.media.app.NotificationCompat.MediaStyle()
                    .setMediaSession(mediaSessionCompat?.sessionToken)
                    .setShowActionsInCompactView(0, 1, 2) // Show all 3 buttons in compact view
            )
            .addAction(
                android.R.drawable.ic_media_previous,
                "Previous",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
                )
            )
            .addAction(
                android.R.drawable.ic_media_pause,
                "Pause",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_PAUSE
                )
            )
            .addAction(
                android.R.drawable.ic_media_next,
                "Next",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_NEXT
                )
            )
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setOngoing(true)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Show on lockscreen
            .build()
    }

    // Last-known metadata/state, retained so lightweight position ticks can
    // rebuild a correct PlaybackState without re-sending the (heavier) metadata
    // and notification. Kept in sync by updateMediaMetadata().
    private var lastTitle: String = ""
    private var lastArtist: String = ""
    private var lastIsPlaying: Boolean = false

    // Base offset (ms) added to every position reported to the lockscreen
    // MediaSession. During a background-audio handoff the audio stream is
    // requested with StartTimeTicks = the handoff point, so ExoPlayer reports
    // position RELATIVE to that point (starting at 0). The metadata duration,
    // however, is the full absolute length — so without this base the scrubber
    // thumb sits near 0:00 on a full-length bar. Set from the known handoff
    // position via setPositionOffset(); 0 for normal playback.
    private var positionOffsetMs: Long = 0L

    /**
     * Set the base position offset (seconds) applied to lockscreen positions.
     * Called by the native layer when entering/exiting a background-audio handoff.
     * Pass 0 to clear (normal playback, where ExoPlayer's position is absolute).
     */
    fun setPositionOffset(offsetSeconds: Double) {
        positionOffsetMs = (offsetSeconds * 1000.0).toLong().coerceAtLeast(0L)
        android.util.Log.d("JellyTauPlaybackService", "Position offset set to ${positionOffsetMs}ms")
    }

    /**
     * Update the MediaSession metadata and playback state, plus the notification.
     *
     * Call this when the track or play/pause state changes. For frequent position
     * updates during playback, use [updatePlaybackPosition] instead, which is much
     * cheaper (no metadata rebuild, no notification rebuild).
     */
    fun updateMediaMetadata(
        title: String,
        artist: String,
        album: String?,
        duration: Long,
        position: Long,
        isPlaying: Boolean
    ) {
        val session = mediaSessionCompat ?: return

        lastTitle = title
        lastArtist = artist
        lastIsPlaying = isPlaying

        // Update MediaSession metadata
        val metadataBuilder = android.support.v4.media.MediaMetadataCompat.Builder()
            .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_TITLE, title)
            .putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
            .putLong(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION, duration)

        album?.let {
            metadataBuilder.putString(android.support.v4.media.MediaMetadataCompat.METADATA_KEY_ALBUM, it)
        }

        session.setMetadata(metadataBuilder.build())

        // Update MediaSession playback state (position made absolute via the base offset).
        session.setPlaybackState(buildPlaybackState(isPlaying, position + positionOffsetMs))

        // While casting, re-assert the remote volume provider. Metadata pushes
        // arrive on the session poller thread and can race with (or arrive
        // before) enableRemoteVolume(); this keeps the session routed to the
        // remote (absolute) volume slider instead of the local media stream.
        if (isRemoteVolumeEnabled) {
            volumeProvider?.let { session.setPlaybackToRemote(it) }
        }

        // Update the notification
        updateNotification(title, artist, isPlaying)
    }

    /**
     * Update only the playback position (and play/pause state) on the MediaSession.
     *
     * This is the cheap path used for the periodic (250ms) position ticks: it
     * refreshes the lockscreen scrubber without rebuilding metadata or the
     * notification. Without this, the lockscreen scrubber freezes at the position
     * from the last play/pause and drifts out of sync with actual playback.
     *
     * @param position Position in milliseconds
     * @param isPlaying Whether playback is currently active
     */
    fun updatePlaybackPosition(position: Long, isPlaying: Boolean) {
        val session = mediaSessionCompat ?: return
        val notificationStateChanged = isPlaying != lastIsPlaying
        lastIsPlaying = isPlaying
        // Absolute position for the scrubber = relative ExoPlayer position + base offset.
        session.setPlaybackState(buildPlaybackState(isPlaying, position + positionOffsetMs))
        // Only rebuild the notification when the play/pause icon actually flips.
        if (notificationStateChanged) {
            updateNotification(lastTitle, lastArtist, isPlaying)
        }
    }

    /**
     * Build a PlaybackStateCompat with the standard transport actions.
     *
     * The reported playback speed is 1.0 while playing and 0.0 while paused so
     * Android does not extrapolate the position past a paused track.
     *
     * While remote volume control is enabled (casting), the state is forced to
     * STATE_PLAYING regardless of [isPlaying]. Android only surfaces the remote
     * (absolute) volume slider for a session that is actively playing; if a
     * periodic metadata/position push reports paused (e.g. before the remote
     * session has actually started), reporting STATE_PAUSED here makes the
     * system tear down the remote slider set up by setPlaybackToRemote() and
     * fall back to the local media-stream volume.
     */
    private fun buildPlaybackState(isPlaying: Boolean, position: Long): PlaybackStateCompat {
        val playing = isPlaying || isRemoteVolumeEnabled
        return PlaybackStateCompat.Builder()
            .setActions(
                PlaybackStateCompat.ACTION_PLAY or
                PlaybackStateCompat.ACTION_PAUSE or
                PlaybackStateCompat.ACTION_STOP or
                PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
                PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
                PlaybackStateCompat.ACTION_SEEK_TO
            )
            .setState(
                if (playing) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED,
                position,
                if (playing) 1.0f else 0.0f
            )
            .build()
    }

    /**
     * Update the notification with current media metadata and playback state.
     * This should be called whenever metadata or playback state changes.
     */
    private fun updateNotification(title: String, artist: String, isPlaying: Boolean) {
        val intent = packageManager.getLaunchIntentForPackage(packageName)
        val pendingIntent = PendingIntent.getActivity(
            this,
            0,
            intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )

        val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle(title)
            .setContentText(artist)
            .setSmallIcon(android.R.drawable.ic_media_play)
            .setContentIntent(pendingIntent)
            .setStyle(
                androidx.media.app.NotificationCompat.MediaStyle()
                    .setMediaSession(mediaSessionCompat?.sessionToken)
                    .setShowActionsInCompactView(0, 1, 2) // Show all 3 buttons in compact view
            )
            .addAction(
                android.R.drawable.ic_media_previous,
                "Previous",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
                )
            )
            .addAction(
                if (isPlaying) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
                if (isPlaying) "Pause" else "Play",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    if (isPlaying) PlaybackStateCompat.ACTION_PAUSE else PlaybackStateCompat.ACTION_PLAY
                )
            )
            .addAction(
                android.R.drawable.ic_media_next,
                "Next",
                androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
                    this,
                    PlaybackStateCompat.ACTION_SKIP_TO_NEXT
                )
            )
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setOngoing(isPlaying)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Show on lockscreen
            .build()

        val notificationManager = getSystemService(NotificationManager::class.java)
        notificationManager.notify(NOTIFICATION_ID, notification)
    }

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

    /**
     * Enable remote volume control for remote playback (e.g., casting to Jellyfin session).
     * Volume button presses will be sent to Rust for forwarding to the remote session.
     *
     * Uses MediaSessionCompat with VolumeProviderCompat to intercept hardware volume buttons.
     *
     * @param initialVolume Initial volume level (0-100)
     */
    fun enableRemoteVolume(initialVolume: Int) {
        android.util.Log.d("JellyTauPlaybackService", "Enabling remote volume control (volume=$initialVolume)")
        isRemoteVolumeEnabled = true
        remoteVolumeLevel = initialVolume.coerceIn(0, 100)

        val session = mediaSessionCompat ?: run {
            android.util.Log.w("JellyTauPlaybackService", "MediaSessionCompat not initialized")
            return
        }

        // Create a VolumeProvider for remote volume control
        volumeProvider = object : VolumeProviderCompat(
            VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, // Control type: absolute volume
            100, // Max volume (0-100)
            remoteVolumeLevel // Initial volume
        ) {
            override fun onSetVolumeTo(volume: Int) {
                if (!isRemoteVolumeEnabled) return

                remoteVolumeLevel = volume.coerceIn(0, 100)
                android.util.Log.d("JellyTauPlaybackService", "Remote volume set to $remoteVolumeLevel")
                nativeOnRemoteVolumeChange("SetVolume", remoteVolumeLevel)
            }

            override fun onAdjustVolume(direction: Int) {
                if (!isRemoteVolumeEnabled) return

                when (direction) {
                    android.media.AudioManager.ADJUST_RAISE -> {
                        remoteVolumeLevel = (remoteVolumeLevel + 2).coerceAtMost(100)
                        android.util.Log.d("JellyTauPlaybackService", "Remote volume up to $remoteVolumeLevel")
                        nativeOnRemoteVolumeChange("VolumeUp", remoteVolumeLevel)
                        // Update the current volume so slider reflects the change
                        currentVolume = remoteVolumeLevel
                    }
                    android.media.AudioManager.ADJUST_LOWER -> {
                        remoteVolumeLevel = (remoteVolumeLevel - 2).coerceAtLeast(0)
                        android.util.Log.d("JellyTauPlaybackService", "Remote volume down to $remoteVolumeLevel")
                        nativeOnRemoteVolumeChange("VolumeDown", remoteVolumeLevel)
                        // Update the current volume so slider reflects the change
                        currentVolume = remoteVolumeLevel
                    }
                }
            }
        }

        // Set the volume provider on the media session to route hardware volume buttons
        session.setPlaybackToRemote(volumeProvider!!)

        // Set playback state to make Android show the volume UI
        // This tells Android that this session is actively controlling media playback
        val playbackState = PlaybackStateCompat.Builder()
            .setState(
                PlaybackStateCompat.STATE_PLAYING,
                PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
                1.0f
            )
            .setActions(
                PlaybackStateCompat.ACTION_PLAY or
                PlaybackStateCompat.ACTION_PAUSE or
                PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
                PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
            )
            .build()
        session.setPlaybackState(playbackState)

        android.util.Log.d("JellyTauPlaybackService", "Remote volume control enabled")
    }

    /**
     * Disable remote volume control and return to local volume control.
     * Volume buttons will control system media volume (ExoPlayer volume).
     */
    fun disableRemoteVolume() {
        android.util.Log.d("JellyTauPlaybackService", "Disabling remote volume control")
        isRemoteVolumeEnabled = false

        val session = mediaSessionCompat ?: run {
            android.util.Log.w("JellyTauPlaybackService", "MediaSessionCompat not initialized")
            return
        }

        // Switch back to local audio stream (device volume)
        session.setPlaybackToLocal(android.media.AudioManager.STREAM_MUSIC)

        // Clear the playback state
        val idleState = PlaybackStateCompat.Builder()
            .setState(
                PlaybackStateCompat.STATE_NONE,
                PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
                0.0f
            )
            .build()
        session.setPlaybackState(idleState)

        // Clear the volume provider
        volumeProvider = null

        // Reset volume level to default
        remoteVolumeLevel = 50

        android.util.Log.d("JellyTauPlaybackService", "Remote volume control disabled")
    }

    /**
     * Update the remote volume level.
     * Call this when volume changes on the remote session to sync the local state.
     *
     * @param volume Volume level (0-100)
     */
    fun updateRemoteVolume(volume: Int) {
        remoteVolumeLevel = volume.coerceIn(0, 100)
        // Update the volume provider's current volume so the UI slider reflects the change
        volumeProvider?.currentVolume = remoteVolumeLevel
        android.util.Log.d("JellyTauPlaybackService", "Remote volume updated to $remoteVolumeLevel")
    }

    override fun onDestroy() {
        mediaSession?.run {
            release()
        }
        mediaSession = null

        mediaSessionCompat?.run {
            isActive = false
            release()
        }
        mediaSessionCompat = null
        volumeProvider = null

        instance = null
        super.onDestroy()
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        // Stop the service when the app is swiped away, unless audio is playing
        val player = mediaSession?.player
        if (player == null || !player.playWhenReady || player.mediaItemCount == 0) {
            stopSelf()
        }
    }

    /**
     * JNI callback to Rust for media commands.
     * Commands: "play", "pause", "next", "previous", "stop", "seek:123.45"
     */
    private external fun nativeOnMediaCommand(command: String)

    /**
     * JNI callback to Rust for remote volume changes.
     * Commands: "SetVolume", "VolumeUp", "VolumeDown"
     * @param command The volume command
     * @param volume The volume level (0-100)
     */
    private external fun nativeOnRemoteVolumeChange(command: String, volume: Int)
}
