package playbackmanager

import (
	"cmp"
	"context"
	"errors"
	"seanime/internal/continuity"
	discordrpc_presence "seanime/internal/discordrpc/presence"
	"seanime/internal/events"
	"seanime/internal/library/anime"
	"seanime/internal/mediaplayers/mediaplayer"
	"seanime/internal/util"

	"github.com/samber/mo"
)

var (
	ErrProgressUpdateAnilist = errors.New("playback manager: Failed to update progress on AniList")
	ErrProgressUpdateMAL     = errors.New("playback manager: Failed to update progress on MyAnimeList")
)

func (pm *PlaybackManager) listenToMediaPlayerEvents(ctx context.Context) {
	// Listen for media player events
	go func() {
		for {
			select {
			// Stop listening when the context is cancelled -- meaning a new MediaPlayer instance is set
			case <-ctx.Done():
				return
			case event := <-pm.mediaPlayerRepoSubscriber.EventCh:
				switch e := event.(type) {
				// Local file events
				case mediaplayer.TrackingStartedEvent: // New video has started playing
					pm.handleTrackingStarted(e.Status)
				case mediaplayer.VideoCompletedEvent: // Video has been watched completely but still tracking
					pm.handleVideoCompleted(e.Status)
				case mediaplayer.TrackingStoppedEvent: // Tracking has stopped completely
					pm.handleTrackingStopped(e.Reason)
				case mediaplayer.PlaybackStatusEvent: // Playback status has changed
					pm.handlePlaybackStatus(e.Status)
				case mediaplayer.TrackingRetryEvent: // Error occurred while starting tracking
					pm.handleTrackingRetry(e.Reason)

				// Streaming events
				case mediaplayer.StreamingTrackingStartedEvent:
					pm.handleStreamingTrackingStarted(e.Status)
				case mediaplayer.StreamingPlaybackStatusEvent:
					pm.handleStreamingPlaybackStatus(e.Status)
				case mediaplayer.StreamingVideoCompletedEvent:
					pm.handleStreamingVideoCompleted(e.Status)
				case mediaplayer.StreamingTrackingStoppedEvent:
					pm.handleStreamingTrackingStopped(e.Reason)
				case mediaplayer.StreamingTrackingRetryEvent:
					// Do nothing
				}
			}
		}
	}()
}

func (pm *PlaybackManager) handleTrackingStarted(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	// Set the playback type
	pm.currentPlaybackType = LocalFilePlayback

	// Reset the history map
	pm.historyMap = make(map[string]PlaybackState)

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getLocalFilePlaybackState(status)
	// Log
	pm.Logger.Debug().Msg("playback manager: Tracking started, extracting metadata...")
	// Send event to the client
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressTrackingStarted, _ps)

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			value.EventCh <- VideoStartedEvent{Filename: status.Filename, Filepath: status.Filepath}
			return true
		})
	}()

	// Retrieve data about the current video playback
	// Set PlaybackManager.currentMediaListEntry to the list entry of the current video
	currentMediaListEntry, currentLocalFile, currentLocalFileWrapperEntry, err := pm.getLocalFilePlaybackDetails(status.Filepath)
	if err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Failed to get media data")
		// Send error event to the client
		pm.wsEventManager.SendEvent(events.ErrorToast, err.Error())
		//
		pm.MediaPlayerRepository.Cancel()
		return
	}

	pm.currentMediaListEntry = mo.Some(currentMediaListEntry)
	pm.currentLocalFile = mo.Some(currentLocalFile)
	pm.currentLocalFileWrapperEntry = mo.Some(currentLocalFileWrapperEntry)
	pm.Logger.Debug().
		Str("media", pm.currentMediaListEntry.MustGet().GetMedia().GetPreferredTitle()).
		Int("episode", pm.currentLocalFile.MustGet().GetEpisodeNumber()).
		Msg("playback manager: Playback started")

	pm.continuityManager.SetExternalPlayerEpisodeDetails(&continuity.ExternalPlayerEpisodeDetails{
		EpisodeNumber: pm.currentLocalFile.MustGet().GetEpisodeNumber(),
		MediaId:       pm.currentMediaListEntry.MustGet().GetMedia().GetID(),
		Filepath:      pm.currentLocalFile.MustGet().GetPath(),
	})

	// ------- Playlist ------- //
	go pm.playlistHub.onVideoStart(pm.currentMediaListEntry.MustGet(), pm.currentLocalFile.MustGet(), _ps)

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.SetAnimeActivity(&discordrpc_presence.AnimeActivity{
			ID:                  pm.currentMediaListEntry.MustGet().GetMedia().GetID(),
			Title:               pm.currentMediaListEntry.MustGet().GetMedia().GetPreferredTitle(),
			Image:               pm.currentMediaListEntry.MustGet().GetMedia().GetCoverImageSafe(),
			IsMovie:             pm.currentMediaListEntry.MustGet().GetMedia().IsMovie(),
			EpisodeNumber:       pm.currentLocalFileWrapperEntry.MustGet().GetProgressNumber(pm.currentLocalFile.MustGet()),
			Progress:            int(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds),
			Duration:            int(pm.currentMediaPlaybackStatus.DurationInSeconds),
			TotalEpisodes:       pm.currentMediaListEntry.MustGet().GetMedia().Episodes,
			CurrentEpisodeCount: pm.currentMediaListEntry.MustGet().GetMedia().GetCurrentEpisodeCountOrNil(),
		})
	}
}

