package models

import (
	"errors"
	"strings"
	"time"

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

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

// AddFriend sends a friend request or accepts one if there was already a pending one.
func AddFriend(sourceUserID, targetUserID uint64) error {
	// Did we already send a friend request?
	f := &Friend{}
	forward := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).First(&f).Error

	// Is there a reverse friend request pending?
	rev := &Friend{}
	reverse := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		targetUserID, sourceUserID,
	).First(&rev).Error

	// If the reverse exists (requested us) but not the forward, this completes the friendship.
	if reverse == nil && forward != nil {
		// Approve the reverse.
		rev.Approved = true
		rev.Ignored = false
		rev.Save()

		// Add the matching forward.
		f = &Friend{
			SourceUserID: sourceUserID,
			TargetUserID: targetUserID,
			Approved:     true,
			Ignored:      false,
		}
		return DB.Create(f).Error
	}

	// If the forward already existed, error.
	if forward == nil {
		if f.Approved {
			return errors.New("you are already friends")
		}
		return errors.New("a friend request had already been sent")
	}

	// Create the pending forward request.
	f = &Friend{
		SourceUserID: sourceUserID,
		TargetUserID: targetUserID,
		Approved:     false,
	}
	return DB.Create(f).Error
}

// AreFriends quickly checks if two user IDs are friends.
func AreFriends(sourceUserID, targetUserID uint64) bool {
	f := &Friend{}
	DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).First(&f)
	return f.Approved
}

// FriendStatus returns an indicator of friendship status: "none", "pending", "approved"
func FriendStatus(sourceUserID, targetUserID uint64) string {
	f := &Friend{}
	result := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		sourceUserID, targetUserID,
	).First(&f)
	if result.Error == nil {
		if f.Approved {
			return "approved"
		}
		return "pending"
	}
	return "none"
}

// FriendIDs returns all user IDs with approved friendship to the user.
func FriendIDs(userId uint64) []uint64 {
	var (
		fs      = []*Friend{}
		userIDs = []uint64{}
	)
	DB.Where("source_user_id = ? AND approved = ?", userId, true).Find(&fs)
	for _, row := range fs {
		userIDs = append(userIDs, row.TargetUserID)
	}
	return userIDs
}

// FilterFriendIDs can filter down a listing of user IDs and return only the ones who are your friends.
func FilterFriendIDs(userIDs []uint64, friendIDs []uint64) []uint64 {
	var (
		seen     = map[uint64]interface{}{}
		filtered = []uint64{}
	)

	// Map the friend IDs out.
	for _, friendID := range friendIDs {
		seen[friendID] = nil
	}

	// Filter the userIDs.
	for _, userID := range userIDs {
		if _, ok := seen[userID]; ok {
			filtered = append(filtered, userID)
		}
	}

	return filtered
}

// FilterFriendUsernames takes a list of usernames and returns only the ones who are your friends.
func FilterFriendUsernames(currentUser *User, usernames []string) []string {
	var (
		fs      = []*Friend{}
		userIDs = []uint64{}
		userMap = map[uint64]string{}
		result  = []string{}
	)

	// Map usernames to user IDs.
	users, err := GetUsersByUsernames(currentUser, usernames)
	if err != nil {
		log.Error("FilterFriendUsernames: GetUsersByUsernames: %s", err)
		return result
	}

	for _, user := range users {
		userIDs = append(userIDs, user.ID)
		userMap[user.ID] = user.Username
	}

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

	DB.Where("source_user_id = ? AND approved = ? AND target_user_id IN ?", currentUser.ID, true, userIDs).Find(&fs)
	for _, row := range fs {
		result = append(result, userMap[row.TargetUserID])
	}

	return result
}

// FriendIDsAreExplicit returns friend IDs who have opted-in for Explicit content,
// e.g. to notify only them when you uploaded a new Explicit photo so that non-explicit
// users don't need to see that notification.
func FriendIDsAreExplicit(userId uint64) []uint64 {
	var (
		userIDs = []uint64{}
	)

	err := DB.Table(
		"friends",
	).Joins(
		"JOIN users ON (users.id = friends.target_user_id)",
	).Select(
		"friends.target_user_id AS friend_id",
	).Where(
		"friends.source_user_id = ? AND friends.approved = ? AND users.explicit = ?",
		userId, true, true,
	).Scan(&userIDs)

	if err.Error != nil {
		log.Error("SQL error collecting explicit FriendIDs for %d: %s", userId, err.Error)
	}

	return userIDs
}

