// Frontend > Refresher > Views
// This package contains the special locations generated by the refresher loop, such as the home view, and the popular. These are called views, and they don't actually compile or generate new data in each way, they're just collections of the existing compiled data in a different order. These are compiled and updated, not rendered on the fly.

package refresher

import (
	"aether-core/aether/frontend/beapiconsumer"
	"aether-core/aether/frontend/kvstore"
	// "aether-core/aether/frontend/clapiconsumer"
	"aether-core/aether/frontend/festructs"
	// "aether-core/aether/io/api"
	// pbstructs "aether-core/aether/protos/mimapi"
	"aether-core/aether/services/globals"
	"aether-core/aether/services/logging"
	// // "github.com/davecgh/go-spew/spew"
	// // "fmt"
	// "encoding/json"
	"strings"
	// "sync"
	"github.com/asdine/storm/q"
	"time"
)

// GenerateHomeView gets the top 10 most popular items in the communities you subscribe to, and sort them by rank.
func GenerateHomeView() {
	logging.Logf(1, "Home view generator is running")
	start := time.Now()
	// get subscribed boards fingerprints
	sbs := globals.FrontendConfig.ContentRelations.GetAllSubbedBoards()
	// Get the underlying compiled boards
	subbedBoardFps := []string{}
	for k, _ := range sbs {
		if !sbs[k].Notify {
			continue
		}
		subbedBoardFps = append(subbedBoardFps, sbs[k].Fingerprint)
	}
	boardCarriers := *getBoardsByFpList(subbedBoardFps)
	var thrs festructs.CThreadBatch
	for k, _ := range boardCarriers {
		// thrlen := min(len(boardCarriers[k].Threads), 10)
		// boardThreads := boardCarriers[k].Threads[0:thrlen]
		boardThreads := *(boardCarriers[k].GetTopThreadsForView(10))
		for j, _ := range boardThreads {
			boardThreads[j].ViewMeta_BoardName = boardCarriers[k].Boards[0].Name
		}
		thrs = append(thrs, boardThreads...)
	}
	thrs.SortByScore()
	logging.Logf(3, "Save happens in GenerateHomeView>Save")
	globals.KvInstance.Save(&festructs.HomeViewCarrier{
		Id:      1,
		Threads: thrs,
	})
	elapsed := time.Since(start)
	logging.Logf(1, "Home view generator took %v seconds.", elapsed.Seconds())
}

/*subbed, notify, lastseen := globals.FrontendConfig.ContentRelations.IsSubbedBoard(resp.Board.Fingerprint)

we also need to care about notify - it controls what gets into the home view.

*/

// GeneratePopularView gets the top 10 most popular items in each of the whitelisted communities and sorts them by rank.
func GeneratePopularView() {
	logging.Logf(1, "Popular view generator is running")
	start := time.Now()
	boardCarriers := []festructs.BoardCarrier{}
	// // check if sfwlist is disabled
	// if globals.FrontendConfig.ContentRelations.SFWList.GetSFWListDisabled() {
	// 	// sfwlist disabled
	// 	err := globals.KvInstance.All(&boardCarriers)
	// 	if err != nil {
	// 		logging.Logf(1, "Getting boards while SFWList disabled errored out. Error: %v", err)
	// 		return
	// 	}
	// } else {
	// 	// sfwlist enabled
	// 	boardCarriers = *getBoardsByFpList(globals.FrontendConfig.ContentRelations.SFWList.Boards)
	// 	logging.Logf(1, "base board carriers length: %v", len(boardCarriers))
	// 	logging.Logf(1, "sfwlist length: %v", len(globals.FrontendConfig.ContentRelations.SFWList.Boards))

	// }
	boardCarriers = *getBoardsByFpList(globals.FrontendConfig.ContentRelations.SFWList.Boards)
	/*
		^ This is a little weird - if this runs before the sfwlist is pulled in, it will result in an empty popular list. But if we make it so that in the case it's empty it generates the popular list from all communities, we might have NSFW threads surfacing up for people who haven't opted in for that.

		Here, I'm opting to show nothing instead of showing potentially risky data. It's a compromise.
	*/
	logging.Logf(1, "base board carriers length: %v", len(boardCarriers))
	logging.Logf(1, "sfwlist length: %v", len(globals.FrontendConfig.ContentRelations.SFWList.Boards))
	var thrs festructs.CThreadBatch
	for k, _ := range boardCarriers {
		// thrlen := min(len(boardCarriers[k].Threads), 10)
		// boardThreads := boardCarriers[k].Threads[0:thrlen]
		// thrs = append(thrs, boardThreads...)
		boardThreads := *(boardCarriers[k].GetTopThreadsForView(10))
		for j, _ := range boardThreads {
			boardThreads[j].ViewMeta_BoardName = boardCarriers[k].Boards[0].Name
		}
		thrs = append(thrs, boardThreads...)
	}
	thrs.SortByScore()
	existingPopularView := festructs.PopularViewCarrier{}
	logging.Logf(3, "Save happens in GeneratePopularView>Save")
	err := globals.KvInstance.One("Id", 1, &existingPopularView)
	if err != nil && !strings.Contains(err.Error(), "not found") {
		logging.Logf(1, "Popular view fetch in new popular view creation encountered an error. Error: %v", err)
		return
	}
	if len(thrs) > 0 || len(existingPopularView.Threads) == 0 {
		logging.Logf(3, "Save happens in GeneratePopularView>Save")
		globals.KvInstance.Save(&festructs.PopularViewCarrier{
			Id:      1,
			Threads: thrs,
		})
	} else {
		logging.Logf(1, "Popular view produced zero threads and thus bailed on updating. This is something that should be looked at.") // TODO FUTURE
	}

	elapsed := time.Since(start)
	logging.Logf(1, "Popular items count: %v", len(thrs))
	logging.Logf(1, "Popular view generator took %v seconds.", elapsed.Seconds())
}

