package local

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"seanime/internal/api/anilist"
	"seanime/internal/api/metadata"
	"seanime/internal/database/db"
	"seanime/internal/database/db_bridge"
	"seanime/internal/events"
	"seanime/internal/library/anime"
	"seanime/internal/manga"
	"seanime/internal/platforms/platform"

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

var (
	ErrAlreadyTracked = fmt.Errorf("local manager: Media already tracked")
)

const (
	AnimeType = "anime"
	MangaType = "manga"
)

type Manager interface {
	// SetAnimeCollection updates the online anime collection in the manager.
	SetAnimeCollection(ac *anilist.AnimeCollection)
	// SetMangaCollection updates the online manga collection in the manager.
	SetMangaCollection(mc *anilist.MangaCollection)
	// GetLocalAnimeCollection returns the local anime collection stored in the local database.
	GetLocalAnimeCollection() mo.Option[*anilist.AnimeCollection]
	// GetLocalMangaCollection returns the local manga collection stored in the local database.
	GetLocalMangaCollection() mo.Option[*anilist.MangaCollection]
	// UpdateLocalAnimeCollection updates the local anime collection using the online data.
	UpdateLocalAnimeCollection(ac *anilist.AnimeCollection)
	// UpdateLocalMangaCollection updates the local manga collection using the online data.
	UpdateLocalMangaCollection(mc *anilist.MangaCollection)
	// GetOfflineMetadataProvider returns the offline metadata provider.
	GetOfflineMetadataProvider() metadata.Provider
	// GetSyncer returns the syncer (used to synchronize the anime and manga snapshots in the local database).
	GetSyncer() *Syncer
	AutoTrackCurrentMedia() (bool, error)
	// TrackAnime adds an anime to track for offline use.
	// It checks that the anime is currently in the user's anime collection.
	TrackAnime(mId int) error
	// UntrackAnime removes the anime from tracking.
	UntrackAnime(mId int) error
	// TrackManga adds a manga to track for offline use.
	// It checks that the manga is currently in the user's manga collection.
	TrackManga(mId int) error
	// UntrackManga removes a manga from tracking.
	UntrackManga(mId int) error
	// IsMediaTracked checks if the media is tracked in the local database.
	IsMediaTracked(aId int, kind string) bool
	// GetTrackedMediaItems returns all tracked media items.
	GetTrackedMediaItems() []*TrackedMediaItem
	// SynchronizeLocal syncs all currently tracked media.
	// Compares the local database with the user's anime and manga collections and updates the local database accordingly.
	SynchronizeLocal() error
	// SynchronizeAnilist syncs the user's AniList data with data stored in the local database.
	SynchronizeAnilist() error
	// SetRefreshAnilistCollectionsFunc sets the function to call to refresh the online AniList collections.
	SetRefreshAnilistCollectionsFunc(func())
	// HasLocalChanges checks if there are any local changes that need to be uploaded or ignored.
	HasLocalChanges() bool
	// SetHasLocalChanges sets the flag to determine if there are local changes that need to be uploaded or ignored.
	SetHasLocalChanges(bool)
	// GetLocalStorageSize returns the size of the local storage in bytes.
	GetLocalStorageSize() int64
	// GetSimulatedAnimeCollection returns the simulated anime collection for unauthenticated users.
	GetSimulatedAnimeCollection() mo.Option[*anilist.AnimeCollection]
	// GetSimulatedMangaCollection returns the simulated manga collection for unauthenticated users.
	GetSimulatedMangaCollection() mo.Option[*anilist.MangaCollection]
	// SaveSimulatedAnimeCollection sets the simulated anime collection for unauthenticated users.
	SaveSimulatedAnimeCollection(ac *anilist.AnimeCollection)
	// SaveSimulatedMangaCollection sets the simulated manga collection for unauthenticated users.
	SaveSimulatedMangaCollection(mc *anilist.MangaCollection)
	// SynchronizeSimulatedCollectionToAnilist synchronizes the simulated anime and manga collections to the user's AniList account.
	SynchronizeSimulatedCollectionToAnilist() error
	// SynchronizeAnilistToSimulatedCollection synchronizes the user's AniList account to the simulated anime and manga collections.
	SynchronizeAnilistToSimulatedCollection() error

	SetOffline(bool)
}

