//
// message.go
//
package srnd

import (
	"bufio"
	"bytes"
	"crypto/sha512"
	"encoding/base64"
	"errors"
	"fmt"
	"github.com/dchest/blake2b"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/mail"
	"net/textproto"
	"strings"
)

type ArticleHeaders map[string][]string

func (self ArticleHeaders) Has(key string) bool {
	_, ok := self[key]
	return ok
}

func (self ArticleHeaders) Set(key, val string) {
	self[key] = []string{val}
}

func (self ArticleHeaders) Add(key, val string) {
	if self.Has(key) {
		self[key] = append(self[key], val)
	} else {
		self.Set(key, val)
	}
}

func (self ArticleHeaders) Len() (l int) {
	for k := range self {
		l += len(self[k])
	}
	return
}

func (self ArticleHeaders) Get(key, fallback string) string {
	val, ok := self[key]
	if ok {
		str := ""
		for _, k := range val {
			str += k + ", "
		}
		return str[:len(str)-2]
	} else {
		return fallback
	}
}

type NNTPMessage interface {
	// this message's messsge id
	MessageID() string
	// the parent message's messageid if it's specified
	Reference() string
	// the newsgroup this post is in
	Newsgroup() string
	// the name of the poster
	Name() string
	// any email address associated with the post
	Email() string
	// the subject of the post
	Subject() string
	// when this was posted
	Posted() int64
	// the path header
	Path() string
	// get signed part
	SignedPart() NNTPAttachment
	// append something to path
	// return message with new path
	AppendPath(part string) NNTPMessage
	// the type of this message usually a mimetype
	ContentType() string
	// was this post a sage?
	Sage() bool
	// was this post a root post?
	OP() bool
	// all attachments
	Attachments() []NNTPAttachment
	FrontendPubkey() string
	// all headers
	Headers() ArticleHeaders
	MIMEHeader() textproto.MIMEHeader
	// write out everything
	WriteTo(wr io.Writer, limit int64) error
	// write out body
	WriteBody(wr io.Writer, limit int64) error
	// attach a file
	Attach(att NNTPAttachment)
	// get the plaintext message if it exists
	Message() string
	// pack the whole message and prepare for write
	Pack()
	// get the pubkey for this message if it was signed, otherwise empty string
	Pubkey() string
	// get the origin encrypted address, i2p destination or empty string for onion posters
	Addr() string
	// reset contents
	Reset()
	// get body as reader
	BodyReader() io.Reader
}

func (self *nntpArticle) FrontendPubkey() string {
	return self.headers.Get("X-Frontend-Pubkey", "")
}

type nntpArticle struct {
	// mime header
	headers ArticleHeaders
	// multipart boundary
	boundary string
	// the text part of the message
	message string
	// any attachments
	attachments []NNTPAttachment
	// the inner nntp message to be verified
	signedPart *nntpAttachment
}

func (self *nntpArticle) Reset() {
	self.headers = nil
	self.boundary = ""
	self.message = ""
	if self.attachments != nil {
		for idx := range self.attachments {
			self.attachments[idx].Reset()
			self.attachments[idx] = nil
		}
	}
	self.attachments = nil
	if self.signedPart != nil {
		self.signedPart.Reset()
		self.signedPart = nil
	}
}

func (self *nntpArticle) SignedPart() NNTPAttachment {
	return self.signedPart
}

// create a simple plaintext nntp message
func newPlaintextArticle(message, email, subject, name, instance, message_id, newsgroup string) NNTPMessage {
	nntp := &nntpArticle{
		headers: make(ArticleHeaders),
	}
	nntp.headers.Set("From", formatAddress(name, email))
	nntp.headers.Set("Subject", subject)
	if isSage(subject) {
		nntp.headers.Set("X-Sage", "1")
	}
	nntp.headers.Set("Path", instance)
	nntp.headers.Set("Message-ID", message_id)
	// posted now
	nntp.headers.Set("Date", timeNowStr())
	nntp.headers.Set("Newsgroups", newsgroup)
	nntp.message = strings.Trim(message, "\r")
	nntp.Pack()
	return nntp
}