// GenerateNewView gets the top 10 newest items in each of the whitelisted communities and sorts them by rank.

/*
	This might need a little bit of a different logic here. we want to take all and sort by new. But we want to do this efficiently... Can't just grab the all boards list because that might be just huge.
*/

var (
	newViewItemCount    float64 = 200
	postsPercentage     float64 = 0.66
	threadsPercentage   float64 = 0.33
	newViewThreadsCount         = int(newViewItemCount * threadsPercentage)
	newViewPostsCount           = int(newViewItemCount * postsPercentage)
)

func GenerateNewView() {
	/*
		Heads up, unlike the othew views, new view does depend on being run in a cycle. So we have to make sure that if we get a feed and it's empty, we just cancel and send the old one.
	*/
	logging.Logf(1, "New view generator is running")
	start := time.Now()
	/*
		- get items from beapi cache
		- remove non-sfwlist entries
		- get bucket from kvstore
		- add cache to bucket
		- sort bucket
		- filter bucket to last 200 items (1/3 threads 2/3 posts ratio)
		- save bucket
		- deliver bucket to the client.
	*/
	// Get the new view carrier in the kvstore.
	firstEverGeneration := false
	nvc := festructs.NewViewCarrier{}
	logging.Logf(3, "Single read happens in GenerateNewView")
	err := globals.KvInstance.One("Id", 1, &nvc)
	if err != nil {
		logging.Logf(1, "New view fetch in GenerateNewView encountered an error. Error: %v", err)
		if err.Error() == "not found" {
			logging.Logf(1, "This is new view generation from scratch, flipping the first ever generation flag true.")
			firstEverGeneration = true
		}
	}
	// Generate the item feed
	newFeed := beapiconsumer.GenerateNewFeed(firstEverGeneration)
	// Vivify the content, so we have the raw payloads.
	posts, threads := kvstore.GetNewFeedContent(newFeed)

	// Add new results, and make the new results be at the beginning.
	posts = append(posts, nvc.Posts...)
	threads = append(threads, nvc.Threads...)
	// Dedupe, so that only the newest instance anything is retained. Otherwise, getting two posts responding to the same thread will surface the same thread twice as well.
	dedupedThreads := dedupeThreads(threads)
	dedupedPosts := dedupePosts(posts)
	// Filter them down so that the total feed won't ever exceed 100 items.
	if len(dedupedThreads) > newViewThreadsCount {
		dedupedThreads = dedupedThreads[0:newViewThreadsCount]
	}
	if len(dedupedPosts) > newViewPostsCount {
		dedupedPosts = dedupedPosts[0:newViewPostsCount]
	}
	// Save it back into the Kvstore.
	globals.KvInstance.Save(&festructs.NewViewCarrier{
		Id:      1,
		Threads: dedupedThreads,
		Posts:   dedupedPosts,
	})
	elapsed := time.Since(start)
	// logging.Logf(1, "Popular items count: %v", len(thrs))
	logging.Logf(1, "New view generator took %v seconds.", elapsed.Seconds())
}

func dedupePosts(posts []festructs.CompiledPost) festructs.CPostBatch {
	m := make(map[string]bool)
	deduped := []festructs.CompiledPost{}
	for k, _ := range posts {
		if m[posts[k].Fingerprint] {
			// Already exists on the map - we have seen this before.
			continue
		}
		// Add to the new list.
		deduped = append(deduped, posts[k])
		m[posts[k].Fingerprint] = true
	}
	return festructs.CPostBatch(deduped)
}

func dedupeThreads(threads []festructs.CompiledThread) festructs.CThreadBatch {
	m := make(map[string]bool)
	deduped := []festructs.CompiledThread{}
	for k, _ := range threads {
		if m[threads[k].Fingerprint] {
			// Already exists on the map - we have seen this before.
			logging.Logf(0, "This thread already exists, dedupe is removing it: %#v", threads[k])
			continue
		}
		// Add to the new list.
		deduped = append(deduped, threads[k])
		m[threads[k].Fingerprint] = true
	}
	return festructs.CThreadBatch(deduped)
}

func getBoardsByFpList(boardFingerprints []string) *[]festructs.BoardCarrier {
	query := globals.KvInstance.Select(q.In("Fingerprint", boardFingerprints))
	var bcs []festructs.BoardCarrier
	query.Find(&bcs)
	return &bcs
}

/*----------  Internal util functions  ----------*/

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
