package models

import (
	"time"

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

// Block table.
type Block struct {
	ID           uint64 `gorm:"primaryKey"`
	SourceUserID uint64 `gorm:"index"`
	TargetUserID uint64 `gorm:"index"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

// AddBlock is sourceUserId adding targetUserId to their block list.
func AddBlock(sourceUserID, targetUserID uint64) error {
	// Unfriend in the process.
	RemoveFriend(sourceUserID, targetUserID)

	// Did we already block this user?
	var b *Block
	forward := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).First(&b).Error

	// Update existing.
	if forward == nil {
		return nil
	}

	// Create the block.
	b = &Block{
		SourceUserID: sourceUserID,
		TargetUserID: targetUserID,
	}
	return DB.Create(b).Error
}

// IsBlocking quickly sees if either user blocks the other.
func IsBlocking(sourceUserID, targetUserID uint64) bool {
	b := &Block{}
	result := DB.Where(
		"(source_user_id = ? AND target_user_id = ?) OR "+
			"(target_user_id = ? AND source_user_id = ?)",
		sourceUserID, targetUserID,
		sourceUserID, targetUserID,
	).First(&b)
	return result.Error == nil
}

// IsBlocked quickly checks if sourceUserID currently blocks targetUserID.
func IsBlocked(sourceUserID, targetUserID uint64) bool {
	b := &Block{}
	result := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).First(&b)
	return result.Error == nil
}

// PaginateBlockList views a user's blocklist.
func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) {
	// We paginate over the Block table.
	var (
		bs      = []*Block{}
		userIDs = []uint64{}
		query   *gorm.DB
	)

	query = DB.Where(
		"source_user_id = ?",
		user.ID,
	)

	query = query.Order(pager.Sort)
	query.Model(&Block{}).Count(&pager.Total)
	result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&bs)
	if result.Error != nil {
		return nil, result.Error
	}

	// Now of these friends get their User objects.
	for _, b := range bs {
		userIDs = append(userIDs, b.TargetUserID)
	}

	return GetUsers(user, userIDs)
}

// BlockedUserIDs returns all user IDs blocked by the user (bidirectional, source or target user).
func BlockedUserIDs(user *User) []uint64 {
	// Have we looked this up already on this request?
	if user.cacheBlockedUserIDs != nil {
		return user.cacheBlockedUserIDs
	}

	var (
		bs      = []*Block{}
		userIDs = []uint64{}
	)
	DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs)
	for _, row := range bs {
		for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} {
			if uid != user.ID {
				userIDs = append(userIDs, uid)
			}
		}
	}

	// Cache the result in the User so we don't query it again.
	user.cacheBlockedUserIDs = userIDs

	return userIDs
}

// MapBlockedUserIDs returns BlockedUserIDs as a lookup hashmap (not for front-end templates currently).
func MapBlockedUserIDs(user *User) map[uint64]interface{} {
	var (
		result     = map[uint64]interface{}{}
		blockedIDs = BlockedUserIDs(user)
	)
	for _, uid := range blockedIDs {
		result[uid] = nil
	}
	return result
}

// FilterBlockingUserIDs narrows down a set of User IDs to remove ones that block (or are blocked by) the current user.
func FilterBlockingUserIDs(currentUser *User, userIDs []uint64) []uint64 {
	var (
		// Get the IDs to exclude.
		blockedIDs = MapBlockedUserIDs(currentUser)

		// Filter the result.
		result = []uint64{}
	)
	for _, uid := range userIDs {
		if _, ok := blockedIDs[uid]; ok {
			continue
		}
		result = append(result, uid)
	}
	return result
}

// BlockedUserIDsByUser returns all user IDs blocked by the user (one directional only)
func BlockedUserIDsByUser(userId uint64) []uint64 {
	var (
		bs      = []*Block{}
		userIDs = []uint64{}
	)
	DB.Where("source_user_id = ?", userId).Find(&bs)
	for _, row := range bs {
		for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} {
			if uid != userId {
				userIDs = append(userIDs, uid)
			}
		}
	}
	return userIDs
}

// BlockedUsernames returns all usernames blocked by (or blocking) the user.
func BlockedUsernames(user *User) []string {
	var (
		userIDs   = BlockedUserIDs(user)
		usernames = []string{}
	)

	if len(userIDs) == 0 {
		return usernames
	}

	if res := DB.Table(
		"users",
	).Select(
		"username",
	).Where(
		"id IN ?", userIDs,
	).Scan(&usernames); res.Error != nil {
		log.Error("BlockedUsernames(%d): %s", user.Username, res.Error)
	}

	return usernames
}

// UnblockUser removes targetUserID from your blocklist.
func UnblockUser(sourceUserID, targetUserID uint64) error {
	result := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).Delete(&Block{})
	return result.Error
}

// Save photo.
func (b *Block) Save() error {
	result := DB.Save(b)
	return result.Error
}
