package debrid_client

import (
	"context"
	"errors"
	"fmt"
	"seanime/internal/database/db_bridge"
	"seanime/internal/debrid/debrid"
	"seanime/internal/directstream"
	"seanime/internal/events"
	hibiketorrent "seanime/internal/extension/hibike/torrent"
	"seanime/internal/hook"
	"seanime/internal/library/playbackmanager"
	"seanime/internal/util"
	"strconv"
	"sync"
	"time"

	"github.com/samber/mo"
)

type (
	StreamManager struct {
		repository            *Repository
		currentTorrentItemId  string
		downloadCtxCancelFunc context.CancelFunc

		currentStreamUrl string

		playbackSubscriberCtxCancelFunc context.CancelFunc
	}

	StreamPlaybackType string

	StreamStatus string

	StreamState struct {
		Status      StreamStatus `json:"status"`
		TorrentName string       `json:"torrentName"`
		Message     string       `json:"message"`
	}

	StartStreamOptions struct {
		MediaId       int
		EpisodeNumber int                         // RELATIVE Episode number to identify the file
		AniDBEpisode  string                      // Anizip episode
		Torrent       *hibiketorrent.AnimeTorrent // Selected torrent
		FileId        string                      // File ID or index
		FileIndex     *int                        // Index of the file to stream (Manual selection)
		UserAgent     string
		ClientId      string
		PlaybackType  StreamPlaybackType
		AutoSelect    bool
	}

	CancelStreamOptions struct {
		// Whether to remove the torrent from the debrid service
		RemoveTorrent bool `json:"removeTorrent"`
	}
)

const (
	StreamStatusDownloading StreamStatus = "downloading"
	StreamStatusReady       StreamStatus = "ready"
	StreamStatusFailed      StreamStatus = "failed"
	StreamStatusStarted     StreamStatus = "started"
)

func NewStreamManager(repository *Repository) *StreamManager {
	return &StreamManager{
		repository:           repository,
		currentTorrentItemId: "",
	}
}

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

const (
	PlaybackTypeNone           StreamPlaybackType = "none"
	PlaybackTypeNoneAndAwait   StreamPlaybackType = "noneAndAwait"
	PlaybackTypeDefault        StreamPlaybackType = "default"
	PlaybackTypeNativePlayer   StreamPlaybackType = "nativeplayer"
	PlaybackTypeExternalPlayer StreamPlaybackType = "externalPlayerLink"
)