type (
	ManagerImpl struct {
		db             *db.Database
		localDb        *Database
		localDir       string
		localAssetsDir string
		isOffline      bool

		logger                  *zerolog.Logger
		metadataProvider        metadata.Provider
		mangaRepository         *manga.Repository
		wsEventManager          events.WSEventManagerInterface
		offlineMetadataProvider metadata.Provider
		anilistPlatform         platform.Platform

		syncer *Syncer

		// Anime collection stored in the local database, without modifications
		localAnimeCollection mo.Option[*anilist.AnimeCollection]
		// Manga collection stored in the local database, without modifications
		localMangaCollection mo.Option[*anilist.MangaCollection]

		// Anime collection from the user's AniList account, changed by ManagerImpl.SetAnimeCollection
		animeCollection mo.Option[*anilist.AnimeCollection]
		// Manga collection from the user's AniList account, changed by ManagerImpl.SetMangaCollection
		mangaCollection mo.Option[*anilist.MangaCollection]

		// Downloaded chapter containers, set by ManagerImpl.Synchronize, accessed by the synchronization Syncer
		downloadedChapterContainers []*manga.ChapterContainer
		// Local files, set by ManagerImpl.Synchronize, accessed by the synchronization Syncer
		localFiles []*anime.LocalFile

		RefreshAnilistCollectionsFunc func()
	}
	TrackedMediaItem struct {
		MediaId    int                     `json:"mediaId"`
		Type       string                  `json:"type"`
		AnimeEntry *anilist.AnimeListEntry `json:"animeEntry,omitempty"`
		MangaEntry *anilist.MangaListEntry `json:"mangaEntry,omitempty"`
	}

	NewManagerOptions struct {
		LocalDir         string
		AssetDir         string
		Logger           *zerolog.Logger
		MetadataProvider metadata.Provider
		MangaRepository  *manga.Repository
		Database         *db.Database
		WSEventManager   events.WSEventManagerInterface
		AnilistPlatform  platform.Platform
		IsOffline        bool
	}
)

func NewManager(opts *NewManagerOptions) (Manager, error) {

	_ = os.MkdirAll(opts.LocalDir, os.ModePerm)

	localDb, err := newLocalSyncDatabase(opts.LocalDir, "local", opts.Logger)
	if err != nil {
		return nil, err
	}

	ret := &ManagerImpl{
		db:                            opts.Database,
		localDb:                       localDb,
		localDir:                      opts.LocalDir,
		localAssetsDir:                opts.AssetDir,
		logger:                        opts.Logger,
		animeCollection:               mo.None[*anilist.AnimeCollection](),
		mangaCollection:               mo.None[*anilist.MangaCollection](),
		localAnimeCollection:          mo.None[*anilist.AnimeCollection](),
		localMangaCollection:          mo.None[*anilist.MangaCollection](),
		metadataProvider:              opts.MetadataProvider,
		mangaRepository:               opts.MangaRepository,
		downloadedChapterContainers:   make([]*manga.ChapterContainer, 0),
		localFiles:                    make([]*anime.LocalFile, 0),
		wsEventManager:                opts.WSEventManager,
		isOffline:                     opts.IsOffline,
		anilistPlatform:               opts.AnilistPlatform,
		RefreshAnilistCollectionsFunc: func() {},
	}

	ret.syncer = NewQueue(ret)
	ret.offlineMetadataProvider = NewOfflineMetadataProvider(ret)

	// Load the local collections
	ret.loadLocalAnimeCollection()
	ret.loadLocalMangaCollection()

	_ = ret.localDb.GetSettings()

	return ret, nil
}

func (m *ManagerImpl) SetRefreshAnilistCollectionsFunc(f func()) {
	m.RefreshAnilistCollectionsFunc = f
}

func (m *ManagerImpl) GetSyncer() *Syncer {
	return m.syncer
}

func (m *ManagerImpl) GetOfflineMetadataProvider() metadata.Provider {
	return m.offlineMetadataProvider
}

func (m *ManagerImpl) SetOffline(enabled bool) {
	m.isOffline = enabled
}

func (m *ManagerImpl) HasLocalChanges() bool {
	s := m.localDb.GetSettings()
	return s.Updated
}

func (m *ManagerImpl) SetHasLocalChanges(b bool) {
	s := m.localDb.GetSettings()
	if s.Updated == b {
		return
	}
	s.Updated = b
	_ = m.localDb.SaveSettings(s)
}

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

