package playbackmanager

import (
	"context"
	"errors"
	"fmt"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/continuity"
	"seanime/internal/database/db"
	"seanime/internal/database/db_bridge"
	discordrpc_presence "seanime/internal/discordrpc/presence"
	"seanime/internal/events"
	"seanime/internal/hook"
	"seanime/internal/library/anime"
	"seanime/internal/mediaplayers/mediaplayer"
	"seanime/internal/platforms/platform"
	"seanime/internal/util"
	"seanime/internal/util/result"
	"sync"
	"sync/atomic"

	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"github.com/samber/mo"
)

const (
	LocalFilePlayback      PlaybackType = "localfile"
	StreamPlayback         PlaybackType = "stream"
	ManualTrackingPlayback PlaybackType = "manual"
)

var playbackStatePool = sync.Pool{
	New: func() interface{} {
		return &PlaybackState{}
	},
}

type (
	PlaybackType string

	// PlaybackManager manages video playback (local and stream) and progress tracking for desktop media players.
	// It receives and dispatch appropriate events for:
	//  - Syncing progress with AniList, etc.
	//  - Sending notifications to the client
	PlaybackManager struct {
		Logger                *zerolog.Logger
		Database              *db.Database
		MediaPlayerRepository *mediaplayer.Repository // MediaPlayerRepository is used to control the media player
		continuityManager     *continuity.Manager

		settings *Settings

		discordPresence            *discordrpc_presence.Presence     // DiscordPresence is used to update the user's Discord presence
		mediaPlayerRepoSubscriber  *mediaplayer.RepositorySubscriber // Used to listen for media player events
		wsEventManager             events.WSEventManagerInterface
		platform                   platform.Platform
		metadataProvider           metadata.Provider
		refreshAnimeCollectionFunc func() // This function is called to refresh the AniList collection
		mu                         sync.Mutex
		eventMu                    sync.RWMutex
		cancel                     context.CancelFunc

		// historyMap stores a PlaybackState whose state is "completed"
		// Since PlaybackState is sent to client continuously, once a PlaybackState is stored in historyMap, only IT will be sent to the client.
		// This is so when the user seeks back to a video, the client can show the last known "completed" state of the video
		historyMap                 map[string]PlaybackState
		currentPlaybackType        PlaybackType
		currentMediaPlaybackStatus *mediaplayer.PlaybackStatus // The current video playback status (can be nil)

		autoPlayMu           sync.Mutex
		nextEpisodeLocalFile mo.Option[*anime.LocalFile] // The next episode's local file (for local file playback)

		// currentMediaListEntry for Local file playback & stream playback
		// For Local file playback, it MUST be set
		// For Stream playback, it is optional
		// See [progress_tracking.go] for how it is handled
		currentMediaListEntry mo.Option[*anilist.AnimeListEntry] // List Entry for the current video playback

		// \/ Local file playback
		currentLocalFile             mo.Option[*anime.LocalFile]             // Local file for the current video playback
		currentLocalFileWrapperEntry mo.Option[*anime.LocalFileWrapperEntry] // This contains the current media entry local file data

		// \/ Stream playback
		// The current episode being streamed, set in [StartStreamingUsingMediaPlayer] by finding the episode in currentStreamEpisodeCollection
		currentStreamEpisode mo.Option[*anime.Episode]
		// The current media being streamed, set in [StartStreamingUsingMediaPlayer]
		currentStreamMedia        mo.Option[*anilist.BaseAnime]
		currentStreamAniDbEpisode mo.Option[string]

		// \/ Manual progress tracking (non-integrated external player)
		manualTrackingCtx           context.Context
		manualTrackingCtxCancel     context.CancelFunc
		manualTrackingPlaybackState PlaybackState
		currentManualTrackingState  mo.Option[*ManualTrackingState]
		manualTrackingWg            sync.WaitGroup

		// \/ Playlist
		playlistHub *playlistHub // The playlist hub

		isOffline       *bool
		animeCollection mo.Option[*anilist.AnimeCollection]

		playbackStatusSubscribers *result.Map[string, *PlaybackStatusSubscriber]
	}

	// PlaybackStatusSubscriber provides a single event channel for all playback events
	PlaybackStatusSubscriber struct {
		EventCh  chan PlaybackEvent
		canceled atomic.Bool
	}

	// PlaybackEvent is the base interface for all playback events
	PlaybackEvent interface {
		Type() string
	}

	PlaybackStartingEvent struct {
		Filepath      string
		PlaybackType  PlaybackType
		Media         *anilist.BaseAnime
		AniDbEpisode  string
		EpisodeNumber int
		WindowTitle   string
	}

	// Local file playback events

	PlaybackStatusChangedEvent struct {
		Status mediaplayer.PlaybackStatus
		State  PlaybackState
	}

	VideoStartedEvent struct {
		Filename string
		Filepath string
	}

	VideoStoppedEvent struct {
		Reason string
	}

	VideoCompletedEvent struct {
		Filename string
	}

	// Stream playback events
	StreamStateChangedEvent struct {
		State PlaybackState
	}

	StreamStatusChangedEvent struct {
		Status mediaplayer.PlaybackStatus
	}

	StreamStartedEvent struct {
		Filename string
		Filepath string
	}

	StreamStoppedEvent struct {
		Reason string
	}

	StreamCompletedEvent struct {
		Filename string
	}

	PlaybackStateType string

	// PlaybackState is used to keep track of the user's current video playback
	// It is sent to the client each time the video playback state is picked up -- this is used to update the client's UI
	PlaybackState struct {
		EpisodeNumber        int     `json:"episodeNumber"`        // The episode number
		AniDbEpisode         string  `json:"aniDbEpisode"`         // The AniDB episode number
		MediaTitle           string  `json:"mediaTitle"`           // The title of the media
		MediaCoverImage      string  `json:"mediaCoverImage"`      // The cover image of the media
		MediaTotalEpisodes   int     `json:"mediaTotalEpisodes"`   // The total number of episodes
		Filename             string  `json:"filename"`             // The filename
		CompletionPercentage float64 `json:"completionPercentage"` // The completion percentage
		CanPlayNext          bool    `json:"canPlayNext"`          // Whether the next episode can be played
		ProgressUpdated      bool    `json:"progressUpdated"`      // Whether the progress has been updated
		MediaId              int     `json:"mediaId"`              // The media ID
	}

	NewPlaybackManagerOptions struct {
		WSEventManager             events.WSEventManagerInterface
		Logger                     *zerolog.Logger
		Platform                   platform.Platform
		MetadataProvider           metadata.Provider
		Database                   *db.Database
		RefreshAnimeCollectionFunc func() // This function is called to refresh the AniList collection
		DiscordPresence            *discordrpc_presence.Presence
		IsOffline                  *bool
		ContinuityManager          *continuity.Manager
	}

	Settings struct {
		AutoPlayNextEpisode bool
	}
)