func (pm *PlaybackManager) handleVideoCompleted(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getLocalFilePlaybackState(status)
	// Log
	pm.Logger.Debug().Msg("playback manager: Received video completed event")

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			value.EventCh <- VideoCompletedEvent{Filename: status.Filename}
			return true
		})
	}()

	//
	// Update the progress on AniList if auto update progress is enabled
	//
	pm.autoSyncCurrentProgress(&_ps)

	// Send the playback state with the `ProgressUpdated` flag
	// The client will use this to notify the user if the progress has been updated
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressVideoCompleted, _ps)
	// Push the video playback state to the history
	pm.historyMap[status.Filename] = _ps

	// ------- Playlist ------- //
	if pm.currentMediaListEntry.IsPresent() && pm.currentLocalFile.IsPresent() {
		go pm.playlistHub.onVideoCompleted(pm.currentMediaListEntry.MustGet(), pm.currentLocalFile.MustGet(), _ps)
	}
}

func (pm *PlaybackManager) handleTrackingStopped(reason string) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	pm.Logger.Debug().Msg("playback manager: Received tracking stopped event")
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressTrackingStopped, reason)

	// Find the next episode and set it to [PlaybackManager.nextEpisodeLocalFile]
	if pm.currentMediaListEntry.IsPresent() && pm.currentLocalFile.IsPresent() && pm.currentLocalFileWrapperEntry.IsPresent() {
		lf, ok := pm.currentLocalFileWrapperEntry.MustGet().FindNextEpisode(pm.currentLocalFile.MustGet())
		if ok {
			pm.nextEpisodeLocalFile = mo.Some(lf)
		} else {
			pm.nextEpisodeLocalFile = mo.None[*anime.LocalFile]()
		}
	}

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- VideoStoppedEvent{Reason: reason}
			return true
		})
	}()

	if pm.currentMediaPlaybackStatus != nil {
		pm.continuityManager.UpdateExternalPlayerEpisodeWatchHistoryItem(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds, pm.currentMediaPlaybackStatus.DurationInSeconds)
	}

	// ------- Playlist ------- //
	go pm.playlistHub.onTrackingStopped()

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.Close()
	}
}

func (pm *PlaybackManager) handlePlaybackStatus(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	pm.currentPlaybackType = LocalFilePlayback

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getLocalFilePlaybackState(status)
	// If the same PlaybackState is in the history, update the ProgressUpdated flag
	// PlaybackStatusCh has no way of knowing if the progress has been updated
	if h, ok := pm.historyMap[status.Filename]; ok {
		_ps.ProgressUpdated = h.ProgressUpdated
	}

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			return true
		})
	}()

	// Send the playback state to the client
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressPlaybackState, _ps)

	// ------- Playlist ------- //
	if pm.currentMediaListEntry.IsPresent() && pm.currentLocalFile.IsPresent() {
		go pm.playlistHub.onPlaybackStatus(pm.currentMediaListEntry.MustGet(), pm.currentLocalFile.MustGet(), _ps)
	}

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.UpdateAnimeActivity(int(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds), int(pm.currentMediaPlaybackStatus.DurationInSeconds), !pm.currentMediaPlaybackStatus.Playing)
	}
}

func (pm *PlaybackManager) handleTrackingRetry(reason string) {
	// DEVNOTE: This event is not sent to the client
	// We notify the playlist hub, so it can play the next episode (it's assumed that the user closed the player)

	// ------- Playlist ------- //
	go pm.playlistHub.onTrackingError()
}