func (m *ManagerImpl) loadLocalAnimeCollection() {
	collection, ok := m.localDb.GetLocalAnimeCollection()
	if !ok {
		m.localAnimeCollection = mo.None[*anilist.AnimeCollection]()
	}
	m.localAnimeCollection = mo.Some(collection)
}

func (m *ManagerImpl) loadLocalMangaCollection() {
	collection, ok := m.localDb.GetLocalMangaCollection()
	if !ok {
		m.localMangaCollection = mo.None[*anilist.MangaCollection]()
	}
	m.localMangaCollection = mo.Some(collection)
}

func (m *ManagerImpl) SetAnimeCollection(ac *anilist.AnimeCollection) {
	if ac == nil {
		m.animeCollection = mo.None[*anilist.AnimeCollection]()
	} else {
		m.animeCollection = mo.Some[*anilist.AnimeCollection](ac)
	}
}

func (m *ManagerImpl) SetMangaCollection(mc *anilist.MangaCollection) {
	if mc == nil {
		m.mangaCollection = mo.None[*anilist.MangaCollection]()
	} else {
		m.mangaCollection = mo.Some[*anilist.MangaCollection](mc)
	}
}

func (m *ManagerImpl) GetLocalAnimeCollection() mo.Option[*anilist.AnimeCollection] {
	return m.localAnimeCollection
}

func (m *ManagerImpl) GetLocalMangaCollection() mo.Option[*anilist.MangaCollection] {
	return m.localMangaCollection
}

func (m *ManagerImpl) UpdateLocalAnimeCollection(ac *anilist.AnimeCollection) {
	_ = m.localDb.SaveAnimeCollection(ac)
	m.loadLocalAnimeCollection()
}

func (m *ManagerImpl) UpdateLocalMangaCollection(mc *anilist.MangaCollection) {
	_ = m.localDb.SaveMangaCollection(mc)
	m.loadLocalMangaCollection()
}

func (m *ManagerImpl) AutoTrackCurrentMedia() (added bool, err error) {

	m.logger.Trace().Msgf("local manager: Saving all current media for offline use")

	trackedMedia := m.GetTrackedMediaItems()
	trackedMediaMap := make(map[int]struct{})
	for _, item := range trackedMedia {
		trackedMediaMap[item.MediaId] = struct{}{}
	}

	groupedLocalFiles := lo.GroupBy(m.localFiles, func(f *anime.LocalFile) int {
		return f.MediaId
	})

	animeCollection, ok := m.animeCollection.Get()
	if ok {
		for _, list := range animeCollection.MediaListCollection.Lists {
			for _, entry := range list.GetEntries() {
				if entry.Status == nil || *entry.GetStatus() != anilist.MediaListStatusCurrent {
					continue
				}
				if _, found := trackedMediaMap[entry.Media.GetID()]; found {
					continue
				}
				m.logger.Trace().Msgf("local manager: Adding anime %d to local database", entry.Media.GetID())

				lfs, ok := groupedLocalFiles[entry.Media.GetID()]
				if !ok || len(lfs) == 0 {
					continue
				}

				err := m.TrackAnime(entry.Media.GetID())
				if err != nil {
					continue
				}
				added = true
			}
		}
	}

	groupedDownloadedChapterContainers := lo.GroupBy(m.downloadedChapterContainers, func(c *manga.ChapterContainer) int {
		return c.MediaId
	})

	mangaCollection, ok := m.mangaCollection.Get()
	if ok {
		for _, list := range mangaCollection.MediaListCollection.Lists {
			for _, entry := range list.GetEntries() {
				if entry.Status == nil || *entry.GetStatus() != anilist.MediaListStatusCurrent {
					continue
				}
				if _, found := trackedMediaMap[entry.Media.GetID()]; found {
					continue
				}
				m.logger.Trace().Msgf("local manager: Adding manga %d to local database", entry.Media.GetID())

				ccs, ok := groupedDownloadedChapterContainers[entry.Media.GetID()]
				if !ok || len(ccs) == 0 {
					continue
				}

				err := m.TrackManga(entry.Media.GetID())
				if err != nil {
					continue
				}
				added = true
			}
		}
	}

	return
}