// Event type implementations
func (e PlaybackStatusChangedEvent) Type() string { return "playback_status_changed" }
func (e VideoStartedEvent) Type() string          { return "video_started" }
func (e VideoStoppedEvent) Type() string          { return "video_stopped" }
func (e VideoCompletedEvent) Type() string        { return "video_completed" }
func (e StreamStateChangedEvent) Type() string    { return "stream_state_changed" }
func (e StreamStatusChangedEvent) Type() string   { return "stream_status_changed" }
func (e StreamStartedEvent) Type() string         { return "stream_started" }
func (e StreamStoppedEvent) Type() string         { return "stream_stopped" }
func (e StreamCompletedEvent) Type() string       { return "stream_completed" }
func (e PlaybackStartingEvent) Type() string      { return "playback_starting" }

func New(opts *NewPlaybackManagerOptions) *PlaybackManager {
	pm := &PlaybackManager{
		Logger:                       opts.Logger,
		Database:                     opts.Database,
		settings:                     &Settings{},
		discordPresence:              opts.DiscordPresence,
		wsEventManager:               opts.WSEventManager,
		platform:                     opts.Platform,
		metadataProvider:             opts.MetadataProvider,
		refreshAnimeCollectionFunc:   opts.RefreshAnimeCollectionFunc,
		mu:                           sync.Mutex{},
		autoPlayMu:                   sync.Mutex{},
		eventMu:                      sync.RWMutex{},
		historyMap:                   make(map[string]PlaybackState),
		isOffline:                    opts.IsOffline,
		nextEpisodeLocalFile:         mo.None[*anime.LocalFile](),
		currentStreamEpisode:         mo.None[*anime.Episode](),
		currentStreamMedia:           mo.None[*anilist.BaseAnime](),
		currentStreamAniDbEpisode:    mo.None[string](),
		animeCollection:              mo.None[*anilist.AnimeCollection](),
		currentManualTrackingState:   mo.None[*ManualTrackingState](),
		currentLocalFile:             mo.None[*anime.LocalFile](),
		currentLocalFileWrapperEntry: mo.None[*anime.LocalFileWrapperEntry](),
		currentMediaListEntry:        mo.None[*anilist.AnimeListEntry](),
		continuityManager:            opts.ContinuityManager,
		playbackStatusSubscribers:    result.NewResultMap[string, *PlaybackStatusSubscriber](),
	}

	pm.playlistHub = newPlaylistHub(pm)

	return pm
}