// FriendIDsInCircle returns friend IDs who are part of the inner circle.
func FriendIDsInCircle(userId uint64) []uint64 {
	var (
		userIDs = []uint64{}
	)

	err := DB.Table(
		"friends",
	).Joins(
		"JOIN users ON (users.id = friends.target_user_id)",
	).Select(
		"friends.target_user_id AS friend_id",
	).Where(
		"friends.source_user_id = ? AND friends.approved = ? AND (users.inner_circle = ? OR users.is_admin = ?)",
		userId, true, true, true,
	).Scan(&userIDs)

	if err.Error != nil {
		log.Error("SQL error collecting circle FriendIDs for %d: %s", userId, err.Error)
	}

	return userIDs
}

// FriendIDsInCircleAreExplicit returns the combined friend IDs who are in the inner circle + have opted in to explicit content.
// It is the combination of FriendIDsAreExplicit and FriendIDsInCircle.
func FriendIDsInCircleAreExplicit(userId uint64) []uint64 {
	var (
		userIDs = []uint64{}
	)

	err := DB.Table(
		"friends",
	).Joins(
		"JOIN users ON (users.id = friends.target_user_id)",
	).Select(
		"friends.target_user_id AS friend_id",
	).Where(
		"friends.source_user_id = ? AND friends.approved = ? AND users.explicit = ? AND (users.inner_circle = ? OR users.is_admin = ?)",
		userId, true, true, true, true,
	).Scan(&userIDs)

	if err.Error != nil {
		log.Error("SQL error collecting explicit FriendIDs for %d: %s", userId, err.Error)
	}

	return userIDs
}

// CountFriendRequests gets a count of pending requests for the user.
func CountFriendRequests(userID uint64) (int64, error) {
	var count int64
	result := DB.Where(
		"target_user_id = ? AND approved = ? AND ignored IS NOT true",
		userID,
		false,
	).Model(&Friend{}).Count(&count)
	return count, result.Error
}

// CountIgnoredFriendRequests gets a count of ignored pending friend requests for the user.
func CountIgnoredFriendRequests(userID uint64) (int64, error) {
	var count int64
	result := DB.Where(
		"target_user_id = ? AND approved = ? AND ignored = ?",
		userID,
		false,
		true,
	).Model(&Friend{}).Count(&count)
	return count, result.Error
}

// CountFriends gets a count of friends for the user.
func CountFriends(userID uint64) int64 {
	var count int64
	result := DB.Where(
		"target_user_id = ? AND approved = ? AND EXISTS (SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active')",
		userID,
		true,
	).Model(&Friend{}).Count(&count)
	if result.Error != nil {
		log.Error("CountFriends(%d): %s", userID, result.Error)
	}
	return count
}

/*
PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered
by friendship date.

The `requests` and `sent` bools are mutually exclusive (use only one, or neither). `requests`
asks for unanswered friend requests to you, and `sent` returns the friend requests that you
have sent and have not been answered.
*/
func PaginateFriends(user *User, requests bool, sent bool, ignored bool, pager *Pagination) ([]*User, error) {
	// We paginate over the Friend table.
	var (
		fs      = []*Friend{}
		userIDs = []uint64{}
		query   *gorm.DB
	)

	if requests && sent && ignored {
		return nil, errors.New("requests and sent are mutually exclusive options, use one or neither")
	}

	if requests {
		query = DB.Where(
			"target_user_id = ? AND approved = ? AND ignored IS NOT true",
			user.ID, false,
		)
	} else if sent {
		query = DB.Where(
			"source_user_id = ? AND approved = ? AND ignored IS NOT true",
			user.ID, false,
		)
	} else if ignored {
		query = DB.Where(
			"target_user_id = ? AND approved = ? AND ignored = ?",
			user.ID, false, true,
		)
	} else {
		query = DB.Where(
			"source_user_id = ? AND approved = ?",
			user.ID, true,
		)
	}

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

	// Now of these friends get their User objects.
	for _, friend := range fs {
		if requests || ignored {
			userIDs = append(userIDs, friend.SourceUserID)
		} else {
			userIDs = append(userIDs, friend.TargetUserID)
		}
	}

	return GetUsers(user, userIDs)
}