// TrackAnime adds an anime to track.
// It checks that the anime is currently in the user's anime collection.
// The anime should have local files, or else ManagerImpl.Synchronize will remove it from tracking.
func (m *ManagerImpl) TrackAnime(mId int) error {

	m.logger.Trace().Msgf("local manager: Adding anime %d to local database", mId)

	s := &TrackedMedia{
		MediaId: mId,
		Type:    AnimeType,
	}

	// Check if the anime is in the user's anime collection
	if m.animeCollection.IsAbsent() {
		m.logger.Error().Msg("local manager: Anime collection not set")
		return fmt.Errorf("anime collection not set")
	}

	if _, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(mId); !found {
		m.logger.Error().Msgf("local manager: Anime %d not found in user's anime collection", mId)
		return fmt.Errorf("anime is not in AniList collection")
	}

	if _, found := m.localDb.GetTrackedMedia(mId, AnimeType); found {
		return ErrAlreadyTracked
	}

	err := m.localDb.gormdb.Create(s).Error
	if err != nil {
		m.logger.Error().Msgf("local manager: Failed to add anime %d to local database: %w", mId, err)
		return fmt.Errorf("failed to add anime %d to local database: %w", mId, err)
	}

	return nil
}

func (m *ManagerImpl) UntrackAnime(mId int) error {

	m.logger.Trace().Msgf("local manager: Removing anime %d from local database", mId)

	if _, found := m.localDb.GetTrackedMedia(mId, AnimeType); !found {
		m.logger.Error().Msgf("local manager: Anime %d not in local database", mId)
		return fmt.Errorf("anime is not in local database")
	}

	err := m.removeAnime(mId)
	if err != nil {
		return err
	}

	m.GetSyncer().refreshCollections()

	return nil
}

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

// TrackManga adds a manga to track.
// It checks that the manga is currently in the user's manga collection.
// The manga should have downloaded chapter containers, or else ManagerImpl.Synchronize will remove it from tracking.
func (m *ManagerImpl) TrackManga(mId int) error {

	m.logger.Trace().Msgf("local manager: Adding manga %d to local database", mId)

	s := &TrackedMedia{
		MediaId: mId,
		Type:    MangaType,
	}

	// Check if the manga is in the user's manga collection
	if m.mangaCollection.IsAbsent() {
		m.logger.Error().Msg("local manager: Manga collection not set")
		return fmt.Errorf("manga collection not set")
	}

	if _, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(mId); !found {
		m.logger.Error().Msgf("local manager: Manga %d not found in user's manga collection", mId)
		return fmt.Errorf("manga is not in AniList collection")
	}

	if _, found := m.localDb.GetTrackedMedia(mId, MangaType); found {
		return ErrAlreadyTracked
	}

	err := m.localDb.gormdb.Create(s).Error
	if err != nil {
		m.logger.Error().Msgf("local manager: Failed to add manga %d to local database: %w", mId, err)
		return fmt.Errorf("failed to add manga %d to local database: %w", mId, err)
	}

	return nil
}

func (m *ManagerImpl) UntrackManga(mId int) error {

	m.logger.Trace().Msgf("local manager: Removing manga %d from local database", mId)

	if _, found := m.localDb.GetTrackedMedia(mId, MangaType); !found {
		m.logger.Error().Msgf("local manager: Manga %d not in local database", mId)
		return fmt.Errorf("manga is not in local database")
	}

	err := m.removeManga(mId)
	if err != nil {
		return err
	}

	m.GetSyncer().refreshCollections()

	return nil
}

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

func (m *ManagerImpl) IsMediaTracked(aId int, kind string) bool {
	_, found := m.localDb.GetTrackedMedia(aId, kind)
	return found
}

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

func (m *ManagerImpl) GetTrackedMediaItems() (ret []*TrackedMediaItem) {
	trackedMedia, ok := m.localDb.GetAllTrackedMedia()
	if !ok {
		return
	}

	if m.animeCollection.IsAbsent() || m.mangaCollection.IsAbsent() {
		return
	}

	for _, item := range trackedMedia {
		if item.Type == AnimeType {
			if localAnimeCollection, found := m.localAnimeCollection.Get(); found {
				if e, found := localAnimeCollection.GetListEntryFromAnimeId(item.MediaId); found {
					ret = append(ret, &TrackedMediaItem{
						MediaId:    item.MediaId,
						Type:       item.Type,
						AnimeEntry: e,
					})
					continue
				}
				if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(item.MediaId); found {
					ret = append(ret, &TrackedMediaItem{
						MediaId:    item.MediaId,
						Type:       item.Type,
						AnimeEntry: e,
					})
					continue
				}
			}
		} else if item.Type == MangaType {
			if localMangaCollection, found := m.localMangaCollection.Get(); found {
				if e, found := localMangaCollection.GetListEntryFromMangaId(item.MediaId); found {
					ret = append(ret, &TrackedMediaItem{
						MediaId:    item.MediaId,
						Type:       item.Type,
						MangaEntry: e,
					})
					continue
				}
			}
			if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(item.MediaId); found {
				ret = append(ret, &TrackedMediaItem{
					MediaId:    item.MediaId,
					Type:       item.Type,
					MangaEntry: e,
				})
				continue
			}
		}
	}

	return
}

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

