package mediaplayer

import (
	"context"
	"errors"
	"fmt"
	"seanime/internal/continuity"
	"seanime/internal/events"
	"seanime/internal/hook"
	"seanime/internal/mediaplayers/iina"
	mpchc2 "seanime/internal/mediaplayers/mpchc"
	"seanime/internal/mediaplayers/mpv"
	vlc2 "seanime/internal/mediaplayers/vlc"
	"seanime/internal/util/result"
	"sync"
	"time"

	"github.com/rs/zerolog"
)

const (
	PlayerClosedEvent = "Player closed"
)

type PlaybackType string

const (
	PlaybackTypeFile   PlaybackType = "file"
	PlaybackTypeStream PlaybackType = "stream"
)

type (
	// Repository provides a common interface to interact with media players
	Repository struct {
		Logger                *zerolog.Logger
		Default               string
		VLC                   *vlc2.VLC
		MpcHc                 *mpchc2.MpcHc
		Mpv                   *mpv.Mpv
		Iina                  *iina.Iina
		wsEventManager        events.WSEventManagerInterface
		continuityManager     *continuity.Manager
		playerInUse           string
		completionThreshold   float64
		mu                    sync.RWMutex
		isRunning             bool
		currentPlaybackStatus *PlaybackStatus
		subscribers           *result.Map[string, *RepositorySubscriber]
		cancel                context.CancelFunc
		exitedCh              chan struct{} // Closed when the media player exits
	}

	NewRepositoryOptions struct {
		Logger            *zerolog.Logger
		Default           string
		VLC               *vlc2.VLC
		MpcHc             *mpchc2.MpcHc
		Mpv               *mpv.Mpv
		Iina              *iina.Iina
		WSEventManager    events.WSEventManagerInterface
		ContinuityManager *continuity.Manager
	}

	// RepositorySubscriber provides a single event channel for all media player events
	RepositorySubscriber struct {
		EventCh chan MediaPlayerEvent
	}

	// MediaPlayerEvent is the base interface for all media player events
	MediaPlayerEvent interface {
		Type() string
	}

	// Local file playback events
	TrackingStartedEvent struct {
		Status *PlaybackStatus
	}

	TrackingRetryEvent struct {
		Reason string
	}

	VideoCompletedEvent struct {
		Status *PlaybackStatus
	}

	TrackingStoppedEvent struct {
		Reason string
	}

	PlaybackStatusEvent struct {
		Status *PlaybackStatus
	}

	// Streaming playback events
	StreamingTrackingStartedEvent struct {
		Status *PlaybackStatus
	}

	StreamingTrackingRetryEvent struct {
		Reason string
	}

	StreamingVideoCompletedEvent struct {
		Status *PlaybackStatus
	}

	StreamingTrackingStoppedEvent struct {
		Reason string
	}

	StreamingPlaybackStatusEvent struct {
		Status *PlaybackStatus
	}

	PlaybackStatus struct {
		CompletionPercentage float64 `json:"completionPercentage"`
		Playing              bool    `json:"playing"`
		Filename             string  `json:"filename"`
		Path                 string  `json:"path"`
		Duration             int     `json:"duration"` // in ms
		Filepath             string  `json:"filepath"`

		CurrentTimeInSeconds float64 `json:"currentTimeInSeconds"` // in seconds
		DurationInSeconds    float64 `json:"durationInSeconds"`    // in seconds

		PlaybackType PlaybackType `json:"playbackType"` // "file", "stream"
	}
)

func (e TrackingStartedEvent) Type() string          { return "tracking_started" }
func (e TrackingRetryEvent) Type() string            { return "tracking_retry" }
func (e VideoCompletedEvent) Type() string           { return "video_completed" }
func (e TrackingStoppedEvent) Type() string          { return "tracking_stopped" }
func (e PlaybackStatusEvent) Type() string           { return "playback_status" }
func (e StreamingTrackingStartedEvent) Type() string { return "streaming_tracking_started" }
func (e StreamingTrackingRetryEvent) Type() string   { return "streaming_tracking_retry" }
func (e StreamingVideoCompletedEvent) Type() string  { return "streaming_video_completed" }
func (e StreamingTrackingStoppedEvent) Type() string { return "streaming_tracking_stopped" }
func (e StreamingPlaybackStatusEvent) Type() string  { return "streaming_playback_status" }