func (pm *PlaybackManager) SetAnimeCollection(ac *anilist.AnimeCollection) {
	pm.animeCollection = mo.Some(ac)
}

func (pm *PlaybackManager) SetSettings(s *Settings) {
	pm.settings = s
}

// SetMediaPlayerRepository sets the media player repository and starts listening to media player events
// - This method is called when the media player is mounted (due to settings change or when the app starts)
func (pm *PlaybackManager) SetMediaPlayerRepository(mediaPlayerRepository *mediaplayer.Repository) {
	go func() {
		// If a previous context exists, cancel it
		if pm.cancel != nil {
			pm.cancel()
		}

		pm.playlistHub.reset()

		// Create a new context for listening to the MediaPlayer instance's event
		// When this is canceled above, the previous listener goroutine will stop -- this is done to prevent multiple listeners
		var ctx context.Context
		ctx, pm.cancel = context.WithCancel(context.Background())

		pm.mu.Lock()
		// Set the new media player repository instance
		pm.MediaPlayerRepository = mediaPlayerRepository
		// Set up event listeners for the media player instance
		pm.mediaPlayerRepoSubscriber = pm.MediaPlayerRepository.Subscribe("playbackmanager")
		pm.mu.Unlock()

		// Start listening to new media player events
		pm.listenToMediaPlayerEvents(ctx)

		// DEVNOTE: pm.listenToClientPlayerEvents()
	}()
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

type StartPlayingOptions struct {
	Payload   string // url or path
	UserAgent string
	ClientId  string
}

func (pm *PlaybackManager) StartPlayingUsingMediaPlayer(opts *StartPlayingOptions) error {

	event := &LocalFilePlaybackRequestedEvent{
		Path: opts.Payload,
	}
	err := hook.GlobalHookManager.OnLocalFilePlaybackRequested().Trigger(event)
	if err != nil {
		return err
	}
	opts.Payload = event.Path

	if event.DefaultPrevented {
		pm.Logger.Debug().Msg("playback manager: Local file playback prevented by hook")
		return nil
	}

	pm.playlistHub.reset()
	if err := pm.checkOrLoadAnimeCollection(); err != nil {
		return err
	}

	// Cancel manual tracking if active
	if pm.manualTrackingCtxCancel != nil {
		pm.manualTrackingCtxCancel()
	}

	// Send the media file to the media player
	err = pm.MediaPlayerRepository.Play(opts.Payload)
	if err != nil {
		return err
	}

	trackingEvent := &PlaybackBeforeTrackingEvent{
		IsStream: false,
	}
	err = hook.GlobalHookManager.OnPlaybackBeforeTracking().Trigger(trackingEvent)
	if err != nil {
		return err
	}

	if trackingEvent.DefaultPrevented {
		return nil
	}

	// Start tracking
	pm.MediaPlayerRepository.StartTracking()

	return nil
}

// StartUntrackedStreamingUsingMediaPlayer starts a stream using the media player without any tracking.
func (pm *PlaybackManager) StartUntrackedStreamingUsingMediaPlayer(windowTitle string, opts *StartPlayingOptions) (err error) {
	defer util.HandlePanicInModuleWithError("library/playbackmanager/StartUntrackedStreamingUsingMediaPlayer", &err)

	event := &StreamPlaybackRequestedEvent{
		WindowTitle:  windowTitle,
		Payload:      opts.Payload,
		Media:        nil,
		AniDbEpisode: "",
	}
	err = hook.GlobalHookManager.OnStreamPlaybackRequested().Trigger(event)
	if err != nil {
		return err
	}

	if event.DefaultPrevented {
		pm.Logger.Debug().Msg("playback manager: Stream playback prevented by hook")
		return nil
	}

	pm.Logger.Trace().Msg("playback manager: Starting the media player")

	pm.mu.Lock()
	defer pm.mu.Unlock()

	episodeNumber := 0

	err = pm.MediaPlayerRepository.Stream(opts.Payload, episodeNumber, 0, windowTitle)
	if err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Failed to start streaming")
		return err
	}

	pm.Logger.Trace().Msg("playback manager: Sent stream to media player")

	return nil
}

