package models

import (
	"errors"
	"strings"
	"time"

	"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                     = "friends" // only friends can see it
	PhotoPrivate                     = "private" // private
	PhotoInnerCircle                 = "circle"  // inner circle
)

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

/*
PaginateUserPhotos gets a page of photos belonging to a user ID.
*/
func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK bool, pager *Pagination) ([]*Photo, error) {
	var p = []*Photo{}

	var explicit = []bool{false}
	if explicitOK {
		explicit = []bool{true, false}
	}

	query := DB.Where(
		"user_id = ? AND visibility IN ? AND explicit IN ?",
		userID,
		visibility,
		explicit,
	).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
}

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

// 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
	ShyView    bool   // Current user is shy (self/friends only)
}

/*
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
		isShy            = conf.ShyView
		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(userID)
		friendIDs      = FriendIDs(userID)
		privateUserIDs = PrivateGrantedUserIDs(userID)
		wheres         = []string{}
		placeholders   = []interface{}{}
	)

	// Define "all photos visibilities"
	var photosAll = PhotoVisibilityAll
	if user.IsInnerCircle() {
		photosAll = PhotoVisibilityCircle
	}

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

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

	// 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.
		wheres = append(wheres,
			"((user_id IN ? AND visibility IN ?) OR "+
				"(user_id IN ? AND visibility IN ?))",
		)
		placeholders = append(placeholders,
			friendIDs, PhotoVisibilityFriends,
			privateUserIDs, photosAll,
		)
	} else {
		// You can see friends' Friend photos but only public for non-friends.
		wheres = append(wheres,
			"((user_id IN ? AND visibility IN ?) OR "+
				"(user_id IN ? AND visibility IN ?) OR "+
				"(user_id NOT IN ? AND visibility = ?))",
		)
		placeholders = append(placeholders,
			friendIDs, PhotoVisibilityFriends,
			privateUserIDs, photosAll,
			friendIDs, PhotoPublic,
		)
	}

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