package arn

import (
	"errors"
	"fmt"

	"sort"

	"github.com/aerogo/nano"
	"github.com/akyoto/color"
)

// QuoteID represents a quote ID.
type QuoteID = ID

// Quote is a quote made by a character in an anime.
type Quote struct {
	Text          QuoteText   `json:"text" editable:"true"`
	CharacterID   CharacterID `json:"characterId" editable:"true"`
	AnimeID       AnimeID     `json:"animeId" editable:"true"`
	EpisodeNumber int         `json:"episode" editable:"true"`
	Time          int         `json:"time" editable:"true"`

	hasID
	hasPosts
	hasCreator
	hasEditor
	hasLikes
	hasDraft
}

// IsMainQuote returns true if the quote is the main quote of the character.
func (quote *Quote) IsMainQuote() bool {
	return quote.CharacterID != "" && quote.Character().MainQuoteID == quote.ID
}

// TitleByUser returns the preferred title for the given user.
func (quote *Quote) TitleByUser(user *User) string {
	character := quote.Character()

	if character == nil {
		return "Quote"
	}

	return fmt.Sprintf("%s's quote", character.Name.ByUser(user))
}

// Link returns a single quote.
func (quote *Quote) Link() string {
	return "/quote/" + quote.ID
}

// Publish checks the quote and publishes it when no errors were found.
func (quote *Quote) Publish() error {
	// No quote text
	if quote.Text.English == "" {
		return errors.New("English quote text is required")
	}

	// No character
	if quote.CharacterID == "" {
		return errors.New("A character is required")
	}

	// No anime
	if quote.AnimeID == "" {
		return errors.New("An anime is required")
	}

	// // No episode number
	// if quote.EpisodeNumber == -1 {
	// 	return errors.New("An episode number is required")
	// }

	// // No time
	// if quote.Time == -1 {
	// 	return errors.New("Time in minutes is required")
	// }

	// Invalid anime ID
	anime := quote.Anime()

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

	// Invalid character ID
	character := quote.Character()

	if character == nil {
		return errors.New("Invalid character ID")
	}

	// Invalid episode number
	maxEpisodes := anime.EpisodeCount

	if maxEpisodes != 0 && quote.EpisodeNumber > maxEpisodes {
		return errors.New("Invalid episode number")
	}

	return publish(quote)
}

// Unpublish ...
func (quote *Quote) Unpublish() error {
	return unpublish(quote)
}

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

// Self returns the object itself.
func (quote *Quote) Self() Loggable {
	return quote
}

// OnLike is called when the quote receives a like.
func (quote *Quote) OnLike(likedBy *User) {
	if !quote.IsValid() {
		color.Red("Invalid quote: %s", quote.ID)
		return
	}

	if likedBy.ID == quote.CreatedBy {
		return
	}

	if !quote.Creator().Settings().Notification.QuoteLikes {
		return
	}

	go func() {
		quote.Creator().SendNotification(&PushNotification{
			Title:   likedBy.Nick + " liked your " + quote.Character().Name.Canonical + " quote",
			Message: quote.Text.English,
			Icon:    "https:" + likedBy.AvatarLink("large"),
			Link:    "https://notify.moe" + likedBy.Link(),
			Type:    NotificationTypeLike,
		})
	}()
}

// IsValid tests the field values and returns true if everything is okay.
func (quote *Quote) IsValid() bool {
	return quote.Character() != nil && quote.Anime() != nil && quote.Creator() != nil
}

// String implements the default string serialization.
func (quote *Quote) String() string {
	return quote.Text.English
}

// GetQuote returns a single quote.
func GetQuote(id QuoteID) (*Quote, error) {
	obj, err := DB.Get("Quote", id)

	if err != nil {
		return nil, err
	}

	return obj.(*Quote), nil
}

// StreamQuotes returns a stream of all quotes.
func StreamQuotes() <-chan *Quote {
	channel := make(chan *Quote, nano.ChannelBufferSize)

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

		close(channel)
	}()

	return channel
}

// AllQuotes returns a slice of all quotes.
func AllQuotes() []*Quote {
	all := make([]*Quote, 0, DB.Collection("Quote").Count())

	stream := StreamQuotes()

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

	return all
}

// Character returns the character cited in the quote
func (quote *Quote) Character() *Character {
	character, _ := GetCharacter(quote.CharacterID)
	return character
}

// Anime fetches the anime where the quote is said.
func (quote *Quote) Anime() *Anime {
	anime, err := GetAnime(quote.AnimeID)

	if err != nil && !quote.IsDraft {
		color.Red("Error fetching anime: %v", err)
	}

	return anime
}

// SortQuotesLatestFirst ...
func SortQuotesLatestFirst(quotes []*Quote) {
	sort.Slice(quotes, func(i, j int) bool {
		return quotes[i].Created > quotes[j].Created
	})
}

// SortQuotesPopularFirst ...
func SortQuotesPopularFirst(quotes []*Quote) {
	sort.Slice(quotes, func(i, j int) bool {
		aLikes := len(quotes[i].Likes)
		bLikes := len(quotes[j].Likes)

		if aLikes == bLikes {
			return quotes[i].Created > quotes[j].Created
		}

		return aLikes > bLikes
	})
}

// FilterQuotes filters all quotes by a custom function.
func FilterQuotes(filter func(*Quote) bool) []*Quote {
	var filtered []*Quote

	for obj := range StreamQuotes() {
		if filter(obj) {
			filtered = append(filtered, obj)
		}
	}

	return filtered
}