func NewRepository(opts *NewRepositoryOptions) *Repository {

	return &Repository{
		Logger:                opts.Logger,
		Default:               opts.Default,
		VLC:                   opts.VLC,
		MpcHc:                 opts.MpcHc,
		Mpv:                   opts.Mpv,
		Iina:                  opts.Iina,
		wsEventManager:        opts.WSEventManager,
		continuityManager:     opts.ContinuityManager,
		completionThreshold:   0.8,
		subscribers:           result.NewResultMap[string, *RepositorySubscriber](),
		currentPlaybackStatus: &PlaybackStatus{},
		exitedCh:              make(chan struct{}),
	}
}

func (m *Repository) Subscribe(id string) *RepositorySubscriber {
	sub := &RepositorySubscriber{
		EventCh: make(chan MediaPlayerEvent, 10), // Buffered channel to avoid blocking
	}
	m.subscribers.Set(id, sub)
	return sub
}

func (m *Repository) Unsubscribe(id string) {
	m.subscribers.Delete(id)
}

func (m *Repository) GetStatus() *PlaybackStatus {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.currentPlaybackStatus
}

// PullStatus returns the current playback status directly from the media player.
func (m *Repository) PullStatus() (*PlaybackStatus, bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	status, err := m.getStatus()
	if err != nil {
		return nil, false
	}

	var ok bool
	if m.currentPlaybackStatus == nil {
		return nil, false
	}

	if m.currentPlaybackStatus.PlaybackType == PlaybackTypeFile {
		ok = m.processStatus(m.Default, status)
	} else {
		ok = m.processStreamStatus(m.Default, status)
	}
	return m.currentPlaybackStatus, ok
}

func (m *Repository) IsRunning() bool {
	return m.isRunning
}

func (m *Repository) GetExecutablePath() string {
	switch m.Default {
	case "vlc":
		return m.VLC.GetExecutablePath()
	case "mpc-hc":
		return m.MpcHc.GetExecutablePath()
	case "mpv":
		return m.Mpv.GetExecutablePath()
	case "iina":
		return m.Iina.GetExecutablePath()
	}
	return ""
}

func (m *Repository) GetDefault() string {
	return m.Default
}

// Play will start the media player and load the video at the given path.
// The implementation of the specific media player is handled by the respective media player package.
// Calling it multiple *should* not open multiple instances of the media player -- subsequent calls should just load a new video if the media player is already open.
func (m *Repository) Play(path string) error {

	m.Logger.Debug().Str("path", path).Msg("media player: Media requested")

	lastWatched := m.continuityManager.GetExternalPlayerEpisodeWatchHistoryItem(path, false, 0, 0)

	switch m.Default {
	case "vlc":
		err := m.VLC.Start()
		if err != nil {
			m.Logger.Error().Err(err).Msg("media player: Could not start media player using VLC")
			return fmt.Errorf("could not start VLC, %w", err)
		}

		err = m.VLC.AddAndPlay(path)
		if err != nil {
			m.Logger.Error().Err(err).Msg("media player: Could not open and play video using VLC")
			if m.VLC.Path != "" {
				return fmt.Errorf("could not open and play video, %w", err)
			} else {
				return fmt.Errorf("could not open and play video, %w", err)
			}
		}

		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			if lastWatched.Found {
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.ForcePause()
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.Seek(fmt.Sprintf("%d", int(lastWatched.Item.CurrentTime)))
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.Resume()
			}
		}

		return nil
	case "mpc-hc":
		err := m.MpcHc.Start()
		if err != nil {
			m.Logger.Error().Err(err).Msg("media player: Could not start media player using MPC-HC")
			return fmt.Errorf("could not start MPC-HC, %w", err)
		}
		_, err = m.MpcHc.OpenAndPlay(path)
		if err != nil {
			m.Logger.Error().Err(err).Msg("media player: Could not open and play video using MPC-HC")
			return fmt.Errorf("could not open and play video, %w", err)
		}

		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			if lastWatched.Found {
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Pause()
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Seek(int(lastWatched.Item.CurrentTime))
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Play()
			}
		}

		return nil
	case "mpv":
		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			var args []string
			if lastWatched.Found {
				//args = append(args, "--no-resume-playback", fmt.Sprintf("--start=+%d", int(lastWatched.Item.CurrentTime)))
				args = append(args, "--no-resume-playback")
			}
			err := m.Mpv.OpenAndPlay(path, args...)
			if err != nil {
				m.Logger.Error().Err(err).Msg("media player: Could not open and play video using MPV")
				return fmt.Errorf("could not open and play video, %w", err)
			}
			if lastWatched.Found {
				_ = m.Mpv.SeekTo(lastWatched.Item.CurrentTime)
			}
		} else {
			err := m.Mpv.OpenAndPlay(path)
			if err != nil {
				m.Logger.Error().Err(err).Msg("media player: Could not open and play video using MPV")
				return fmt.Errorf("could not open and play video, %w", err)
			}
		}

		return nil
	case "iina":
		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			var args []string
			if lastWatched.Found {
				//args = append(args, "--mpv-no-resume-playback", fmt.Sprintf("--mpv-start=+%d", int(lastWatched.Item.CurrentTime)))
				args = append(args, "--mpv-no-resume-playback")
			}
			err := m.Iina.OpenAndPlay(path, args...)
			if err != nil {
				m.Logger.Error().Err(err).Msg("media player: Could not open and play video using IINA")
				return fmt.Errorf("could not open and play video, %w", err)
			}
			if lastWatched.Found {
				_ = m.Iina.SeekTo(lastWatched.Item.CurrentTime)
			}
		} else {
			err := m.Iina.OpenAndPlay(path)
			if err != nil {
				m.Logger.Error().Err(err).Msg("media player: Could not open and play video using IINA")
				return fmt.Errorf("could not open and play video, %w", err)
			}
		}

		return nil
	default:
		return errors.New("no default media player set")
	}

}

