package arn

import (
	"errors"
	"fmt"
	"sort"
	"sync"

	"github.com/aerogo/nano"
)

// AnimeList is a list of anime list items.
type AnimeList struct {
	UserID UserID           `json:"userId" primary:"true"`
	Items  []*AnimeListItem `json:"items"`

	sync.Mutex
}

// Add adds an anime to the list if it hasn't been added yet.
func (list *AnimeList) Add(animeID AnimeID) error {
	if list.Contains(animeID) {
		return errors.New("Anime " + animeID + " has already been added")
	}

	creationDate := DateTimeUTC()

	item := &AnimeListItem{
		AnimeID: animeID,
		Status:  AnimeListStatusPlanned,
		Rating:  AnimeListItemRating{},
		Created: creationDate,
		Edited:  creationDate,
	}

	if item.Anime() == nil {
		return errors.New("Invalid anime ID")
	}

	list.Lock()
	list.Items = append(list.Items, item)
	list.Unlock()

	return nil
}

// Remove removes the anime ID from the list.
func (list *AnimeList) Remove(animeID AnimeID) bool {
	list.Lock()
	defer list.Unlock()

	for index, item := range list.Items {
		if item.AnimeID == animeID {
			list.Items = append(list.Items[:index], list.Items[index+1:]...)
			return true
		}
	}

	return false
}

// Contains checks if the list contains the anime ID already.
func (list *AnimeList) Contains(animeID AnimeID) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return true
		}
	}

	return false
}

// HasItemsWithStatus checks if the list contains an anime with the given status.
func (list *AnimeList) HasItemsWithStatus(status string) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			return true
		}
	}

	return false
}

// Find returns the list item with the specified anime ID, if available.
func (list *AnimeList) Find(animeID AnimeID) *AnimeListItem {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return item
		}
	}

	return nil
}

// Import adds an anime to the list if it hasn't been added yet
// and if it did exist it will update episode, rating and notes.
func (list *AnimeList) Import(item *AnimeListItem) {
	existing := list.Find(item.AnimeID)

	// If it doesn't exist yet: Simply add it.
	if existing == nil {
		list.Lock()
		list.Items = append(list.Items, item)
		list.Unlock()
		return
	}

	// Temporary save it before changing the status
	// because status changes can modify the episode count.
	// This will prevent loss of "episodes watched" data.
	existingEpisodes := existing.Episodes

	// Status
	existing.Status = item.Status
	existing.OnStatusChange()

	// Episodes
	if item.Episodes > existingEpisodes {
		existing.Episodes = item.Episodes
	} else {
		existing.Episodes = existingEpisodes
	}

	existing.OnEpisodesChange()

	// Rating
	if existing.Rating.Overall == 0 {
		existing.Rating.Overall = item.Rating.Overall
		existing.Rating.Clamp()
	}

	if existing.Notes == "" {
		existing.Notes = item.Notes
	}

	if item.RewatchCount > existing.RewatchCount {
		existing.RewatchCount = item.RewatchCount
	}

	// Edited
	existing.Edited = DateTimeUTC()
}

// User returns the user this anime list belongs to.
func (list *AnimeList) User() *User {
	user, _ := GetUser(list.UserID)
	return user
}

// Top returns the top entries.
func (list *AnimeList) Top(count int) []*AnimeListItem {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if a.Rating.Overall == b.Rating.Overall {
			return a.Anime().Title.Canonical < b.Anime().Title.Canonical
		}

		return a.Rating.Overall > b.Rating.Overall
	})

	if count > len(list.Items) {
		count = len(list.Items)
	}

	tmp := make([]*AnimeListItem, count)
	copy(tmp, list.Items[:count])
	return tmp
}

// Watching ...
func (list *AnimeList) Watching() *AnimeList {
	return list.FilterStatus(AnimeListStatusWatching)
}

