package torrentstream

import (
	"context"
	"fmt"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/directstream"
	"seanime/internal/events"
	hibiketorrent "seanime/internal/extension/hibike/torrent"
	"seanime/internal/hook"
	"seanime/internal/library/playbackmanager"
	"seanime/internal/util"
	"strconv"
	"time"

	"github.com/anacrolix/torrent"
	"github.com/samber/mo"
)

type PlaybackType string

const (
	PlaybackTypeExternal           PlaybackType = "default" // External player
	PlaybackTypeExternalPlayerLink PlaybackType = "externalPlayerLink"
	PlaybackTypeNativePlayer       PlaybackType = "nativeplayer"
	PlaybackTypeNone               PlaybackType = "none"
	PlaybackTypeNoneAndAwait       PlaybackType = "noneAndAwait"
)

type StartStreamOptions struct {
	MediaId       int
	EpisodeNumber int                         // RELATIVE Episode number to identify the file
	AniDBEpisode  string                      // Animap episode
	AutoSelect    bool                        // Automatically select the best file to stream
	Torrent       *hibiketorrent.AnimeTorrent // Selected torrent (Manual selection)
	FileIndex     *int                        // Index of the file to stream (Manual selection)
	UserAgent     string
	ClientId      string
	PlaybackType  PlaybackType
}

// StartStream is called by the client to start streaming a torrent
func (r *Repository) StartStream(ctx context.Context, opts *StartStreamOptions) (err error) {
	defer util.HandlePanicInModuleWithError("torrentstream/stream/StartStream", &err)
	// DEVNOTE: Do not
	//r.Shutdown()

	r.previousStreamOptions = mo.Some(opts)

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

	r.sendStateEvent(eventLoading)
	r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
	defer func() {
		r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
	}()

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

	//
	// Get the media info
	//
	media, _, err := r.GetMediaInfo(ctx, opts.MediaId)
	if err != nil {
		return err
	}

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

	//
	// Find the best torrent / Select the torrent
	//
	var torrentToStream *playbackTorrent
	if opts.AutoSelect {
		torrentToStream, err = r.findBestTorrent(media, aniDbEpisode, episodeNumber)
		if err != nil {
			r.sendStateEvent(eventLoadingFailed)
			return err
		}
	} else {
		if opts.Torrent == nil {
			return fmt.Errorf("torrentstream: No torrent provided")
		}
		torrentToStream, err = r.findBestTorrentFromManualSelection(opts.Torrent, media, aniDbEpisode, opts.FileIndex)
		if err != nil {
			r.sendStateEvent(eventLoadingFailed)
			return err
		}
	}

	if torrentToStream == nil {
		r.sendStateEvent(eventLoadingFailed)
		return fmt.Errorf("torrentstream: No torrent selected")
	}

	//
	// Set current file & torrent
	//
	r.client.currentFile = mo.Some(torrentToStream.File)
	r.client.currentTorrent = mo.Some(torrentToStream.Torrent)

	r.sendStateEvent(eventLoading, TLSStateSendingStreamToMediaPlayer)

	go func() {
		// Add the torrent to the history if it is a batch & manually selected
		if len(r.client.currentTorrent.MustGet().Files()) > 1 && opts.Torrent != nil {
			r.AddBatchHistory(opts.MediaId, opts.Torrent) // ran in goroutine
		}
	}()

	//
	// Start the playback
	//
	go func() {
		switch opts.PlaybackType {
		case PlaybackTypeNone:
			r.logger.Warn().Msg("torrentstream: Playback type is set to 'none'")
			// Signal to the client that the torrent has started playing (remove loading status)
			// There will be no tracking
			r.sendStateEvent(eventTorrentStartedPlaying)
		case PlaybackTypeNoneAndAwait:
			r.logger.Warn().Msg("torrentstream: Playback type is set to 'noneAndAwait'")
			// Signal to the client that the torrent has started playing (remove loading status)
			// There will be no tracking
			for {
				if r.client.readyToStream() {
					break
				}
				time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
			}
			r.sendStateEvent(eventTorrentStartedPlaying)
		//
		// External player
		//
		case PlaybackTypeExternal, PlaybackTypeExternalPlayerLink:
			r.sendStreamToExternalPlayer(opts, media, aniDbEpisode)
		//
		// Direct stream
		//
		case PlaybackTypeNativePlayer:
			readyCh, err := r.directStreamManager.PlayTorrentStream(ctx, directstream.PlayTorrentStreamOptions{
				ClientId:      opts.ClientId,
				EpisodeNumber: opts.EpisodeNumber,
				AnidbEpisode:  opts.AniDBEpisode,
				Media:         media.ToBaseAnime(),
				Torrent:       r.client.currentTorrent.MustGet(),
				File:          r.client.currentFile.MustGet(),
			})
			if err != nil {
				r.logger.Error().Err(err).Msg("torrentstream: Failed to prepare new stream")
				r.sendStateEvent(eventLoadingFailed)
				return
			}

			if opts.PlaybackType == PlaybackTypeNativePlayer {
				r.directStreamManager.PrepareNewStream(opts.ClientId, "Downloading metadata...")
			}

			// Make sure the client is ready and the torrent is partially downloaded
			for {
				if r.client.readyToStream() {
					break
				}
				// If for some reason the torrent is dropped, we kill the goroutine
				if r.client.torrentClient.IsAbsent() || r.client.currentTorrent.IsAbsent() {
					return
				}
				r.logger.Debug().Msg("torrentstream: Waiting for playable threshold to be reached")
				time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
			}
			close(readyCh)
		}
	}()

	r.sendStateEvent(eventTorrentLoaded)
	r.logger.Info().Msg("torrentstream: Stream started")

	return nil
}

