package torrentstream

import (
	"fmt"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/hook"
	"seanime/internal/library/anime"
	"strconv"
	"sync"

	"github.com/samber/lo"
)

type (
	// StreamCollection is used to "complete" the anime.LibraryCollection if the user chooses
	// to include torrent streams in the library view.
	StreamCollection struct {
		ContinueWatchingList []*anime.Episode             `json:"continueWatchingList"`
		Anime                []*anilist.BaseAnime         `json:"anime"`
		ListData             map[int]*anime.EntryListData `json:"listData"`
	}

	HydrateStreamCollectionOptions struct {
		AnimeCollection   *anilist.AnimeCollection
		LibraryCollection *anime.LibraryCollection
		MetadataProvider  metadata.Provider
	}
)

func (r *Repository) HydrateStreamCollection(opts *HydrateStreamCollectionOptions) {

	reqEvent := new(anime.AnimeLibraryStreamCollectionRequestedEvent)
	reqEvent.AnimeCollection = opts.AnimeCollection
	reqEvent.LibraryCollection = opts.LibraryCollection
	err := hook.GlobalHookManager.OnAnimeLibraryStreamCollectionRequested().Trigger(reqEvent)
	if err != nil {
		return
	}
	opts.AnimeCollection = reqEvent.AnimeCollection
	opts.LibraryCollection = reqEvent.LibraryCollection

	lists := opts.AnimeCollection.MediaListCollection.GetLists()
	// Get the anime that are currently being watched
	var currentlyWatching *anilist.AnimeCollection_MediaListCollection_Lists
	for _, list := range lists {
		if list.Status == nil {
			continue
		}
		if *list.Status == anilist.MediaListStatusCurrent {
			//currentlyWatching = list.CopyT()
			currentlyWatching = &anilist.AnimeCollection_MediaListCollection_Lists{
				Status:       lo.ToPtr(anilist.MediaListStatusCurrent),
				Name:         lo.ToPtr("CURRENT"),
				IsCustomList: lo.ToPtr(false),
				Entries:      list.Entries,
			}
			continue
		}
	}
	for _, list := range lists {
		if list.Status == nil {
			continue
		}
		if *list.Status == anilist.MediaListStatusRepeating {
			if currentlyWatching == nil {
				currentlyWatching = list
			} else {
				for _, entry := range list.Entries {
					currentlyWatching.Entries = append(currentlyWatching.Entries, entry)
				}
			}
			break
		}
	}

	if currentlyWatching == nil {
		return
	}

	ret := &StreamCollection{
		ContinueWatchingList: make([]*anime.Episode, 0),
		Anime:                make([]*anilist.BaseAnime, 0),
		ListData:             make(map[int]*anime.EntryListData),
	}

	visitedMediaIds := make(map[int]struct{})

	animeAdded := make(map[int]*anilist.AnimeListEntry)

	// Go through each entry in the currently watching list
	wg := sync.WaitGroup{}
	mu := sync.Mutex{}
	wg.Add(len(currentlyWatching.Entries))
	for _, entry := range currentlyWatching.Entries {
		go func(entry *anilist.AnimeListEntry) {
			defer wg.Done()

			mu.Lock()
			if _, found := visitedMediaIds[entry.GetMedia().GetID()]; found {
				mu.Unlock()
				return
			}
			// Get the next episode to watch
			// i.e. if the user has watched episode 1, the next episode to watch is 2
			nextEpisodeToWatch := entry.GetProgressSafe() + 1
			if nextEpisodeToWatch > entry.GetMedia().GetCurrentEpisodeCount() {
				mu.Unlock()
				return // Skip this entry if the user has watched all episodes
			}
			mediaId := entry.GetMedia().GetID()
			visitedMediaIds[mediaId] = struct{}{}
			mu.Unlock()
			// Check if the anime's "next episode to watch" is already in the library collection
			// If it is, we don't need to add it to the stream collection
			for _, libraryEp := range opts.LibraryCollection.ContinueWatchingList {
				if libraryEp.BaseAnime.ID == mediaId && libraryEp.GetProgressNumber() == nextEpisodeToWatch {
					return
				}
			}

			if *entry.GetMedia().GetStatus() == anilist.MediaStatusNotYetReleased {
				return
			}

			// Get the media info
			animeMetadata, err := opts.MetadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mediaId)
			if err != nil {
				r.logger.Error().Err(err).Msg("torrentstream: could not fetch AniDB media")
				return
			}

			_, found := animeMetadata.FindEpisode(strconv.Itoa(nextEpisodeToWatch))
			//if !found {
			//	r.logger.Error().Msg("torrentstream: could not find episode in AniDB")
			//	return
			//}

			progressOffset := 0
			anidbEpisode := strconv.Itoa(nextEpisodeToWatch)
			if anime.FindDiscrepancy(entry.GetMedia(), animeMetadata) == anime.DiscrepancyAniListCountsEpisodeZero {
				progressOffset = 1
				if nextEpisodeToWatch == 1 {
					anidbEpisode = "S1"
				}
			}

			// Add the anime & episode
			episode := anime.NewEpisode(&anime.NewEpisodeOptions{
				LocalFile:            nil,
				OptionalAniDBEpisode: anidbEpisode,
				AnimeMetadata:        animeMetadata,
				Media:                entry.GetMedia(),
				ProgressOffset:       progressOffset,
				IsDownloaded:         false,
				MetadataProvider:     r.metadataProvider,
			})
			if !found {
				episode.EpisodeTitle = entry.GetMedia().GetPreferredTitle()
				episode.DisplayTitle = fmt.Sprintf("Episode %d", nextEpisodeToWatch)
				episode.ProgressNumber = nextEpisodeToWatch
				episode.EpisodeNumber = nextEpisodeToWatch
				episode.EpisodeMetadata = &anime.EpisodeMetadata{
					Image: entry.GetMedia().GetBannerImageSafe(),
				}
			}

			if episode == nil {
				r.logger.Error().Msg("torrentstream: could not get anime entry episode")
				return
			}

			mu.Lock()
			ret.ContinueWatchingList = append(ret.ContinueWatchingList, episode)
			animeAdded[mediaId] = entry
			mu.Unlock()
		}(entry)
	}
	wg.Wait()

	libraryAnimeMap := make(map[int]struct{})

	// Remove anime that are already in the library collection
	for _, list := range opts.LibraryCollection.Lists {
		if list.Status == anilist.MediaListStatusCurrent {
			for _, entry := range list.Entries {
				libraryAnimeMap[entry.MediaId] = struct{}{}
				if _, found := animeAdded[entry.MediaId]; found {
					delete(animeAdded, entry.MediaId)
				}
			}
		}
	}

	for _, entry := range currentlyWatching.Entries {
		if _, found := libraryAnimeMap[entry.GetMedia().GetID()]; found {
			continue
		}
		if *entry.GetMedia().GetStatus() == anilist.MediaStatusNotYetReleased {
			continue
		}
		animeAdded[entry.GetMedia().GetID()] = entry
	}

	for _, a := range animeAdded {
		ret.Anime = append(ret.Anime, a.GetMedia())
		ret.ListData[a.GetMedia().GetID()] = &anime.EntryListData{
			Progress:    a.GetProgressSafe(),
			Score:       a.GetScoreSafe(),
			Status:      a.GetStatus(),
			Repeat:      a.GetRepeatSafe(),
			StartedAt:   anilist.FuzzyDateToString(a.StartedAt),
			CompletedAt: anilist.FuzzyDateToString(a.CompletedAt),
		}
	}

	if len(ret.ContinueWatchingList) == 0 && len(ret.Anime) == 0 {
		return
	}

	sc := &anime.StreamCollection{
		ContinueWatchingList: ret.ContinueWatchingList,
		Anime:                ret.Anime,
		ListData:             ret.ListData,
	}

	event := new(anime.AnimeLibraryStreamCollectionEvent)
	event.StreamCollection = sc
	err = hook.GlobalHookManager.OnAnimeLibraryStreamCollection().Trigger(event)
	if err != nil {
		return
	}

	opts.LibraryCollection.Stream = event.StreamCollection
}
