package models

import (
	"strings"
	"time"

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

// Comment table - in forum threads, on profiles or photos, etc.
type Comment struct {
	ID        uint64 `gorm:"primaryKey"`
	TableName string `gorm:"index"`
	TableID   uint64 `gorm:"index"`
	UserID    uint64 `gorm:"index"`
	User      User
	Message   string
	CreatedAt time.Time
	UpdatedAt time.Time
}

// CommentableTables are the set of table names that allow comments (via the
// generic "/comments" URI which accepts a table_name param)
var CommentableTables = map[string]interface{}{
	"photos":  nil,
	"threads": nil,
}

// Preload related tables for the forum (classmethod).
func (c *Comment) Preload() *gorm.DB {
	return DB.Preload("User.ProfilePhoto")
}

// GetComment by ID.
func GetComment(id uint64) (*Comment, error) {
	c := &Comment{}
	result := c.Preload().First(&c, id)
	return c, result.Error
}

// GetComments queries a set of comment IDs and returns them mapped.
func GetComments(IDs []uint64) (map[uint64]*Comment, error) {
	var (
		mt = map[uint64]*Comment{}
		ts = []*Comment{}
	)

	result := (&Comment{}).Preload().Where("id IN ?", IDs).Find(&ts)
	for _, row := range ts {
		mt[row.ID] = row
	}

	return mt, result.Error
}

// AddComment about anything.
func AddComment(user *User, tableName string, tableID uint64, message string) (*Comment, error) {
	c := &Comment{
		TableName: tableName,
		TableID:   tableID,
		User:      *user,
		Message:   message,
	}
	result := DB.Create(c)
	return c, result.Error
}

// PaginateComments provides a page of comments on something.
func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagination) ([]*Comment, error) {
	var (
		cs             = []*Comment{}
		query          = (&Comment{}).Preload()
		blockedUserIDs = BlockedUserIDs(user)
		wheres         = []string{}
		placeholders   = []interface{}{}
	)

	wheres = append(wheres, "table_name = ? AND table_id = ?")
	placeholders = append(placeholders, tableName, tableID)

	if len(blockedUserIDs) > 0 {
		wheres = append(wheres, "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'
		)
	`)

	query = query.Where(
		strings.Join(wheres, " AND "),
		placeholders...,
	).Order(pager.Sort)

	query.Model(&Comment{}).Count(&pager.Total)
	result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cs)

	// Inject user relationships into these comments' authors.
	SetUserRelationshipsInComments(user, cs)

	return cs, result.Error
}

// ListComments returns a complete set of comments without paging.
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
	var (
		cs             []*Comment
		blockedUserIDs = BlockedUserIDs(user)
		wheres         = []string{}
		placeholders   = []interface{}{}
	)

	wheres = append(wheres, "table_name = ? AND table_id = ?")
	placeholders = append(placeholders, tableName, tableID)

	if len(blockedUserIDs) > 0 {
		wheres = append(wheres, "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'
		)
	`)

	result := (&Comment{}).Preload().Where(
		strings.Join(wheres, " AND "),
		placeholders...,
	).Order("created_at asc").Find(&cs)
	return cs, result.Error
}

// Save a comment.
func (c *Comment) Save() error {
	return DB.Save(c).Error
}

// Delete a comment.
func (c *Comment) Delete() error {
	return DB.Delete(c).Error
}

// IsEdited returns if a comment was reasonably edited after it was created.
func (c *Comment) IsEdited() bool {
	return c.UpdatedAt.Sub(c.CreatedAt) > 5*time.Second
}

type CommentCountMap map[uint64]int64

// MapCommentCounts collects total numbers of comments over a set of table IDs. Returns a
// map of table ID (uint64) to comment counts for each (int64).
func MapCommentCounts(tableName string, tableIDs []uint64) CommentCountMap {
	var result = CommentCountMap{}

	// Initialize the result set.
	for _, id := range tableIDs {
		result[id] = 0
	}

	// Hold the result of the grouped count query.
	type group struct {
		ID       uint64
		Comments int64
	}
	var groups = []group{}

	// Map the counts of comments to each of these IDs.
	if res := DB.Table(
		"comments",
	).Select(
		"table_id AS id, count(id) AS comments",
	).Where(
		"table_name = ? AND table_id IN ?",
		tableName, tableIDs,
	).Group("table_id").Scan(&groups); res.Error != nil {
		log.Error("MapCommentCounts: count query: %s", res.Error)
	}

	// Map the counts back in.
	for _, row := range groups {
		result[row.ID] = row.Comments
	}

	return result
}

// Get a comment count for the given table ID from the map.
func (cc CommentCountMap) Get(id uint64) int64 {
	if value, ok := cc[id]; ok {
		return value
	}
	return 0
}
