package controllers

import (
	"bytes"
	"fmt"
	"html/template"
	"net/http"
	"strconv"
	"strings"

	"git.kirsle.net/apps/gophertype/pkg/authentication"
	"git.kirsle.net/apps/gophertype/pkg/mail"
	"git.kirsle.net/apps/gophertype/pkg/models"
	"git.kirsle.net/apps/gophertype/pkg/responses"
	"git.kirsle.net/apps/gophertype/pkg/session"
	"github.com/albrow/forms"
	uuid "github.com/satori/go.uuid"
)

// RenderComments returns the partial comments HTML to embed on a page.
func RenderComments(w http.ResponseWriter, r *http.Request, subject string, ids ...string) template.HTML {
	thread := strings.Join(ids, "-")
	return renderComments(w, r, subject, thread, false)
}

// RenderCommentsRO returns a read-only comment view.
func RenderCommentsRO(w http.ResponseWriter, r *http.Request, ids ...string) template.HTML {
	thread := strings.Join(ids, "-")
	return renderComments(w, r, "", thread, true)
}

// RenderComment renders the HTML partial for a single comment in the thread.
func RenderComment(w http.ResponseWriter, r *http.Request, com models.Comment, originURL string, editable bool) template.HTML {
	var (
		html      = bytes.NewBuffer([]byte{})
		v         = responses.NewTemplateVars(html, r)
		editToken = getEditToken(w, r)
	)
	v.V["Comment"] = com
	v.V["Editable"] = editable && (com.EditToken == editToken || authentication.LoggedIn(r))
	v.V["OriginURL"] = originURL
	responses.PartialTemplate(html, r, "_builtin/comments/entry.partial.gohtml", v)
	return template.HTML(html.String())
}

// RenderCommentForm renders the comment entry form HTML onto a page.
func RenderCommentForm(r *http.Request, com models.Comment, subject, threadID, originURL string) template.HTML {
	var (
		html = bytes.NewBuffer([]byte{})
		v    = responses.NewTemplateVars(html, r)
	)
	v.V["Comment"] = com
	v.V["Subject"] = subject
	v.V["ThreadID"] = threadID
	v.V["OriginURL"] = originURL
	responses.PartialTemplate(html, r, "_builtin/comments/form.partial.gohtml", v)
	return template.HTML(html.String())
}

// renderComments is the internal logic for both RenderComments and RenderCommentsRO.
func renderComments(w http.ResponseWriter, r *http.Request, subject string, thread string, readonly bool) template.HTML {
	var (
		html = bytes.NewBuffer([]byte{})
		v    = responses.NewTemplateVars(w, r)
		ses  = session.Get(r)
	)

	comments, err := models.Comments.GetThread(thread)
	if err != nil {
		return template.HTML(fmt.Sprintf("[comment error: %s]", err))
	}

	// Load their cached name and email from any previous comments the user posted.
	var (
		name      = ses.Name
		email     = ses.Email
		editToken = ses.EditToken
	)

	// Logged in? Populate defaults from the user info.
	if currentUser, err := authentication.CurrentUser(r); err == nil {
		name = currentUser.Name
		email = currentUser.Email
	}

	// v.V["posts"] = posts.Posts
	v.V["Readonly"] = readonly
	v.V["Subject"] = subject
	v.V["ThreadID"] = thread
	v.V["Comments"] = comments
	v.V["OriginURL"] = r.URL.Path
	v.V["NewComment"] = models.Comment{
		Name:      name,
		Email:     email,
		EditToken: editToken,
	}
	responses.PartialTemplate(html, r, "_builtin/comments/comments.partial.gohtml", v)
	return template.HTML(html.String())
}

// ReadComments handles the GET /comments for viewing recently added comments
// side wide.
func ReadComments(w http.ResponseWriter, r *http.Request) {
	var (
		v     = responses.NewTemplateVars(w, r)
		query = r.URL.Query()
	)

	// Query parameters.
	page, _ := strconv.Atoi(query.Get("page"))
	perPage, _ := strconv.Atoi(query.Get("per_page"))

	// Get the comments.
	comments, err := models.Comments.GetRecent(page, perPage)
	if err != nil {
		responses.Error(w, r, http.StatusInternalServerError, err.Error())
		return
	}

	v.V["PagedComments"] = comments
	responses.RenderTemplate(w, r, "_builtin/comments/recent.gohtml", v)
}

