package account

import (
	"fmt"
	"net/http"
	nm "net/mail"
	"strconv"
	"strings"
	"time"

	"code.nonshy.com/nonshy/website/pkg/config"
	"code.nonshy.com/nonshy/website/pkg/geoip"
	"code.nonshy.com/nonshy/website/pkg/log"
	"code.nonshy.com/nonshy/website/pkg/mail"
	"code.nonshy.com/nonshy/website/pkg/models"
	"code.nonshy.com/nonshy/website/pkg/redis"
	"code.nonshy.com/nonshy/website/pkg/session"
	"code.nonshy.com/nonshy/website/pkg/templates"
	"code.nonshy.com/nonshy/website/pkg/utility"
	"github.com/google/uuid"
)

// ChangeEmailToken for Redis.
type ChangeEmailToken struct {
	Token    string
	UserID   uint64
	NewEmail string
}

// Delete the change email token.
func (t ChangeEmailToken) Delete() error {
	return redis.Delete(fmt.Sprintf(config.ChangeEmailRedisKey, t.Token))
}

// User settings page. (/settings).
func Settings() http.HandlerFunc {
	tmpl := templates.Must("account/settings.html")
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		vars := map[string]interface{}{
			"Enum": config.ProfileEnums,
		}

		// Load the current user in case of updates.
		user, err := session.CurrentUser(r)
		if err != nil {
			session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
			templates.Redirect(w, r.URL.Path)
			return
		}

		// URL hashtag to redirect to
		var hashtag string

		// Are we POSTing?
		if r.Method == http.MethodPost {
			intent := r.PostFormValue("intent")
			switch intent {
			case "profile":
				// Setting profile values.
				hashtag = "#profile"
				var (
					displayName = r.PostFormValue("display_name")
					dob         = r.PostFormValue("dob")
				)

				// Set user attributes.
				user.Name = &displayName

				// Birthdate, now required.
				if birthdate, err := time.Parse("2006-01-02", dob); err != nil {
					session.FlashError(w, r, "Incorrect format for birthdate; should be in yyyy-mm-dd format but got: %s", dob)
				} else {
					// Validate birthdate is at least age 18.
					if utility.Age(birthdate) < 18 {
						session.FlashError(w, r, "Invalid birthdate: you must be at least 18 years old to use this site.")
						templates.Redirect(w, r.URL.Path)
						return
					}

					// If the user changes their birthdate, notify the admin.
					if !user.Birthdate.IsZero() && user.Birthdate.Format("2006-01-02") != dob {
						// Create an admin Feedback model.
						fb := &models.Feedback{
							Intent:    "report",
							Subject:   "report.dob",
							UserID:    user.ID,
							TableName: "users",
							TableID:   user.ID,
							Message: fmt.Sprintf(
								"A user has modified their birthdate on their profile page!\n\n"+
									"* Original: %s (age %d)\n* Updated: %s (age %d)",
								user.Birthdate, utility.Age(user.Birthdate),
								birthdate, utility.Age(birthdate),
							),
						}

						// Save the feedback.
						if err := models.CreateFeedback(fb); err != nil {
							log.Error("Couldn't save feedback from user updating their DOB: %s", err)
						}
					}

					// Work around DST issues: set the hour to noon.
					user.Birthdate = birthdate.Add(12 * time.Hour)
				}

				// Set profile attributes.
				for _, attr := range config.ProfileFields {
					user.SetProfileField(attr, r.PostFormValue(attr))
				}

				// "Looking For" checkbox list.
				if hereFor, ok := r.PostForm["here_for"]; ok {
					user.SetProfileField("here_for", strings.Join(hereFor, ","))
				}

				if err := user.Save(); err != nil {
					session.FlashError(w, r, "Failed to save user to database: %s", err)
				}

				session.Flash(w, r, "Profile settings updated!")
			case "preferences":
				hashtag = "#prefs"
				var (
					explicit     = r.PostFormValue("explicit") == "true"
					blurExplicit = r.PostFormValue("blur_explicit")
					autoplayGif  = r.PostFormValue("autoplay_gif")
				)

				user.Explicit = explicit

				// Set profile field prefs.
				user.SetProfileField("blur_explicit", blurExplicit)
				if autoplayGif != "true" {
					autoplayGif = "false"
				}
				user.SetProfileField("autoplay_gif", autoplayGif)

				if err := user.Save(); err != nil {
					session.FlashError(w, r, "Failed to save user to database: %s", err)
				}

				session.Flash(w, r, "Website preferences updated!")
			case "privacy":
				hashtag = "#privacy"
				var (
					visibility = models.UserVisibility(r.PostFormValue("visibility"))
					dmPrivacy  = r.PostFormValue("dm_privacy")
				)

				user.Visibility = models.UserVisibilityPublic

				for _, cmp := range models.UserVisibilityOptions {
					if visibility == cmp {
						user.Visibility = visibility
					}
				}

				// Set profile field prefs.
				user.SetProfileField("dm_privacy", dmPrivacy)

				if err := user.Save(); err != nil {
					session.FlashError(w, r, "Failed to save user to database: %s", err)
				}

				session.Flash(w, r, "Privacy settings updated!")
			case "notifications":
				hashtag = "#notifications"

				// Store their notification opt-outs.
				for _, key := range config.NotificationOptOutFields {
					var value = r.PostFormValue(key)

					// Boolean flip for DB storage:
					// - Pre-existing users before these options are added have no pref stored in the DB
					// - The default pref is opt-IN (receive all notifications)
					// - The checkboxes on front-end are on by default, uncheck them to opt-out, checkbox value="true"
					// - So when they post as "true" (default), we keep the notifications sending
					// - If they uncheck the box, no value is sent and that's an opt-out.
					if value == "" {
						value = "true" // opt-out, store opt-out=true in the DB
					} else if value == "true" {
						value = "false" // the box remained checked, they don't opt-out, store opt-out=false in the DB
					}

					// Save it. TODO: fires off inserts/updates for each one,
					// probably not performant to do.
					user.SetProfileField(key, value)
				}
				session.Flash(w, r, "Notification preferences updated!")

				// Save the user for new fields to be committed to DB.
				if err := user.Save(); err != nil {
					session.FlashError(w, r, "Failed to save user to database: %s", err)
				}

				// Are they unsubscribing from all threads?
				if r.PostFormValue("unsubscribe_all_threads") == "true" {
					if err := models.UnsubscribeAllThreads(user); err != nil {
						session.FlashError(w, r, "Couldn't unsubscribe from threads: %s", err)
					} else {
						session.Flash(w, r, "Unsubscribed from all comment threads!")
					}
				}
			case "location":
				hashtag = "#location"
				var (
					source = r.PostFormValue("source")
					latStr = r.PostFormValue("latitude")
					lonStr = r.PostFormValue("longitude")
				)

				// Get and update the user's location.
				location := models.GetUserLocation(user.ID)
				location.Source = source

				if lat, err := strconv.ParseFloat(latStr, 64); err == nil {
					location.Latitude = lat
				} else {
					location.Latitude = 0
				}

				if lon, err := strconv.ParseFloat(lonStr, 64); err == nil {
					location.Longitude = lon
				} else {
					location.Longitude = 0
				}

				// Save it.
				if err := location.Save(); err != nil {
					session.FlashError(w, r, "Couldn't save your location preference: %s", err)
				} else {
					session.Flash(w, r, "Location settings updated!")
				}
			case "settings":
				hashtag = "#account"
				var (
					oldPassword = r.PostFormValue("old_password")
					changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email")))
					password1   = strings.TrimSpace(r.PostFormValue("new_password"))
					password2   = strings.TrimSpace(r.PostFormValue("new_password2"))
				)

				// Their old password is needed to make any changes to their account.
				if err := user.CheckPassword(oldPassword); err != nil {
					session.FlashError(w, r, "Could not make changes to your account settings as the 'current password' you entered was incorrect.")
					templates.Redirect(w, r.URL.Path)
					return
				}

				// Changing their email?
				if changeEmail != user.Email {
					// Validate the email.
					if _, err := nm.ParseAddress(changeEmail); err != nil {
						session.FlashError(w, r, "The email address you entered is not valid: %s", err)
						templates.Redirect(w, r.URL.Path)
						return
					}

					// Email must not already exist.
					if _, err := models.FindUser(changeEmail); err == nil {
						session.FlashError(w, r, "That email address is already in use.")
						templates.Redirect(w, r.URL.Path)
						return
					}

					// Create a tokenized link.
					token := ChangeEmailToken{
						Token:    uuid.New().String(),
						UserID:   user.ID,
						NewEmail: changeEmail,
					}
					if err := redis.Set(fmt.Sprintf(config.ChangeEmailRedisKey, token.Token), token, config.SignupTokenExpires); err != nil {
						session.FlashError(w, r, "Failed to create change email token: %s", err)
						templates.Redirect(w, r.URL.Path)
						return
					}

					err := mail.Send(mail.Message{
						To:       changeEmail,
						Subject:  "Verify your e-mail address",
						Template: "email/verify_email.html",
						Data: map[string]interface{}{
							"Title":       config.Title,
							"URL":         config.Current.BaseURL + "/settings/confirm-email?token=" + token.Token,
							"ChangeEmail": true,
						},
					})
					if err != nil {
						session.FlashError(w, r, "Error sending a confirmation email to %s: %s", changeEmail, err)
					} else {
						session.Flash(w, r, "Please verify your new email address. A link has been sent to %s to confirm.", changeEmail)
					}
				}

				// Changing their password?
				if password1 != "" {
					if password2 != password1 {
						log.Error("pw1=%s  pw2=%s", password1, password2)
						session.FlashError(w, r, "Couldn't change your password: your new passwords do not match.")
					} else {
						// Hash the new password.
						if err := user.HashPassword(password1); err != nil {
							session.FlashError(w, r, "Failed to hash your new password: %s", err)
						} else {
							// Save the user row.
							if err := user.Save(); err != nil {
								session.FlashError(w, r, "Failed to update your password in the database: %s", err)
							} else {
								session.Flash(w, r, "Your password has been updated.")
							}
						}
					}
				}
			default:
				session.FlashError(w, r, "Unknown POST intent value. Please try again.")
			}

			templates.Redirect(w, r.URL.Path+hashtag+".")
			return
		}

		// For the Location tab: get GeoIP insights.
		insights, err := geoip.GetRequestInsights(r)
		if err != nil {
			log.Error("GetRequestInsights: %s", err)
		}
		vars["GeoIPInsights"] = insights
		vars["UserLocation"] = models.GetUserLocation(user.ID)

		// Show enabled status for 2FA.
		vars["TwoFactorEnabled"] = models.Get2FA(user.ID).Enabled

		// Count of subscribed comment threads.
		vars["SubscriptionCount"] = models.CountSubscriptions(user)

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

// ConfirmEmailChange after a user tries to change their email.
func ConfirmEmailChange() http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var tokenStr = r.FormValue("token")

		if tokenStr != "" {
			var token ChangeEmailToken
			if err := redis.Get(fmt.Sprintf(config.ChangeEmailRedisKey, tokenStr), &token); err != nil {
				session.FlashError(w, r, "Invalid token. Please try again to change your email address.")
				templates.Redirect(w, "/")
				return
			}

			// Verify new email still doesn't already exist.
			if _, err := models.FindUser(token.NewEmail); err == nil {
				session.FlashError(w, r, "Couldn't update your email address: it is already in use by another member.")
				templates.Redirect(w, "/")
				return
			}

			// Look up the user.
			user, err := models.GetUser(token.UserID)
			if err != nil {
				session.FlashError(w, r, "Didn't find the user that this email change was for. Please try again.")
				templates.Redirect(w, "/")
				return
			}

			// Burn the token.
			if err := token.Delete(); err != nil {
				log.Error("ChangeEmail: couldn't delete Redis token: %s", err)
			}

			// Make the change.
			user.Email = token.NewEmail
			if err := user.Save(); err != nil {
				session.FlashError(w, r, "Couldn't save the change to your user: %s", err)
			} else {
				session.Flash(w, r, "Your email address has been confirmed and updated.")
				templates.Redirect(w, "/")
			}
		} else {
			session.FlashError(w, r, "Invalid change email token. Please try again.")
		}

		templates.Redirect(w, "/")
	})
}
