package comments

import (
	"bytes"
	"crypto/subtle"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/peterbourgon/diskv"
)

type Comment struct {
	Name, Text string
	IP         string
	Date       time.Time
	Spam       bool
}

func MarshalJSON(c Comment) ([]byte, error) {
	buf := new(bytes.Buffer)
	err := json.NewEncoder(buf).Encode(c)
	return buf.Bytes(), err
}

func UnmarshalJSON(b []byte) (Comment, error) {
	var c Comment
	err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&c)
	return c, err
}

func Default() *Service {
	return WithStorage(NewDiskv("comments"))
}

func WithStorage(storage Storage) *Service {
	return &Service{Storage: storage}
}

type Service struct {
	Storage  Storage
	Password string
}

type Storage interface {
	Store(post string, c Comment) error
	Retreive(post string) ([]Comment, error)
	ReadAll(post string) (io.Reader, error)
}

func (cs *Service) User(r *http.Request) (string, bool) {
	if cs.Password == "" {
		return "", true
	}

	user, providedPass, ok := r.BasicAuth()
	if ok && subtle.ConstantTimeCompare([]byte(cs.Password), []byte(providedPass)) == 1 {
		return user, true
	}

	return "", false
}

func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Request) error {
	user, allowed := cs.User(r)
	if !allowed {
		fmt.Fprintf(w, "<a href='/login'>click here to enter your name and the comments password</a>")
		w.WriteHeader(401)
		return nil
	}

	if r.Method == "POST" {
		return cs.Comment(post, Comment{
			Name: user,
			Text: readCommentText(r),
			IP:   r.RemoteAddr,
			Date: time.Now(),
		})
	}

	w.Header().Set("Content-Type", "text/html")
	_, err := cs.WriteHTML(post, w)
	if err != nil {
		return err
	}

	return nil
}

func readCommentText(r *http.Request) string {
	comment := struct{ Text string }{}
	json.NewDecoder(r.Body).Decode(&comment)
	return comment.Text
}

func (cs *Service) Comment(post string, c Comment) error {
	return cs.Storage.Store(post, c)
}

func (cs *Service) Load(post string) ([]Comment, error) {
	return cs.Storage.Retreive(post)
}

func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) {
	buf := &bytes.Buffer{}
	tmpl := template.Must(template.New("").Parse(
		`<style>
		comment {
			margin: 10px 0;
		}
		comment, comment name, comment text {
			display: block;
		}
		comment name::after {
			content: " says:";
		}
		comment text {
			margin-left: 5px;
			padding-left: 5px;
			border-left: 2px solid gray;
		}
		</style>
		{{range .}}{{ if .Text }}<comment>
		<name>{{.Name}}</name>
		<text>{{.Text}}</text>
		</comment>
		{{end}}{{end}}
		<form onsubmit="submitComment(event)">
		<p>
			<div>
			<label for="comment">post a comment:</label>
			</div>
			<div>
			<textarea placeholder="comment..." name="comment"></textarea>
			</div>
			<div>
			<button type="submit">post</button>
			</div>
		</p>
		</form>`))

	comments, err := cs.Load(post)
	if err != nil {
		return 0, err
	}

	err = tmpl.Execute(buf, comments)
	if err != nil {
		return 0, err
	}
	return io.Copy(w, buf)
}

func (cs *Service) WriteJSON(post string, w io.Writer) (int64, error) {
	r, err := cs.Storage.ReadAll(post)
	if err != nil {
		return 0, err
	}

	return io.Copy(w, r)
}

type diskvStorage struct {
	*diskv.Diskv

	keyMtx       sync.Mutex
	keyLastKnown int64
}

func (d *diskvStorage) nextKey(post string) (string, error) {
	d.keyMtx.Lock()
	defer d.keyMtx.Unlock()

	prefix := post + "-"

	lastKey := prefix

	keys := d.KeysPrefix(prefix, nil)
	for key := range keys {
		lastKey = key
	}

	if lastKey == prefix {
		return prefix + "1", nil
	}

	minusPrefix := lastKey[len(prefix):]
	lastIdx, err := strconv.Atoi(minusPrefix)
	if err != nil {
		return "", fmt.Errorf("comment key %q seems malformed", lastKey)
	}

	key := prefix + fmt.Sprint(lastIdx+1)
	err = d.WriteStream(key, &bytes.Buffer{}, true)

	return key, err
}

func (d *diskvStorage) Store(post string, c Comment) error {
	b, err := MarshalJSON(c)
	if err != nil {
		return fmt.Errorf("error marshaling comment for storage: %v", err)
	}

	key, err := d.nextKey(post)
	if err != nil {
		return fmt.Errorf("error recording comment: %v", err)
	}

	err = d.Write(key, b)
	if err != nil {
		return fmt.Errorf("error writing comment to storage: %v", err)
	}

	return nil
}

type multiReadCloser struct {
	rcs []io.ReadCloser
}

func (mrc *multiReadCloser) Read(b []byte) (int, error) {
	if len(mrc.rcs) == 0 {
		return 0, io.EOF
	}

	n, err := mrc.rcs[0].Read(b)
	if n == len(b) { // we read enough
		return n, err
	}

	for (err == nil || err == io.EOF) && len(mrc.rcs) > 1 {
		var n2 int
		closeErr := mrc.rcs[0].Close()
		if closeErr != nil {
			return n, closeErr
		}

		mrc.rcs = mrc.rcs[1:]
		n2, err = mrc.rcs[0].Read(b[n:])
		n += n2
	}

	return n, err
}

func (mrc *multiReadCloser) Close() error {
	var err error

	for _, rc := range mrc.rcs {
		err2 := rc.Close()
		if err2 != nil {
			err = err2
		}
	}

	if err != nil {
		return err
	}

	return nil
}

func (d *diskvStorage) ReadAll(post string) (io.Reader, error) {
	prefix := post + "-"

	var rcs []io.ReadCloser

	keys := d.KeysPrefix(prefix, nil)
	for key := range keys {
		rc, err := d.ReadStream(key, false)
		if err != nil {
			return nil, err
		}
		rcs = append(rcs, rc)
	}

	return &multiReadCloser{rcs}, nil
}

func (d *diskvStorage) Retreive(post string) ([]Comment, error) {
	var comments []Comment

	r, err := d.ReadAll(post)
	if err != nil {
		return comments, fmt.Errorf("error reading comment from storage: %v", err)
	}

	dec := json.NewDecoder(r)
	for dec.More() {
		comment := Comment{}
		err := dec.Decode(&comment)
		if err != nil {
			return comments, fmt.Errorf("error unmarshaling comment: %v", err)
		}
		comments = append(comments, comment)
	}

	return comments, nil
}

func NewDiskv(path string) Storage {
	return &diskvStorage{
		Diskv: diskv.New(diskv.Options{
			BasePath:     path,
			CacheSizeMax: 1024 * 1024,
		}),
	}
}