// sign an article with a seed
func signArticle(nntp NNTPMessage, seed []byte) (signed *nntpArticle, err error) {
	signed = new(nntpArticle)
	signed.headers = make(ArticleHeaders)
	h := nntp.Headers()
	// copy headers
	// copy into signed part
	for k := range h {
		if k == "X-PubKey-Ed25519" || k == "X-Signature-Ed25519-SHA512" || k == "X-Signature-Ed25519-BLAKE2B" {
			// don't set signature or pubkey header
		} else if k == "Content-Type" {
			signed.headers.Set(k, "message/rfc822")
		} else {
			v := h[k][0]
			signed.headers.Set(k, v)
		}
	}
	sha := sha512.New()
	blake := blake2b.New512()
	signed.signedPart = &nntpAttachment{}
	// write body to sign buffer
	mw := io.MultiWriter(sha, blake, signed.signedPart)
	err = nntp.WriteTo(mw, MaxMessageSize)
	mw.Write([]byte{10})
	if err == nil {
		// build keypair
		pk, sk := naclSeedToKeyPair(seed)
		// sign it nigguh
		digest := sha.Sum(nil)
		sig := cryptoSignFucky(digest, sk)
		// log that we signed it
		// log.Printf("signed %s pubkey=%s sig=%s hash=%s", nntp.MessageID(), pk, sig, hexify(digest))
		signed.headers.Set("X-Signature-Ed25519-SHA512", sig)
		signed.headers.Set("X-PubKey-Ed25519", hexify(pk[:]))
		//sig = cryptoSignProper(blake.Sum(nil), sk)
		//signed.headers.Set("X-Signature-Ed25519-BLAKE2B", sig)
	}
	return
}

func (self *nntpArticle) BodyReader() io.Reader {
	if self.Pubkey() == "" {
		buff := new(bytes.Buffer)
		self.WriteBody(buff, MaxMessageSize)
		return buff
	} else {
		return self.signedPart.body
	}
}

func (self *nntpArticle) WriteTo(wr io.Writer, limit int64) (err error) {
	// write headers
	var n int
	hdrs := self.headers
	for hdr, hdr_vals := range hdrs {
		for _, hdr_val := range hdr_vals {
			n, err = wr.Write([]byte(hdr))
			limit -= int64(n)
			n, err = wr.Write([]byte(": "))
			limit -= int64(n)
			n, err = wr.Write([]byte(hdr_val))
			limit -= int64(n)
			n, err = wr.Write([]byte{10})
			limit -= int64(n)
			if err != nil {
				log.Println("error while writing headers", err)
				return
			}
		}
	}
	// done headers
	n, err = wr.Write([]byte{10})
	limit -= int64(n)
	if err != nil {
		log.Println("error while writing body", err)
		return
	}

	if limit > 0 {
		err = self.WriteBody(wr, limit)
	} else {
		err = ErrOversizedMessage
	}
	return
}

func (self *nntpArticle) Pubkey() string {
	return self.headers.Get("X-PubKey-Ed25519", self.headers.Get("X-Pubkey-Ed25519", ""))
}

func (self *nntpArticle) MessageID() (msgid string) {
	for _, h := range []string{"Message-ID", "Message-Id"} {
		mid := self.headers.Get(h, "")
		if mid != "" {
			msgid = string(mid)
			return
		}
	}
	return
}

func (self *nntpArticle) Pack() {
	if len(self.attachments) > 0 {
		if len(self.boundary) == 0 {
			// we have no boundry, set it
			self.boundary = randStr(24)
			// set headers
			self.headers.Set("Mime-Version", "1.0")
			self.headers.Set("Content-Type", fmt.Sprintf("multipart/mixed; boundary=%s", self.boundary))
		}
	} else if self.signedPart == nil {
		self.headers.Set("Content-Type", "text/plain; charset=utf-8")
	}
}