func (m *Repository) Pause() error {
	switch m.Default {
	case "vlc":
		return m.VLC.Pause()
	case "mpc-hc":
		return m.MpcHc.Pause()
	case "mpv":
		return m.Mpv.Pause()
	case "iina":
		return m.Iina.Pause()
	default:
		return errors.New("no default media player set")
	}
}

func (m *Repository) Resume() error {
	switch m.Default {
	case "vlc":
		return m.VLC.Resume()
	case "mpc-hc":
		return m.MpcHc.Play()
	case "mpv":
		return m.Mpv.Resume()
	case "iina":
		return m.Iina.Resume()
	default:
		return errors.New("no default media player set")
	}
}

func (m *Repository) Seek(seconds float64) error {
	switch m.Default {
	case "vlc":
		return m.VLC.Seek(fmt.Sprintf("%d", int(seconds)))
	case "mpc-hc":
		return m.MpcHc.Seek(int(seconds))
	case "mpv":
		return m.Mpv.Seek(seconds)
	case "iina":
		return m.Iina.Seek(seconds)
	default:
		return errors.New("no default media player set")
	}
}

func (m *Repository) Stream(streamUrl string, episode int, mediaId int, windowTitle string) error {

	m.Logger.Debug().Str("streamUrl", streamUrl).Msg("media player: Stream requested")
	var err error

	switch m.Default {
	case "vlc":
		err = m.VLC.Start()
	case "mpc-hc":
		err = m.MpcHc.Start()
		_, err = m.MpcHc.OpenAndPlay(streamUrl)
	case "mpv":
		// MPV does not need to be started
	case "iina":
		// IINA does not need to be started
	default:
		return errors.New("no default media player set")
	}

	if err != nil {
		m.Logger.Error().Err(err).Msg("media player: Could not start media player for stream")
		return fmt.Errorf("could not open media player, %w", err)
	}

	lastWatched := m.continuityManager.GetExternalPlayerEpisodeWatchHistoryItem("", true, episode, mediaId)

	switch m.Default {
	case "vlc":
		err = m.VLC.AddAndPlay(streamUrl)

		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			if lastWatched.Found {
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.ForcePause()
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.Seek(fmt.Sprintf("%d", int(lastWatched.Item.CurrentTime)))
				time.Sleep(400 * time.Millisecond)
				_ = m.VLC.Resume()
			}
		}

	case "mpc-hc":
		_, err = m.MpcHc.OpenAndPlay(streamUrl)

		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			if lastWatched.Found {
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Pause()
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Seek(int(lastWatched.Item.CurrentTime))
				time.Sleep(400 * time.Millisecond)
				_ = m.MpcHc.Play()
			}
		}

	case "mpv":
		args := []string{}
		if windowTitle != "" {
			args = append(args, fmt.Sprintf("--title=%q", windowTitle))
		}
		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			err = m.Mpv.OpenAndPlay(streamUrl, args...)
			if lastWatched.Found {
				_ = m.Mpv.SeekTo(lastWatched.Item.CurrentTime)
			}
		} else {
			err = m.Mpv.OpenAndPlay(streamUrl, args...)
		}

	case "iina":
		args := []string{}
		if windowTitle != "" {
			args = append(args, fmt.Sprintf("--mpv-title=%q", windowTitle))
		}
		if m.continuityManager.GetSettings().WatchContinuityEnabled {
			err = m.Iina.OpenAndPlay(streamUrl, args...)
			if lastWatched.Found {
				_ = m.Iina.SeekTo(lastWatched.Item.CurrentTime)
			}
		} else {
			err = m.Iina.OpenAndPlay(streamUrl, args...)
		}

	}

	if err != nil {
		m.Logger.Error().Err(err).Msg("media player: Could not open and play stream")
		return fmt.Errorf("could not open and play stream, %w", err)
	}

	return nil
}

