package models

import (
	"strings"
	"time"

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

// Notification table.
type Notification struct {
	ID          uint64           `gorm:"primaryKey"`
	UserID      uint64           `gorm:"index"` // who it belongs to
	AboutUserID *uint64          `form:"index"` // the other party of this notification
	AboutUser   User             `gorm:"foreignKey:about_user_id"`
	Type        NotificationType // like, comment, ...
	Read        bool             `gorm:"index"`
	TableName   string           // on which of your tables (photos, comments, ...)
	TableID     uint64
	Message     string // text associated, e.g. copy of comment added
	Link        string // associated URL, e.g. for comments
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

// Preload related tables for the forum (classmethod).
func (n *Notification) Preload() *gorm.DB {
	return DB.Preload("AboutUser.ProfilePhoto")
}

type NotificationType string

const (
	NotificationLike           NotificationType = "like"
	NotificationFriendApproved NotificationType = "friendship_approved"
	NotificationComment        NotificationType = "comment"
	NotificationAlsoCommented  NotificationType = "also_comment"
	NotificationAlsoPosted     NotificationType = "also_posted" // forum replies
	NotificationCertRejected   NotificationType = "cert_rejected"
	NotificationCertApproved   NotificationType = "cert_approved"
	NotificationPrivatePhoto   NotificationType = "private_photo"
	NotificationNewPhoto       NotificationType = "new_photo"
	NotificationInnerCircle    NotificationType = "inner_circle"
	NotificationCustom         NotificationType = "custom" // custom message pushed
)

// CreateNotification
func CreateNotification(n *Notification) error {
	// Insert via raw SQL query, reasoning:
	// the AboutUser relationship has gorm do way too much work:
	// - Upsert the user profile photo
	// - Upsert the user profile fields
	// - Upsert the user row itself
	// .. and if we notify all your friends, all these wasteful queries ran
	// for every single notification created!
	if n.AboutUserID == nil && n.AboutUser.ID > 0 {
		n.AboutUserID = &n.AboutUser.ID
	}
	return DB.Exec(
		`
		INSERT INTO notifications
		(user_id, about_user_id, type, read, table_name, table_id, message, link, created_at, updated_at)
		VALUES
		(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`,
		n.UserID,
		n.AboutUserID,
		n.Type,
		false,
		n.TableName,
		n.TableID,
		n.Message,
		n.Link,
		time.Now(),
		time.Now(),
	).Error
}

// GetNotification by ID.
func GetNotification(id uint64) (*Notification, error) {
	var n *Notification
	result := DB.Model(n).First(&n, id)
	return n, result.Error
}

// NotificationOptOut checks whether the user opts-out of a class of notification.
func (u *User) NotificationOptOut(name string) bool {
	return u.GetProfileField(name) == "true"
}

// RemoveNotification about a table ID, e.g. when removing a like.
func RemoveNotification(tableName string, tableID uint64) error {
	result := DB.Where(
		"table_name = ? AND table_id = ?",
		tableName, tableID,
	).Delete(&Notification{})
	return result.Error
}

// RemoveNotificationBulk about several table IDs, e.g. when bulk removing private photo upload
// notifications for everybody on the site.
func RemoveNotificationBulk(tableName string, tableIDs []uint64) error {
	result := DB.Where(
		"table_name = ? AND table_id IN ?",
		tableName, tableIDs,
	).Delete(&Notification{})
	return result.Error
}

// RemoveSpecificNotification to remove more specialized notifications where just removing by
// table name+ID is not adequate, e.g. for Private Photo Unlocks.
func RemoveSpecificNotification(userID uint64, t NotificationType, tableName string, tableID uint64) error {
	result := DB.Where(
		"user_id = ? AND type = ? AND table_name = ? AND table_id = ?",
		userID, t, tableName, tableID,
	).Delete(&Notification{})
	return result.Error
}

// RemoveSpecificNotificationBulk can remove notifications about several TableIDs of the same type,
// e.g. to bulk remove new private photo upload notifications.
func RemoveSpecificNotificationBulk(userID uint64, t NotificationType, tableName string, tableIDs []uint64) error {
	result := DB.Where(
		"user_id = ? AND type = ? AND table_name = ? AND table_id IN ?",
		userID, t, tableName, tableIDs,
	).Delete(&Notification{})
	return result.Error
}

// MarkNotificationsRead sets all a user's notifications to read.
func MarkNotificationsRead(user *User) error {
	return DB.Model(&Notification{}).Where(
		"user_id = ? AND read IS NOT TRUE",
		user.ID,
	).Update("read", true).Error
}

// ClearAllNotifications removes a user's entire notification table.
func ClearAllNotifications(user *User) error {
	return DB.Where(
		"user_id = ?", user.ID,
	).Delete(&Notification{}).Error
}

// CountUnreadNotifications gets the count of unread Notifications for a user.
func CountUnreadNotifications(user *User) (int64, error) {
	var (
		blockedUserIDs = BlockedUserIDs(user)
		where          = []string{
			"user_id = ? AND read = ?",
		}
		placeholders = []interface{}{
			user.ID, false,
		}
	)

	// Blocking user IDs?
	if len(blockedUserIDs) > 0 {
		where = append(where, "about_user_id NOT IN ?")
		placeholders = append(placeholders, blockedUserIDs)
	}

	// Don't show messages from banned or disabled accounts.
	where = append(where, `
		EXISTS (
			SELECT 1
			FROM users
			WHERE users.id = notifications.about_user_id
			AND users.status = 'active'
		)
	`)

	query := DB.Where(
		strings.Join(where, " AND "),
		placeholders...,
	)

	var count int64
	result := query.Model(&Notification{}).Count(&count)
	return count, result.Error
}

// PaginateNotifications returns the user's notifications.
func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, error) {
	var (
		ns             = []*Notification{}
		blockedUserIDs = BlockedUserIDs(user)
		where          = []string{
			"user_id = ?",
		}
		placeholders = []interface{}{
			user.ID,
		}
	)

	// Suppress historic notifications about blocked users.
	if len(blockedUserIDs) > 0 {
		where = append(where, "about_user_id NOT IN ?")
		placeholders = append(placeholders, blockedUserIDs)
	}

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

	query := (&Notification{}).Preload().Where(
		strings.Join(where, " AND "),
		placeholders...,
	).Order(
		pager.Sort,
	)

	query.Model(&Notification{}).Count(&pager.Total)
	result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&ns)
	return ns, result.Error
}