// sendStreamToExternalPlayer sends the stream to the desktop player or external player link.
// It blocks until the some pieces have been downloaded before sending the stream for faster playback.
func (r *Repository) sendStreamToExternalPlayer(opts *StartStreamOptions, completeAnime *anilist.CompleteAnime, aniDbEpisode string) {

	baseAnime := completeAnime.ToBaseAnime()

	r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
	defer func() {
		r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
	}()

	// Make sure the client is ready and the torrent is partially downloaded
	for {
		if r.client.readyToStream() {
			break
		}
		// If for some reason the torrent is dropped, we kill the goroutine
		if r.client.torrentClient.IsAbsent() || r.client.currentTorrent.IsAbsent() {
			return
		}
		r.logger.Debug().Msg("torrentstream: Waiting for playable threshold to be reached")
		time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
	}

	event := &TorrentStreamSendStreamToMediaPlayerEvent{
		WindowTitle:  "",
		StreamURL:    r.client.GetStreamingUrl(),
		Media:        baseAnime,
		AniDbEpisode: aniDbEpisode,
		PlaybackType: string(opts.PlaybackType),
	}
	err := hook.GlobalHookManager.OnTorrentStreamSendStreamToMediaPlayer().Trigger(event)
	if err != nil {
		r.logger.Error().Err(err).Msg("torrentstream: Failed to trigger hook")
		return
	}
	windowTitle := event.WindowTitle
	streamURL := event.StreamURL
	baseAnime = event.Media
	aniDbEpisode = event.AniDbEpisode
	playbackType := PlaybackType(event.PlaybackType)

	if event.DefaultPrevented {
		r.logger.Debug().Msg("torrentstream: Stream prevented by hook")
		return
	}

	switch playbackType {
	//
	// Desktop player
	//
	case PlaybackTypeExternal:
		r.logger.Debug().Msgf("torrentstream: Starting the media player %s", streamURL)
		err = r.playbackManager.StartStreamingUsingMediaPlayer(windowTitle, &playbackmanager.StartPlayingOptions{
			Payload:   streamURL,
			UserAgent: opts.UserAgent,
			ClientId:  opts.ClientId,
		}, baseAnime, aniDbEpisode)
		if err != nil {
			// Failed to start the stream, we'll drop the torrents and stop the server
			r.sendStateEvent(eventLoadingFailed)
			_ = r.StopStream()
			r.logger.Error().Err(err).Msg("torrentstream: Failed to start the stream")
			r.wsEventManager.SendEventTo(opts.ClientId, events.ErrorToast, err.Error())
		}

		r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
		defer func() {
			r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
		}()

		r.playbackManager.RegisterMediaPlayerCallback(func(event playbackmanager.PlaybackEvent, cancelFunc func()) {
			switch event.(type) {
			case playbackmanager.StreamStartedEvent:
				r.logger.Debug().Msg("torrentstream: Media player started playing")
				r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
				cancelFunc()
			}
		})

	//
	// External player link
	//
	case PlaybackTypeExternalPlayerLink:
		r.logger.Debug().Msgf("torrentstream: Sending stream to external player %s", streamURL)
		r.wsEventManager.SendEventTo(opts.ClientId, events.ExternalPlayerOpenURL, struct {
			Url           string `json:"url"`
			MediaId       int    `json:"mediaId"`
			EpisodeNumber int    `json:"episodeNumber"`
			MediaTitle    string `json:"mediaTitle"`
		}{
			Url:           r.client.GetExternalPlayerStreamingUrl(),
			MediaId:       opts.MediaId,
			EpisodeNumber: opts.EpisodeNumber,
			MediaTitle:    baseAnime.GetPreferredTitle(),
		})

		// Signal to the client that the torrent has started playing (remove loading status)
		// We can't know for sure
		r.sendStateEvent(eventTorrentStartedPlaying)
	}
}

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

