package models

import (
	"strings"
	"time"

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

// Like table.
type Like struct {
	ID        uint64 `gorm:"primaryKey"`
	UserID    uint64 `gorm:"index"` // who it belongs to
	TableName string
	TableID   uint64
	CreatedAt time.Time
	UpdatedAt time.Time
}

// LikeableTables are the set of table names that allow likes (used by the JSON API).
var LikeableTables = map[string]interface{}{
	"photos":   nil,
	"users":    nil,
	"comments": nil,
}

// AddLike to something.
func AddLike(user *User, tableName string, tableID uint64) error {
	// Already has a like?
	var like = &Like{}
	exist := DB.Model(like).Where(
		"user_id = ? AND table_name = ? AND table_id = ?",
		user.ID, tableName, tableID,
	).First(&like)
	if exist.Error == nil {
		return nil
	}

	// Create it.
	like = &Like{
		UserID:    user.ID,
		TableName: tableName,
		TableID:   tableID,
	}
	return DB.Create(like).Error
}

// Unlike something.
func Unlike(user *User, tableName string, tableID uint64) error {
	result := DB.Where(
		"user_id = ? AND table_name = ? AND table_id = ?",
		user.ID, tableName, tableID,
	).Delete(&Like{})
	return result.Error
}

// CountLikes on something.
func CountLikes(tableName string, tableID uint64) int64 {
	var count int64
	DB.Model(&Like{}).Where(
		"table_name = ? AND table_id = ?",
		tableName, tableID,
	).Count(&count)
	return count
}

// WhoLikes something. Returns the first couple users and a count of the remainder.
func WhoLikes(currentUser *User, tableName string, tableID uint64) ([]*User, int64, error) {
	var (
		userIDs        = []uint64{}
		likes          = []*Like{}
		total          = CountLikes(tableName, tableID)
		remainder      = total
		wheres         = []string{}
		placeholders   = []interface{}{}
		blockedUserIDs = BlockedUserIDs(currentUser)
	)

	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)
	}

	res := DB.Model(&Like{}).Where(
		strings.Join(wheres, " AND "),
		placeholders...,
	).Order("created_at DESC").Limit(2).Scan(&likes)

	if res.Error != nil {
		return nil, 0, res.Error
	}

	// Subtract the (up to two) likes from the total.
	remainder -= int64(len(likes))

	// Collect the user IDs to look up.
	for _, row := range likes {
		userIDs = append(userIDs, row.UserID)
	}

	// Look up the users and return the remainder.
	users, err := GetUsers(nil, userIDs)
	if err != nil {
		return nil, 0, err
	}

	return users, remainder, nil
}

// PaginateLikes returns a paged view of users who've liked something.
func PaginateLikes(currentUser *User, tableName string, tableID uint64, pager *Pagination) ([]*User, error) {
	var (
		l              = []*Like{}
		userIDs        = []uint64{}
		wheres         = []string{}
		placeholders   = []interface{}{}
		blockedUserIDs = BlockedUserIDs(currentUser)
	)

	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)
	}

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

	// Get the total count.
	query.Model(&Like{}).Count(&pager.Total)

	// Get the page of likes.
	result := query.Offset(
		pager.GetOffset(),
	).Limit(pager.PerPage).Find(&l)
	if result.Error != nil {
		return nil, result.Error
	}

	// Map the user IDs in.
	for _, like := range l {
		userIDs = append(userIDs, like.UserID)
	}
	return GetUsers(currentUser, userIDs)
}

// LikedIDs filters a set of table IDs to ones the user likes.
func LikedIDs(user *User, tableName string, tableIDs []uint64) ([]uint64, error) {
	var result = []uint64{}
	if r := DB.Table(
		"likes",
	).Select(
		"table_id",
	).Where(
		"user_id = ? AND table_name = ? AND table_id IN ?",
		user.ID, tableName, tableIDs,
	).Scan(&result); r.Error != nil {
		return result, r.Error
	}

	return result, nil
}

// LikeMap maps table IDs to Likes metadata.
type LikeMap map[uint64]*LikeStats

// Get like stats from the map.
func (lm LikeMap) Get(id uint64) *LikeStats {
	if stats, ok := lm[id]; ok {
		return stats
	}
	return &LikeStats{}
}

// LikeStats holds mapped statistics about liked objects.
type LikeStats struct {
	Count     int64 // how many total
	UserLikes bool  // current user likes it
}

// MapLikes over a set of table IDs.
func MapLikes(user *User, tableName string, tableIDs []uint64) LikeMap {
	var result = LikeMap{}

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

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

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

	// Map the counts back in.
	for _, row := range groups {
		if stats, ok := result[row.ID]; ok {
			stats.Count = row.Likes
		}
	}

	// Does the CURRENT USER like any of these IDs?
	if likedIDs, err := LikedIDs(user, tableName, tableIDs); err == nil {
		log.Error("USER LIKES IDS: %+v", likedIDs)
		for _, id := range likedIDs {
			if stats, ok := result[id]; ok {
				stats.UserLikes = true
			}
		}
	}

	return result
}
