package models

import (
	"errors"
	"fmt"
	"strings"
	"time"

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

// Photo table.
type Photo struct {
	ID              uint64 `gorm:"primaryKey"`
	UserID          uint64 `gorm:"index"`
	Filename        string
	CroppedFilename string // if cropped, e.g. for profile photo
	Filesize        int64
	Caption         string
	Flagged         bool // photo has been reported by the community
	Visibility      PhotoVisibility
	Gallery         bool // photo appears in the public gallery (if public)
	Explicit        bool // is an explicit photo
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

// PhotoVisibility settings.
type PhotoVisibility string

const (
	PhotoPublic      PhotoVisibility = "public"  // on profile page and/or public gallery
	PhotoFriends     PhotoVisibility = "friends" // only friends can see it
	PhotoPrivate     PhotoVisibility = "private" // private
	PhotoInnerCircle PhotoVisibility = "circle"  // inner circle

	// Special visibility in case, on User Gallery view, user applies a filter
	// for friends-only picture but they are not friends with the user.
	PhotoNotAvailable PhotoVisibility = "not_available"
)

// PhotoVisibility preset settings.
var (
	PhotoVisibilityAll = []PhotoVisibility{
		PhotoPublic,
		PhotoFriends,
		PhotoPrivate,
	}

	// "All" but also for Inner Circle members.
	PhotoVisibilityCircle = []PhotoVisibility{
		PhotoPublic,
		PhotoFriends,
		PhotoPrivate,
		PhotoInnerCircle,
	}

	// Site Gallery visibility for when your friends show up in the gallery.
	// Or: "Friends + Gallery" photos can appear to your friends in the Site Gallery.
	PhotoVisibilityFriends = []string{
		string(PhotoPublic),
		string(PhotoFriends),
	}
)

// CreatePhoto with most of the settings you want (not ID or timestamps) in the database.
func CreatePhoto(tmpl Photo) (*Photo, error) {
	if tmpl.UserID == 0 {
		return nil, errors.New("UserID required")
	}

	p := &Photo{
		UserID:          tmpl.UserID,
		Filename:        tmpl.Filename,
		CroppedFilename: tmpl.CroppedFilename,
		Filesize:        tmpl.Filesize,
		Caption:         tmpl.Caption,
		Visibility:      tmpl.Visibility,
		Gallery:         tmpl.Gallery,
		Explicit:        tmpl.Explicit,
	}

	result := DB.Create(p)
	return p, result.Error
}

// GetPhoto by ID.
func GetPhoto(id uint64) (*Photo, error) {
	p := &Photo{}
	result := DB.First(&p, id)
	return p, result.Error
}

// GetPhotos by an array of IDs, mapped to their IDs.
func GetPhotos(IDs []uint64) (map[uint64]*Photo, error) {
	var (
		mp = map[uint64]*Photo{}
		ps = []*Photo{}
	)

	result := DB.Model(&Photo{}).Where("id IN ?", IDs).Find(&ps)
	for _, row := range ps {
		mp[row.ID] = row
	}

	return mp, result.Error
}

// UserGallery configuration for filtering gallery pages.
type UserGallery struct {
	Explicit   string // "", "true", "false"
	Visibility []PhotoVisibility
}

/*
PaginateUserPhotos gets a page of photos belonging to a user ID.
*/
func PaginateUserPhotos(userID uint64, conf UserGallery, pager *Pagination) ([]*Photo, error) {
	var (
		p            = []*Photo{}
		wheres       = []string{}
		placeholders = []interface{}{}
	)

	var explicit = []bool{}
	switch conf.Explicit {
	case "true":
		explicit = []bool{true}
	case "false":
		explicit = []bool{false}
	}

	wheres = append(wheres, "user_id = ? AND visibility IN ?")
	placeholders = append(placeholders, userID, conf.Visibility)

	if len(explicit) > 0 {
		wheres = append(wheres, "explicit = ?")
		placeholders = append(placeholders, explicit[0])
	}

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

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

	result := query.Offset(
		pager.GetOffset(),
	).Limit(pager.PerPage).Find(&p)

	return p, result.Error
}

// CountPhotos returns the total number of photos on a user's account.
func CountPhotos(userID uint64) int64 {
	var count int64
	result := DB.Where(
		"user_id = ?",
		userID,
	).Model(&Photo{}).Count(&count)
	if result.Error != nil {
		log.Error("CountPhotos(%d): %s", userID, result.Error)
	}
	return count
}

// CountPhotosICanSee returns the number of photos on an account which can be seen by the given viewer.
func CountPhotosICanSee(user *User, viewer *User) int64 {
	// Visibility filters to query by.
	var visibilities = []PhotoVisibility{
		PhotoPublic,
	}

	// Is the viewer in the inner circle?
	if viewer.IsInnerCircle() {
		visibilities = append(visibilities, PhotoInnerCircle)
	}

	// Is the viewer friends with the target?
	if AreFriends(user.ID, viewer.ID) {
		visibilities = append(visibilities, PhotoFriends)
	}

	// Is the viewer granted private access?
	if IsPrivateUnlocked(user.ID, viewer.ID) {
		visibilities = append(visibilities, PhotoPrivate)
	}

	// Get the photo count now.
	var count int64
	result := DB.Where(
		"user_id = ? AND visibility IN ?",
		user.ID, visibilities,
	).Model(&Photo{}).Count(&count)
	if result.Error != nil {
		log.Error("CountPhotosICanSee(%d, %d): %s", user.ID, viewer.ID, result.Error)
	}
	return count
}

// MapPhotoCounts returns a mapping of user ID to the CountPhotos()-equivalent result for each.
// It's used on the member directory to show photo counts on each user card.
func MapPhotoCounts(users []*User) PhotoCountMap {
	var (
		userIDs = []uint64{}
		result  = PhotoCountMap{}
	)

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

	type group struct {
		UserID     uint64
		PhotoCount int64
	}
	var groups = []group{}

	if res := DB.Table(
		"photos",
	).Select(
		"user_id, count(id) AS photo_count",
	).Where(
		"user_id IN ? AND visibility = ?", userIDs, PhotoPublic,
	).Group("user_id").Scan(&groups); res.Error != nil {
		log.Error("CountPhotosForUsers: %s", res.Error)
	}

	// Map the results in.
	for _, row := range groups {
		result[row.UserID] = row.PhotoCount
	}

	return result
}

// MapPhotoCounts returns a mapping of user ID to the CountPhotosICanSee()-equivalent result for each.
// It's used on the member directory to show photo counts on each user card.
/* TODO: under construction..
func MapPhotoCounts(users []*User, viewer *User) PhotoCountMap {
	var (
		userIDs = []uint64{}
		result  = PhotoCountMap{}

		wheres       = []string{}
		placeholders = []interface{}{}

		// User ID filters for the viewer's context.
		myFriendIDs         = FriendIDs(viewer.ID)
		myPrivateGrantedIDs = PrivateGrantedUserIDs(viewer.ID)
		isInnerCircle       = viewer.IsInnerCircle()
	)

	// Define "all photos visibilities"
	var (
		photosPublic = []PhotoVisibility{
			PhotoPublic,
		}
		photosFriends = []PhotoVisibility{
			PhotoPublic,
			PhotoFriends,
		}
		photosPrivate = []PhotoVisibility{
			PhotoPublic,
			PhotoPrivate,
		}
	)
	if isInnerCircle {
		photosPublic = append(photosPublic, PhotoInnerCircle)
		photosFriends = append(photosFriends, PhotoInnerCircle)
	}

	// Flatten the userIDs of all passed in users.
	for _, user := range users {
		userIDs = append(userIDs, user.ID)
	}

	// Build the where clause.
	wheres = append(wheres, "user_id IN ?")
	placeholders = append(placeholders, userIDs)

	log.Error("FRIEND IDS: %+v", myFriendIDs)

	// Filter by which photos are visible to us.
	wheres = append(wheres,
		"((user_id IN ? AND visibility IN ?) OR "+
			"(user_id IN ? AND visibility IN ?) OR "+
			"(user_id NOT IN ? AND visibility IN ?))",
	)
	placeholders = append(placeholders,
		myFriendIDs, photosFriends,
		myPrivateGrantedIDs, photosPrivate,
		myFriendIDs, photosPublic,
	)

	type group struct {
		UserID     uint64
		PhotoCount int64
	}
	var groups = []group{}

	if res := DB.Table(
		"photos",
	).Select(
		"user_id, count(id) AS photo_count",
	).Where(
		strings.Join(wheres, " AND "),
		placeholders...,
	).Group("user_id").Scan(&groups); res.Error != nil {
		log.Error("CountPhotosForUsers: %s", res.Error)
	}

	// Map the results in.
	for _, row := range groups {
		result[row.UserID] = row.PhotoCount
	}

	return result
}
*/

type PhotoCountMap map[uint64]int64

// Get a photo count for the given user ID from the map.
func (pc PhotoCountMap) Get(id uint64) int64 {
	if value, ok := pc[id]; ok {
		return value
	}
	return 0
}

// CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist)
func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) {
	query := DB.Where(
		"user_id = ? AND visibility IN ? AND explicit = ?",
		userID,
		visibility,
		true,
	)

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

// CountPublicPhotos returns the number of public photos on a user's page (to control whether the "invite to circle" prompt can appear).
func CountPublicPhotos(userID uint64) int64 {
	query := DB.Where(
		"user_id = ? AND visibility = ?",
		userID,
		PhotoPublic,
	)

	var count int64
	result := query.Model(&Photo{}).Count(&count)
	if result.Error != nil {
		log.Error("CountPublicPhotos(%d): %s", userID, result.Error)
	}
	return count
}

// DistinctPhotoTypes returns types of photos the user has: a set of public, friends, or private.
//
// The result is cached on the User the first time it's queried.
func (u *User) DistinctPhotoTypes() (result map[PhotoVisibility]struct{}) {
	if u.cachePhotoTypes != nil {
		return u.cachePhotoTypes
	}

	result = map[PhotoVisibility]struct{}{}

	var results = []*Photo{}
	query := DB.Model(&Photo{}).
		Select("DISTINCT photos.visibility").
		Where("user_id = ?", u.ID).
		Group("photos.visibility").
		Find(&results)
	if query.Error != nil {
		log.Error("User.DistinctPhotoTypes(%s): %s", u.Username, query.Error)
		return
	}

	for _, row := range results {
		log.Warn("DistinctPhotoTypes(%s): got %+v", u.Username, row)
		result[row.Visibility] = struct{}{}
	}

	u.cachePhotoTypes = result
	return
}

// Gallery config for the main Gallery paginator.
type Gallery struct {
	Explicit    string // Explicit filter
	Visibility  string // Visibility filter
	AdminView   bool   // Show all images
	IsShy       bool   // Current user is like a Shy Account (or: show self/friends and private photo grants only)
	FriendsOnly bool   // Only show self/friends instead of everybody's pics
}

/*
PaginateGalleryPhotos gets a page of all public user photos for the site gallery.

Admin view returns ALL photos regardless of Gallery status.
*/
func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Photo, error) {
	var (
		filterExplicit   = conf.Explicit
		filterVisibility = conf.Visibility
		adminView        = conf.AdminView
		friendsOnly      = conf.FriendsOnly // Show only self and friends pictures
		isShy            = conf.IsShy       // Self, friends, and private photo grants only
		p                = []*Photo{}
		query            *gorm.DB

		// Get the user ID and their preferences.
		userID     = user.ID
		explicitOK = user.Explicit // User opted-in for explicit content

		blocklist                = BlockedUserIDs(user)
		friendIDs                = FriendIDs(userID)
		privateUserIDs           = PrivateGrantedUserIDs(userID)
		privateUserIDsAreFriends = PrivateGrantedUserIDsAreFriends(user)
		wheres                   = []string{}
		placeholders             = []interface{}{}
	)

	// Define "all photos visibilities"
	var (
		photosPublic = []PhotoVisibility{
			PhotoPublic,
		}
		photosFriends = []PhotoVisibility{
			PhotoPublic,
			PhotoFriends,
		}
		photosPrivate = []PhotoVisibility{
			PhotoPrivate,
		}
	)
	if user.IsInnerCircle() {
		photosPublic = append(photosPublic, PhotoInnerCircle)
		photosFriends = append(photosFriends, PhotoInnerCircle)
	}

	// Admins see everything on the site (only an admin user can get an admin view).
	adminView = user.HasAdminScope(config.ScopePhotoModerator) && adminView

	// Include ourself in our friend IDs.
	friendIDs = append(friendIDs, userID)

	// What sets of User ID * Visibility filters to query under?
	var (
		visOrs          = []string{}
		visPlaceholders = []interface{}{}
	)

	// Whose photos can you see on the Site Gallery?
	if isShy {
		// Shy users can only see their Friends photos (public or friends visibility)
		// and any Private photos to whom they were granted access.
		visOrs = append(visOrs,
			"(user_id IN ? AND visibility IN ?)",
			"(user_id IN ? AND visibility IN ?)",
		)
		visPlaceholders = append(visPlaceholders,
			friendIDs, photosFriends,
			privateUserIDs, photosPrivate,
		)
	} else if friendsOnly {
		// User wants to see only self and friends photos.
		visOrs = append(visOrs, "(user_id IN ? AND visibility IN ?)")
		visPlaceholders = append(visPlaceholders, friendIDs, photosFriends)

		// If their friends granted private photos, include those too.
		if len(privateUserIDsAreFriends) > 0 {
			visOrs = append(visOrs, "(user_id IN ? AND visibility IN ?)")
			visPlaceholders = append(visPlaceholders, privateUserIDsAreFriends, photosPrivate)
		}
	} else {
		// You can see friends' Friend photos but only public for non-friends.
		visOrs = append(visOrs,
			"(user_id IN ? AND visibility IN ?)",
			"(user_id IN ? AND visibility IN ?)",
			"(user_id NOT IN ? AND visibility IN ?)",
		)
		visPlaceholders = append(placeholders,
			friendIDs, photosFriends,
			privateUserIDs, photosPrivate,
			friendIDs, photosPublic,
		)
	}

	// Join the User ID * Visibility filters into a nested "OR"
	wheres = append(wheres, fmt.Sprintf("(%s)", strings.Join(visOrs, " OR ")))
	placeholders = append(placeholders, visPlaceholders...)

	// Gallery photos only.
	wheres = append(wheres, "gallery = ?")
	placeholders = append(placeholders, true)

	// Filter blocked users.
	if len(blocklist) > 0 {
		wheres = append(wheres, "user_id NOT IN ?")
		placeholders = append(placeholders, blocklist)
	}

	// Non-explicit pics unless the user opted in. Allow explicit filter setting to override.
	if filterExplicit != "" {
		wheres = append(wheres, "explicit = ?")
		placeholders = append(placeholders, filterExplicit == "true")
	} else if !explicitOK {
		wheres = append(wheres, "explicit = ?")
		placeholders = append(placeholders, false)
	}

	// Is the user furthermore clamping the visibility filter?
	if filterVisibility != "" {
		wheres = append(wheres, "visibility = ?")
		placeholders = append(placeholders, filterVisibility)
	}

	// Only certified (and not banned) user photos.
	wheres = append(wheres,
		"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true AND status='active')",
	)

	// Exclude private users' photos.
	wheres = append(wheres,
		"NOT EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND visibility = 'private')",
	)

	// Admin view: get ALL PHOTOS on the site, period.
	if adminView {
		query = DB

		// Admin may filter too.
		if filterVisibility != "" {
			query = query.Where("visibility = ?", filterVisibility)
		}
		if filterExplicit != "" {
			query = query.Where("explicit = ?", filterExplicit == "true")
		}
	} else {
		query = DB.Where(
			strings.Join(wheres, " AND "),
			placeholders...,
		)
	}

	query = query.Order(pager.Sort)

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

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

// Delete photo.
func (p *Photo) Delete() error {
	result := DB.Delete(p)
	return result.Error
}
