package anilist

import (
	"context"
	"github.com/samber/lo"
	"seanime/internal/util"
	"seanime/internal/util/limiter"
	"seanime/internal/util/result"
	"sync"
)

type (
	CompleteAnimeRelationTree struct {
		*result.Map[int, *CompleteAnime]
	}

	FetchMediaTreeRelation = string
)

const (
	FetchMediaTreeSequels  FetchMediaTreeRelation = "sequels"
	FetchMediaTreePrequels FetchMediaTreeRelation = "prequels"
	FetchMediaTreeAll      FetchMediaTreeRelation = "all"
)

// NewCompleteAnimeRelationTree returns a new result.Map[int, *CompleteAnime].
// It is used to store the results of FetchMediaTree or FetchMediaTree calls.
func NewCompleteAnimeRelationTree() *CompleteAnimeRelationTree {
	return &CompleteAnimeRelationTree{result.NewResultMap[int, *CompleteAnime]()}
}

func (m *BaseAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) {
	if m == nil {
		return nil
	}

	defer util.HandlePanicInModuleWithError("anilist/BaseAnime.FetchMediaTree", &err)

	rl.Wait()
	res, err := anilistClient.CompleteAnimeByID(context.Background(), &m.ID)
	if err != nil {
		return err
	}
	return res.GetMedia().FetchMediaTree(rel, anilistClient, rl, tree, cache)
}

// FetchMediaTree populates the CompleteAnimeRelationTree with the given media's sequels and prequels.
// It also takes a CompleteAnimeCache to store the fetched media in and avoid duplicate fetches.
// It also takes a limiter.Limiter to limit the number of requests made to the AniList API.
func (m *CompleteAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) {
	if m == nil {
		return nil
	}

	defer util.HandlePanicInModuleWithError("anilist/CompleteAnime.FetchMediaTree", &err)

	if tree.Has(m.ID) {
		cache.Set(m.ID, m)
		return nil
	}
	cache.Set(m.ID, m)
	tree.Set(m.ID, m)

	if m.Relations == nil {
		return nil
	}

	// Get all edges
	edges := m.GetRelations().GetEdges()
	// Filter edges
	edges = lo.Filter(edges, func(_edge *CompleteAnime_Relations_Edges, _ int) bool {
		return (*_edge.RelationType == MediaRelationSequel || *_edge.RelationType == MediaRelationPrequel) &&
			*_edge.GetNode().Status != MediaStatusNotYetReleased &&
			_edge.IsBroadRelationFormat() && !tree.Has(_edge.GetNode().ID)
	})

	if len(edges) == 0 {
		return nil
	}

	doneCh := make(chan struct{})
	processEdges(edges, rel, anilistClient, rl, tree, cache, doneCh)

	for {
		select {
		case <-doneCh:
			return nil
		default:
		}
	}
}

// processEdges fetches the next node(s) for each edge in parallel.
func processEdges(edges []*CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache, doneCh chan struct{}) {
	var wg sync.WaitGroup
	wg.Add(len(edges))

	for i, item := range edges {
		go func(edge *CompleteAnime_Relations_Edges, _ int) {
			defer wg.Done()
			if edge == nil {
				return
			}
			processEdge(edge, rel, anilistClient, rl, tree, cache)
		}(item, i)
	}

	wg.Wait()

	go func() {
		close(doneCh)
	}()
}

func processEdge(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) {
	defer util.HandlePanicInModuleThen("anilist/processEdge", func() {})
	cacheV, ok := cache.Get(edge.GetNode().ID)
	edgeCompleteAnime := cacheV
	if !ok {
		rl.Wait()
		// Fetch the next node
		res, err := anilistClient.CompleteAnimeByID(context.Background(), &edge.GetNode().ID)
		if err == nil {
			edgeCompleteAnime = res.GetMedia()
			cache.Set(edgeCompleteAnime.ID, edgeCompleteAnime)
		}
	}
	if edgeCompleteAnime == nil {
		return
	}
	// Get the relation type to fetch for the next node
	edgeRel := getEdgeRelation(edge, rel)
	// Fetch the next node(s)
	err := edgeCompleteAnime.FetchMediaTree(edgeRel, anilistClient, rl, tree, cache)
	if err != nil {
		return
	}
}

// getEdgeRelation returns the relation to fetch for the next node based on the current edge and the relation to fetch.
// If the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreePrequels for prequels and FetchMediaTreeSequels for sequels.
//
// For example, if the current node is a sequel and the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreeSequels so that
// only sequels are fetched for the next node.
func getEdgeRelation(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation) FetchMediaTreeRelation {
	if rel == FetchMediaTreeAll {
		if *edge.RelationType == MediaRelationPrequel {
			return FetchMediaTreePrequels
		}
		if *edge.RelationType == MediaRelationSequel {
			return FetchMediaTreeSequels
		}
	}
	return rel
}