// StartStreamingUsingMediaPlayer starts streaming a video using the media player.
// This sets PlaybackManager.currentStreamMedia and PlaybackManager.currentStreamEpisode used for progress tracking.
// Note that PlaybackManager.currentStreamEpisodeCollection is not required to start streaming but is needed for progress tracking.
func (pm *PlaybackManager) StartStreamingUsingMediaPlayer(windowTitle string, opts *StartPlayingOptions, media *anilist.BaseAnime, aniDbEpisode string) (err error) {
	defer util.HandlePanicInModuleWithError("library/playbackmanager/StartStreamingUsingMediaPlayer", &err)

	event := &StreamPlaybackRequestedEvent{
		WindowTitle:  windowTitle,
		Payload:      opts.Payload,
		Media:        media,
		AniDbEpisode: aniDbEpisode,
	}
	err = hook.GlobalHookManager.OnStreamPlaybackRequested().Trigger(event)
	if err != nil {
		return err
	}

	aniDbEpisode = event.AniDbEpisode
	windowTitle = event.WindowTitle

	if event.DefaultPrevented {
		pm.Logger.Debug().Msg("playback manager: Stream playback prevented by hook")
		return nil
	}

	pm.playlistHub.reset()
	if *pm.isOffline {
		return errors.New("cannot stream when offline")
	}

	if event.Media == nil || aniDbEpisode == "" {
		pm.Logger.Error().Msg("playback manager: cannot start streaming, missing options [StartStreamingUsingMediaPlayer]")
		return errors.New("cannot start streaming, not enough data provided")
	}

	pm.Logger.Trace().Msg("playback manager: Starting the media player")

	pm.mu.Lock()
	defer pm.mu.Unlock()

	// Cancel manual tracking if active
	if pm.manualTrackingCtxCancel != nil {
		pm.manualTrackingCtxCancel()
	}

	pm.currentStreamMedia = mo.Some(event.Media)
	episodeNumber := 0

	// Find the current episode being stream
	episodeCollection, err := anime.NewEpisodeCollection(anime.NewEpisodeCollectionOptions{
		AnimeMetadata:    nil,
		Media:            event.Media,
		MetadataProvider: pm.metadataProvider,
		Logger:           pm.Logger,
	})

	pm.currentStreamAniDbEpisode = mo.Some(aniDbEpisode)

	if episode, ok := episodeCollection.FindEpisodeByAniDB(aniDbEpisode); ok {
		episodeNumber = episode.EpisodeNumber
		pm.currentStreamEpisode = mo.Some(episode)
	} else {
		pm.Logger.Warn().Str("episode", aniDbEpisode).Msg("playback manager: Failed to find episode in episode collection")
	}

	err = pm.MediaPlayerRepository.Stream(event.Payload, episodeNumber, event.Media.ID, windowTitle)
	if err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Failed to start streaming")
		return err
	}

	pm.Logger.Trace().Msg("playback manager: Sent stream to media player")

	trackingEvent := &PlaybackBeforeTrackingEvent{
		IsStream: true,
	}
	err = hook.GlobalHookManager.OnPlaybackBeforeTracking().Trigger(trackingEvent)
	if err != nil {
		return err
	}

	if trackingEvent.DefaultPrevented {
		return nil
	}

	pm.MediaPlayerRepository.StartTrackingTorrentStream()

	pm.Logger.Trace().Msg("playback manager: Started tracking torrent stream")

	return nil
}