// SynchronizeLocal should be called after updates to the user's anime and manga collections.
//
//   - After adding/removing an anime or manga to track
//   - After the user's anime and manga collections have been updated (e.g. after a user's anime and manga list has been updated)
//
// It will add media list entries from the user's collection to the Syncer only if the media is tracked.
//   - The Syncer will then synchronize the anime & manga with the local database if needed
//
// It will remove any anime & manga from the local database that are not in the user's collection anymore.
// It will then update the ManagerImpl.localAnimeCollection and ManagerImpl.localMangaCollection
func (m *ManagerImpl) SynchronizeLocal() error {

	localStorageSizeCache = 0

	m.loadLocalAnimeCollection()
	m.loadLocalMangaCollection()

	settings := m.localDb.GetSettings()
	if settings.Updated {
		return fmt.Errorf("cannot sync, upload or ignore local changes before syncing")
	}

	lfs, _, err := db_bridge.GetLocalFiles(m.db)
	if err != nil {
		return fmt.Errorf("local manager: Couldn't start syncing, failed to get local files: %w", err)
	}

	// Check if the anime and manga collections are set
	if m.animeCollection.IsAbsent() {
		return fmt.Errorf("local manager: Couldn't start syncing, anime collection not set")
	}

	if m.mangaCollection.IsAbsent() {
		return fmt.Errorf("local manager: Couldn't start syncing, manga collection not set")
	}

	mangaChapterContainers, err := m.mangaRepository.GetDownloadedChapterContainers(m.mangaCollection.MustGet())
	if err != nil {
		return fmt.Errorf("local manager: Couldn't start syncing, failed to get downloaded chapter containers: %w", err)
	}

	return m.synchronize(lfs, mangaChapterContainers)
}

func (m *ManagerImpl) synchronize(lfs []*anime.LocalFile, mangaChapterContainers []*manga.ChapterContainer) error {

	m.logger.Trace().Msg("local manager: Synchronizing local database with user's anime and manga collections")

	m.localFiles = lfs
	m.downloadedChapterContainers = mangaChapterContainers

	// Check if the anime and manga collections are set
	if m.animeCollection.IsAbsent() {
		return fmt.Errorf("local manager: Anime collection not set")
	}

	if m.mangaCollection.IsAbsent() {
		return fmt.Errorf("local manager: Manga collection not set")
	}

	trackedAnimeMap, trackedMangaMap := m.loadTrackedMedia()

	// Remove anime and manga from the local database that are not in the user's anime and manga collections
	for _, item := range trackedAnimeMap {
		// If the anime is not in the user's anime collection, remove it from the local database
		if _, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(item.MediaId); !found {
			err := m.removeAnime(item.MediaId)
			if err != nil {
				return fmt.Errorf("local manager: Failed to remove anime %d from local database: %w", item.MediaId, err)
			}
		}
	}
	for _, item := range trackedMangaMap {
		// If the manga is not in the user's manga collection, remove it from the local database
		if _, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(item.MediaId); !found {
			err := m.removeManga(item.MediaId)
			if err != nil {
				return fmt.Errorf("local manager: Failed to remove manga %d from local database: %w", item.MediaId, err)
			}
		}
	}

	// Get snapshots for all tracked anime and manga
	animeSnapshots, _ := m.localDb.GetAnimeSnapshots()
	mangaSnapshots, _ := m.localDb.GetMangaSnapshots()

	// Create a map of the snapshots
	animeSnapshotMap := make(map[int]*AnimeSnapshot)
	for _, snapshot := range animeSnapshots {
		animeSnapshotMap[snapshot.MediaId] = snapshot
	}

	mangaSnapshotMap := make(map[int]*MangaSnapshot)
	for _, snapshot := range mangaSnapshots {
		mangaSnapshotMap[snapshot.MediaId] = snapshot
	}

	m.syncer.runDiffs(trackedAnimeMap, animeSnapshotMap, trackedMangaMap, mangaSnapshotMap, m.localFiles, m.downloadedChapterContainers)

	return nil
}

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

