package local

import (
	"fmt"
	"seanime/internal/api/anilist"
	hibikemanga "seanime/internal/extension/hibike/manga"
	"seanime/internal/library/anime"
	"seanime/internal/manga"
	"slices"
	"strings"

	"github.com/rs/zerolog"
	"github.com/samber/lo"
	"github.com/samber/mo"
)

// DEVNOTE: Here we compare the media data from the current up-to-date collections with the local data.
// Outdated media are added to the Syncer to be updated.
// If the media doesn't have a snapshot -> a new snapshot is created.
// If the reference key is different -> the metadata is re-fetched and the snapshot is updated.
// If the list data is different -> the list data is updated.

const (
	DiffTypeMissing  DiffType = iota // We need to add a new snapshot
	DiffTypeMetadata                 // We need to re-fetch the snapshot metadata (episode metadata / chapter containers), list data will be updated as well
	DiffTypeListData                 // We need to update the list data
)

type (
	Diff struct {
		Logger *zerolog.Logger
	}

	DiffType int
)

//----------------------------------------------------------------------------------------------------------------------------------------------------

type GetAnimeDiffOptions struct {
	Collection      *anilist.AnimeCollection
	LocalCollection mo.Option[*anilist.AnimeCollection]
	LocalFiles      []*anime.LocalFile
	TrackedAnime    map[int]*TrackedMedia
	Snapshots       map[int]*AnimeSnapshot
}

type AnimeDiffResult struct {
	AnimeEntry    *anilist.AnimeListEntry
	AnimeSnapshot *AnimeSnapshot
	DiffType      DiffType
}

// GetAnimeDiffs returns the anime that have changed.
// The anime is considered changed if:
// - It doesn't have a snapshot
// - The reference key is different (e.g. the number of local files has changed), meaning we need to update the snapshot.
func (d *Diff) GetAnimeDiffs(opts GetAnimeDiffOptions) map[int]*AnimeDiffResult {

	collection := opts.Collection
	localCollection := opts.LocalCollection
	trackedAnimeMap := opts.TrackedAnime
	snapshotMap := opts.Snapshots

	changedMap := make(map[int]*AnimeDiffResult)

	if len(collection.MediaListCollection.Lists) == 0 || len(trackedAnimeMap) == 0 {
		return changedMap
	}

	for _, _list := range collection.MediaListCollection.Lists {
		if _list.GetStatus() == nil || _list.GetEntries() == nil {
			continue
		}
		for _, _entry := range _list.GetEntries() {
			// Check if the anime is tracked
			_, isTracked := trackedAnimeMap[_entry.GetMedia().GetID()]
			if !isTracked {
				continue
			}

			if localCollection.IsAbsent() {
				d.Logger.Trace().Msgf("local manager: Diff > Anime %d, local collection is missing", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
					AnimeEntry: _entry,
					DiffType:   DiffTypeMissing,
				}
				continue // Go to the next anime
			}

			// Check if the anime has a snapshot
			snapshot, hasSnapshot := snapshotMap[_entry.GetMedia().GetID()]
			if !hasSnapshot {
				d.Logger.Trace().Msgf("local manager: Diff > Anime %d is missing a snapshot", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
					AnimeEntry: _entry,
					DiffType:   DiffTypeMissing,
				}
				continue // Go to the next anime
			}

			_lfs := lo.Filter(opts.LocalFiles, func(lf *anime.LocalFile, _ int) bool {
				return lf.MediaId == _entry.GetMedia().GetID()
			})

			// Check if the anime has changed
			_referenceKey := GetAnimeReferenceKey(_entry.Media, _lfs)

			// Check if the reference key is different
			if snapshotMap[_entry.GetMedia().GetID()].ReferenceKey != _referenceKey {
				d.Logger.Trace().Str("localReferenceKey", snapshotMap[_entry.GetMedia().GetID()].ReferenceKey).Str("currentReferenceKey", _referenceKey).Msgf("local manager: Diff > Anime %d has an outdated snapshot", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
					AnimeEntry:    _entry,
					AnimeSnapshot: snapshot,
					DiffType:      DiffTypeMetadata,
				}
				continue // Go to the next anime
			}

			localEntry, found := localCollection.MustGet().GetListEntryFromAnimeId(_entry.GetMedia().GetID())
			if !found {
				d.Logger.Trace().Msgf("local manager: Diff > Anime %d is missing from the local collection", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
					AnimeEntry:    _entry,
					AnimeSnapshot: snapshot,
					DiffType:      DiffTypeMissing,
				}
				continue // Go to the next anime
			}

			// Check if the list data has changed
			_listDataKey := GetAnimeListDataKey(_entry)
			localListDataKey := GetAnimeListDataKey(localEntry)

			if _listDataKey != localListDataKey {
				d.Logger.Trace().Str("localListDataKey", localListDataKey).Str("currentListDataKey", _listDataKey).Msgf("local manager: Diff > Anime %d has changed list data", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
					AnimeEntry:    _entry,
					AnimeSnapshot: snapshot,
					DiffType:      DiffTypeListData,
				}
				continue // Go to the next anime
			}

		}
	}

	return changedMap
}

