package models

import (
	"crypto/md5"
	"errors"
	"fmt"
	"html/template"
	"io"
	"math"
	"net/mail"
	"regexp"
	"strconv"
	"strings"

	"git.kirsle.net/apps/gophertype/pkg/console"
	"git.kirsle.net/apps/gophertype/pkg/markdown"
	"github.com/albrow/forms"
	uuid "github.com/satori/go.uuid"
)

// Regexp to match a comment thread ID for a blog post
var rePostThread = regexp.MustCompile(`^post-(\d+?)$`)

type commentMan struct{}

// Comments is a singleton manager class for Comment model access.
var Comments = commentMan{}

// Comment model.
type Comment struct {
	BaseModel

	Thread      string `gorm:"index"` // name of comment thread
	UserID      int    // foreign key to User.ID
	PostID      int    // if a comment on a blog post
	OriginURL   string // original URL of comment page
	Name        string
	Email       string
	Subscribe   bool // user subscribes to future comments on same thread
	Avatar      string
	Body        string
	EditToken   string // So users can edit their own recent comments.
	DeleteToken string `gorm:"unique_index"` // Quick-delete token for spam.

	Post Post
	User User `gorm:"foreign_key:UserID"`
}

// PagedComments holds a paginated view of multiple comments.
type PagedComments struct {
	Comments     []Comment
	Page         int
	PerPage      int
	Pages        int
	Total        int
	NextPage     int
	PreviousPage int
}

// New creates a new Comment model.
func (m commentMan) New() Comment {
	return Comment{
		DeleteToken: uuid.NewV4().String(),
	}
}

// Load a comment by ID.
func (m commentMan) Load(id int) (Comment, error) {
	var com Comment
	r := DB.Preload("User").Preload("Post").First(&com, id)
	return com, r.Error
}

// LoadByDeleteToken loads a comment by its DeleteToken.
func (m commentMan) LoadByDeleteToken(token string) (Comment, error) {
	var com Comment
	r := DB.Preload("User").Where("delete_token = ?", token).First(&com)
	return com, r.Error
}

// GetIndex returns the index page of blog posts.
func (m commentMan) GetThread(thread string) ([]Comment, error) {
	var coms []Comment
	r := DB.Preload("User").
		Where("thread = ?", thread).
		Order("created_at asc").
		Find(&coms)
	return coms, r.Error
}

// GetRecent pages through the comments by recency.
func (m commentMan) GetRecent(page int, perPage int) (PagedComments, error) {
	var pc = PagedComments{
		Page:    page,
		PerPage: perPage,
	}

	if pc.Page < 1 {
		pc.Page = 1
	}
	if pc.PerPage <= 0 {
		pc.PerPage = 20
	}

	query := DB.Preload("User").Preload("Post").
		Order("created_at desc")

	// Count the total number of rows.
	query.Model(&Comment{}).Count(&pc.Total)

	// Query the paged slice of results.
	r := query.
		Offset((pc.Page - 1) * pc.PerPage).
		Limit(pc.PerPage).
		Find(&pc.Comments)

	// Populate paging details.
	pc.Pages = int(math.Ceil(float64(pc.Total) / float64(pc.PerPage)))
	if pc.Page < pc.Pages {
		pc.NextPage = pc.Page + 1
	}
	if pc.Page > 1 {
		pc.PreviousPage = pc.Page - 1
	}

	return pc, r.Error
}

// GetSubscribers returns the subscriber email addresses that are watching a comment thread.
func (m commentMan) GetSubscribers(thread string) ([]string, error) {
	var result []string

	var comments []Comment
	r := DB.Where("thread = ? AND subscribe = ?", thread, true).Find(&comments)

	// Filter them down to valid emails only.
	for _, com := range comments {
		// TODO: validate its an email
		if len(com.Email) > 0 {
			result = append(result, com.Email)
		}
	}

	return result, r.Error
}

// UnsubscribeThread unsubscribes a user's email from a comment thread.
func (m commentMan) UnsubscribeThread(thread string, email string) error {
	// Verify the thread is valid.
	var count int
	DB.Model(&Comment{}).Where("thread=?", thread).Count(&count)
	if count == 0 {
		return errors.New("invalid comment thread")
	}

	r := DB.Table("comments").Where("thread=? AND subscribe=?", thread, true).Updates(map[string]interface{}{
		"subscribe": false,
	})
	return r.Error
}

// UnsubscribeFromAll remove's an email subscription for ALL comment threads.
func (m commentMan) UnsubscribeFromAll(email string) error {
	r := DB.Table("comments").Where("email=?", email).Updates(map[string]interface{}{
		"subscribe": false,
	})
	return r.Error
}

// HTML returns the comment's body as rendered HTML code.
func (c Comment) HTML() template.HTML {
	return template.HTML(markdown.RenderMarkdown(c.Body))
}

// Save a comment.
func (c *Comment) Save() error {
	// Ensure the delete token is unique!
	{
		if exist, err := Comments.LoadByDeleteToken(c.DeleteToken); err != nil && exist.ID != c.ID {
			console.Debug("Comment.Save: delete token is not unique, trying to resolve")
			var resolved bool
			for i := 2; i <= 100; i++ {
				token := uuid.NewV4().String()
				_, err = Comments.LoadByDeleteToken(token)
				if err == nil {
					continue
				}

				c.DeleteToken = token
				resolved = true
				break
			}

			if !resolved {
				return fmt.Errorf("failed to generate a unique delete token after 100 attempts")
			}
		}
	}

	// Parse the thread name if it looks like a post ID.
	if m := rePostThread.FindStringSubmatch(c.Thread); len(m) > 0 {
		if postID, err := strconv.Atoi(m[1]); err == nil {
			c.PostID = postID
		}
	}

	// If there's a PostID, validate that the post exists.
	if c.PostID > 0 {
		if _, err := Posts.Load(int(c.PostID)); err != nil {
			console.Error("Comment had a PostID=%d but the post wasn't found!", c.PostID)
			c.PostID = 0
		}
	}

	console.Info("Save comment: %+v", c)

	// Save the post.
	if DB.NewRecord(c) {
		console.Warn("NEw Record!")
		return DB.Create(&c).Error
	}

	return DB.Save(&c).Error
}

// Delete a comment.
func (c Comment) Delete() error {
	return DB.Delete(&c).Error
}

// ParseForm populates a Post from an HTTP form.
func (c *Comment) ParseForm(form *forms.Data) {
	c.Thread = form.Get("thread")
	c.Name = form.Get("name")
	c.Email = strings.ToLower(strings.TrimSpace(form.Get("email")))
	c.Body = form.Get("body")
	c.Subscribe = form.Get("subscribe") == "true"
	c.LoadAvatar()
}

// LoadAvatar calculates the user's avatar for the comment.
func (c *Comment) LoadAvatar() {
	// MD5 hash the email address for Gravatar.
	if _, err := mail.ParseAddress(c.Email); err == nil {
		h := md5.New()
		io.WriteString(h, c.Email)
		hash := fmt.Sprintf("%x", h.Sum(nil))
		c.Avatar = fmt.Sprintf(
			"//www.gravatar.com/avatar/%s?s=96",
			hash,
		)
	} else {
		// Default gravatar.
		c.Avatar = "https://www.gravatar.com/avatar/00000000000000000000000000000000"
	}
}