// Cancel will stop the tracking process and publish an "abnormal" event
func (m *Repository) Cancel() {
	m.mu.Lock()
	if m.cancel != nil {
		m.Logger.Debug().Msg("media player: Cancel request received")
		m.cancel()
		m.trackingStopped("Something went wrong, tracking cancelled")
	} else {
		m.Logger.Debug().Msg("media player: Cancel request received, but no context found")
	}
	// Close MPV if it's the default player
	if m.Default == "mpv" {
		m.Mpv.CloseAll()
	}
	m.mu.Unlock()
}

// Stop will stop the tracking process and publish a "normal" event
func (m *Repository) Stop() {
	m.mu.Lock()
	if m.cancel != nil {
		m.Logger.Debug().Msg("media player: Stop request received")
		m.cancel()
		m.cancel = nil
		m.trackingStopped("Tracking stopped")
		// Close MPV if it's the default player
		if m.Default == "mpv" {
			go m.Mpv.CloseAll()
		}
	}
	m.mu.Unlock()
}

// StartTrackingTorrentStream will start tracking media player status for torrent streaming
func (m *Repository) StartTrackingTorrentStream() {
	m.mu.Lock()
	// If a previous context exists, cancel it
	if m.cancel != nil {
		m.Logger.Debug().Msg("media player: Cancelling previous context")
		m.cancel()
	}

	// Create a new context
	var trackingCtx context.Context
	trackingCtx, m.cancel = context.WithCancel(context.Background())

	done := make(chan struct{})
	var filename string
	var completed bool
	var retries int

	hookEvent := &MediaPlayerStreamTrackingRequestedEvent{
		StartRefreshDelay:    3,
		RefreshDelay:         1,
		MaxRetries:           5,
		MaxRetriesAfterStart: 5,
	}
	_ = hook.GlobalHookManager.OnMediaPlayerStreamTrackingRequested().Trigger(hookEvent)
	startRefreshDelay := hookEvent.StartRefreshDelay
	maxTries := hookEvent.MaxRetries
	refreshDelay := hookEvent.RefreshDelay
	maxRetriesAfterStart := hookEvent.MaxRetriesAfterStart

	// Default prevented, do not track
	if hookEvent.DefaultPrevented {
		m.Logger.Debug().Msg("media player: Tracking cancelled by hook")
		return
	}

	// Unlike normal tracking when the file is downloaded, we may need to wait a bit before we can get the status,
	// so we won't count retries until it's confirmed that the file has started playing.
	var trackingStarted bool
	var waitInSeconds int

	m.isRunning = true
	gotFirstStatus := false

	m.mu.Unlock()

	go func() {
		defer func() {
			m.mu.Lock()
			m.isRunning = false
			if m.cancel != nil {
				m.cancel()
			}
			m.mu.Unlock()
		}()
		for {
			select {
			case <-done:
				m.mu.Lock()
				m.Logger.Debug().Msg("media player: Connection lost")
				m.isRunning = false
				m.mu.Unlock()
				return
			case <-trackingCtx.Done():
				m.mu.Lock()
				m.Logger.Debug().Msg("media player: Context cancelled")
				m.isRunning = false
				m.mu.Unlock()
				return
			//case <-m.exitedCh:
			//	m.mu.Lock()
			//	m.Logger.Debug().Msg("media player: Player exited")
			//	m.isRunning = false
			//	m.streamingTrackingStopped(PlayerClosedEvent)
			//	m.mu.Unlock()
			//	return
			default:
				// Wait at least 3 seconds before we start checking the status
				if !gotFirstStatus {
					time.Sleep(time.Duration(startRefreshDelay) * time.Second)
				} else {
					time.Sleep(time.Duration(refreshDelay) * time.Second)
				}
				status, err := m.getStatus()
				if err != nil {
					if !trackingStarted {
						if waitInSeconds > 60 {
							m.Logger.Warn().Msg("media player: Ending goroutine, waited too long")
							return
						}
						m.Logger.Trace().Msgf("media player: Waiting for stream, %d seconds", waitInSeconds)
						waitInSeconds += refreshDelay
						continue
					} else {
						m.streamingTrackingRetry("Failed to get player status")
						m.Logger.Error().Msgf("media player: Failed to get player status, retrying (%d/%d)", retries+1, maxTries)

						// Video is completed, and we are unable to get the status
						// We can safely assume that the player has been closed
						if retries == 1 && (completed || m.continuityManager.GetSettings().WatchContinuityEnabled) {
							m.Logger.Debug().Msg("media player: Sending player closed event")
							m.streamingTrackingStopped(PlayerClosedEvent)
							close(done)
							break
						}

						if retries >= maxTries-1 {
							m.Logger.Debug().Msg("media player: Sending failed status query event")
							m.streamingTrackingStopped("Failed to get player status")
							close(done)
							break
						}
						retries++
						continue
					}
				}

				trackingStarted = true
				ok := m.processStreamStatus(m.Default, status)

				if !ok {
					m.streamingTrackingRetry("Failed to get player status")
					m.Logger.Error().Interface("status", status).Msgf("media player: Failed to process status, retrying (%d/%d)", retries+1, maxRetriesAfterStart)
					if retries >= maxRetriesAfterStart-1 {
						m.Logger.Debug().Msg("media player: Sending failed status query event")
						m.streamingTrackingStopped("Failed to process status")
						close(done)
						break
					}
					retries++
					continue
				}

				// New video has started playing \/
				if filename == "" || filename != m.currentPlaybackStatus.Filename {
					m.Logger.Debug().Str("previousFilename", filename).Str("newFilename", m.currentPlaybackStatus.Filename).Msg("media player: Video loaded")
					m.streamingTrackingStarted(m.currentPlaybackStatus)
					filename = m.currentPlaybackStatus.Filename
					completed = false
				}

				// Video completed \/
				if m.currentPlaybackStatus.CompletionPercentage > m.completionThreshold && !completed {
					m.Logger.Debug().Msg("media player: Video completed")
					m.streamingVideoCompleted(m.currentPlaybackStatus)
					completed = true
				}

				m.streamingPlaybackStatus(m.currentPlaybackStatus)
			}
		}
	}()
}