func (m *ManagerImpl) SynchronizeAnilist() error {
	if m.animeCollection.IsAbsent() {
		return fmt.Errorf("local manager: Anime collection not set")
	}

	if m.mangaCollection.IsAbsent() {
		return fmt.Errorf("local manager: Manga collection not set")
	}

	m.loadLocalAnimeCollection()
	m.loadLocalMangaCollection()

	if localAnimeCollection, ok := m.localAnimeCollection.Get(); ok {
		for _, list := range localAnimeCollection.MediaListCollection.Lists {
			if list.GetStatus() == nil || list.GetEntries() == nil {
				continue
			}
			for _, entry := range list.GetEntries() {
				if entry.GetStatus() == nil {
					continue
				}

				// Get the entry from AniList
				var originalEntry *anilist.AnimeListEntry
				if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(entry.GetMedia().GetID()); found {
					originalEntry = e
				}
				if originalEntry == nil {
					continue
				}

				key1 := GetAnimeListDataKey(entry)
				key2 := GetAnimeListDataKey(originalEntry)

				// If the entry is the same, skip
				if key1 == key2 {
					continue
				}

				var startDate *anilist.FuzzyDateInput
				if entry.GetStartedAt() != nil {
					startDate = &anilist.FuzzyDateInput{
						Year:  entry.GetStartedAt().GetYear(),
						Month: entry.GetStartedAt().GetMonth(),
						Day:   entry.GetStartedAt().GetDay(),
					}
				}

				var endDate *anilist.FuzzyDateInput
				if entry.GetCompletedAt() != nil {
					endDate = &anilist.FuzzyDateInput{
						Year:  entry.GetCompletedAt().GetYear(),
						Month: entry.GetCompletedAt().GetMonth(),
						Day:   entry.GetCompletedAt().GetDay(),
					}
				}

				var score *int
				if entry.GetScore() != nil {
					score = lo.ToPtr(int(*entry.GetScore()))
				}

				_ = m.anilistPlatform.UpdateEntry(
					context.Background(),
					entry.GetMedia().GetID(),
					entry.GetStatus(),
					score,
					entry.GetProgress(),
					startDate,
					endDate,
				)
			}
		}
	}

	if localMangaCollection, ok := m.localMangaCollection.Get(); ok {
		for _, list := range localMangaCollection.MediaListCollection.Lists {
			if list.GetStatus() == nil || list.GetEntries() == nil {
				continue
			}
			for _, entry := range list.GetEntries() {
				if entry.GetStatus() == nil {
					continue
				}

				// Get the entry from AniList
				var originalEntry *anilist.MangaListEntry
				if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(entry.GetMedia().GetID()); found {
					originalEntry = e
				}
				if originalEntry == nil {
					continue
				}

				key1 := GetMangaListDataKey(entry)
				key2 := GetMangaListDataKey(originalEntry)

				// If the entry is the same, skip
				if key1 == key2 {
					continue
				}

				var startDate *anilist.FuzzyDateInput
				if entry.GetStartedAt() != nil {
					startDate = &anilist.FuzzyDateInput{
						Year:  entry.GetStartedAt().GetYear(),
						Month: entry.GetStartedAt().GetMonth(),
						Day:   entry.GetStartedAt().GetDay(),
					}
				}

				var endDate *anilist.FuzzyDateInput
				if entry.GetCompletedAt() != nil {
					endDate = &anilist.FuzzyDateInput{
						Year:  entry.GetCompletedAt().GetYear(),
						Month: entry.GetCompletedAt().GetMonth(),
						Day:   entry.GetCompletedAt().GetDay(),
					}
				}

				var score *int
				if entry.GetScore() != nil {
					score = lo.ToPtr(int(*entry.GetScore()))
				}

				_ = m.anilistPlatform.UpdateEntry(
					context.Background(),
					entry.GetMedia().GetID(),
					entry.GetStatus(),
					score,
					entry.GetProgress(),
					startDate,
					endDate,
				)
			}
		}
	}

	m.RefreshAnilistCollectionsFunc()

	m.wsEventManager.SendEvent(events.RefreshedAnilistAnimeCollection, nil)
	m.wsEventManager.SendEvent(events.RefreshedAnilistMangaCollection, nil)

	return nil
}

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

