package models

import (
	"code.nonshy.com/nonshy/website/pkg/log"
)

// ForumStatistics queries for forum-level statistics.
type ForumStatistics struct {
	RecentThread *Thread
	RecentPost   *Comment // latest post on the recent thread
	Threads      uint64
	Posts        uint64
	Users        uint64
}

type ForumStatsMap map[uint64]*ForumStatistics

// MapForumStatistics looks up statistics for a set of forums.
func MapForumStatistics(forums []*Forum) ForumStatsMap {
	var (
		result = ForumStatsMap{}
		IDs    = []uint64{}
	)

	// Collect forum IDs and initialize the map.
	for _, forum := range forums {
		IDs = append(IDs, forum.ID)
		result[forum.ID] = &ForumStatistics{}
	}

	// Gather all the statistics.
	result.generateThreadCount(IDs)
	result.generatePostCount(IDs)
	result.generateUserCount(IDs)
	result.generateRecentThreads(IDs)
	result.generateRecentPosts(IDs)

	return result
}

// Has stats for this thread? (we should..)
func (ts ForumStatsMap) Has(threadID uint64) bool {
	_, ok := ts[threadID]
	return ok
}

// Get thread stats.
func (ts ForumStatsMap) Get(threadID uint64) *ForumStatistics {
	if stats, ok := ts[threadID]; ok {
		return stats
	}
	return nil
}

// Compute the count of threads in each of the forum 'IDs'.
func (ts ForumStatsMap) generateThreadCount(IDs []uint64) {
	// Hold the result of the count/group by query.
	type group struct {
		ID      uint64
		Threads uint64
	}
	var groups = []group{}

	// Count comments grouped by thread IDs.
	err := DB.Table(
		"threads",
	).Select(
		"forum_id AS id, count(id) AS threads",
	).Where(
		"forum_id IN ?",
		IDs,
	).Group("forum_id").Scan(&groups)

	if err.Error != nil {
		log.Error("MapForumStatistics: SQL error: %s", err.Error)
	}

	// Map the results in.
	for _, row := range groups {
		if stats, ok := ts[row.ID]; ok {
			stats.Threads = row.Threads
		}
	}
}

// Compute the count of all posts in each of the forum 'IDs'.
func (ts ForumStatsMap) generatePostCount(IDs []uint64) {
	type group struct {
		ID    uint64
		Posts uint64
	}
	var groups = []group{}

	err := DB.Table(
		"comments",
	).Joins(
		"JOIN threads ON (table_name = 'threads' AND table_id = threads.id)",
	).Joins(
		"JOIN forums ON (threads.forum_id = forums.id)",
	).Select(
		"forums.id AS id, count(comments.id) AS posts",
	).Where(
		`table_name = 'threads' AND EXISTS (
			SELECT 1
			FROM threads
			WHERE table_id = threads.id
			AND threads.forum_id IN ?
		)`,
		IDs,
	).Group("forums.id").Scan(&groups)

	if err.Error != nil {
		log.Error("SQL error collecting posts for forum: %s", err.Error)
	}

	// Map the results in.
	for _, row := range groups {
		if stats, ok := ts[row.ID]; ok {
			stats.Posts = row.Posts
		}
	}
}

// Compute the count of all users in each of the forum 'IDs'.
func (ts ForumStatsMap) generateUserCount(IDs []uint64) {
	type group struct {
		ForumID uint64
		Users   uint64
	}
	var groups = []group{}

	err := DB.Table(
		"comments",
	).Joins(
		"JOIN threads ON (table_name = 'threads' AND table_id = threads.id)",
	).Joins(
		"JOIN forums ON (threads.forum_id = forums.id)",
	).Select(
		"forums.id AS forum_id, count(distinct(comments.user_id)) AS users",
	).Where(
		"forums.id IN ?",
		IDs,
	).Group("forums.id").Scan(&groups)

	if err.Error != nil {
		log.Error("SQL error collecting users for forum: %s", err.Error)
	}

	// Map the results in.
	for _, row := range groups {
		if stats, ok := ts[row.ForumID]; ok {
			stats.Users = row.Users
		}
	}
}

// Compute the recent threads for each of the forum 'IDs'.
func (ts ForumStatsMap) generateRecentThreads(IDs []uint64) {
	var threadIDs = []map[string]interface{}{}
	err := DB.Table(
		"threads",
	).Select(
		"forum_id, id AS thread_id, updated_at",
	).Where(
		`updated_at = (SELECT MAX(updated_at)
			               FROM threads t2
						   WHERE threads.forum_id = t2.forum_id)
			AND threads.forum_id IN ?`,
		IDs,
	).Order(
		"updated_at desc",
	).Scan(&threadIDs)

	if err.Error != nil {
		log.Error("Getting most recent thread IDs: %s", err.Error)
	}

	// Map them easier.
	var (
		threadForumMap = map[uint64]uint64{}
		allThreadIDs   = []uint64{}
	)
	for _, row := range threadIDs {
		if row["thread_id"] == nil || row["forum_id"] == nil {
			continue
		}

		var (
			threadID = uint64(row["thread_id"].(int64))
			forumID  = uint64(row["forum_id"].(int64))
		)

		allThreadIDs = append(allThreadIDs, threadID)
		threadForumMap[threadID] = forumID
	}

	// Select and map these threads in.
	if threadMap, err := GetThreads(allThreadIDs); err == nil {
		for threadID, thread := range threadMap {
			if forumID, ok := threadForumMap[threadID]; ok {
				if stats, ok := ts[forumID]; ok {
					stats.RecentThread = thread
				}
			}
		}
	}
}

// Compute the recent post on each recent thread of each forum.
func (ts ForumStatsMap) generateRecentPosts(IDs []uint64) {
	// We already have the RecentThread of each forum - map these Thread IDs to recent comments.
	var (
		threadIDs      = []uint64{}
		threadStatsMap = map[uint64]*ForumStatistics{}
	)
	for _, stats := range ts {
		if stats.RecentThread != nil {
			threadIDs = append(threadIDs, stats.RecentThread.ID)
			threadStatsMap[stats.RecentThread.ID] = stats
		}
	}

	// The newest posts in these threads.
	type scanner struct {
		ThreadID  uint64
		CommentID uint64
	}
	var scan []scanner
	err := DB.Table(
		"comments",
	).Select(
		"table_id AS thread_id, id AS comment_id",
		// "forum_id, id AS thread_id, updated_at",
	).Where(
		`table_name='threads' AND table_id IN ?
		AND updated_at = (SELECT MAX(updated_at)
						  FROM comments c2
						  WHERE c2.table_name=comments.table_name
						  AND c2.table_id=comments.table_id
		)`,
		threadIDs,
	).Order(
		"updated_at desc",
	).Scan(&scan)
	if err.Error != nil {
		log.Error("Getting most recent post IDs: %s", err.Error)
	}

	// Gather the ThreadID:CommentID map.
	var (
		commentIDs      = []uint64{}
		commentStatsMap = map[uint64]*ForumStatistics{}
	)
	for _, row := range scan {
		if stats, ok := threadStatsMap[row.ThreadID]; ok {
			commentStatsMap[row.CommentID] = stats
			commentIDs = append(commentIDs, row.CommentID)
		}
	}

	// Select all these comments and map them in.
	if commentMap, err := GetComments(commentIDs); err == nil {
		for commentId, comment := range commentMap {
			if stats, ok := commentStatsMap[commentId]; ok {
				stats.RecentPost = comment
			}
		}
	}
}