// StartTracking will start tracking media player status.
// This method is safe to call multiple times -- it will cancel the previous context and start a new one.
func (m *Repository) StartTracking() {
	m.mu.Lock()
	// If a previous context exists, cancel it
	if m.cancel != nil {
		m.Logger.Debug().Msg("media player: Cancelling previous context")
		m.cancel()
	}

	// Create a new context
	var trackingCtx context.Context
	trackingCtx, m.cancel = context.WithCancel(context.Background())

	done := make(chan struct{})
	var filename string
	var completed bool
	var retries int

	hookEvent := &MediaPlayerLocalFileTrackingRequestedEvent{
		StartRefreshDelay: 3,
		RefreshDelay:      1,
		MaxRetries:        5,
	}
	_ = hook.GlobalHookManager.OnMediaPlayerLocalFileTrackingRequested().Trigger(hookEvent)
	startRefreshDelay := hookEvent.StartRefreshDelay
	maxTries := hookEvent.MaxRetries
	refreshDelay := hookEvent.RefreshDelay

	// Default prevented, do not track
	if hookEvent.DefaultPrevented {
		m.Logger.Debug().Msg("media player: Tracking cancelled by hook")
		return
	}

	m.isRunning = true
	gotFirstStatus := false

	m.mu.Unlock()

	go func() {
		for {
			select {
			case <-done:
				m.mu.Lock()
				m.Logger.Debug().Msg("media player: Connection lost")
				m.isRunning = false
				m.mu.Unlock()
				if m.cancel != nil {
					m.cancel()
					m.cancel = nil
				}
				return
			case <-trackingCtx.Done():
				m.mu.Lock()
				m.Logger.Debug().Msg("media player: Context cancelled")
				m.isRunning = false
				m.cancel = nil
				m.mu.Unlock()
				return
			//case <-m.exitedCh:
			//	m.mu.Lock()
			//	m.Logger.Debug().Msg("media player: Player exited")
			//	m.isRunning = false
			//	m.trackingStopped(PlayerClosedEvent)
			//	m.mu.Unlock()
			//	return
			default:
				// Wait at least X seconds before we start checking the status
				if !gotFirstStatus {
					time.Sleep(time.Duration(startRefreshDelay) * time.Second)
				} else {
					time.Sleep(time.Duration(refreshDelay) * time.Second)
				}
				status, err := m.getStatus()
				if err != nil {
					m.trackingRetry("Failed to get player status")
					m.Logger.Error().Msgf("media player: Failed to get player status, retrying (%d/%d)", retries+1, maxTries)

					// Video is completed, and we are unable to get the status
					// We can safely assume that the player has been closed
					if retries == 1 && (completed || m.continuityManager.GetSettings().WatchContinuityEnabled) {
						m.trackingStopped(PlayerClosedEvent)
						close(done)
						break
					}

					if retries >= maxTries-1 {
						m.trackingStopped("Failed to get player status")
						close(done)
						break
					}
					retries++
					continue
				}

				gotFirstStatus = true

				ok := m.processStatus(m.Default, status)

				if !ok {
					m.trackingRetry("Failed to get player status")
					m.Logger.Error().Interface("status", status).Msgf("media player: Failed to process status, retrying (%d/%d)", retries+1, maxTries)
					if retries >= maxTries-1 {
						m.trackingStopped("Failed to process status")
						close(done)
						break
					}
					retries++
					continue
				}

				// New video has started playing \/
				if filename == "" || filename != m.currentPlaybackStatus.Filename {
					m.Logger.Debug().Str("previousFilename", filename).Str("newFilename", m.currentPlaybackStatus.Filename).Msg("media player: Video started playing")
					m.Logger.Debug().Interface("currentPlaybackStatus", m.currentPlaybackStatus).Msg("media player: Playback status")
					m.trackingStarted(m.currentPlaybackStatus)
					filename = m.currentPlaybackStatus.Filename
					completed = false
				}

				// Video completed \/
				if m.currentPlaybackStatus.CompletionPercentage > m.completionThreshold && !completed {
					m.Logger.Debug().Msg("media player: Video completed")
					m.Logger.Debug().Interface("currentPlaybackStatus", m.currentPlaybackStatus).Msg("media player: Playback status")
					m.videoCompleted(m.currentPlaybackStatus)
					completed = true
				}

				m.playbackStatus(m.currentPlaybackStatus)
			}
		}
	}()
}

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