func (m *ManagerImpl) loadTrackedMedia() (trackedAnimeMap map[int]*TrackedMedia, trackedMangaMap map[int]*TrackedMedia) {
	trackedAnime, _ := m.localDb.GetAllTrackedMediaByType(AnimeType)
	trackedManga, _ := m.localDb.GetAllTrackedMediaByType(MangaType)

	trackedAnimeMap = make(map[int]*TrackedMedia)
	for _, item := range trackedAnime {
		trackedAnimeMap[item.MediaId] = item
	}

	trackedMangaMap = make(map[int]*TrackedMedia)
	for _, m := range trackedManga {
		trackedMangaMap[m.MediaId] = m
	}

	m.GetSyncer().trackedMangaMap = trackedMangaMap
	m.GetSyncer().trackedAnimeMap = trackedAnimeMap

	return trackedAnimeMap, trackedMangaMap
}

func (m *ManagerImpl) removeAnime(aId int) error {
	m.logger.Trace().Msgf("local manager: Removing anime %d from local database", aId)
	// Remove the tracked anime
	err := m.localDb.RemoveTrackedMedia(aId, AnimeType)
	if err != nil {
		return fmt.Errorf("local manager: Failed to remove anime %d from local database: %w", aId, err)
	}
	// Remove the anime snapshot
	_ = m.localDb.RemoveAnimeSnapshot(aId)
	// Remove the images
	_ = m.removeMediaImages(aId)
	return nil
}

func (m *ManagerImpl) removeManga(mId int) error {
	m.logger.Trace().Msgf("local manager: Removing manga %d from local database", mId)
	// Remove the tracked manga
	err := m.localDb.RemoveTrackedMedia(mId, MangaType)
	if err != nil {
		return fmt.Errorf("local manager: Failed to remove manga %d from local database: %w", mId, err)
	}
	// Remove the manga snapshot
	_ = m.localDb.RemoveMangaSnapshot(mId)
	// Remove the images
	_ = m.removeMediaImages(mId)
	return nil
}

// removeMediaImages removes the images for the media with the given ID.
//   - The images are stored in the local assets' directory.
//   - e.g. datadir/local/assets/{mediaId}/*
func (m *ManagerImpl) removeMediaImages(mediaId int) error {
	m.logger.Trace().Msgf("local manager: Removing images for media %d", mediaId)
	path := filepath.Join(m.localAssetsDir, fmt.Sprintf("%d", mediaId))
	_ = os.RemoveAll(path)
	//if err != nil {
	//	return fmt.Errorf("local manager: Failed to remove images for media %d: %w", mediaId, err)
	//}
	return nil
}

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

// Avoids recalculating the size of the cache directory every time it is requested
var localStorageSizeCache int64

func (m *ManagerImpl) GetLocalStorageSize() int64 {

	if localStorageSizeCache != 0 {
		return localStorageSizeCache
	}

	var size int64
	_ = filepath.Walk(m.localDir, func(path string, info os.FileInfo, err error) error {
		if info != nil {
			size += info.Size()
		}
		return nil
	})

	localStorageSizeCache = size

	return size
}

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

func (m *ManagerImpl) GetSimulatedAnimeCollection() mo.Option[*anilist.AnimeCollection] {
	ac, ok := m.localDb.GetSimulatedAnimeCollection()
	if !ok {
		return mo.None[*anilist.AnimeCollection]()
	}
	return mo.Some(ac)
}

func (m *ManagerImpl) GetSimulatedMangaCollection() mo.Option[*anilist.MangaCollection] {
	mc, ok := m.localDb.GetSimulatedMangaCollection()
	if !ok {
		return mo.None[*anilist.MangaCollection]()
	}
	return mo.Some(mc)
}

func (m *ManagerImpl) SaveSimulatedAnimeCollection(ac *anilist.AnimeCollection) {
	//// Remove airing dates from each entry
	//for _, list := range ac.MediaListCollection.Lists {
	//	for _, entry := range list.Entries {
	//		entry.GetMedia().NextAiringEpisode = nil
	//	}
	//}
	_ = m.localDb.SaveSimulatedAnimeCollection(ac)
}

func (m *ManagerImpl) SaveSimulatedMangaCollection(mc *anilist.MangaCollection) {
	_ = m.localDb.SaveSimulatedMangaCollection(mc)
}

