package hu.bbara.purefin.core.player.manager

import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import dagger.hilt.android.scopes.ViewModelScoped
import hu.bbara.purefin.core.data.PlayableMediaRepository
import hu.bbara.purefin.core.data.PlaybackMediaItemTag
import hu.bbara.purefin.core.data.PlaybackMethod
import hu.bbara.purefin.core.data.PlaybackReportContext
import hu.bbara.purefin.core.player.model.MetadataState
import hu.bbara.purefin.core.player.model.PlaybackProgressSnapshot
import hu.bbara.purefin.core.player.model.PlaybackStateSnapshot
import hu.bbara.purefin.core.player.model.SegmentStatus
import hu.bbara.purefin.core.player.model.TrackOption
import hu.bbara.purefin.core.player.model.TrackType
import hu.bbara.purefin.core.player.preference.TrackMatcher
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
import hu.bbara.purefin.core.settings.SettingsOptions
import hu.bbara.purefin.core.settings.SettingsRepository
import hu.bbara.purefin.model.AudioTrackProperties
import hu.bbara.purefin.model.MediaSegment
import hu.bbara.purefin.model.PlayableMedia
import hu.bbara.purefin.model.SegmentType
import hu.bbara.purefin.model.SubtitleTrackProperties
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs

/**
 * Encapsulates the Media3 [Player] wiring and exposes reactive updates for the UI layer.
 */