//----------------------------------------------------------------------------------------------------------------------------------------------------

type GetMangaDiffOptions struct {
	Collection                  *anilist.MangaCollection
	LocalCollection             mo.Option[*anilist.MangaCollection]
	DownloadedChapterContainers []*manga.ChapterContainer
	TrackedManga                map[int]*TrackedMedia
	Snapshots                   map[int]*MangaSnapshot
}

type MangaDiffResult struct {
	MangaEntry    *anilist.MangaListEntry
	MangaSnapshot *MangaSnapshot
	DiffType      DiffType
}

// GetMangaDiffs returns the manga that have changed.
func (d *Diff) GetMangaDiffs(opts GetMangaDiffOptions) map[int]*MangaDiffResult {

	collection := opts.Collection
	localCollection := opts.LocalCollection
	trackedMangaMap := opts.TrackedManga
	snapshotMap := opts.Snapshots

	changedMap := make(map[int]*MangaDiffResult)

	if len(collection.MediaListCollection.Lists) == 0 || len(trackedMangaMap) == 0 {
		return changedMap
	}

	for _, _list := range collection.MediaListCollection.Lists {
		if _list.GetStatus() == nil || _list.GetEntries() == nil {
			continue
		}
		for _, _entry := range _list.GetEntries() {
			// Check if the manga is tracked
			_, isTracked := trackedMangaMap[_entry.GetMedia().GetID()]
			if !isTracked {
				continue
			}

			if localCollection.IsAbsent() {
				d.Logger.Trace().Msgf("local manager: Diff > Manga %d, local collection is missing", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
					MangaEntry: _entry,
					DiffType:   DiffTypeMissing,
				}
				continue // Go to the next manga
			}

			// Check if the manga has a snapshot
			snapshot, hasSnapshot := snapshotMap[_entry.GetMedia().GetID()]
			if !hasSnapshot {
				d.Logger.Trace().Msgf("local manager: Diff > Manga %d is missing a snapshot", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
					MangaEntry: _entry,
					DiffType:   DiffTypeMissing,
				}
				continue // Go to the next manga
			}

			// Check if the manga has changed
			_referenceKey := GetMangaReferenceKey(_entry.Media, opts.DownloadedChapterContainers)

			// Check if the reference key is different
			if snapshotMap[_entry.GetMedia().GetID()].ReferenceKey != _referenceKey {
				d.Logger.Trace().Str("localReferenceKey", snapshotMap[_entry.GetMedia().GetID()].ReferenceKey).Str("currentReferenceKey", _referenceKey).Msgf("local manager: Diff > Manga %d has an outdated snapshot", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
					MangaEntry:    _entry,
					MangaSnapshot: snapshot,
					DiffType:      DiffTypeMetadata,
				}
				continue // Go to the next manga
			}

			localEntry, found := localCollection.MustGet().GetListEntryFromMangaId(_entry.GetMedia().GetID())
			if !found {
				d.Logger.Trace().Msgf("local manager: Diff > Manga %d is missing from the local collection", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
					MangaEntry:    _entry,
					MangaSnapshot: snapshot,
					DiffType:      DiffTypeMissing,
				}
				continue // Go to the next manga
			}

			// Check if the list data has changed
			_listDataKey := GetMangaListDataKey(_entry)
			localListDataKey := GetMangaListDataKey(localEntry)

			if _listDataKey != localListDataKey {
				d.Logger.Trace().Str("localListDataKey", localListDataKey).Str("currentListDataKey", _listDataKey).Msgf("local manager: Diff > Manga %d has changed list data", _entry.GetMedia().GetID())
				changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
					MangaEntry:    _entry,
					MangaSnapshot: snapshot,
					DiffType:      DiffTypeListData,
				}
				continue // Go to the next manga
			}

		}
	}

	return changedMap
}

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