func (pm *PlaybackManager) handleStreamingTrackingStarted(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	if pm.currentStreamEpisode.IsAbsent() || pm.currentStreamMedia.IsAbsent() {
		return
	}

	//// Get the media list entry
	//// Note that it might be absent if the user is watching a stream that is not in the library
	pm.currentMediaListEntry = pm.getStreamPlaybackDetails(pm.currentStreamMedia.MustGet().GetID())

	// Set the playback type
	pm.currentPlaybackType = StreamPlayback

	// Reset the history map
	pm.historyMap = make(map[string]PlaybackState)

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getStreamPlaybackState(status)

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			value.EventCh <- StreamStartedEvent{Filename: status.Filename, Filepath: status.Filepath}
			return true
		})
	}()

	// Log
	pm.Logger.Debug().Msg("playback manager: Tracking started for stream")
	// Send event to the client
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressTrackingStarted, _ps)

	pm.continuityManager.SetExternalPlayerEpisodeDetails(&continuity.ExternalPlayerEpisodeDetails{
		EpisodeNumber: pm.currentStreamEpisode.MustGet().GetProgressNumber(),
		MediaId:       pm.currentStreamMedia.MustGet().GetID(),
		Filepath:      "",
	})

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.SetAnimeActivity(&discordrpc_presence.AnimeActivity{
			ID:                  pm.currentStreamMedia.MustGet().GetID(),
			Title:               pm.currentStreamMedia.MustGet().GetPreferredTitle(),
			Image:               pm.currentStreamMedia.MustGet().GetCoverImageSafe(),
			IsMovie:             pm.currentStreamMedia.MustGet().IsMovie(),
			EpisodeNumber:       pm.currentStreamEpisode.MustGet().GetProgressNumber(),
			Progress:            int(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds),
			Duration:            int(pm.currentMediaPlaybackStatus.DurationInSeconds),
			TotalEpisodes:       pm.currentStreamMedia.MustGet().Episodes,
			CurrentEpisodeCount: pm.currentStreamMedia.MustGet().GetCurrentEpisodeCountOrNil(),
		})
	}
}

func (pm *PlaybackManager) handleStreamingPlaybackStatus(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	if pm.currentStreamEpisode.IsAbsent() {
		return
	}

	pm.currentPlaybackType = StreamPlayback

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getStreamPlaybackState(status)
	// If the same PlaybackState is in the history, update the ProgressUpdated flag
	// PlaybackStatusCh has no way of knowing if the progress has been updated
	if h, ok := pm.historyMap[status.Filename]; ok {
		_ps.ProgressUpdated = h.ProgressUpdated
	}

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			return true
		})
	}()

	// Send the playback state to the client
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressPlaybackState, _ps)

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.UpdateAnimeActivity(int(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds), int(pm.currentMediaPlaybackStatus.DurationInSeconds), !pm.currentMediaPlaybackStatus.Playing)
	}
}

func (pm *PlaybackManager) handleStreamingVideoCompleted(status *mediaplayer.PlaybackStatus) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	if pm.currentStreamEpisode.IsAbsent() {
		return
	}

	// Set the current media playback status
	pm.currentMediaPlaybackStatus = status
	// Get the playback state
	_ps := pm.getStreamPlaybackState(status)
	// Log
	pm.Logger.Debug().Msg("playback manager: Received video completed event")

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- PlaybackStatusChangedEvent{Status: *status, State: _ps}
			value.EventCh <- StreamCompletedEvent{Filename: status.Filename}
			return true
		})
	}()
	//
	// Update the progress on AniList if auto update progress is enabled
	//
	pm.autoSyncCurrentProgress(&_ps)

	// Send the playback state with the `ProgressUpdated` flag
	// The client will use this to notify the user if the progress has been updated
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressVideoCompleted, _ps)
	// Push the video playback state to the history
	pm.historyMap[status.Filename] = _ps
}

