package ch.threema.app.services

import android.Manifest
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.net.toUri
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.Callback
import androidx.media3.session.MediaSessionService
import ch.threema.android.ToastDuration
import ch.threema.android.buildNotification
import ch.threema.android.isConnectedToHeadsetOrSpeaker
import ch.threema.android.showToast
import ch.threema.app.R
import ch.threema.app.listeners.SensorListener
import ch.threema.app.listeners.SensorListener.keyIsNear
import ch.threema.app.managers.ServiceManager
import ch.threema.app.notifications.NotificationChannels
import ch.threema.app.notifications.NotificationIDs.VOICE_MESSAGE_PLAYER_NOTIFICATION_ID
import ch.threema.app.preference.service.PreferenceService
import ch.threema.app.utils.ConfigUtils
import ch.threema.app.voicemessage.SamsungQuirkRenderersFactory
import ch.threema.base.utils.getThreemaLogger
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture

private val logger = getThreemaLogger("VoiceMessagePlayerService")

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class VoiceMessagePlayerService :
    MediaSessionService(),
    SensorListener,
    OnAudioFocusChangeListener {
    private val audioBecomingNoisyFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
    private val audioBecomingNoisyReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
                player.pause()
            }
        }
    }

    private lateinit var player: ExoPlayer
    private var mediaSession: MediaSession? = null
    private lateinit var audioManager: AudioManager
    private lateinit var audioFocusRequestCompat: AudioFocusRequestCompat

    private var sensorService: SensorService? = null
    private var preferenceService: PreferenceService? = null
    private var hasAudioFocus = false

    override fun onCreate() {
        logger.info("onCreate")
        super.onCreate()

        ServiceManager.get()?.let {
            this.sensorService = it.sensorService
            this.preferenceService = it.preferenceService
        }

        audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
        audioFocusRequestCompat =
            AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT)
                .setAudioAttributes(
                    AudioAttributesCompat.Builder()
                        .setLegacyStreamType(AudioManager.STREAM_MUSIC).build(),
                )
                .setOnAudioFocusChangeListener(this)
                .build()

        val mediaNotificationProvider = DefaultMediaNotificationProvider.Builder(this)
            .setChannelName(R.string.notification_channel_voice_message_player)
            .setChannelId(NotificationChannels.NOTIFICATION_CHANNEL_VOICE_MSG_PLAYER)
            .setNotificationIdProvider { VOICE_MESSAGE_PLAYER_NOTIFICATION_ID }
            .build()
        mediaNotificationProvider.setSmallIcon(R.drawable.ic_notification_small)
        setMediaNotificationProvider(mediaNotificationProvider)

        val sessionWasCreated = initializeSessionAndPlayer()
        if (sessionWasCreated) {
            setListener(MediaSessionServiceListener())
        }
    }

    override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
        if (player.playbackState == Player.STATE_ENDED && !startInForegroundRequired) {
            logger.info("Playback ended.")
        }
        super.onUpdateNotification(session, true)
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
        mediaSession

    override fun onTaskRemoved(rootIntent: Intent?) {
        logger.info("onTaskRemoved")
        if (!player.playWhenReady) {
            stopSelf()
        }
    }

    override fun onDestroy() {
        logger.info("onDestroy")
        destroySelf()
        super.onDestroy()
    }

    private fun destroySelf() {
        if (preferenceService?.isUseProximitySensor() == true) {
            sensorService?.unregisterSensors(TAG)
        }
        releaseAudioFocus()
        player.release()
        mediaSession?.release()
        clearListener()
    }

    private fun initializeSessionAndPlayer(): Boolean {
        player = ExoPlayer.Builder(this)
            .setRenderersFactory(SamsungQuirkRenderersFactory(this))
            .setAudioAttributes(getRegularAudioAttributes(), false)
            .setWakeMode(C.WAKE_MODE_LOCAL)
            .setLoadControl(
                DefaultLoadControl.Builder()
                    .setBufferDurationsMs(
                        Integer.MAX_VALUE,
                        Integer.MAX_VALUE,
                        Integer.MAX_VALUE,
                        Integer.MAX_VALUE,
                    )
                    .build(),
            )
            .build()

        preferenceService?.let { preferenceService ->
            player.addListener(
                object : Player.Listener {
                    override fun onIsPlayingChanged(isPlaying: Boolean) {
                        logger.debug("onIsPlayingChanged {}", isPlaying)
                        if (isPlaying) {
                            logger.info("Start playing")
                            if (preferenceService.isUseProximitySensor()) {
                                if (audioManager.isConnectedToHeadsetOrSpeaker()) {
                                    logger.info("Headphones or speaker connected")
                                } else {
                                    sensorService?.registerSensors(TAG, this@VoiceMessagePlayerService)
                                }
                            }
                            requestAudioFocus()
                        } else {
                            logger.info("Stop playing")
                            if (preferenceService.isUseProximitySensor()) {
                                sensorService?.unregisterSensors(TAG)
                            }
                            releaseAudioFocus()
                        }
                    }
                },
            )
        }

        val uniqueControllerIds: MutableSet<Int> = mutableSetOf()
        val mediaSessionCallback = object : Callback {
            override fun onAddMediaItems(
                mediaSession: MediaSession,
                controller: MediaSession.ControllerInfo,
                mediaItems: MutableList<MediaItem>,
            ): ListenableFuture<List<MediaItem>> {
                val resolvedMediaItems = mediaItems.map { mediaItem ->
                    MediaItem.Builder()
                        .setUri(mediaItem.mediaId.toUri())
                        .setMediaId(mediaItem.mediaId)
                        .setMediaMetadata(mediaItem.mediaMetadata)
                        .build()
                }
                return Futures.immediateFuture(resolvedMediaItems)
            }

            override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
                logger.info("Controller {} connected with hints: {}", controller.uid, controller.connectionHints)
                uniqueControllerIds.add(controller.uid)
            }

            override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
                logger.info("Controller {} disconnected", controller.uid)
                uniqueControllerIds.remove(controller.uid)
                if (uniqueControllerIds.isEmpty()) {
                    logger.info("Stopping self because the last controller disconnected")
                    pauseAllPlayersAndStopSelf()
                }
            }
        }

        val mediaSessionBuilder = MediaSession
            .Builder(this, player)
            .setCallback(mediaSessionCallback)
            .setSessionActivity(getSessionActivityPendingIntent())

        try {
            mediaSession = mediaSessionBuilder.build()
        } catch (exception: Exception) {
            logger.error("Failed to create a media session.", exception)
            destroySelf()
            stopSelf()
            return false
        }
        return true
    }

    private fun getSessionActivityPendingIntent(): PendingIntent {
        val intent = packageManager
            .getLaunchIntentForPackage(packageName)
            ?.setPackage(null)
            ?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)

        return getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
    }

    private inner class MediaSessionServiceListener : Listener {
        /**
         * This method is only required to be implemented on Android 12 or above when an attempt is made
         * by a media controller to resume playback when the {@link MediaSessionService} is in the
         * background.
         */
        override fun onForegroundServiceStartNotAllowedException() {
            val context = this@VoiceMessagePlayerService
            @SuppressLint("MissingPermission")
            if (ConfigUtils.isPermissionGranted(
                    this@VoiceMessagePlayerService,
                    Manifest.permission.POST_NOTIFICATIONS,
                )
            ) {
                val notification = buildNotification(context, NotificationChannels.NOTIFICATION_CHANNEL_ALERT) {
                    setContentIntent(getSessionActivityPendingIntent())
                    setSmallIcon(R.drawable.ic_notification_small)
                    setColor(ResourcesCompat.getColor(resources, R.color.md_theme_light_primary, theme))
                    setContentTitle(getString(R.string.vm_fg_service_not_allowed))
                    setStyle(
                        NotificationCompat.BigTextStyle()
                            .bigText(getString(R.string.vm_fg_service_not_allowed_explain)),
                    )
                    setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    setAutoCancel(true)
                }
                NotificationManagerCompat.from(context).notify(VOICE_MESSAGE_PLAYER_NOTIFICATION_ID, notification)
            } else {
                context.showToast(R.string.notifications_disabled_title, ToastDuration.LONG)
            }
        }
    }

    override fun onSensorChanged(key: String?, value: Boolean) {
        if (keyIsNear == key) {
            if (value) {
                player.setAudioAttributes(getEarpieceAudioAttributes(), false)
            } else {
                player.setAudioAttributes(getRegularAudioAttributes(), false)
            }
        }
    }

    private fun requestAudioFocus() {
        if (!hasAudioFocus) {
            if (
                AudioManagerCompat.requestAudioFocus(
                    audioManager,
                    audioFocusRequestCompat,
                )
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
            ) {
                hasAudioFocus = true
                registerReceiver(audioBecomingNoisyReceiver, audioBecomingNoisyFilter)
            }
        }
    }

    private fun releaseAudioFocus() {
        if (hasAudioFocus) {
            AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequestCompat)
            hasAudioFocus = false
            try {
                unregisterReceiver(audioBecomingNoisyReceiver)
            } catch (_: IllegalArgumentException) {
                // not registered... ignore exceptions
            }
        }
    }

    override fun onAudioFocusChange(focusChange: Int) {
        when (focusChange) {
            AudioManager.AUDIOFOCUS_GAIN -> {
                player.volume = 1.0f
            }

            AudioManager.AUDIOFOCUS_LOSS -> {
                player.pause()
            }

            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                player.pause()
            }

            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                player.volume = 0.2f
            }
        }
    }

    private fun getRegularAudioAttributes(): AudioAttributes {
        return AudioAttributes.Builder()
            .setUsage(C.USAGE_MEDIA)
            .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
            .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_NONE)
            .build()
    }

    private fun getEarpieceAudioAttributes(): AudioAttributes {
        return AudioAttributes.Builder()
            .setUsage(C.USAGE_VOICE_COMMUNICATION)
            .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
            .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_NONE)
            .build()
    }

    companion object {
        private const val TAG = "VoiceMessagePlayerService"
    }
}
