package comments

import (
	"crypto/md5"
	"errors"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"net/mail"
	"time"

	"github.com/google/uuid"
	"github.com/kirsle/blog/jsondb"
	"github.com/kirsle/golog"
)

// DB is a reference to the parent app's JsonDB object.
var DB *jsondb.DB

var log *golog.Logger

func init() {
	log = golog.GetLogger("blog")
}

// Thread contains a thread of comments, for a blog post or otherwise.
type Thread struct {
	ID       string     `json:"id"`
	Comments []*Comment `json:"comments"`
}

// Comment contains the data for a single comment in a thread.
type Comment struct {
	ID          string    `json:"id"`
	UserID      int       `json:"userId,omitempty"`
	Name        string    `json:"name,omitempty"`
	Email       string    `json:"email,omitempty"`
	Avatar      string    `json:"avatar"`
	Body        string    `json:"body"`
	EditToken   string    `json:"editToken"`
	DeleteToken string    `json:"deleteToken"`
	Created     time.Time `json:"created"`
	Updated     time.Time `json:"updated"`

	// Private form use only.
	CSRF      string        `json:"-"`
	Subscribe bool          `json:"-"`
	ThreadID  string        `json:"-"`
	OriginURL string        `json:"-"`
	Subject   string        `json:"-"`
	HTML      template.HTML `json:"-"`
	Trap1     string        `json:"-"`
	Trap2     string        `json:"-"`

	// Even privater fields.
	IsAuthenticated bool   `json:"-"`
	Username        string `json:"-"`
	Editable        bool   `json:"-"`
	Editing         bool   `json:"-"`
}

type ByCreated []*Comment

func (a ByCreated) Len() int           { return len(a) }
func (a ByCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByCreated) Less(i, j int) bool { return a[i].Created.Before(a[j].Created) }

// New initializes a new comment thread.
func New(id string) Thread {
	return Thread{
		ID:       id,
		Comments: []*Comment{},
	}
}

// Load a comment thread.
func Load(id string) (Thread, error) {
	t := Thread{}
	err := DB.Get(fmt.Sprintf("comments/threads/%s", id), &t)
	return t, err
}

// Post a comment to a thread.
func (t *Thread) Post(c *Comment) error {
	// If it has an ID, update an existing comment.
	if len(c.ID) > 0 {
		idx := -1
		for i, comment := range t.Comments {
			if comment.ID == c.ID {
				idx = i
				break
			}
		}

		// Replace the comment by index.
		if idx >= 0 && idx < len(t.Comments) {
			t.Comments[idx] = c
			DB.Commit(fmt.Sprintf("comments/threads/%s", t.ID), t)
			return nil
		}
	}

	// Assign an ID.
	if c.ID == "" {
		c.ID = uuid.New().String()
	}
	if c.DeleteToken == "" {
		c.DeleteToken = uuid.New().String()
	}

	t.Comments = append(t.Comments, c)
	DB.Commit(fmt.Sprintf("comments/threads/%s", t.ID), t)
	return nil
}

// Find a comment by its ID.
func (t *Thread) Find(id string) (*Comment, error) {
	for _, c := range t.Comments {
		if c.ID == id {
			return c, nil
		}
	}
	return nil, errors.New("comment not found")
}

// Delete a comment by its ID.
func (t *Thread) Delete(id string) error {
	keep := []*Comment{}
	var found bool
	for _, c := range t.Comments {
		if c.ID != id {
			keep = append(keep, c)
		} else {
			found = true
		}
	}

	if !found {
		return errors.New("comment not found")
	}

	t.Comments = keep
	DB.Commit(fmt.Sprintf("comments/threads/%s", t.ID), t)
	return nil
}

// FindByDeleteToken finds a comment by its deletion token.
func (t *Thread) FindByDeleteToken(token string) (*Comment, error) {
	for _, c := range t.Comments {
		if c.DeleteToken == token {
			return c, nil
		}
	}

	return nil, errors.New("comment not found")
}

// ParseForm populates a Comment from a form.
func (c *Comment) ParseForm(r *http.Request) {
	// Helper function to set an attribute only if the
	// attribute is currently empty.
	define := func(target *string, value string) {
		if value != "" {
			*target = value
		}
	}

	define(&c.ThreadID, r.FormValue("thread"))
	define(&c.OriginURL, r.FormValue("origin"))
	define(&c.Subject, r.FormValue("subject"))

	define(&c.Name, r.FormValue("name"))
	define(&c.Email, r.FormValue("email"))
	define(&c.Body, r.FormValue("body"))
	c.Subscribe = r.FormValue("subscribe") == "true"

	// When editing a post
	c.Editing = r.FormValue("editing") == "true"

	c.Trap1 = r.FormValue("url")
	c.Trap2 = r.FormValue("comment")

	// Default the timestamp values.
	if c.Created.IsZero() {
		c.Created = time.Now().UTC()
		c.Updated = c.Created
	} else {
		c.Updated = time.Now().UTC()
	}

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

// Validate checks the comment's fields for validity.
func (c *Comment) Validate() error {
	// Spambot trap fields.
	if c.Trap1 != "http://" || c.Trap2 != "" {
		return errors.New("find a human")
	}

	// Required metadata fields.
	if len(c.ThreadID) == 0 {
		return errors.New("you lost the comment thread ID")
	} else if len(c.Subject) == 0 {
		return errors.New("this comment thread is missing a subject")
	}

	if len(c.Body) == 0 {
		return errors.New("the message is required")
	}

	return nil
}