func (m *ManagerImpl) SynchronizeAnilistToSimulatedCollection() error {
	if animeCollection, ok := m.animeCollection.Get(); ok {
		m.SaveSimulatedAnimeCollection(animeCollection)
	}

	if mangaCollection, ok := m.mangaCollection.Get(); ok {
		m.SaveSimulatedMangaCollection(mangaCollection)
	}

	return nil
}

func (m *ManagerImpl) SynchronizeSimulatedCollectionToAnilist() error {
	if localAnimeCollection, ok := m.localDb.GetSimulatedAnimeCollection(); ok {
		for _, list := range localAnimeCollection.MediaListCollection.Lists {
			if list.GetStatus() == nil || list.GetEntries() == nil {
				continue
			}
			for _, entry := range list.GetEntries() {
				if entry.GetStatus() == nil {
					continue
				}

				// Get the entry from AniList
				var originalEntry *anilist.AnimeListEntry
				if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(entry.GetMedia().GetID()); found {
					originalEntry = e
				}
				if originalEntry == nil {
					continue
				}

				key1 := GetAnimeListDataKey(entry)
				key2 := GetAnimeListDataKey(originalEntry)

				// If the entry is the same, skip
				if key1 == key2 {
					continue
				}

				var startDate *anilist.FuzzyDateInput
				if entry.GetStartedAt() != nil {
					startDate = &anilist.FuzzyDateInput{
						Year:  entry.GetStartedAt().GetYear(),
						Month: entry.GetStartedAt().GetMonth(),
						Day:   entry.GetStartedAt().GetDay(),
					}
				}

				var endDate *anilist.FuzzyDateInput
				if entry.GetCompletedAt() != nil {
					endDate = &anilist.FuzzyDateInput{
						Year:  entry.GetCompletedAt().GetYear(),
						Month: entry.GetCompletedAt().GetMonth(),
						Day:   entry.GetCompletedAt().GetDay(),
					}
				}

				var score *int
				if entry.GetScore() != nil {
					score = lo.ToPtr(int(*entry.GetScore()))
				} else {
					score = lo.ToPtr(0)
				}

				_ = m.anilistPlatform.UpdateEntry(
					context.Background(),
					entry.GetMedia().GetID(),
					entry.GetStatus(),
					score,
					entry.GetProgress(),
					startDate,
					endDate,
				)
			}
		}
	}

	if localMangaCollection, ok := m.localDb.GetSimulatedMangaCollection(); ok {
		for _, list := range localMangaCollection.MediaListCollection.Lists {
			if list.GetStatus() == nil || list.GetEntries() == nil {
				continue
			}
			for _, entry := range list.GetEntries() {
				if entry.GetStatus() == nil {
					continue
				}

				// Get the entry from AniList
				var originalEntry *anilist.MangaListEntry
				if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(entry.GetMedia().GetID()); found {
					originalEntry = e
				}
				if originalEntry == nil {
					continue
				}

				key1 := GetMangaListDataKey(entry)
				key2 := GetMangaListDataKey(originalEntry)

				// If the entry is the same, skip
				if key1 == key2 {
					continue
				}

				var startDate *anilist.FuzzyDateInput
				if entry.GetStartedAt() != nil {
					startDate = &anilist.FuzzyDateInput{
						Year:  entry.GetStartedAt().GetYear(),
						Month: entry.GetStartedAt().GetMonth(),
						Day:   entry.GetStartedAt().GetDay(),
					}
				}

				var endDate *anilist.FuzzyDateInput
				if entry.GetCompletedAt() != nil {
					endDate = &anilist.FuzzyDateInput{
						Year:  entry.GetCompletedAt().GetYear(),
						Month: entry.GetCompletedAt().GetMonth(),
						Day:   entry.GetCompletedAt().GetDay(),
					}
				}

				var score *int
				if entry.GetScore() != nil {
					score = lo.ToPtr(int(*entry.GetScore()))
				} else {
					score = lo.ToPtr(0)
				}

				_ = m.anilistPlatform.UpdateEntry(
					context.Background(),
					entry.GetMedia().GetID(),
					entry.GetStatus(),
					score,
					entry.GetProgress(),
					startDate,
					endDate,
				)
			}
		}
	}

	m.RefreshAnilistCollectionsFunc()

	m.wsEventManager.SendEvent(events.RefreshedAnilistAnimeCollection, nil)
	m.wsEventManager.SendEvent(events.RefreshedAnilistMangaCollection, nil)

	return nil
}