// FilterPhotoUploadNotificationUserIDs will narrow a set of UserIDs who would be notified about
// a new photo upload to respect each user's preference for notification opt-outs.
//
// It is assumed that userIDs are already narrowed down to Friends of the current user.
func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint64 {
	var (
		result = []uint64{}

		// Collect notification opt-out profile fields and map them by user ID for easy lookup.
		prefs    = []*ProfileField{}
		mapPrefs = map[uint64]map[string]bool{}
	)
	if len(userIDs) == 0 {
		return userIDs
	}

	// Collect opt-out preferences for these users.
	r := DB.Model(&ProfileField{}).Where(
		"user_id IN ? AND name IN ?",
		userIDs, []string{
			config.NotificationOptOutFriendPhotos,   // all friends' photos
			config.NotificationOptOutPrivatePhotos,  // private photos from friends
			config.NotificationOptOutExplicitPhotos, // explicit photos
		},
	).Find(&prefs)
	if r.Error != nil {
		log.Error("FilterPhotoUploadNotificationUserIDs: couldn't collect user preferences: %s", r.Error)
	}

	// Map the preferences by user ID.
	for _, row := range prefs {
		if _, ok := mapPrefs[row.UserID]; !ok {
			mapPrefs[row.UserID] = map[string]bool{}
		}
		mapPrefs[row.UserID][row.Name] = row.Value == "true"
	}

	// Narrow the notification recipients based on photo property and their preferences.
	for _, userID := range userIDs {
		// Skip explicit photo notification?
		if photo.Explicit && mapPrefs[userID][config.NotificationOptOutExplicitPhotos] {
			continue
		}

		// Skip private photo notification?
		if photo.Visibility == PhotoPrivate && mapPrefs[userID][config.NotificationOptOutPrivatePhotos] {
			continue
		}

		// Skip friend photo notifications?
		if mapPrefs[userID][config.NotificationOptOutFriendPhotos] {
			continue
		}

		// They get the notification.
		result = append(result, userID)
	}

	return result
}

// Save a notification.
func (n *Notification) Save() error {
	return DB.Save(n).Error
}

// Delete a notification.
func (n *Notification) Delete() error {
	return DB.Delete(n).Error
}

// NotificationBody can store remote tables mapped.
type NotificationBody struct {
	PhotoID   uint64
	ThreadID  uint64
	CommentID uint64
	Photo     *Photo
	Thread    *Thread
	Comment   *Comment
}

type NotificationMap map[uint64]*NotificationBody

// Get a notification's body from the map.
func (m NotificationMap) Get(id uint64) *NotificationBody {
	if body, ok := m[id]; ok {
		return body
	}
	return &NotificationBody{}
}