// PlayNextEpisode plays the next episode of the local media that is being watched
//   - Called when the user clicks on the "Next" button in the client
//   - Should not be called when the user is watching a playlist
//   - Should not be called when no next episode is available
func (pm *PlaybackManager) PlayNextEpisode() (err error) {
	defer util.HandlePanicInModuleWithError("library/playbackmanager/PlayNextEpisode", &err)

	switch pm.currentPlaybackType {
	case LocalFilePlayback:
		if pm.currentLocalFile.IsAbsent() || pm.currentMediaListEntry.IsAbsent() || pm.currentLocalFileWrapperEntry.IsAbsent() {
			return errors.New("could not play next episode")
		}

		nextLf, found := pm.currentLocalFileWrapperEntry.MustGet().FindNextEpisode(pm.currentLocalFile.MustGet())
		if !found {
			return errors.New("could not play next episode")
		}

		err = pm.MediaPlayerRepository.Play(nextLf.Path)
		if err != nil {
			return err
		}
		// Start tracking the video
		pm.MediaPlayerRepository.StartTracking()

	case StreamPlayback:
		// TODO: Implement it for torrentstream
		// Check if torrent stream etc...
	}

	return nil
}

// GetNextEpisode gets the next [anime.LocalFile] of the local media that is being watched.
// It will return nil if there is no next episode.
// This is used by the client's "Auto Play" feature.
func (pm *PlaybackManager) GetNextEpisode() (ret *anime.LocalFile) {
	defer util.HandlePanicInModuleThen("library/playbackmanager/GetNextEpisode", func() {
		ret = nil
	})

	switch pm.currentPlaybackType {
	case LocalFilePlayback:
		if lf, found := pm.nextEpisodeLocalFile.Get(); found {
			ret = lf
		}
		return
	}

	return nil
}

// AutoPlayNextEpisode will play the next episode of the local media that is being watched.
// This calls [PlaybackManager.PlayNextEpisode] only once if multiple clients made the request.
func (pm *PlaybackManager) AutoPlayNextEpisode() error {
	pm.autoPlayMu.Lock()
	defer pm.autoPlayMu.Unlock()

	pm.Logger.Trace().Msg("playback manager: Auto play request received")

	if !pm.settings.AutoPlayNextEpisode {
		return nil
	}

	lf := pm.GetNextEpisode()
	// This shouldn't happen because the client should check if there is a next episode before sending the request.
	// However, it will happen if there are multiple clients launching the request.
	if lf == nil {
		pm.Logger.Warn().Msg("playback manager: No next episode to play")
		return nil
	}

	if err := pm.PlayNextEpisode(); err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Failed to auto play next episode")
		return fmt.Errorf("failed to auto play next episode: %w", err)
	}

	// Remove the next episode from the queue
	pm.nextEpisodeLocalFile = mo.None[*anime.LocalFile]()

	return nil
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Pause pauses the current media player playback.
func (pm *PlaybackManager) Pause() error {
	return pm.MediaPlayerRepository.Pause()
}

// Resume resumes the current media player playback.
func (pm *PlaybackManager) Resume() error {
	return pm.MediaPlayerRepository.Resume()
}

// Seek seeks to the specified time in the current media.
func (pm *PlaybackManager) Seek(seconds float64) error {
	return pm.MediaPlayerRepository.Seek(seconds)
}

// PullStatus pulls the current media player playback status at the time of the call.
func (pm *PlaybackManager) PullStatus() (*mediaplayer.PlaybackStatus, bool) {
	return pm.MediaPlayerRepository.PullStatus()
}

