package photo

import (
	"bytes"
	"io"
	"net/http"
	"os"
	"path/filepath"

	"code.nonshy.com/nonshy/website/pkg/log"
	"code.nonshy.com/nonshy/website/pkg/models"
	"code.nonshy.com/nonshy/website/pkg/photo"
	"code.nonshy.com/nonshy/website/pkg/session"
	"code.nonshy.com/nonshy/website/pkg/templates"
)

// Upload photos controller.
func Upload() http.HandlerFunc {
	tmpl := templates.Must("photo/upload.html")
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var vars = map[string]interface{}{
			"Intent":    r.FormValue("intent"),
			"NeedsCrop": false,
		}

		// Query string parameters: what is the intent of this photo upload?
		// - If profile picture, the user will crop their image before posting it.
		// - If regular photo, user simply picks a picture and doesn't need to crop it.
		if vars["Intent"] == "profile_pic" {
			vars["NeedsCrop"] = true
		}

		user, err := session.CurrentUser(r)
		if err != nil {
			session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser")
		}

		// Get the current user's quota.
		var photoCount, photoQuota = photo.QuotaForUser(user)
		vars["PhotoCount"] = photoCount
		vars["PhotoQuota"] = photoQuota

		// If they do not have a profile picture currently set (and are not uploading one now),
		// the front-end should point this out to them.
		if (user.ProfilePhotoID == nil || *user.ProfilePhotoID == 0) && vars["Intent"] != "profile_pic" {
			// If they have no photo at all, make the default intent to upload one.
			if photoCount == 0 {
				templates.Redirect(w, r.URL.Path+"?intent=profile_pic")
				return
			}
			vars["NoProfilePicture"] = true
		}

		// Are they POSTing?
		if r.Method == http.MethodPost {
			var (
				caption    = r.PostFormValue("caption")
				isExplicit = r.PostFormValue("explicit") == "true"
				visibility = r.PostFormValue("visibility")
				isGallery  = r.PostFormValue("gallery") == "true"
				cropCoords = r.PostFormValue("crop")
				confirm1   = r.PostFormValue("confirm1") == "true"
				confirm2   = r.PostFormValue("confirm2") == "true"
			)

			// Are they at quota already?
			if photoCount >= photoQuota {
				session.FlashError(w, r, "You have too many photos to upload a new one. Please delete a photo to make room for a new one.")
				templates.Redirect(w, "/photo/u/"+user.Username)
				return
			}

			// They checked both boxes. The browser shouldn't allow them to
			// post but validate it here anyway...
			if !confirm1 || !confirm2 {
				session.FlashError(w, r, "You must agree to the terms to upload this picture.")
				templates.Redirect(w, r.URL.Path)
				return
			}

			// Parse and validate crop coordinates.
			var crop []int
			if vars["NeedsCrop"] == true {
				crop = photo.ParseCropCoords(cropCoords)
			}

			log.Error("parsed crop coords: %+v", crop)

			// Get their file upload.
			file, header, err := r.FormFile("file")
			if err != nil {
				session.FlashError(w, r, "Error receiving your file: %s", err)
				templates.Redirect(w, r.URL.Path)
				return
			}

			// GIF can not be uploaded for a profile picture.
			if filepath.Ext(header.Filename) == ".gif" && vars["Intent"] == "profile_pic" {
				session.FlashError(w, r, "GIF images are not acceptable for your profile picture.")
				templates.Redirect(w, r.URL.Path)
				return
			}

			// Read the file contents.
			log.Debug("Receiving uploaded file (%d bytes): %s", header.Size, header.Filename)
			var buf bytes.Buffer
			io.Copy(&buf, file)

			filename, cropFilename, err := photo.UploadPhoto(photo.UploadConfig{
				User:      user,
				Extension: filepath.Ext(header.Filename),
				Data:      buf.Bytes(),
				Crop:      crop,
			})
			if err != nil {
				session.FlashError(w, r, "Error in UploadPhoto: %s", err)
				templates.Redirect(w, r.URL.Path)
				return
			}

			// Configuration for the DB entry.
			ptmpl := models.Photo{
				UserID:          user.ID,
				Filename:        filename,
				CroppedFilename: cropFilename,
				Caption:         caption,
				Visibility:      models.PhotoVisibility(visibility),
				Gallery:         isGallery,
				Explicit:        isExplicit,
			}

			// Get the filesize.
			if stat, err := os.Stat(photo.DiskPath(filename)); err == nil {
				ptmpl.Filesize = stat.Size()
			}

			// Create it in DB!
			p, err := models.CreatePhoto(ptmpl)
			if err != nil {
				session.FlashError(w, r, "Couldn't create Photo in DB: %s", err)
			} else {
				log.Info("New photo! %+v", p)
			}

			// Are we uploading a profile pic? If so, set the user's pic now.
			if vars["Intent"] == "profile_pic" && cropFilename != "" {
				log.Info("User %s is setting their profile picture", user.Username)
				user.ProfilePhoto = *p
				user.Save()
			}

			// Notify all of our friends that we posted a new picture.
			go notifyFriendsNewPhoto(p, user)

			session.Flash(w, r, "Your photo has been uploaded successfully.")
			templates.Redirect(w, "/photo/u/"+user.Username)
			return
		}

		if err := tmpl.Execute(w, r, vars); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
}

