package smtp

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/textproto"
	"strings"
	"time"
)

func buildMessage(req *SendRequest) string {
	if len(req.Attachments) == 0 {
		return buildMessageWithoutAttachments(req)
	}
	return buildMessageWithAttachments(req)
}

func buildMessageWithoutAttachments(req *SendRequest) string {
	var b strings.Builder
	writeCommonHeaders(&b, req)

	if req.BodyHTML != "" {
		boundary := fmt.Sprintf("----=_Part_%d", time.Now().UnixNano())
		b.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
		b.WriteString("\r\n")
		b.WriteString(fmt.Sprintf("--%s\r\n", boundary))
		b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n\r\n")
		b.WriteString(req.BodyText)
		b.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary))
		b.WriteString("Content-Type: text/html; charset=UTF-8\r\n\r\n")
		b.WriteString(req.BodyHTML)
		b.WriteString(fmt.Sprintf("\r\n--%s--\r\n", boundary))
	} else {
		b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n\r\n")
		b.WriteString(req.BodyText)
	}

	return b.String()
}

func buildMessageWithAttachments(req *SendRequest) string {
	body := &bytes.Buffer{}
	mixed := multipart.NewWriter(body)

	_ = writeMessageBodyPart(mixed, req.BodyText, req.BodyHTML)
	for _, att := range req.Attachments {
		_ = writeAttachmentPart(mixed, att)
	}
	_ = mixed.Close()

	var head strings.Builder
	writeCommonHeaders(&head, req)
	head.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", mixed.Boundary()))
	head.Write(body.Bytes())
	return head.String()
}

func writeCommonHeaders(b *strings.Builder, req *SendRequest) {
	b.WriteString(fmt.Sprintf("From: %s\r\n", req.From))
	b.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(req.To, ", ")))
	if len(req.Cc) > 0 {
		b.WriteString(fmt.Sprintf("Cc: %s\r\n", strings.Join(req.Cc, ", ")))
	}
	b.WriteString(fmt.Sprintf("Subject: %s\r\n", req.Subject))
	b.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().Format(time.RFC1123Z)))
	b.WriteString(fmt.Sprintf("Message-ID: %s\r\n", generateMessageID(req.From)))
	b.WriteString("MIME-Version: 1.0\r\n")

	if req.InReplyTo != "" {
		b.WriteString(fmt.Sprintf("In-Reply-To: %s\r\n", req.InReplyTo))
	}
	if len(req.References) > 0 {
		b.WriteString(fmt.Sprintf("References: %s\r\n", strings.Join(req.References, " ")))
	}
}

func writeMessageBodyPart(w *multipart.Writer, text, html string) error {
	if html != "" {
		altBuf := &bytes.Buffer{}
		alt := multipart.NewWriter(altBuf)

		if err := writeTextPart(alt, "text/plain; charset=UTF-8", text); err != nil {
			return err
		}
		if err := writeTextPart(alt, "text/html; charset=UTF-8", html); err != nil {
			return err
		}
		if err := alt.Close(); err != nil {
			return err
		}

		h := textproto.MIMEHeader{}
		h.Set("Content-Type", fmt.Sprintf("multipart/alternative; boundary=\"%s\"", alt.Boundary()))
		part, err := w.CreatePart(h)
		if err != nil {
			return err
		}
		_, err = part.Write(altBuf.Bytes())
		return err
	}

	h := textproto.MIMEHeader{}
	h.Set("Content-Type", "text/plain; charset=UTF-8")
	part, err := w.CreatePart(h)
	if err != nil {
		return err
	}
	_, err = part.Write([]byte(text))
	return err
}

func writeTextPart(w *multipart.Writer, contentType, body string) error {
	h := textproto.MIMEHeader{}
	h.Set("Content-Type", contentType)
	part, err := w.CreatePart(h)
	if err != nil {
		return err
	}
	_, err = part.Write([]byte(body))
	return err
}

func writeAttachmentPart(w *multipart.Writer, att SendAttachment) error {
	contentType := att.ContentType
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	h := textproto.MIMEHeader{}
	if att.Filename != "" {
		h.Set("Content-Type", mime.FormatMediaType(contentType, map[string]string{
			"name": att.Filename,
		}))
	} else {
		h.Set("Content-Type", contentType)
	}
	h.Set("Content-Transfer-Encoding", "base64")

	disposition := "attachment"
	if att.IsInline {
		disposition = "inline"
	}
	if att.Filename != "" {
		h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{
			"filename": att.Filename,
		}))
	} else {
		h.Set("Content-Disposition", disposition)
	}

	if att.IsInline && att.ContentID != "" {
		cid := att.ContentID
		if !strings.HasPrefix(cid, "<") {
			cid = "<" + cid + ">"
		}
		h.Set("Content-ID", cid)
	}

	part, err := w.CreatePart(h)
	if err != nil {
		return err
	}
	return writeBase64(part, att.Data)
}

func writeBase64(w io.Writer, data []byte) error {
	encoded := base64.StdEncoding.EncodeToString(data)
	for i := 0; i < len(encoded); i += 76 {
		end := i + 76
		if end > len(encoded) {
			end = len(encoded)
		}
		if _, err := io.WriteString(w, encoded[i:end]); err != nil {
			return err
		}
		if end < len(encoded) {
			if _, err := io.WriteString(w, "\r\n"); err != nil {
				return err
			}
		}
	}
	return nil
}