/*
PaginateOtherUserFriends gets a page of friends from another user, for their profile page.
*/
func PaginateOtherUserFriends(currentUser *User, user *User, pager *Pagination) ([]*User, error) {
	// We paginate over the Friend table.
	var (
		fs             = []*Friend{}
		blockedUserIDs = BlockedUserIDs(currentUser)
		userIDs        = []uint64{}
		wheres         = []string{}
		placeholders   = []interface{}{}
		query          = DB.Model(&Friend{})
	)

	// Get friends of the target user.
	wheres = append(wheres, "source_user_id = ? AND approved = ?")
	placeholders = append(placeholders, user.ID, true)

	// Don't show our blocked users in the result.
	if len(blockedUserIDs) > 0 {
		wheres = append(wheres, "target_user_id NOT IN ?")
		placeholders = append(placeholders, blockedUserIDs)
	}

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

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

	// Get the total count.
	query.Count(&pager.Total)

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

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

	return GetUsers(currentUser, userIDs)
}

// GetFriendRequests returns all pending friend requests for a user.
func GetFriendRequests(userID uint64) ([]*Friend, error) {
	var fs = []*Friend{}
	result := DB.Where(
		"target_user_id = ? AND approved = ?",
		userID,
		false,
	).Find(&fs)
	return fs, result.Error
}

// IgnoreFriendRequest ignores a pending friend request that was sent to targetUserID.
func IgnoreFriendRequest(currentUser *User, fromUser *User) error {
	// Is there a reverse friend request pending? (The one we ideally hope to mark Ignored)
	rev := &Friend{}
	reverse := DB.Where(
		"source_user_id = ? AND target_user_id = ?",
		fromUser.ID, currentUser.ID,
	).First(&rev).Error

	// If the reverse exists (requested us) mark it as Ignored.
	if reverse == nil {
		// Ignore the reverse friend request (happy path).
		log.Error("%s ignoring friend request from %s", currentUser.Username, fromUser.Username)
		rev.Approved = false
		rev.Ignored = true
		return rev.Save()
	}

	log.Error("rev: %+v", rev)
	return errors.New("unexpected error while ignoring friend request")
}

// RemoveFriend severs a friend connection both directions, used when
// rejecting a request or removing a friend.
func RemoveFriend(sourceUserID, targetUserID uint64) error {
	result := DB.Where(
		"(source_user_id = ? AND target_user_id = ?) OR "+
			"(target_user_id = ? AND source_user_id = ?)",
		sourceUserID, targetUserID,
		sourceUserID, targetUserID,
	).Delete(&Friend{})
	return result.Error
}

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

// FriendMap maps user IDs to friendship status for the current user.
type FriendMap map[uint64]bool

// MapFriends looks up a set of user IDs in bulk and returns a FriendMap suitable for templates.
func MapFriends(currentUser *User, users []*User) FriendMap {
	var (
		usermap  = FriendMap{}
		set      = map[uint64]interface{}{}
		distinct = []uint64{}
	)

	// Uniqueify users.
	for _, user := range users {
		if _, ok := set[user.ID]; ok {
			continue
		}
		set[user.ID] = nil
		distinct = append(distinct, user.ID)
	}

	var (
		matched = []*Friend{}
		result  = DB.Model(&Friend{}).Where(
			"source_user_id = ? AND target_user_id IN ? AND approved = ?",
			currentUser.ID, distinct, true,
		).Find(&matched)
	)

	if result.Error == nil {
		for _, row := range matched {
			usermap[row.TargetUserID] = true
		}
	}

	return usermap
}

// Get a user from the FriendMap.
func (um FriendMap) Get(id uint64) bool {
	return um[id]
}