// Create a notification for all our Friends about a new photo.
// Run in a background goroutine in case it takes a while.
func notifyFriendsNewPhoto(photo *models.Photo, currentUser *models.User) {
	var (
		friendIDs     []uint64
		notifyUserIDs []uint64
	)

	// Get the user's literal list of friends (with explicit opt-ins).
	if photo.Explicit {
		friendIDs = models.FriendIDsAreExplicit(currentUser.ID)
	} else {
		friendIDs = models.FriendIDs(currentUser.ID)
	}

	// Who to notify about this upload?
	if photo.Visibility == models.PhotoPrivate {
		// Private grantees
		if photo.Explicit {
			notifyUserIDs = models.PrivateGranteeAreExplicitUserIDs(currentUser.ID)
			log.Info("Notify %d EXPLICIT private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username)
		} else {
			notifyUserIDs = models.PrivateGranteeUserIDs(currentUser.ID)
			log.Info("Notify %d private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username)
		}
	} else if photo.Visibility == models.PhotoInnerCircle {
		// Inner circle members. If the pic is also Explicit, further narrow to explicit friend IDs.
		if photo.Explicit {
			notifyUserIDs = models.FriendIDsInCircleAreExplicit(currentUser.ID)
			log.Info("Notify %d EXPLICIT circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username)
		} else {
			notifyUserIDs = models.FriendIDsInCircle(currentUser.ID)
			log.Info("Notify %d circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username)
		}
	} else {
		// Friends only: we will notify exactly the friends we selected above.
		notifyUserIDs = friendIDs
	}

	// Filter down the notifyUserIDs to only include the user's friends.
	// Example: someone unlocked private photos for you, but you are not their friend.
	// You should not get notified about their new private photos.
	notifyUserIDs = models.FilterFriendIDs(notifyUserIDs, friendIDs)

	// Filter down the notifyUserIDs further to respect their notification opt-out preferences.
	notifyUserIDs = models.FilterPhotoUploadNotificationUserIDs(photo, notifyUserIDs)

	for _, fid := range notifyUserIDs {
		notif := &models.Notification{
			UserID:    fid,
			AboutUser: *currentUser,
			Type:      models.NotificationNewPhoto,
			TableName: "photos",
			TableID:   photo.ID,
		}
		if err := models.CreateNotification(notif); err != nil {
			log.Error("Couldn't notify user %d about %s's new photo: %s", fid, currentUser.Username, err)
		}
	}
}