type StartUntrackedStreamOptions struct {
	Magnet       string
	FileIndex    int
	WindowTitle  string
	UserAgent    string
	ClientId     string
	PlaybackType PlaybackType
}

func (r *Repository) StopStream() error {
	defer func() {
		if r := recover(); r != nil {
		}
	}()
	r.logger.Info().Msg("torrentstream: Stopping stream")

	// Stop the client
	// This will stop the stream and close the server
	// This also sends the eventTorrentStopped event
	r.client.mu.Lock()
	//r.client.stopCh = make(chan struct{})
	r.client.repository.logger.Debug().Msg("torrentstream: Handling media player stopped event")
	// This is to prevent the client from downloading the whole torrent when the user stops watching
	// Also, the torrent might be a batch - so we don't want to download the whole thing
	if r.client.currentTorrent.IsPresent() {
		if r.client.currentTorrentStatus.ProgressPercentage < 70 {
			r.client.repository.logger.Debug().Msg("torrentstream: Dropping torrent, completion is less than 70%")
			r.client.dropTorrents()
		}
		r.client.repository.logger.Debug().Msg("torrentstream: Resetting current torrent and status")
	}
	r.client.currentTorrent = mo.None[*torrent.Torrent]()        // Reset the current torrent
	r.client.currentFile = mo.None[*torrent.File]()              // Reset the current file
	r.client.currentTorrentStatus = TorrentStatus{}              // Reset the torrent status
	r.client.repository.sendStateEvent(eventTorrentStopped, nil) // Send torrent stopped event
	r.client.repository.mediaPlayerRepository.Stop()             // Stop the media player gracefully if it's running
	r.client.mu.Unlock()

	go func() {
		r.nativePlayer.Stop()
	}()

	r.logger.Info().Msg("torrentstream: Stream stopped")

	return nil
}

func (r *Repository) DropTorrent() error {
	r.logger.Info().Msg("torrentstream: Dropping last torrent")

	if r.client.torrentClient.IsAbsent() {
		return nil
	}

	for _, t := range r.client.torrentClient.MustGet().Torrents() {
		t.Drop()
	}

	r.mediaPlayerRepository.Stop()

	r.logger.Info().Msg("torrentstream: Dropped last torrent")

	return nil
}

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

func (r *Repository) GetMediaInfo(ctx context.Context, mediaId int) (media *anilist.CompleteAnime, animeMetadata *metadata.AnimeMetadata, err error) {
	// Get the media
	var found bool
	media, found = r.completeAnimeCache.Get(mediaId)
	if !found {
		// Fetch the media
		media, err = r.platform.GetAnimeWithRelations(ctx, mediaId)
		if err != nil {
			return nil, nil, fmt.Errorf("torrentstream: Failed to fetch media: %w", err)
		}
	}

	// Get the media
	animeMetadata, err = r.metadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mediaId)
	if err != nil {
		//return nil, nil, fmt.Errorf("torrentstream: Could not fetch AniDB media: %w", err)
		animeMetadata = &metadata.AnimeMetadata{
			Titles:       make(map[string]string),
			Episodes:     make(map[string]*metadata.EpisodeMetadata),
			EpisodeCount: 0,
			SpecialCount: 0,
			Mappings: &metadata.AnimeMappings{
				AnilistId: media.GetID(),
			},
		}
		animeMetadata.Titles["en"] = media.GetTitleSafe()
		animeMetadata.Titles["x-jat"] = media.GetRomajiTitleSafe()
		err = nil
	}

	return
}