@ViewModelScoped
@OptIn(UnstableApi::class)
class PlayerManager @Inject constructor(
    val player: Player,
    private val trackMapper: TrackMapper,
    private val playableMediaRepository: PlayableMediaRepository,
    private val trackPreferencesRepository: TrackPreferencesRepository,
    private val trackMatcher: TrackMatcher,
    private val settingsRepository: SettingsRepository
) {
    companion object {
        private const val SEEK_SETTLE_TOLERANCE_MS = 750L
    }

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    private val exoPlayer = player as ExoPlayer
    private val mediaSegmentManager = MediaSegmentManager(exoPlayer)

    private val _currentMediaId = MutableStateFlow<UUID?>(null)
    private val _playlist = MutableStateFlow(emptyList<PlayableMedia>())
    val playlist: StateFlow<List<PlayableMedia>> = _playlist.asStateFlow()

    val currentPlayableMedia: Flow<PlayableMedia?> =
        combine(_currentMediaId, _playlist) { mediaId, playlist ->
            playlist.firstOrNull { it.id == mediaId }
        }

    private var pendingSeekPositionMs: Long? = null
    private var pendingFallbackSeek: PendingFallbackSeek? = null
    private val _playbackState = MutableStateFlow(PlaybackStateSnapshot())

    val playbackState: StateFlow<PlaybackStateSnapshot> = _playbackState.asStateFlow()
    private val _progress = MutableStateFlow(PlaybackProgressSnapshot())

    val progress: StateFlow<PlaybackProgressSnapshot> = _progress.asStateFlow()
    private val _metadata = MutableStateFlow(MetadataState())

    val metadata: StateFlow<MetadataState> = _metadata.asStateFlow()
    private val _tracks = MutableStateFlow(TrackSelectionState())
    val tracks: StateFlow<TrackSelectionState> = _tracks.asStateFlow()
    private val _activeSkippableSegment = MutableStateFlow<MediaSegment?>(null)

    val activeSkippableSegment: StateFlow<MediaSegment?> = _activeSkippableSegment.asStateFlow()

    private val mediaSegmentListener = object : MediaSegmentManager.MediaSegmentListener {
        override fun onEvent(mediaSegment: MediaSegment, status: SegmentStatus) {
            handleMediaSegmentEvent(mediaSegment, status)
        }
    }

    private val listener = object : Player.Listener {
        override fun onPositionDiscontinuity(
            oldPosition: Player.PositionInfo,
            newPosition: Player.PositionInfo,
            reason: Int
        ) {
            syncPendingSeek(newPosition.positionMs)
        }

        override fun onPlayerError(error: PlaybackException) {
            if (switchToTranscodingFallback()) {
                return
            }
            _playbackState.update {
                it.copy(error = mapPlayerError(error))
            }
        }

        override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
            clearActiveSkippableSegment()
            val mediaId = mediaItem?.mediaId?.let { UUID.fromString(it) }
            _currentMediaId.value = mediaId
            scope.launch {
                val currentMedia = _playlist.value.firstOrNull { it.id == _currentMediaId.value }
                if (currentMedia == null) {
                    _playbackState.update { it.copy(error = "Media not found in playlist") }
                    return@launch
                }
                val fallbackSeek = pendingFallbackSeek
                pendingFallbackSeek = null
                val fallbackSeekPositionMs = fallbackSeek
                    ?.takeIf { it.mediaId == mediaItem?.mediaId }
                    ?.positionMs
                seekTo(fallbackSeekPositionMs ?: currentMedia.resumePositionMs)
                installMediaSegments(currentMedia.mediaSegments)
                // Only updatePlaylist when episodes are being played. First item is handled when playMedia is being called first time.
                if (fallbackSeekPositionMs == null && currentMedia is PlayableMedia.Episode) {
                    updatePlaylist()
                }
            }
        }

        override fun onEvents(player: Player, events: Player.Events) {
            updateFromPlayer(player)
            if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
                scope.launch {
                    applyTrackPreferences()
                }
            }
        }
    }

    init {
        mediaSegmentManager.registerListener(mediaSegmentListener)
        player.addListener(listener)
        updateFromPlayer(player)
        observeAutoPlayNextMedia()
        startProgressLoop()
    }

    private fun observeAutoPlayNextMedia() {
        scope.launch {
            settingsRepository.value(SettingsOptions.autoPlayNextMedia).collect { autoPlayNextMedia ->
                exoPlayer.pauseAtEndOfMediaItems = !autoPlayNextMedia
            }
        }
    }

    fun play(mediaId: UUID) {
        scope.launch {
            if (_playlist.value.map { it.id }.contains(mediaId)) {
                playFromPlaylist(mediaId)
            } else {
                playNewMedia(mediaId)
            }
        }
    }

    private fun playFromPlaylist(mediaId: UUID) {
        val index = _playlist.value.indexOfFirst { it.id == mediaId }
        if (index != -1) {
            player.seekToDefaultPosition(index)
        } else {
            _playbackState.update { it.copy(error = "Media not found in playlist") }
        }
    }

    private suspend fun playNewMedia(mediaId: UUID) {
        val playableMedia = playableMediaRepository.getPlayableMedia(mediaId)
        if (playableMedia == null) {
            _playbackState.update { it.copy(error = "Media not found") }
            return
        }
        _playlist.update { it + playableMedia }
        player.setMediaItem(playableMedia.mediaItem)
        player.prepare()
    }

    private suspend fun updatePlaylist() {
        val nextUpPlayableMedias = playableMediaRepository.getNextUpPlayableMedias(
            episodeId = _currentMediaId.value ?: return,
            existingIds = _playlist.value.map { it.id }.toSet(),
            count = 10
        )
        addToQueue(nextUpPlayableMedias)
    }

    private fun addToQueue(playableMedias: List<PlayableMedia>) {
        _playlist.update { it + playableMedias }
        player.addMediaItems(playableMedias.map { it.mediaItem })
    }

    fun togglePlayPause() {
        if (player.isPlaying) player.pause() else player.play()
    }

    fun pausePlayback() {
        if (player.isPlaying) {
            player.pause()
        }
    }

    fun resumePlayback() {
        if (!player.isPlaying) {
            player.play()
        }
    }

    fun seekTo(positionMs: Long) {
        val target = clampSeekPosition(positionMs)
        clearActiveSegmentIfPositionOutside(target)
        pendingSeekPositionMs = target
        _progress.update { progress ->
            progress.copy(
                durationMs = resolveDurationMs(),
                positionMs = target,
                isLive = player.isCurrentMediaItemLive
            )
        }
        player.seekTo(target)
    }

    fun seekBy(deltaMs: Long) {
        val basePosition = pendingSeekPositionMs ?: player.currentPosition
        val target = clampSeekPosition(basePosition + deltaMs)
        seekTo(target)
    }

    fun seekToLiveEdge() {
        clearPendingSeek()
        if (player.isCurrentMediaItemLive) {
            player.seekToDefaultPosition()
            player.play()
        }
    }

    fun next() {
        clearPendingSeek()
        clearActiveSkippableSegment()
        if (player.hasNextMediaItem()) {
            player.seekToNextMediaItem()
        }
    }

    fun previous() {
        clearPendingSeek()
        clearActiveSkippableSegment()
        if (player.hasPreviousMediaItem()) {
            player.seekToPreviousMediaItem()
        }
    }

    fun selectTrack(option: TrackOption) {
        scope.launch {
            if (option.type != TrackType.VIDEO) {
                saveTrackPreference(option)
            }
            applyTrackSelection(option)
        }
    }

    private fun applyTrackSelection(option: TrackOption) {
        val builder = player.trackSelectionParameters.buildUpon()
        when (option.type) {
            TrackType.TEXT -> {
                if (option.isOff) {
                    builder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
                    builder.clearOverridesOfType(C.TRACK_TYPE_TEXT)
                } else {
                    builder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
                    builder.clearOverridesOfType(C.TRACK_TYPE_TEXT)
                    val group = player.currentTracks.groups.getOrNull(option.groupIndex) ?: return
                    builder.addOverride(
                        TrackSelectionOverride(group.mediaTrackGroup, listOf(option.trackIndex))
                    )
                }
            }

            TrackType.AUDIO -> {
                builder.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
                val group = player.currentTracks.groups.getOrNull(option.groupIndex) ?: return
                builder.addOverride(
                    TrackSelectionOverride(group.mediaTrackGroup, listOf(option.trackIndex))
                )
            }

            TrackType.VIDEO -> {
                builder.clearOverridesOfType(C.TRACK_TYPE_VIDEO)
                val group = player.currentTracks.groups.getOrNull(option.groupIndex) ?: return
                builder.addOverride(
                    TrackSelectionOverride(group.mediaTrackGroup, listOf(option.trackIndex))
                )
            }
        }
        player.trackSelectionParameters = builder.build()
    }

    private fun installMediaSegments(mediaSegments: List<MediaSegment>) {
        mediaSegmentManager.addMediaSegments(
            mediaSegments = mediaSegments
        )
    }

    fun retry() {
        clearPendingSeek()
        clearActiveSkippableSegment()
        player.prepare()
        player.playWhenReady = true
    }

    fun skipActiveSegment() {
        val mediaSegment = _activeSkippableSegment.value ?: return
        seekTo(mediaSegment.endMs)
        clearActiveSkippableSegment()
    }

    fun clearError() {
        _playbackState.update { it.copy(error = null) }
    }

    private suspend fun applyTrackPreferences() {
        val currentMedia = currentPlayableMedia.firstOrNull() ?: return
        val currentTrackState = _tracks.value
        if (currentTrackState.audioTracks.isEmpty() && currentTrackState.textTracks.isEmpty()) return

        val preferences = trackPreferencesRepository
            .getMediaPreferences(currentMedia.preferenceMediaId.toString())
            .firstOrNull()
            ?: return

        // Apply audio preference
        preferences.audioPreference?.let { audioPreference ->
            val matchedAudio = trackMatcher.findBestAudioMatch(
                currentTrackState.audioTracks,
                audioPreference
            )
            if (matchedAudio != null && matchedAudio.id != currentTrackState.selectedAudioTrackId) {
                applyTrackSelection(matchedAudio)
            }
        }

        // Apply subtitle preference
        preferences.subtitlePreference?.let { subtitlePreference ->
            val matchedSubtitle = trackMatcher.findBestSubtitleMatch(
                currentTrackState.textTracks,
                subtitlePreference
            )
            if (matchedSubtitle != null && matchedSubtitle.id != currentTrackState.selectedTextTrackId) {
                applyTrackSelection(matchedSubtitle)
            }
        }
    }

    private suspend fun saveTrackPreference(option: TrackOption) {
        val preferenceMediaId = currentPlayableMedia.firstOrNull()
            ?.preferenceMediaId
            ?.toString()
            ?: return

        when (option.type) {
            TrackType.AUDIO -> {
                val properties = AudioTrackProperties(
                    language = option.language,
                    channelCount = option.channelCount,
                    label = option.label
                )
                trackPreferencesRepository.saveAudioPreference(preferenceMediaId, properties)
            }

            TrackType.TEXT -> {
                val properties = SubtitleTrackProperties(
                    language = option.language,
                    forced = option.forced,
                    label = option.label,
                    isOff = option.isOff
                )
                trackPreferencesRepository.saveSubtitlePreference(preferenceMediaId, properties)
            }

            TrackType.VIDEO -> {
                // Video preferences not implemented in this feature
            }
        }
    }

    fun release() {
        clearActiveSkippableSegment()
        mediaSegmentManager.release()
        scope.cancel()
        player.removeListener(listener)
        player.release()
    }

    private fun startProgressLoop() {
        scope.launch {
            while (isActive) {
                val duration = player.duration.takeIf { it > 0 } ?: _progress.value.durationMs
                val actualPosition = player.currentPosition
                syncPendingSeek(actualPosition)
                val position = pendingSeekPositionMs ?: actualPosition
                val buffered = player.bufferedPosition
                _progress.value = PlaybackProgressSnapshot(
                    durationMs = duration,
                    positionMs = position,
                    bufferedMs = buffered,
                    isLive = player.isCurrentMediaItemLive
                )
                delay(500)
            }
        }
    }

    private fun updateFromPlayer(player: Player) {
        val currentMediaItem = player.currentMediaItem
        val playbackState = player.playbackState
        val playbackReportContext = currentMediaItem?.playbackReportContext()
        val currentMetadata = player.mediaMetadata
        val playerError = player.playerError

        _playbackState.value = PlaybackStateSnapshot(
            isPlaying = player.isPlaying,
            isBuffering = playbackState == Player.STATE_BUFFERING,
            isEnded = playbackState == Player.STATE_ENDED,
            error = mapPlayerError(playerError)
        )
        _metadata.value = MetadataState(
            mediaId = currentMediaItem?.mediaId,
            title = currentMetadata.title?.toString(),
            subtitle = currentMetadata.subtitle?.toString(),
            playbackReportContext = playbackReportContext,
        )
        _tracks.value = trackMapper.map(player.currentTracks)
    }

    private fun clampSeekPosition(positionMs: Long): Long {
        val duration = resolveDurationMs()
        return if (duration > 0) {
            positionMs.coerceIn(0L, duration)
        } else {
            positionMs.coerceAtLeast(0L)
        }
    }

    private fun resolveDurationMs(): Long {
        return player.duration.takeIf { it > 0 } ?: _progress.value.durationMs
    }

    private fun clearPendingSeek() {
        pendingSeekPositionMs = null
    }

    private fun clearActiveSkippableSegment() {
        _activeSkippableSegment.value = null
    }

    private fun clearActiveSegmentIfPositionOutside(positionMs: Long) {
        val activeSegment = _activeSkippableSegment.value ?: return
        if (positionMs < activeSegment.startMs || positionMs >= activeSegment.endMs) {
            clearActiveSkippableSegment()
        }
    }

    private fun handleMediaSegmentEvent(mediaSegment: MediaSegment, status: SegmentStatus) {
        if (mediaSegment.type == SegmentType.MAIN_CONTENT) return
        when (status) {
            SegmentStatus.START -> _activeSkippableSegment.value = mediaSegment
            SegmentStatus.END -> {
                if (_activeSkippableSegment.value?.id == mediaSegment.id) {
                    clearActiveSkippableSegment()
                }
            }
        }
    }

    private fun mapPlayerError(error: PlaybackException?): String? {
        return error?.errorCodeName ?: error?.localizedMessage ?: error?.message
    }

    private fun switchToTranscodingFallback(): Boolean {
        val currentMediaItem = player.currentMediaItem ?: return false
        val playbackTag = currentMediaItem.localConfiguration?.tag as? PlaybackMediaItemTag
            ?: return false
        val fallbackUrl = playbackTag.transcodingFallbackUrl?.takeIf { it.isNotBlank() } ?: return false
        val currentMediaItemIndex = player.currentMediaItemIndex
        if (currentMediaItemIndex == C.INDEX_UNSET) return false

        val fallbackMediaItem = currentMediaItem.buildUpon()
            .setUri(fallbackUrl)
            .setTag(
                playbackTag.copy(
                    playbackReportContext = playbackTag.playbackReportContext.copy(
                        playMethod = PlaybackMethod.TRANSCODE
                    ),
                    transcodingFallbackUrl = null,
                )
            )
            .build()
        val fallbackPositionMs = fallbackPositionMs()
        val playWhenReady = player.playWhenReady

        pendingFallbackSeek = PendingFallbackSeek(
            mediaId = currentMediaItem.mediaId,
            positionMs = fallbackPositionMs,
        )
        clearStalePendingFallbackSeek(currentMediaItem.mediaId)
        player.replaceMediaItem(currentMediaItemIndex, fallbackMediaItem)
        replacePlaylistMediaItem(fallbackMediaItem)
        clearActiveSkippableSegment()
        player.seekTo(currentMediaItemIndex, fallbackPositionMs)
        player.prepare()
        player.playWhenReady = playWhenReady
        _playbackState.update { it.copy(error = null) }

        return true
    }

    private fun fallbackPositionMs(): Long {
        val positionMs = pendingSeekPositionMs ?: player.currentPosition
        return if (positionMs > 0L) {
            positionMs
        } else {
            _progress.value.positionMs.coerceAtLeast(0L)
        }
    }

    private fun replacePlaylistMediaItem(mediaItem: MediaItem) {
        val mediaId = runCatching { UUID.fromString(mediaItem.mediaId) }.getOrNull() ?: return
        _playlist.update { playlist ->
            playlist.map { playableMedia ->
                if (playableMedia.id == mediaId) {
                    playableMedia.withMediaItem(mediaItem)
                } else {
                    playableMedia
                }
            }
        }
    }

    private fun PlayableMedia.withMediaItem(mediaItem: MediaItem): PlayableMedia {
        return when (this) {
            is PlayableMedia.Episode -> copy(mediaItem = mediaItem)
            is PlayableMedia.Movie -> copy(mediaItem = mediaItem)
            is PlayableMedia.Series -> copy(mediaItem = mediaItem)
        }
    }

    private fun MediaItem.playbackReportContext(): PlaybackReportContext? {
        return when (val tag = localConfiguration?.tag) {
            is PlaybackMediaItemTag -> tag.playbackReportContext
            is PlaybackReportContext -> tag
            else -> null
        }
    }

    private fun clearStalePendingFallbackSeek(mediaId: String) {
        scope.launch {
            delay(1_000)
            if (pendingFallbackSeek?.mediaId == mediaId) {
                pendingFallbackSeek = null
            }
        }
    }

    private data class PendingFallbackSeek(
        val mediaId: String,
        val positionMs: Long,
    )

    private fun syncPendingSeek(positionMs: Long) {
        val pendingPosition = pendingSeekPositionMs ?: return
        if (abs(positionMs - pendingPosition) <= SEEK_SETTLE_TOLERANCE_MS) {
            clearPendingSeek()
        }
    }
}