func (m *Repository) trackingStopped(reason string) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- TrackingStoppedEvent{Reason: reason}
		return true
	})
}

func (m *Repository) trackingStarted(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- TrackingStartedEvent{Status: status}
		return true
	})
}

func (m *Repository) trackingRetry(reason string) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- TrackingRetryEvent{Reason: reason}
		return true
	})
}

func (m *Repository) videoCompleted(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- VideoCompletedEvent{Status: status}
		return true
	})
}

func (m *Repository) playbackStatus(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- PlaybackStatusEvent{Status: status}
		return true
	})
}

func (m *Repository) streamingTrackingStopped(reason string) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- StreamingTrackingStoppedEvent{Reason: reason}
		return true
	})
}

func (m *Repository) streamingTrackingStarted(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- StreamingTrackingStartedEvent{Status: status}
		return true
	})
}

func (m *Repository) streamingTrackingRetry(reason string) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- StreamingTrackingRetryEvent{Reason: reason}
		return true
	})
}

func (m *Repository) streamingVideoCompleted(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- StreamingVideoCompletedEvent{Status: status}
		return true
	})
}

func (m *Repository) streamingPlaybackStatus(status *PlaybackStatus) {
	m.subscribers.Range(func(key string, value *RepositorySubscriber) bool {
		value.EventCh <- StreamingPlaybackStatusEvent{Status: status}
		return true
	})
}