// startStream is called by the client to start streaming a torrent
func (s *StreamManager) startStream(ctx context.Context, opts *StartStreamOptions) (err error) {
	defer util.HandlePanicInModuleWithError("debrid/client/StartStream", &err)

	s.repository.previousStreamOptions = mo.Some(opts)

	s.repository.logger.Info().
		Str("clientId", opts.ClientId).
		Any("playbackType", opts.PlaybackType).
		Int("mediaId", opts.MediaId).Msgf("debridstream: Starting stream for episode %s", opts.AniDBEpisode)

	// Cancel the download context if it's running
	if s.downloadCtxCancelFunc != nil {
		s.downloadCtxCancelFunc()
		s.downloadCtxCancelFunc = nil
	}

	if s.playbackSubscriberCtxCancelFunc != nil {
		s.playbackSubscriberCtxCancelFunc()
		s.playbackSubscriberCtxCancelFunc = nil
	}

	provider, err := s.repository.GetProvider()
	if err != nil {
		return fmt.Errorf("debridstream: Failed to start stream: %w", err)
	}

	s.repository.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "debridstream")
	//defer func() {
	//	s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
	//}()

	if opts.PlaybackType == PlaybackTypeNativePlayer {
		s.repository.directStreamManager.PrepareNewStream(opts.ClientId, "Selecting torrent...")
	}

	//
	// Get the media info
	//
	media, _, err := s.getMediaInfo(ctx, opts.MediaId)
	if err != nil {
		s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
		return err
	}

	episodeNumber := opts.EpisodeNumber
	aniDbEpisode := strconv.Itoa(episodeNumber)

	selectedTorrent := opts.Torrent
	fileId := opts.FileId

	if opts.AutoSelect {

		s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
			Status:      StreamStatusDownloading,
			TorrentName: "-",
			Message:     "Selecting best torrent...",
		})

		st, fi, err := s.repository.findBestTorrent(provider, media, opts.EpisodeNumber)
		if err != nil {
			s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
				Status:      StreamStatusFailed,
				TorrentName: "-",
				Message:     fmt.Sprintf("Failed to select best torrent, %v", err),
			})
			s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
			return fmt.Errorf("debridstream: Failed to start stream: %w", err)
		}
		selectedTorrent = st
		fileId = fi
	} else {
		// Manual selection
		if selectedTorrent == nil {
			s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
			return fmt.Errorf("debridstream: Failed to start stream, no torrent provided")
		}

		s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
			Status:      StreamStatusDownloading,
			TorrentName: selectedTorrent.Name,
			Message:     "Analyzing selected torrent...",
		})

		// If no fileId is provided, we need to analyze the torrent to find the correct file
		if fileId == "" {
			var chosenFileIndex *int
			if opts.FileIndex != nil {
				chosenFileIndex = opts.FileIndex
			}
			st, fi, err := s.repository.findBestTorrentFromManualSelection(provider, selectedTorrent, media, opts.EpisodeNumber, chosenFileIndex)
			if err != nil {
				s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
					Status:      StreamStatusFailed,
					TorrentName: selectedTorrent.Name,
					Message:     fmt.Sprintf("Failed to analyze torrent, %v", err),
				})
				s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
				return fmt.Errorf("debridstream: Failed to analyze torrent: %w", err)
			}
			selectedTorrent = st
			fileId = fi
		}
	}

	if selectedTorrent == nil {
		s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
		return fmt.Errorf("debridstream: Failed to start stream, no torrent provided")
	}

	s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
		Status:      StreamStatusDownloading,
		TorrentName: selectedTorrent.Name,
		Message:     "Adding torrent...",
	})

	// Add the torrent to the debrid service
	torrentItemId, err := provider.AddTorrent(debrid.AddTorrentOptions{
		MagnetLink:   selectedTorrent.MagnetLink,
		InfoHash:     selectedTorrent.InfoHash,
		SelectFileId: fileId, // RD-only, download only the selected file
	})
	if err != nil {
		s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
			Status:      StreamStatusFailed,
			TorrentName: selectedTorrent.Name,
			Message:     fmt.Sprintf("Failed to add torrent, %v", err),
		})
		s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
		return fmt.Errorf("debridstream: Failed to add torrent: %w", err)
	}

	time.Sleep(1 * time.Second)

	// Save the current torrent item id
	s.currentTorrentItemId = torrentItemId
	ctx, cancelCtx := context.WithCancel(context.Background())
	s.downloadCtxCancelFunc = cancelCtx

	readyCh := make(chan struct{})
	readyOnce := sync.Once{}
	ready := func() {
		readyOnce.Do(func() {
			close(readyCh)
		})
	}

	// Launch a goroutine that will listen to the added torrent's status
	go func(ctx context.Context) {
		defer util.HandlePanicInModuleThen("debrid/client/StartStream", func() {})
		defer func() {
			s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
		}()

		defer func() {
			// Cancel the context
			if s.downloadCtxCancelFunc != nil {
				s.downloadCtxCancelFunc()
				s.downloadCtxCancelFunc = nil
			}
		}()

		s.repository.logger.Debug().Msg("debridstream: Listening to torrent status")

		s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
			Status:      StreamStatusDownloading,
			TorrentName: selectedTorrent.Name,
			Message:     fmt.Sprintf("Downloading torrent..."),
		})

		itemCh := make(chan debrid.TorrentItem, 1)

		go func() {
			for item := range itemCh {
				if opts.PlaybackType == PlaybackTypeNativePlayer {
					s.repository.directStreamManager.PrepareNewStream(opts.ClientId, fmt.Sprintf("Awaiting stream: %d%%", item.CompletionPercentage))
				}

				s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
					Status:      StreamStatusDownloading,
					TorrentName: item.Name,
					Message:     fmt.Sprintf("Downloading torrent: %d%%", item.CompletionPercentage),
				})
			}
		}()

		// Await the stream URL
		// For Torbox, this will wait until the entire torrent is downloaded
		streamUrl, err := provider.GetTorrentStreamUrl(ctx, debrid.StreamTorrentOptions{
			ID:     torrentItemId,
			FileId: fileId,
		}, itemCh)

		go func() {
			close(itemCh)
		}()

		if ctx.Err() != nil {
			s.repository.logger.Debug().Msg("debridstream: Context cancelled, stopping stream")
			ready()
			return
		}

		if err != nil {
			s.repository.logger.Err(err).Msg("debridstream: Failed to get stream URL")
			if !errors.Is(err, context.Canceled) {
				s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
					Status:      StreamStatusFailed,
					TorrentName: selectedTorrent.Name,
					Message:     fmt.Sprintf("Failed to get stream URL, %v", err),
				})
			}
			ready()
			return
		}

		skipCheckEvent := &DebridSkipStreamCheckEvent{
			StreamURL:  streamUrl,
			Retries:    4,
			RetryDelay: 8,
		}
		_ = hook.GlobalHookManager.OnDebridSkipStreamCheck().Trigger(skipCheckEvent)
		streamUrl = skipCheckEvent.StreamURL

		// Default prevented, we check if we can stream the file
		if skipCheckEvent.DefaultPrevented {
			s.repository.logger.Debug().Msg("debridstream: Stream URL received, checking stream file")
			s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
				Status:      StreamStatusDownloading,
				TorrentName: selectedTorrent.Name,
				Message:     "Checking stream file...",
			})

			retries := 0

		streamUrlCheckLoop:
			for { // Retry loop for a total of 4 times (32 seconds)
				select {
				case <-ctx.Done():
					s.repository.logger.Debug().Msg("debridstream: Context cancelled, stopping stream")
					return
				default:
					// Check if we can stream the URL
					if canStream, reason := CanStream(streamUrl); !canStream {
						if retries >= skipCheckEvent.Retries {
							s.repository.logger.Error().Msg("debridstream: Cannot stream the file")

							s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
								Status:      StreamStatusFailed,
								TorrentName: selectedTorrent.Name,
								Message:     fmt.Sprintf("Cannot stream this file: %s", reason),
							})
							return
						}
						s.repository.logger.Warn().Msg("debridstream: Rechecking stream file in 8 seconds")
						s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
							Status:      StreamStatusDownloading,
							TorrentName: selectedTorrent.Name,
							Message:     "Checking stream file...",
						})
						retries++
						time.Sleep(time.Duration(skipCheckEvent.RetryDelay) * time.Second)
						continue
					}
					break streamUrlCheckLoop
				}
			}
		}

		s.repository.logger.Debug().Msg("debridstream: Stream is ready")

		// Signal to the client that the torrent is ready to stream
		s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
			Status:      StreamStatusReady,
			TorrentName: selectedTorrent.Name,
			Message:     "Ready to stream the file",
		})

		if ctx.Err() != nil {
			s.repository.logger.Debug().Msg("debridstream: Context cancelled, stopping stream")
			ready()
			return
		}

		windowTitle := media.GetPreferredTitle()
		if !media.IsMovieOrSingleEpisode() {
			windowTitle += fmt.Sprintf(" - Episode %s", aniDbEpisode)
		}

		event := &DebridSendStreamToMediaPlayerEvent{
			WindowTitle:  windowTitle,
			StreamURL:    streamUrl,
			Media:        media.ToBaseAnime(),
			AniDbEpisode: aniDbEpisode,
			PlaybackType: string(opts.PlaybackType),
		}
		err = hook.GlobalHookManager.OnDebridSendStreamToMediaPlayer().Trigger(event)
		if err != nil {
			s.repository.logger.Err(err).Msg("debridstream: Failed to send stream to media player")
		}
		windowTitle = event.WindowTitle
		streamUrl = event.StreamURL
		media := event.Media
		aniDbEpisode := event.AniDbEpisode
		playbackType := StreamPlaybackType(event.PlaybackType)

		if event.DefaultPrevented {
			s.repository.logger.Debug().Msg("debridstream: Stream prevented by hook")
			ready()
			return
		}

		s.currentStreamUrl = streamUrl

		switch playbackType {
		case PlaybackTypeNone:
			// No playback type selected, just signal to the client that the stream is ready
			s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
				Status:      StreamStatusReady,
				TorrentName: selectedTorrent.Name,
				Message:     "External player link sent",
			})
		case PlaybackTypeNoneAndAwait:
			// No playback type selected, just signal to the client that the stream is ready
			s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
				Status:      StreamStatusReady,
				TorrentName: selectedTorrent.Name,
				Message:     "External player link sent",
			})
			ready()

		case PlaybackTypeDefault:
			//
			// Start the stream
			//
			s.repository.logger.Debug().Msg("debridstream: Starting the media player")

			s.repository.wsEventManager.SendEvent(events.InfoToast, "Sending stream to media player...")
			s.repository.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "debridstream")

			var playbackSubscriberCtx context.Context
			playbackSubscriberCtx, s.playbackSubscriberCtxCancelFunc = context.WithCancel(context.Background())
			playbackSubscriber := s.repository.playbackManager.SubscribeToPlaybackStatus("debridstream")

			// Sends the stream to the media player
			// DEVNOTE: Events are handled by the torrentstream.Repository module
			err = s.repository.playbackManager.StartStreamingUsingMediaPlayer(windowTitle, &playbackmanager.StartPlayingOptions{
				Payload:   streamUrl,
				UserAgent: opts.UserAgent,
				ClientId:  opts.ClientId,
			}, media, aniDbEpisode)
			if err != nil {
				go s.repository.playbackManager.UnsubscribeFromPlaybackStatus("debridstream")
				if s.playbackSubscriberCtxCancelFunc != nil {
					s.playbackSubscriberCtxCancelFunc()
					s.playbackSubscriberCtxCancelFunc = nil
				}
				// Failed to start the stream, we'll drop the torrents and stop the server
				s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
					Status:      StreamStatusFailed,
					TorrentName: selectedTorrent.Name,
					Message:     fmt.Sprintf("Failed to send the stream to the media player, %v", err),
				})
				return
			}

			// Listen to the playback status
			// Reset the current stream url when playback is stopped
			go func() {
				defer util.HandlePanicInModuleThen("debridstream/PlaybackSubscriber", func() {})
				defer func() {
					if s.playbackSubscriberCtxCancelFunc != nil {
						s.playbackSubscriberCtxCancelFunc()
						s.playbackSubscriberCtxCancelFunc = nil
					}
				}()
				select {
				case <-playbackSubscriberCtx.Done():
					s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
					s.repository.playbackManager.UnsubscribeFromPlaybackStatus("debridstream")
					s.currentStreamUrl = ""
				case event := <-playbackSubscriber.EventCh:
					switch event.(type) {
					case playbackmanager.StreamStartedEvent:
						s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
					case playbackmanager.StreamStoppedEvent:
						go s.repository.playbackManager.UnsubscribeFromPlaybackStatus("debridstream")
						s.currentStreamUrl = ""
					}
				}
			}()

		case PlaybackTypeExternalPlayer:
			// Send the external player link
			s.repository.wsEventManager.SendEventTo(opts.ClientId, events.ExternalPlayerOpenURL, struct {
				Url           string `json:"url"`
				MediaId       int    `json:"mediaId"`
				EpisodeNumber int    `json:"episodeNumber"`
				MediaTitle    string `json:"mediaTitle"`
			}{
				Url:           streamUrl,
				MediaId:       opts.MediaId,
				EpisodeNumber: opts.EpisodeNumber,
				MediaTitle:    media.GetPreferredTitle(),
			})

			// Signal to the client that the torrent has started playing (remove loading status)
			// We can't know for sure
			s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
				Status:      StreamStatusReady,
				TorrentName: selectedTorrent.Name,
				Message:     "External player link sent",
			})
		case PlaybackTypeNativePlayer:
			err := s.repository.directStreamManager.PlayDebridStream(ctx, directstream.PlayDebridStreamOptions{
				StreamUrl:     streamUrl,
				MediaId:       media.ID,
				EpisodeNumber: opts.EpisodeNumber,
				AnidbEpisode:  opts.AniDBEpisode,
				Media:         media,
				Torrent:       selectedTorrent,
				FileId:        fileId,
				UserAgent:     opts.UserAgent,
				ClientId:      opts.ClientId,
				AutoSelect:    false,
			})
			if err != nil {
				s.repository.logger.Error().Err(err).Msg("directstream: Failed to prepare new stream")
				return
			}
		}

		go func() {
			defer util.HandlePanicInModuleThen("debridstream/AddBatchHistory", func() {})

			_ = db_bridge.InsertTorrentstreamHistory(s.repository.db, media.GetID(), selectedTorrent)
		}()
	}(ctx)

	s.repository.wsEventManager.SendEvent(events.DebridStreamState, StreamState{
		Status:      StreamStatusStarted,
		TorrentName: selectedTorrent.Name,
		Message:     "Stream started",
	})
	s.repository.logger.Info().Msg("debridstream: Stream started")

	if opts.PlaybackType == PlaybackTypeNoneAndAwait {
		s.repository.logger.Debug().Msg("debridstream: Waiting for stream to be ready")
		<-readyCh
		s.repository.wsEventManager.SendEvent(events.HideIndefiniteLoader, "debridstream")
	}

	return nil
}

func (s *StreamManager) cancelStream(opts *CancelStreamOptions) {
	if s.downloadCtxCancelFunc != nil {
		s.downloadCtxCancelFunc()
		s.downloadCtxCancelFunc = nil
	}

	s.repository.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "debridstream")

	s.currentStreamUrl = ""

	if opts.RemoveTorrent && s.currentTorrentItemId != "" {
		// Remove the torrent from the debrid service
		provider, err := s.repository.GetProvider()
		if err != nil {
			s.repository.logger.Err(err).Msg("debridstream: Failed to remove torrent")
			return
		}

		// Remove the torrent from the debrid service
		err = provider.DeleteTorrent(s.currentTorrentItemId)
		if err != nil {
			s.repository.logger.Err(err).Msg("debridstream: Failed to remove torrent")
		}
	}
}

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