func (self *nntpArticle) Reference() string {
	return self.headers.Get("Reference", self.headers.Get("References", ""))
}

func (self *nntpArticle) Newsgroup() string {
	return self.headers.Get("Newsgroups", "")
}

func (self *nntpArticle) Name() string {
	const defname = "Anonymous"

	from := strings.TrimSpace(self.headers.Get("From", ""))
	if from == "" {
		return defname
	}

	a, e := mail.ParseAddress(from)
	var name string
	if e != nil {
		// try older method - some nodes generate non-compliant stuff
		if i := strings.IndexByte(from, '<'); i > 1 {
			name = from[:i]
		} else {
			return "[Invalid From header]"
		}
	} else {
		name = a.Name
	}

	name = safeHeader(name)
	if name == "" {
		return defname
	}

	return name
}

func (self *nntpArticle) Addr() (addr string) {
	addr = self.headers.Get("X-Encrypted-Ip", "")
	if addr != "" {
		return
	}

	addr = self.headers.Get("X-Encrypted-IP", "")
	if addr != "" {
		return
	}

	addr = self.headers.Get("X-I2P-DestHash", "")
	if addr != "" {
		if addr == "None" {
			return ""
		}
		return
	}

	addr = self.headers.Get("X-I2p-Desthash", "")
	return
}

func (self *nntpArticle) Email() string {
	from := strings.TrimSpace(self.headers.Get("From", ""))
	if from == "" {
		return ""
	}
	a, e := mail.ParseAddress(from)
	if e != nil {
		return fmt.Sprintf("[Invalid From header: %v]", e)
	}
	return a.Address
}

func (self *nntpArticle) Subject() string {
	return self.headers.Get("Subject", "")
}

func (self *nntpArticle) Posted() int64 {
	posted := self.headers.Get("Date", "")
	t, err := mail.ParseDate(posted)
	if err == nil {
		return t.Unix()
	}
	return 0
}

func (self *nntpArticle) Message() string {
	return strings.Trim(self.message, "\x00")
}

func (self *nntpArticle) Path() string {
	return self.headers.Get("Path", "unspecified")
}

func (self *nntpArticle) Headers() ArticleHeaders {
	return self.headers
}

func (self *nntpArticle) MIMEHeader() textproto.MIMEHeader {
	h := make(textproto.MIMEHeader)
	for k, v := range self.headers {
		h[k] = v
	}
	return h
}

func (self *nntpArticle) AppendPath(part string) NNTPMessage {
	if self.headers.Has("Path") {
		self.headers.Set("Path", part+"!"+self.Path())
	} else {
		self.headers.Set("Path", part)
	}
	return self
}
func (self *nntpArticle) ContentType() string {
	// assumes text/plain if unspecified
	return self.headers.Get("Content-Type", "text/plain; charset=UTF-8")
}

func (self *nntpArticle) Sage() bool {
	return self.headers.Get("X-Sage", "") == "1"
}

func (self *nntpArticle) OP() bool {
	ref := self.Reference()
	return ref == "" || ref == self.MessageID()
}

func (self *nntpArticle) Attachments() []NNTPAttachment {
	return self.attachments
}

func (self *nntpArticle) Attach(att NNTPAttachment) {
	self.attachments = append(self.attachments, att)
}

