package models

import (
	"strings"
	"time"

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

// RecentPost drives the "Forums / Newest" page - carrying all forum comments
// on all threads sorted by date.
type RecentPost struct {
	CommentID uint64
	ThreadID  uint64
	ForumID   uint64
	UpdatedAt time.Time
	Thread    *Thread
	Comment   *Comment
	Forum     *Forum
}

// PaginateRecentPosts returns all of the comments on a forum paginated.
func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]*RecentPost, error) {
	var (
		result         = []*RecentPost{}
		query          = (&Comment{}).Preload()
		blockedUserIDs = BlockedUserIDs(user)
		wheres         = []string{"table_name = 'threads'"}
		placeholders   = []interface{}{}
	)

	if len(categories) > 0 {
		wheres = append(wheres, "forums.category IN ?")
		placeholders = append(placeholders, categories)
	}

	// Hide explicit forum if user hasn't opted into it.
	if !user.Explicit && !user.IsAdmin {
		wheres = append(wheres, "forums.explicit = false")
	}

	// Circle membership.
	if !user.IsInnerCircle() {
		wheres = append(wheres, "forums.inner_circle is not true")
	}

	// Blocked users?
	if len(blockedUserIDs) > 0 {
		wheres = append(wheres, "comments.user_id NOT IN ?")
		placeholders = append(placeholders, blockedUserIDs)
	}

	// Don't show comments from banned or disabled accounts.
	wheres = append(wheres, `
		EXISTS (
			SELECT 1
			FROM users
			WHERE users.id = comments.user_id
			AND users.status = 'active'
		)
	`)

	// Get the page of recent forum comment IDs of all time.
	type scanner struct {
		CommentID uint64
		ThreadID  *uint64
		ForumID   *uint64
	}
	var scan []scanner
	query = DB.Table("comments").Select(
		`comments.id AS comment_id,
		 threads.id AS thread_id,
		 forums.id AS forum_id`,
	).Joins(
		"LEFT OUTER JOIN threads ON (table_name = 'threads' AND table_id = threads.id)",
	).Joins(
		"LEFT OUTER JOIN forums ON (threads.forum_id = forums.id)",
	).Where(
		strings.Join(wheres, " AND "),
		placeholders...,
	).Order("comments.updated_at desc")

	// Get the total for the pager and scan the page of ID sets.
	query.Model(&Comment{}).Count(&pager.Total)
	query = query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&scan)
	if query.Error != nil {
		return nil, query.Error
	}

	// Ingest the results.
	var (
		commentIDs   = []uint64{} // collect distinct IDs
		threadIDs    = []uint64{}
		forumIDs     = []uint64{}
		seenComments = map[uint64]interface{}{} // deduplication
		seenThreads  = map[uint64]interface{}{}
		seenForums   = map[uint64]interface{}{}
		mapCommentRC = map[uint64]*RecentPost{} // map commentID to result
	)
	for _, row := range scan {
		// Upsert the result set.
		var rp *RecentPost
		if existing, ok := mapCommentRC[row.CommentID]; ok {
			rp = existing
		} else {
			rp = &RecentPost{
				CommentID: row.CommentID,
			}
			mapCommentRC[row.CommentID] = rp
			result = append(result, rp)
		}

		// Got a thread ID?
		if row.ThreadID != nil {
			rp.ThreadID = *row.ThreadID
			if _, ok := seenThreads[rp.ThreadID]; !ok {
				seenThreads[rp.ThreadID] = nil
				threadIDs = append(threadIDs, rp.ThreadID)
			}

		}

		// Got a forum ID?
		if row.ForumID != nil {
			rp.ForumID = *row.ForumID
			if _, ok := seenForums[rp.ForumID]; !ok {
				seenForums[rp.ForumID] = nil
				forumIDs = append(forumIDs, rp.ForumID)
			}
		}

		// Collect distinct comment IDs.
		if _, ok := seenComments[rp.CommentID]; !ok {
			seenComments[rp.CommentID] = nil
			commentIDs = append(commentIDs, rp.CommentID)
		}
	}

	// Load all of the distinct comments, threads and forums.
	var (
		comments = map[uint64]*Comment{}
		threads  = map[uint64]*Thread{}
		forums   = map[uint64]*Forum{}
	)

	if len(commentIDs) > 0 {
		comments, _ = GetComments(commentIDs)
	}
	if len(threadIDs) > 0 {
		threads, _ = GetThreadsAsUser(user, threadIDs)
	}
	if len(forumIDs) > 0 {
		forums, _ = GetForums(forumIDs)
	}

	// Collect comments so we can inject UserRelationships in efficiently.
	var (
		coms = []*Comment{}
		thrs = []*Thread{}
	)

	// Merge all the objects back in.
	for _, rc := range result {
		if com, ok := comments[rc.CommentID]; ok {
			rc.Comment = com
			coms = append(coms, com)
		}

		if thr, ok := threads[rc.ThreadID]; ok {
			rc.Thread = thr
			thrs = append(thrs, thr)
		} else {
			log.Error("RecentPosts: didn't find thread ID %d in map!", rc.ThreadID)

			// Create a dummy placeholder Thread (e.g.: the thread originator has been
			// banned or disabled, but the thread summary is shown on the new comment view)
			rc.Thread = &Thread{
				Comment: Comment{
					Message: "[unavailable]",
				},
			}
		}

		if f, ok := forums[rc.ForumID]; ok {
			rc.Forum = f
		}
	}

	// Inject user relationships into all comment users now.
	SetUserRelationshipsInComments(user, coms)
	SetUserRelationshipsInThreads(user, thrs)

	return result, nil
}