// MapNotifications loads associated assets, like Photos, mapped to their notification ID.
func MapNotifications(ns []*Notification) NotificationMap {
	var (
		IDs    = []uint64{}
		result = NotificationMap{}
	)

	// Collect notification IDs.
	for _, row := range ns {
		IDs = append(IDs, row.ID)
		result[row.ID] = &NotificationBody{}
	}

	result.mapNotificationPhotos(IDs)
	result.mapNotificationThreads(IDs)

	// NOTE: comment loading is not used - was added when trying to add "Like" buttons inside
	// your Comment notifications. But when a photo is commented on, the notification table_name=photos,
	// with the comment ID not so readily accessible.
	//
	// result.mapNotificationComments(IDs)

	return result
}

// Helper function of MapNotifications to eager load Photo attachments.
func (nm NotificationMap) mapNotificationPhotos(IDs []uint64) {
	type scanner struct {
		PhotoID        uint64
		NotificationID uint64
	}
	var scan []scanner

	// Load all of these that have photos.
	err := DB.Table(
		"notifications",
	).Joins(
		"JOIN photos ON (notifications.table_name='photos' AND notifications.table_id=photos.id)",
	).Select(
		"photos.id AS photo_id",
		"notifications.id AS notification_id",
	).Where(
		"notifications.id IN ?",
		IDs,
	).Scan(&scan)
	if err.Error != nil {
		log.Error("Couldn't select photo IDs for notifications: %s", err.Error)
	}

	// Collect and load all the photos by ID.
	var photoIDs = []uint64{}
	for _, row := range scan {
		// Store the photo ID in the result now.
		nm[row.NotificationID].PhotoID = row.PhotoID
		photoIDs = append(photoIDs, row.PhotoID)
	}

	// Load the photos.
	if len(photoIDs) > 0 {
		if photos, err := GetPhotos(photoIDs); err != nil {
			log.Error("Couldn't load photo IDs for notifications: %s", err)
		} else {
			// Marry them to their notification IDs.
			for _, body := range nm {
				if photo, ok := photos[body.PhotoID]; ok {
					body.Photo = photo
				}
			}
		}
	}
}

// Helper function of MapNotifications to eager load Thread attachments.
func (nm NotificationMap) mapNotificationThreads(IDs []uint64) {
	type scanner struct {
		ThreadID       uint64
		NotificationID uint64
	}
	var scan []scanner

	// Load all of these that have threads.
	err := DB.Table(
		"notifications",
	).Joins(
		"JOIN threads ON (notifications.table_name='threads' AND notifications.table_id=threads.id)",
	).Select(
		"threads.id AS thread_id",
		"notifications.id AS notification_id",
	).Where(
		"notifications.id IN ?",
		IDs,
	).Scan(&scan)
	if err.Error != nil {
		log.Error("Couldn't select thread IDs for notifications: %s", err.Error)
	}

	// Collect and load all the threads by ID.
	var threadIDs = []uint64{}
	for _, row := range scan {
		// Store the thread ID in the result now.
		nm[row.NotificationID].ThreadID = row.ThreadID
		threadIDs = append(threadIDs, row.ThreadID)
	}

	// Load the threads.
	if len(threadIDs) > 0 {
		if threads, err := GetThreads(threadIDs); err != nil {
			log.Error("Couldn't load thread IDs for notifications: %s", err)
		} else {
			// Marry them to their notification IDs.
			for _, body := range nm {
				if thread, ok := threads[body.ThreadID]; ok {
					body.Thread = thread
				}
			}
		}
	}
}

// Helper function of MapNotifications to eager load Comment attachments.
func (nm NotificationMap) mapNotificationComments(IDs []uint64) {
	type scanner struct {
		CommentID      uint64
		NotificationID uint64
	}
	var scan []scanner

	// Load all of these that have comments.
	err := DB.Table(
		"notifications",
	).Joins(
		"JOIN comments ON (notifications.table_name='comments' AND notifications.table_id=comments.id)",
	).Select(
		"comments.id AS comment_id",
		"notifications.id AS notification_id",
	).Where(
		"notifications.id IN ?",
		IDs,
	).Scan(&scan)
	if err.Error != nil {
		log.Error("Couldn't select comment IDs for notifications: %s", err.Error)
	}

	// Collect and load all the comments by ID.
	var commentIDs = []uint64{}
	for _, row := range scan {
		// Store the comment ID in the result now.
		nm[row.NotificationID].CommentID = row.CommentID
		commentIDs = append(commentIDs, row.CommentID)
	}

	// Load the comments.
	if len(commentIDs) > 0 {
		if comments, err := GetComments(commentIDs); err != nil {
			log.Error("Couldn't load comment IDs for notifications: %s", err)
		} else {
			// Marry them to their notification IDs.
			for _, body := range nm {
				if comment, ok := comments[body.CommentID]; ok {
					body.Comment = comment
				}
			}
		}
	}
}