func GetAnimeReferenceKey(bAnime *anilist.BaseAnime, lfs []*anime.LocalFile) string {
	// Reference key is used to compare the snapshot with the current data.
	// If the reference key is different, the snapshot is outdated.
	animeLfs := lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
		return lf.MediaId == bAnime.ID
	})

	// Extract the paths and sort them to maintain a consistent order.
	paths := lo.Map(animeLfs, func(lf *anime.LocalFile, _ int) string {
		return lf.Path
	})
	slices.Sort(paths)

	return fmt.Sprintf("%d-%s", bAnime.ID, strings.Join(paths, ","))
}

func GetMangaReferenceKey(bManga *anilist.BaseManga, dcc []*manga.ChapterContainer) string {
	// Reference key is used to compare the snapshot with the current data.
	// If the reference key is different, the snapshot is outdated.
	mangaDcc := lo.Filter(dcc, func(dc *manga.ChapterContainer, _ int) bool {
		return dc.MediaId == bManga.ID
	})

	slices.SortFunc(mangaDcc, func(i, j *manga.ChapterContainer) int {
		return strings.Compare(i.Provider, j.Provider)
	})
	var k string
	for _, dc := range mangaDcc {
		l := dc.Provider + "-"
		slices.SortFunc(dc.Chapters, func(i, j *hibikemanga.ChapterDetails) int {
			return strings.Compare(i.ID, j.ID)
		})
		for _, c := range dc.Chapters {
			l += c.ID + "-"
		}
		k += l
	}

	return fmt.Sprintf("%d-%s", bManga.ID, k)
}

func GetAnimeListDataKey(entry *anilist.AnimeListEntry) string {
	return fmt.Sprintf("%s-%d-%f-%d-%v-%v-%v-%v-%v-%v",
		MediaListStatusPointerValue(entry.GetStatus()),
		IntPointerValue(entry.GetProgress()),
		Float64PointerValue(entry.GetScore()),
		IntPointerValue(entry.GetRepeat()),
		IntPointerValue(entry.GetStartedAt().GetYear()),
		IntPointerValue(entry.GetStartedAt().GetMonth()),
		IntPointerValue(entry.GetStartedAt().GetDay()),
		IntPointerValue(entry.GetCompletedAt().GetYear()),
		IntPointerValue(entry.GetCompletedAt().GetMonth()),
		IntPointerValue(entry.GetCompletedAt().GetDay()),
	)
}

func GetMangaListDataKey(entry *anilist.MangaListEntry) string {
	return fmt.Sprintf("%s-%d-%f-%d-%v-%v-%v-%v-%v-%v",
		MediaListStatusPointerValue(entry.GetStatus()),
		IntPointerValue(entry.GetProgress()),
		Float64PointerValue(entry.GetScore()),
		IntPointerValue(entry.GetRepeat()),
		IntPointerValue(entry.GetStartedAt().GetYear()),
		IntPointerValue(entry.GetStartedAt().GetMonth()),
		IntPointerValue(entry.GetStartedAt().GetDay()),
		IntPointerValue(entry.GetCompletedAt().GetYear()),
		IntPointerValue(entry.GetCompletedAt().GetMonth()),
		IntPointerValue(entry.GetCompletedAt().GetDay()),
	)
}