func (m *Repository) getStatus() (interface{}, error) {
	switch m.Default {
	case "vlc":
		return m.VLC.GetStatus()
	case "mpc-hc":
		return m.MpcHc.GetVariables()
	case "mpv":
		return m.Mpv.GetPlaybackStatus()
	case "iina":
		return m.Iina.GetPlaybackStatus()
	}
	return nil, errors.New("unsupported media player")
}

func (m *Repository) processStatus(player string, status interface{}) bool {
	m.currentPlaybackStatus.PlaybackType = PlaybackTypeFile
	switch player {
	case "vlc":
		// Process VLC status
		st, ok := status.(*vlc2.Status)
		if !ok || st == nil {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position
		m.currentPlaybackStatus.Playing = st.State == "playing"
		m.currentPlaybackStatus.Filename = st.Information.Category["meta"].Filename
		m.currentPlaybackStatus.Duration = int(st.Length * 1000)
		m.currentPlaybackStatus.Filepath = st.Information.Category["meta"].Filename

		m.currentPlaybackStatus.CurrentTimeInSeconds = float64(st.Time)
		m.currentPlaybackStatus.DurationInSeconds = float64(st.Length)
		return true
	case "mpc-hc":
		// Process MPC-HC status
		st, ok := status.(*mpchc2.Variables)
		if !ok || st == nil || st.Duration == 0 {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = st.State == 2
		m.currentPlaybackStatus.Filename = st.File
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.FilePath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position / 1000
		m.currentPlaybackStatus.DurationInSeconds = st.Duration / 1000

		return true
	case "mpv":
		// Process MPV status
		st, ok := status.(*mpv.Playback)
		if !ok || st == nil || st.Duration == 0 || st.IsRunning == false {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = !st.Paused
		m.currentPlaybackStatus.Filename = st.Filename
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.Filepath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position
		m.currentPlaybackStatus.DurationInSeconds = st.Duration

		return true
	case "iina":
		// Process IINA status
		st, ok := status.(*iina.Playback)
		if !ok || st == nil || st.Duration == 0 || st.IsRunning == false {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = !st.Paused
		m.currentPlaybackStatus.Filename = st.Filename
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.Filepath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position
		m.currentPlaybackStatus.DurationInSeconds = st.Duration

		return true
	default:
		return false
	}
}

func (m *Repository) processStreamStatus(player string, status interface{}) bool {
	m.currentPlaybackStatus.PlaybackType = PlaybackTypeStream
	switch player {
	case "vlc":
		// Process VLC status
		st, ok := status.(*vlc2.Status)
		if !ok || st == nil {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position
		m.currentPlaybackStatus.Playing = st.State == "playing"
		m.currentPlaybackStatus.Filename = st.Information.Category["meta"].Filename
		m.currentPlaybackStatus.Duration = int(st.Length * 1000)
		m.currentPlaybackStatus.Filepath = st.Information.Category["meta"].Filename // VLC does not provide the filepath, use filename

		m.currentPlaybackStatus.CurrentTimeInSeconds = float64(st.Time)
		m.currentPlaybackStatus.DurationInSeconds = float64(st.Length)

		return true
	case "mpc-hc":
		// Process MPC-HC status
		st, ok := status.(*mpchc2.Variables)
		if !ok || st == nil {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = st.State == 2
		m.currentPlaybackStatus.Filename = st.File
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.FilePath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position / 1000
		m.currentPlaybackStatus.DurationInSeconds = st.Duration / 1000

		return true
	case "mpv":
		// Process MPV status
		st, ok := status.(*mpv.Playback)
		if !ok || st == nil || st.Duration == 0 || st.IsRunning == false {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = !st.Paused
		m.currentPlaybackStatus.Filename = st.Filename
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.Filepath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position
		m.currentPlaybackStatus.DurationInSeconds = st.Duration

		return true
	case "iina":
		// Process IINA status
		st, ok := status.(*iina.Playback)
		if !ok || st == nil || st.Duration == 0 || st.IsRunning == false {
			return false
		}

		m.currentPlaybackStatus.CompletionPercentage = st.Position / st.Duration
		m.currentPlaybackStatus.Playing = !st.Paused
		m.currentPlaybackStatus.Filename = st.Filename
		m.currentPlaybackStatus.Duration = int(st.Duration)
		m.currentPlaybackStatus.Filepath = st.Filepath

		m.currentPlaybackStatus.CurrentTimeInSeconds = st.Position
		m.currentPlaybackStatus.DurationInSeconds = st.Duration

		return true
	default:
		return false
	}
}
