package sisyphus

import (
	"bufio"
	"errors"
	"fmt"
	"math"
	"mime/quotedprintable"
	"net/mail"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"unicode/utf8"

	log "github.com/sirupsen/logrus"

	"github.com/carlostrub/maildir"
	"github.com/kennygrant/sanitize"
)

// Maildir represents the address to a Maildir directory
type Maildir string

// Mail includes the key of a mail in Maildir
type Mail struct {
	Key           string
	Subject, Body *string
	Junk, New     bool
	DryRun        bool
}

// CreateDirs creates all the required dirs -- if not already there.
func (d Maildir) CreateDirs() error {

	dir := string(d)

	log.WithFields(log.Fields{
		"dir": dir,
	}).Info("Create missing directories")

	err := os.MkdirAll(filepath.Join(dir, ".Junk", "cur"), 0700)
	if err != nil {
		return err
	}
	err = os.MkdirAll(filepath.Join(dir, "new"), 0700)
	if err != nil {
		return err
	}
	err = os.MkdirAll(filepath.Join(dir, "cur"), 0700)

	return err
}

// Index loads all mail keys from the Maildir directory for processing.
func (d Maildir) Index() (m []*Mail, err error) {

	dir := string(d)

	log.WithFields(log.Fields{
		"dir": dir,
	}).Info("Start indexing mails")

	dirs := []string{dir, filepath.Join(dir, ".Junk")}
	for _, val := range dirs {
		j, err := maildir.Dir(val).Keys()
		if err != nil {
			return m, err
		}
		for _, v := range j {
			var new Mail
			new.Key = v
			if val == filepath.Join(dir, ".Junk") {
				new.Junk = true
			}
			m = append(m, &new)
		}
	}

	log.WithFields(log.Fields{
		"dir": dir,
	}).Info("All mails indexed")

	return m, nil
}

// Load reads a mail's subject and body
func (m *Mail) Load(dir Maildir) (err error) {

	var message *mail.Message

	switch {
	case m.Junk:
		dir = Maildir(filepath.Join(string(dir), ".Junk"))
	case m.New:
		dir = Maildir(filepath.Join(string(dir), "new"))
	}

	message, err = maildir.Dir(dir).Message(m.Key)
	if err != nil {
		return err
	}

	// get Subject
	if m.Subject != nil {
		return errors.New("there is already a subject")
	}
	subject := message.Header.Get("Subject")
	m.Subject = &subject

	// get Body
	bQ := quotedprintable.NewReader(message.Body)
	var b []string
	bScanner := bufio.NewScanner(bQ)
	for bScanner.Scan() {
		raw := bScanner.Text()
		b = append(b, raw)
	}

	body := strings.Join(b, " ")
	if m.Body != nil {
		return errors.New("there is already a body")
	}
	m.Body = &body

	return nil
}

// Unload removes a mail's subject and body from the internal cache
func (m *Mail) Unload(dir Maildir) (err error) {

	m.Subject = nil
	m.Body = nil

	return nil
}

func trimStringFromBase64(s string) string {
	if idx := strings.Index(s, "Content-Transfer-Encoding: base64"); idx != -1 {
		return s[:idx-1]
	}
	return s
}

func cleanString(i string) (s string) {

	s = sanitize.Accents(i)
	s = sanitize.HTML(s)
	s = strings.ToLower(s)

	bad := []string{
		"boundary=", "charset", "content-transfer-encoding",
		"content-type", "image/jpeg", "multipart/alternative",
		"multipart/related", "name=", "nextpart", "quoted-printable",
		"text/html", "text/plain", "this email must be viewed in html mode",
		"this is a multi-part message in mime format",
		"windows-1251", "windows-1252", "!", "#", "$", "%", "&", "'",
		"(", ")", "*", "+", ",", ". ", "<", "=", ">", "?", "@", "[",
		"\"", "\\", "\n", "\t", "]", "^", "_", "{", "|", "}",
	}
	for _, b := range bad {
		s = strings.Replace(s, b, " ", -1)
	}
	for i := 0; i < 10; i++ {
		s = strings.Replace(s, "  ", " ", -1)
	}

	return s
}

// Clean cleans the mail's subject and body
func (m *Mail) Clean() error {
	if m.Subject != nil {
		s := trimStringFromBase64(*m.Subject)
		s = cleanString(s)
		m.Subject = &s
	}

	if m.Body != nil {
		b := trimStringFromBase64(*m.Body)
		b = cleanString(b)
		m.Body = &b
	}

	return nil
}

// wordlist takes a string of space separated text and returns a list of unique
// words in a space separated string
func wordlist(s string) (l []string, err error) {
	list := make(map[string]int)

	raw := strings.Split(s, " ")
	var clean []string

	// use regexp compile for use in the loop that follows
	regexMatcher, err := regexp.Compile("(^[a-z]+$)")
	if err != nil {
		return l, err
	}

	for _, w := range raw {
		str := w
		for len(str) > 0 {
			r, size := utf8.DecodeLastRuneInString(str)
			if size > 2 {
				clean = append(clean, fmt.Sprintf("%c", r))
			}

			str = str[:len(str)-size]
		}

		// no long or too short words
		length := len(w)
		if length < 4 || length > 10 {
			continue
		}

		// no numbers, special characters, etc. -- only words
		match := regexMatcher.MatchString(w)
		if !match {
			continue
		} else {
			clean = append(clean, w)
		}
	}

	// only the first 1000 words count
	maxWords := int(math.Min(1000, float64(len(clean))))
	for i := 0; i < maxWords; i++ {
		w := clean[i]
		list[w]++
	}

	for word := range list {
		l = append(l, word)
	}

	return l, nil
}

// Wordlist prepares the mail for training
func (m *Mail) Wordlist() (w []string, err error) {
	var s string

	if m.Subject != nil {
		s = s + " " + *m.Subject
	}

	if m.Body != nil {
		s = s + " " + *m.Body
	}

	w, err = wordlist(s)

	return w, err
}

// cleanWordlist combines Clean and Wordlist in one internal function
func (m *Mail) cleanWordlist() (w []string, err error) {
	err = m.Clean()
	if err != nil {
		return w, err
	}

	w, err = m.Wordlist()

	return w, err
}

// LoadMails loads all mails from a given slice of Maildirs
func LoadMails(d []Maildir) (mails map[Maildir][]*Mail, err error) {
	mails = make(map[Maildir][]*Mail)

	// create missing directories and write index
	for _, val := range d {
		var m []*Mail
		m, err = val.Index()
		if err != nil {
			return mails, err
		}

		mails[val] = m
	}

	return mails, nil
}

// LoadMaildirs creates Maildirs and required directories, if missing
func LoadMaildirs(d []Maildir) (err error) {

	// create missing directories and write index
	for _, val := range d {
		err := val.CreateDirs()
		if err != nil {
			return err
		}
	}

	return nil
}