// FilterStatus ...
func (list *AnimeList) FilterStatus(status string) *AnimeList {
	newList := &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// WithoutPrivateItems returns a new anime list with the private items removed.
func (list *AnimeList) WithoutPrivateItems() *AnimeList {
	list.Lock()
	defer list.Unlock()

	newList := &AnimeList{
		UserID: list.UserID,
		Items:  make([]*AnimeListItem, 0, len(list.Items)),
	}

	for _, item := range list.Items {
		if !item.Private {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// SplitByStatus splits the anime list into multiple ones by status.
func (list *AnimeList) SplitByStatus() map[string]*AnimeList {
	statusToList := map[string]*AnimeList{
		AnimeListStatusWatching:  {UserID: list.UserID},
		AnimeListStatusCompleted: {UserID: list.UserID},
		AnimeListStatusPlanned:   {UserID: list.UserID},
		AnimeListStatusHold:      {UserID: list.UserID},
		AnimeListStatusDropped:   {UserID: list.UserID},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		statusList := statusToList[item.Status]
		statusList.Items = append(statusList.Items, item)
	}

	return statusToList
}

// NormalizeRatings normalizes all ratings so that they are perfectly stretched among the full scale.
func (list *AnimeList) NormalizeRatings() {
	list.Lock()
	defer list.Unlock()

	mapped := map[float64]float64{}
	all := make([]float64, 0, len(list.Items))

	for _, item := range list.Items {
		// Zero rating counts as not rated
		if item.Rating.Overall == 0 {
			continue
		}

		_, found := mapped[item.Rating.Overall]

		if !found {
			mapped[item.Rating.Overall] = item.Rating.Overall
			all = append(all, item.Rating.Overall)
		}
	}

	sort.Slice(all, func(i, j int) bool {
		return all[i] < all[j]
	})

	count := len(all)

	// Prevent division by zero
	if count <= 1 {
		return
	}

	step := 9.9 / float64(count-1)
	currentRating := 0.1

	for _, rating := range all {
		mapped[rating] = currentRating
		currentRating += step
	}

	for _, item := range list.Items {
		item.Rating.Overall = mapped[item.Rating.Overall]
		item.Rating.Clamp()
	}
}

// GetID returns the anime ID.
func (list *AnimeList) GetID() string {
	return list.UserID
}

// TypeName returns the type name.
func (list *AnimeList) TypeName() string {
	return "AnimeList"
}

// Self returns the object itself.
func (list *AnimeList) Self() Loggable {
	return list
}

// Genres returns a map of genre names mapped to the list items that belong to that genre.
func (list *AnimeList) Genres() map[string][]*AnimeListItem {
	genreToListItems := map[string][]*AnimeListItem{}

	for _, item := range list.Items {
		for _, genre := range item.Anime().Genres {
			genreToListItems[genre] = append(genreToListItems[genre], item)
		}
	}

	return genreToListItems
}

// TopGenres returns the most liked genres for the user's anime list.
func (list *AnimeList) TopGenres(count int) []string {
	genreItems := list.Genres()
	genreAffinity := map[string]float64{}
	bestGenres := make([]string, 0, len(genreItems))

	for genre, animeListItems := range genreItems {
		if genre == "Action" || genre == "Comedy" {
			continue
		}

		affinity := 0.0

		for _, item := range animeListItems {
			if item.Status != AnimeListStatusCompleted {
				continue
			}

			if item.Rating.Overall != 0 {
				affinity += item.Rating.Overall - AverageRating
			} else {
				// Add 0.1 to avoid all affinities being 0 when a user doesn't have any rated anime.
				affinity += 0.1
			}
		}

		genreAffinity[genre] = affinity
		bestGenres = append(bestGenres, genre)
	}

	sort.Slice(bestGenres, func(i, j int) bool {
		aAffinity := genreAffinity[bestGenres[i]]
		bAffinity := genreAffinity[bestGenres[j]]

		if aAffinity == bAffinity {
			return bestGenres[i] < bestGenres[j]
		}

		return aAffinity > bAffinity
	})

	if len(bestGenres) > count {
		bestGenres = bestGenres[:count]
	}

	return bestGenres
}

// RemoveDuplicates removes duplicate entries.
func (list *AnimeList) RemoveDuplicates() {
	list.Lock()
	defer list.Unlock()

	existed := map[string]bool{}
	newItems := make([]*AnimeListItem, 0, len(list.Items))

	for _, item := range list.Items {
		_, exists := existed[item.AnimeID]

		if exists {
			fmt.Println(list.User().Nick, "removed anime list item duplicate", item.AnimeID)
			continue
		}

		newItems = append(newItems, item)
		existed[item.AnimeID] = true
	}

	list.Items = newItems
}

// StreamAnimeLists returns a stream of all anime.
func StreamAnimeLists() <-chan *AnimeList {
	channel := make(chan *AnimeList, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("AnimeList") {
			channel <- obj.(*AnimeList)
		}

		close(channel)
	}()

	return channel
}

// AllAnimeLists returns a slice of all anime.
func AllAnimeLists() ([]*AnimeList, error) {
	all := make([]*AnimeList, 0, DB.Collection("AnimeList").Count())

	stream := StreamAnimeLists()

	for obj := range stream {
		all = append(all, obj)
	}

	return all, nil
}

// GetAnimeList ...
func GetAnimeList(userID UserID) (*AnimeList, error) {
	animeList, err := DB.Get("AnimeList", userID)

	if err != nil {
		return nil, err
	}

	return animeList.(*AnimeList), nil
}