func (pm *PlaybackManager) handleStreamingTrackingStopped(reason string) {
	pm.eventMu.Lock()
	defer pm.eventMu.Unlock()

	if pm.currentStreamEpisode.IsAbsent() {
		return
	}

	if pm.currentMediaPlaybackStatus != nil {
		pm.continuityManager.UpdateExternalPlayerEpisodeWatchHistoryItem(pm.currentMediaPlaybackStatus.CurrentTimeInSeconds, pm.currentMediaPlaybackStatus.DurationInSeconds)
	}

	// Notify subscribers
	go func() {
		pm.playbackStatusSubscribers.Range(func(key string, value *PlaybackStatusSubscriber) bool {
			if value.canceled.Load() {
				return true
			}
			value.EventCh <- StreamStoppedEvent{Reason: reason}
			return true
		})
	}()

	pm.Logger.Debug().Msg("playback manager: Received tracking stopped event")
	pm.wsEventManager.SendEvent(events.PlaybackManagerProgressTrackingStopped, reason)

	// ------- Discord ------- //
	if pm.discordPresence != nil && !*pm.isOffline {
		go pm.discordPresence.Close()
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Local File
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// getLocalFilePlaybackState returns a new PlaybackState
func (pm *PlaybackManager) getLocalFilePlaybackState(status *mediaplayer.PlaybackStatus) PlaybackState {
	pm.mu.Lock()
	defer pm.mu.Unlock()

	currentLocalFileWrapperEntry, ok := pm.currentLocalFileWrapperEntry.Get()
	if !ok {
		return PlaybackState{}
	}

	currentLocalFile, ok := pm.currentLocalFile.Get()
	if !ok {
		return PlaybackState{}
	}

	currentMediaListEntry, ok := pm.currentMediaListEntry.Get()
	if !ok {
		return PlaybackState{}
	}

	// Find the following episode
	_, canPlayNext := currentLocalFileWrapperEntry.FindNextEpisode(currentLocalFile)

	return PlaybackState{
		EpisodeNumber:        currentLocalFileWrapperEntry.GetProgressNumber(currentLocalFile),
		AniDbEpisode:         currentLocalFile.GetAniDBEpisode(),
		MediaTitle:           currentMediaListEntry.GetMedia().GetPreferredTitle(),
		MediaTotalEpisodes:   currentMediaListEntry.GetMedia().GetCurrentEpisodeCount(),
		MediaCoverImage:      currentMediaListEntry.GetMedia().GetCoverImageSafe(),
		MediaId:              currentMediaListEntry.GetMedia().GetID(),
		Filename:             status.Filename,
		CompletionPercentage: status.CompletionPercentage,
		CanPlayNext:          canPlayNext,
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Stream
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// getStreamPlaybackState returns a new PlaybackState
func (pm *PlaybackManager) getStreamPlaybackState(status *mediaplayer.PlaybackStatus) PlaybackState {
	pm.mu.Lock()
	defer pm.mu.Unlock()

	currentStreamEpisode, ok := pm.currentStreamEpisode.Get()
	if !ok {
		return PlaybackState{}
	}

	currentStreamMedia, ok := pm.currentStreamMedia.Get()
	if !ok {
		return PlaybackState{}
	}

	currentStreamAniDbEpisode, ok := pm.currentStreamAniDbEpisode.Get()
	if !ok {
		return PlaybackState{}
	}

	return PlaybackState{
		EpisodeNumber:        currentStreamEpisode.GetProgressNumber(),
		AniDbEpisode:         currentStreamAniDbEpisode,
		MediaTitle:           currentStreamMedia.GetPreferredTitle(),
		MediaTotalEpisodes:   currentStreamMedia.GetCurrentEpisodeCount(),
		MediaCoverImage:      currentStreamMedia.GetCoverImageSafe(),
		MediaId:              currentStreamMedia.GetID(),
		Filename:             cmp.Or(status.Filename, "Stream"),
		CompletionPercentage: status.CompletionPercentage,
		CanPlayNext:          false, // DEVNOTE: This is not used for streams
	}
}

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

// autoSyncCurrentProgress syncs the current video playback progress with providers.
// This is called once when a "video complete" event is heard.
func (pm *PlaybackManager) autoSyncCurrentProgress(_ps *PlaybackState) {

	shouldUpdate, err := pm.Database.AutoUpdateProgressIsEnabled()
	if err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Failed to check if auto update progress is enabled")
		return
	}

	if !shouldUpdate {
		return
	}

	switch pm.currentPlaybackType {
	case LocalFilePlayback:
		// Note :currentMediaListEntry MUST be defined since we assume that the media is in the user's library
		if pm.currentMediaListEntry.IsAbsent() || pm.currentLocalFileWrapperEntry.IsAbsent() || pm.currentLocalFile.IsAbsent() {
			return
		}
		// Check if we should update the progress
		// If the current progress is lower than the episode progress number
		epProgressNum := pm.currentLocalFileWrapperEntry.MustGet().GetProgressNumber(pm.currentLocalFile.MustGet())
		if *pm.currentMediaListEntry.MustGet().Progress >= epProgressNum {
			return
		}

	case StreamPlayback:
		if pm.currentStreamEpisode.IsAbsent() || pm.currentStreamMedia.IsAbsent() {
			return
		}
		// Do not auto update progress is the media is in the library AND the progress is higher than the current episode
		epProgressNum := pm.currentStreamEpisode.MustGet().GetProgressNumber()
		if pm.currentMediaListEntry.IsPresent() && *pm.currentMediaListEntry.MustGet().Progress >= epProgressNum {
			return
		}
	}

	// Update the progress on AniList
	pm.Logger.Debug().Msg("playback manager: Updating progress on AniList")
	err = pm.updateProgress()

	if err != nil {
		_ps.ProgressUpdated = false
		pm.wsEventManager.SendEvent(events.ErrorToast, "Failed to update progress on AniList")
	} else {
		_ps.ProgressUpdated = true
		pm.wsEventManager.SendEvent(events.PlaybackManagerProgressUpdated, _ps)
	}

}

// SyncCurrentProgress syncs the current video playback progress with providers
// This method is called when the user manually requests to sync the progress
//   - This method will return an error only if the progress update fails on AniList
//   - This method will refresh the anilist collection
func (pm *PlaybackManager) SyncCurrentProgress() error {
	pm.eventMu.RLock()

	err := pm.updateProgress()
	if err != nil {
		pm.eventMu.RUnlock()
		return err
	}

	// Push the current playback state to the history
	if pm.currentMediaPlaybackStatus != nil {
		var _ps PlaybackState
		switch pm.currentPlaybackType {
		case LocalFilePlayback:
			pm.getLocalFilePlaybackState(pm.currentMediaPlaybackStatus)
		case StreamPlayback:
			pm.getStreamPlaybackState(pm.currentMediaPlaybackStatus)
		}
		_ps.ProgressUpdated = true
		pm.historyMap[pm.currentMediaPlaybackStatus.Filename] = _ps
		pm.wsEventManager.SendEvent(events.PlaybackManagerProgressUpdated, _ps)
	}

	pm.refreshAnimeCollectionFunc()

	pm.eventMu.RUnlock()
	return nil
}

// updateProgress updates the progress of the current video playback on AniList and MyAnimeList.
// This only returns an error if the progress update fails on AniList
//   - /!\ When this is called, the PlaybackState should have been pushed to the history
func (pm *PlaybackManager) updateProgress() (err error) {

	var mediaId int
	var epNum int
	var totalEpisodes int

	switch pm.currentPlaybackType {
	case LocalFilePlayback:
		//
		// Local File
		//
		if pm.currentLocalFileWrapperEntry.IsAbsent() || pm.currentLocalFile.IsAbsent() || pm.currentMediaListEntry.IsAbsent() {
			return errors.New("no video is being watched")
		}

		defer util.HandlePanicInModuleWithError("playbackmanager/updateProgress", &err)

		/// Online
		mediaId = pm.currentMediaListEntry.MustGet().GetMedia().GetID()
		epNum = pm.currentLocalFileWrapperEntry.MustGet().GetProgressNumber(pm.currentLocalFile.MustGet())
		totalEpisodes = pm.currentMediaListEntry.MustGet().GetMedia().GetTotalEpisodeCount() // total episode count or -1

	case StreamPlayback:
		//
		// Stream
		//
		// Last sanity check
		if pm.currentStreamEpisode.IsAbsent() || pm.currentStreamMedia.IsAbsent() {
			return errors.New("no video is being watched")
		}

		mediaId = pm.currentStreamMedia.MustGet().ID
		epNum = pm.currentStreamEpisode.MustGet().GetProgressNumber()
		totalEpisodes = pm.currentStreamMedia.MustGet().GetTotalEpisodeCount() // total episode count or -1

	case ManualTrackingPlayback:
		//
		// Manual Tracking
		//
		if pm.currentManualTrackingState.IsAbsent() {
			return errors.New("no media file is being manually tracked")
		}

		defer func() {
			if pm.manualTrackingCtxCancel != nil {
				pm.manualTrackingCtxCancel()
			}
		}()

		/// Online
		mediaId = pm.currentManualTrackingState.MustGet().MediaId
		epNum = pm.currentManualTrackingState.MustGet().EpisodeNumber
		totalEpisodes = pm.currentManualTrackingState.MustGet().TotalEpisodes

	default:
		return errors.New("unknown playback type")
	}

	if mediaId == 0 { // Sanity check
		return errors.New("media ID not found")
	}

	// Update the progress on AniList
	err = pm.platform.UpdateEntryProgress(
		context.Background(),
		mediaId,
		epNum,
		&totalEpisodes,
	)
	if err != nil {
		pm.Logger.Error().Err(err).Msg("playback manager: Error occurred while updating progress on AniList")
		return ErrProgressUpdateAnilist
	}

	pm.refreshAnimeCollectionFunc() // Refresh the AniList collection

	pm.Logger.Info().Msg("playback manager: Updated progress on AniList")

	return nil
}