func (self *nntpArticle) WriteBody(wr io.Writer, limit int64) (err error) {
	// this is a signed message, don't treat it special
	if self.signedPart != nil {
		_, err = wr.Write(self.signedPart.Bytes())
		return
	}
	self.Pack()
	content_type := self.ContentType()
	_, params, err := mime.ParseMediaType(content_type)
	if err != nil {
		log.Println("failed to parse media type", err)
		return err
	}

	boundary, ok := params["boundary"]
	if ok {
		nlw := NewLineWriter(wr, limit)
		w := multipart.NewWriter(nlw)

		err = w.SetBoundary(boundary)
		if err == nil {
			attachments := []NNTPAttachment{createPlaintextAttachment([]byte(self.message))}
			attachments = append(attachments, self.attachments...)
			for _, att := range attachments {
				if att == nil {
					continue
				}
				hdr := att.Header()
				hdr.Set("Content-Transfer-Encoding", "base64")
				part, err := w.CreatePart(hdr)
				if err != nil {
					log.Println("failed to create part?", err)
				}
				var buff [1024]byte
				var b io.ReadCloser
				b, err = att.OpenBody()
				if err == nil {
					enc := base64.NewEncoder(base64.StdEncoding, part)
					_, err = io.CopyBuffer(enc, b, buff[:])
					b.Close()
					enc.Close()
					if err != nil {
						break
					}
				}
				part = nil
			}
		}
		if err != nil {
			log.Println("error writing part", err)
		}
		err = w.Close()
		w = nil
	} else {
		nlw := NewLineWriter(wr, limit)
		// write out message
		_, err = io.WriteString(nlw, self.message)
	}
	return err
}

// verify a signed message's body
// innerHandler must close reader when done
// returns error if one happens while verifying article
func verifyMessageSHA512(pk, sig string, body io.Reader, innerHandler func(ArticleHeaders, io.Reader)) (err error) {
	log.Println("unwrapping signed message from", pk)
	pk_bytes := unhex(pk)
	sig_bytes := unhex(sig)
	h := sha512.New()
	pr, pw := io.Pipe()
	// read header
	// handle inner body
	go func(hdr_reader *io.PipeReader) {
		r := bufio.NewReader(hdr_reader)
		msg, err := readMIMEHeader(r)
		if err == nil {
			innerHandler(ArticleHeaders(msg.Header), msg.Body)
		}
		hdr_reader.Close()
	}(pr)
	body = io.TeeReader(body, pw)
	// copy body 128 bytes at a time
	var buff [128]byte
	_, err = io.CopyBuffer(h, body, buff[:])
	if err == nil {
		hash := h.Sum(nil)
		log.Printf("hash=%s", hexify(hash))
		log.Printf("sig=%s", hexify(sig_bytes))
		if naclCryptoVerifyFucky(hash, sig_bytes, pk_bytes) {
			log.Println("signature is valid :^)")
		} else {
			err = errors.New("invalid signature")
		}
	}
	// flush pipe
	pw.Close()
	return
}

func verifyMessageBLAKE2B(pk, sig string, body io.Reader, innerHandler func(ArticleHeaders, io.Reader)) (err error) {
	log.Println("unwrapping signed message from", pk)
	pk_bytes := unhex(pk)
	sig_bytes := unhex(sig)
	h := blake2b.New512()
	pr, pw := io.Pipe()
	// read header
	// handle inner body
	go func(hdr_reader *io.PipeReader) {
		r := bufio.NewReader(hdr_reader)
		msg, err := readMIMEHeader(r)
		if err == nil {
			innerHandler(ArticleHeaders(msg.Header), msg.Body)
		}
		hdr_reader.Close()
	}(pr)
	body = io.TeeReader(body, pw)
	// copy body 128 bytes at a time
	var buff [128]byte
	_, err = io.CopyBuffer(h, body, buff[:])
	if err == nil {
		hash := h.Sum(nil)
		log.Printf("hash=%s", hexify(hash))
		log.Printf("sig=%s", hexify(sig_bytes))
		if naclCryptoVerifyFucky(hash, sig_bytes, pk_bytes) {
			log.Println("signature is valid :^)")
		} else {
			err = errors.New("invalid signature")
		}
	}
	// flush pipe
	pw.Close()
	return
}