// PostComment handles all of the top-level blog index routes
// for Post, Preview, Delete comments.
func PostComment(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		ReadComments(w, r)
		return
	}

	var (
		v         = responses.NewTemplateVars(w, r)
		editToken = getEditToken(w, r)
		comment   = models.Comments.New()
		ses       = session.Get(r)
		editing   bool // true if editing an existing comment
	)

	// Check if the user is logged in.
	var loggedIn bool
	currentUser, err := authentication.CurrentUser(r)
	loggedIn = err == nil

	// Get form parameters.
	form, _ := forms.Parse(r)
	v.V["Subject"] = form.Get("subject")
	v.V["ThreadID"] = form.Get("thread")
	v.V["OriginURL"] = form.Get("origin")

	// Are they editing an existing post ID?
	if id := form.GetInt("id"); id > 0 {
		// Load the comment from DB.
		com, err := models.Comments.Load(id)
		if err != nil {
			responses.NotFound(w, r)
			return
		}

		// Verify the user's EditToken matches this comment.
		if editToken != com.EditToken && !loggedIn {
			responses.Forbidden(w, r, "You do not have permission to edit that comment.")
			return
		}

		editing = true
		comment = com
	}

	comment.EditToken = editToken

	for {
		// Validate form parameters.
		val := form.Validator()
		val.Require("body")
		if !form.GetBool("editing") {
			comment.ParseForm(form)
		}

		if val.HasErrors() {
			v.ValidationError = val.ErrorMap()
			break
		}

		// Cache their name and email in their session, for future requests.
		ses.Email = form.Get("email")
		ses.Name = form.Get("name")
		ses.Save(w)

		switch form.Get("submit") {
		case "delete":
			v.V["deleting"] = true
		case "confirm-delete":
			// Delete the comment.
			err := comment.Delete()
			if err != nil {
				session.Flash(w, r, "Error deleting the comment: %s", err)
			} else {
				session.Flash(w, r, "Comment has been deleted!")
			}
			responses.Redirect(w, r, form.Get("origin"))
			return
		case "preview":
			v.V["preview"] = comment.HTML()
		case "post":
			// If we're logged in, tag our user ID with this post.
			if loggedIn && !editing {
				comment.UserID = currentUser.ID
			}

			// Store the OriginURL for this comment.
			comment.OriginURL = form.Get("origin")

			// Post their comment.
			err := comment.Save()
			if err != nil {
				session.Flash(w, r, "Error posting comment: %s", err)
				responses.Redirect(w, r, form.Get("origin"))
				return
			}

			// Notify site admins and subscribers by email.
			go mail.NotifyComment(v.V["Subject"].(string), v.V["OriginURL"].(string), comment)

			session.Flash(w, r, "Your comment has been added!")
			responses.Redirect(w, r, form.Get("origin"))
			return
		}

		break
	}

	v.V["NewComment"] = comment
	responses.RenderTemplate(w, r, "_builtin/comments/preview.gohtml", v)
}

// ManageSubscription helps users unsubscribe from comment threads.
func ManageSubscription(w http.ResponseWriter, r *http.Request) {
	var (
		v       = responses.NewTemplateVars(w, r)
		query   = r.URL.Query()
		qThread = query.Get("t")
		qEmail  = strings.ToLower(strings.TrimSpace(query.Get("e")))
		qAll    = query.Get("all")
	)

	// Are we unsubscribing from all?
	if qEmail != "" && qAll != "" {
		err := models.Comments.UnsubscribeFromAll(qEmail)
		if err != nil {
			session.Flash(w, r, "Error unsubscribing you: %s", err)
		} else {
			session.Flash(w, r, "Success: '%s' has been unsubscribed from ALL comment threads.", qEmail)
		}
		responses.Redirect(w, r, "/comments/subscription")
		return
	}

	// Is there a thread and email in the query string?
	if qThread != "" && qEmail != "" {
		err := models.Comments.UnsubscribeThread(qThread, qEmail)
		if err != nil {
			session.Flash(w, r, "Error unsubscribing you: %s", err)
		} else {
			session.Flash(w, r, "Success: '%s' has been unsubscribed from this comment thread.", qEmail)
		}
		responses.Redirect(w, r, "/comments/subscription")
		return
	}

	responses.RenderTemplate(w, r, "_builtin/comments/subscription.gohtml", v)
}

// CommentQuickDelete handles quick-delete links to remove spam comments.
func CommentQuickDelete(w http.ResponseWriter, r *http.Request) {
	// Query parameters.
	var (
		deleteToken = r.URL.Query().Get("d")
		nextURL     = r.URL.Query().Get("next")
	)

	// Look up the comment by thread and quick-delete token.
	comment, err := models.Comments.LoadByDeleteToken(deleteToken)
	if err != nil {
		responses.Forbidden(w, r, "Comment by Delete Token not found.")
		return
	}

	comment.Delete()

	session.Flash(w, r, "Comment has been quick-deleted!")

	if nextURL == "" {
		nextURL = "/"
	}
	responses.Redirect(w, r, nextURL)
}

// getEditToken gets the edit token from the user's session.
func getEditToken(w http.ResponseWriter, r *http.Request) string {
	ses := session.Get(r)
	if ses.EditToken != "" {
		return ses.EditToken
	}

	token := uuid.NewV4().String()
	ses.EditToken = token
	ses.Save(w)
	return token
}