package models

import (
	"errors"
	"strings"
	"time"

	"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
)

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

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

/*
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, error) {
	var count int64
	result := DB.Where(
		"user_id = ?",
		userID,
	).Model(&Photo{}).Count(&count)
	return count, result.Error
}

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

// PaginateGalleryPhotos gets a page of all public user photos for the site gallery. Admin view
// returns ALL photos regardless of Gallery status.
func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager *Pagination) ([]*Photo, error) {
	var (
		p            = []*Photo{}
		query        *gorm.DB
		blocklist    = BlockedUserIDs(userID)
		friendIDs    = FriendIDs(userID)
		wheres       = []string{}
		placeholders = []interface{}{}
	)

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

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

	// Only certified user photos.
	wheres = append(wheres,
		"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)",
	)

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