// Cancel stops the current media player playback and publishes a "normal" event.
func (pm *PlaybackManager) Cancel() error {
	pm.MediaPlayerRepository.Stop()
	return nil
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Playlist
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// CancelCurrentPlaylist cancels the current playlist.
// This is an action triggered by the client.
func (pm *PlaybackManager) CancelCurrentPlaylist() error {
	go pm.playlistHub.reset()
	return nil
}

// RequestNextPlaylistFile will play the next file in the playlist.
// This is an action triggered by the client.
func (pm *PlaybackManager) RequestNextPlaylistFile() error {
	go pm.playlistHub.playNextFile()
	return nil
}

// StartPlaylist starts a playlist.
// This action is triggered by the client.
func (pm *PlaybackManager) StartPlaylist(playlist *anime.Playlist) (err error) {
	defer util.HandlePanicInModuleWithError("library/playbackmanager/StartPlaylist", &err)

	pm.playlistHub.loadPlaylist(playlist)

	_ = pm.checkOrLoadAnimeCollection()

	// Play the first video in the playlist
	firstVidPath := playlist.LocalFiles[0].Path
	err = pm.MediaPlayerRepository.Play(firstVidPath)
	if err != nil {
		return err
	}

	// Start tracking the video
	pm.MediaPlayerRepository.StartTracking()

	// Create a new context for the playlist hub
	var ctx context.Context
	ctx, pm.playlistHub.cancel = context.WithCancel(context.Background())

	// Listen to new play requests
	go func() {
		pm.Logger.Debug().Msg("playback manager: Listening for new file requests")
		for {
			select {
			// When the playlist hub context is cancelled (No playlist is being played)
			case <-ctx.Done():
				pm.Logger.Debug().Msg("playback manager: Playlist context cancelled")
				// Send event to the client -- nil signals that no playlist is being played
				pm.wsEventManager.SendEvent(events.PlaybackManagerPlaylistState, nil)
				return
			case path := <-pm.playlistHub.requestNewFileCh:
				// requestNewFileCh receives the path of the next video to play
				// The channel is fed when it's time to play the next video or when the client requests the next video
				// see: RequestNextPlaylistFile, playlistHub code
				pm.Logger.Debug().Str("path", path).Msg("playback manager: Playing next file")
				// Send notification to the client
				pm.wsEventManager.SendEvent(events.InfoToast, "Playing next file in playlist")
				// Play the requested video
				err := pm.MediaPlayerRepository.Play(path)
				if err != nil {
					pm.Logger.Error().Err(err).Msg("playback manager: Failed to play next file in playlist")
					pm.playlistHub.cancel()
					return
				}
				// Start tracking the video
				pm.MediaPlayerRepository.StartTracking()
			case <-pm.playlistHub.endOfPlaylistCh:
				pm.Logger.Debug().Msg("playback manager: End of playlist")
				pm.wsEventManager.SendEvent(events.InfoToast, "End of playlist")
				// Send event to the client -- nil signals that no playlist is being played
				pm.wsEventManager.SendEvent(events.PlaybackManagerPlaylistState, nil)
				go pm.MediaPlayerRepository.Stop()
				pm.playlistHub.cancel()
				return
			}
		}
	}()

	// Delete playlist in goroutine
	go func() {
		err := db_bridge.DeletePlaylist(pm.Database, playlist.DbId)
		if err != nil {
			pm.Logger.Error().Err(err).Str("name", playlist.Name).Msgf("playback manager: Failed to delete playlist")
			return
		}
		pm.Logger.Debug().Str("name", playlist.Name).Msgf("playback manager: Deleted playlist")
	}()

	return nil
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

func (pm *PlaybackManager) checkOrLoadAnimeCollection() (err error) {
	defer util.HandlePanicInModuleWithError("library/playbackmanager/checkOrLoadAnimeCollection", &err)

	if pm.animeCollection.IsAbsent() {
		// If the anime collection is not present, we retrieve it from the platform
		collection, err := pm.platform.GetAnimeCollection(context.Background(), false)
		if err != nil {
			return err
		}
		pm.animeCollection = mo.Some(collection)
	}
	return nil
}

func (pm *PlaybackManager) SubscribeToPlaybackStatus(id string) *PlaybackStatusSubscriber {
	subscriber := &PlaybackStatusSubscriber{
		EventCh: make(chan PlaybackEvent, 100),
	}
	pm.playbackStatusSubscribers.Set(id, subscriber)
	return subscriber
}

func (pm *PlaybackManager) RegisterMediaPlayerCallback(callback func(event PlaybackEvent, cancelFunc func())) (cancel func()) {
	id := uuid.NewString()
	playbackSubscriber := pm.SubscribeToPlaybackStatus(id)
	cancel = func() {
		pm.UnsubscribeFromPlaybackStatus(id)
	}
	go func(playbackSubscriber *PlaybackStatusSubscriber) {
		for event := range playbackSubscriber.EventCh {
			callback(event, cancel)
		}
	}(playbackSubscriber)

	return cancel
}

func (pm *PlaybackManager) UnsubscribeFromPlaybackStatus(id string) {
	defer func() {
		if r := recover(); r != nil {
			pm.Logger.Warn().Msg("playback manager: Failed to unsubscribe from playback status")
		}
	}()
	subscriber, ok := pm.playbackStatusSubscribers.Get(id)
	if !ok {
		return
	}
	subscriber.canceled.Store(true)
	pm.playbackStatusSubscribers.Delete(id)
	close(subscriber.EventCh)
}
