// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT

package mail

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	ht "html/template"
	"io"
	"mime"
	"net/mail"
	"os"
	"os/exec"
	"syscall"
	tt "text/template"
	"time"
)

const (
	// errTplExecuteFailed is issued when the template execution was not successful
	errTplExecuteFailed = "failed to execute template: %w"

	// errTplPointerNil is issued when a template pointer is expected but it is nil
	errTplPointerNil = "template pointer is nil"

	// errParseMailAddr is used when a mail address could not be validated
	errParseMailAddr = "failed to parse mail address %q: %w"
)

// Msg is the mail message struct
type Msg struct {
	// addrHeader is a slice of strings that the different mail AddrHeader fields
	addrHeader map[AddrHeader][]*mail.Address

	// boundary is the MIME content boundary
	boundary string

	// charset represents the charset of the mail (defaults to UTF-8)
	charset Charset

	// encoder represents a mime.WordEncoder from the std lib
	encoder mime.WordEncoder

	// encoding represents the message encoding (the encoder will be a corresponding WordEncoder)
	encoding Encoding

	// genHeader is a slice of strings that the different generic mail Header fields
	genHeader map[Header][]string

	// mimever represents the MIME version
	mimever MIMEVersion

	// parts represent the different parts of the Msg
	parts []*Part

	// preformHeader is a slice of strings that the different generic mail Header fields
	// of which content is already preformatted and will not be affected by the automatic line
	// breaks
	preformHeader map[Header]string

	// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
	sendError error
}

// SendmailPath is the default system path to the sendmail binary
const SendmailPath = "/usr/sbin/sendmail"

// MsgOption returns a function that can be used for grouping Msg options
type MsgOption func(*Msg)

// NewMsg returns a new Msg pointer
func NewMsg() *Msg {
	m := &Msg{
		addrHeader:    make(map[AddrHeader][]*mail.Address),
		charset:       CharsetUTF8,
		encoding:      EncodingQP,
		genHeader:     make(map[Header][]string),
		preformHeader: make(map[Header]string),
		mimever:       Mime10,
	}

	// Set the matching mime.WordEncoder for the Msg
	m.setEncoder()

	return m
}

// SetGenHeader sets a generic header field of the Msg
// For adding address headers like "To:" or "From", see SetAddrHeader
func (m *Msg) SetGenHeader(h Header, v ...string) {
	if m.genHeader == nil {
		m.genHeader = make(map[Header][]string)
	}
	for i, hv := range v {
		v[i] = m.encodeString(hv)
	}
	m.genHeader[h] = v
}

// SetAddrHeader sets an address related header field of the Msg
func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error {
	if m.addrHeader == nil {
		m.addrHeader = make(map[AddrHeader][]*mail.Address)
	}
	var al []*mail.Address
	for _, av := range v {
		a, err := mail.ParseAddress(av)
		if err != nil {
			return fmt.Errorf(errParseMailAddr, av, err)
		}
		al = append(al, a)
	}
	switch h {
	case HeaderFrom:
		if len(al) > 0 {
			m.addrHeader[h] = []*mail.Address{al[0]}
		}
	default:
		m.addrHeader[h] = al
	}
	return nil
}

// From takes and validates a given mail address and sets it as "From" genHeader of the Msg
func (m *Msg) From(f string) error {
	return m.SetAddrHeader(HeaderFrom, f)
}

// To takes and validates a given mail address list sets the To: addresses of the Msg
func (m *Msg) To(t ...string) error {
	return m.SetAddrHeader(HeaderTo, t...)
}

// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg
func (m *Msg) ReplyTo(r string) error {
	rt, err := mail.ParseAddress(r)
	if err != nil {
		return fmt.Errorf("failed to parse reply-to address: %w", err)
	}
	m.SetGenHeader(HeaderReplyTo, rt.String())
	return nil
}

// Subject sets the "Subject" header field of the Msg
func (m *Msg) Subject(s string) {
	m.SetGenHeader(HeaderSubject, s)
}

// SetMessageID generates a random message id for the mail
func (m *Msg) SetMessageID() {
	hn, err := os.Hostname()
	if err != nil {
		hn = "localhost.localdomain"
	}
	rn, _ := randNum(100000000)
	rm, _ := randNum(10000)
	rs, _ := randomStringSecure(17)
	pid := os.Getpid() * rm
	mid := fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, hn)
	m.SetMessageIDWithValue(mid)
}

// SetMessageIDWithValue sets the message id for the mail
func (m *Msg) SetMessageIDWithValue(v string) {
	m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", v))
}

// SetDate sets the Date genHeader field to the current time in a valid format
func (m *Msg) SetDate() {
	ts := time.Now().Format(time.RFC1123Z)
	m.SetGenHeader(HeaderDate, ts)
}

// SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error)) {
	p := m.newPart(ct)
	p.w = w
	m.parts = []*Part{p}
}

// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
// The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}) error {
	if t == nil {
		return fmt.Errorf(errTplPointerNil)
	}
	buf := bytes.Buffer{}
	if err := t.Execute(&buf, d); err != nil {
		return fmt.Errorf(errTplExecuteFailed, err)
	}
	w := writeFuncFromBuffer(&buf)
	m.SetBodyWriter(TypeTextPlain, w)
	return nil
}

// AddAlternativeWriter sets the body of the message.
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error)) {
	p := m.newPart(ct)
	p.w = w
	m.parts = append(m.parts, p)
}

// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
// The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}) error {
	if t == nil {
		return fmt.Errorf(errTplPointerNil)
	}
	buf := bytes.Buffer{}
	if err := t.Execute(&buf, d); err != nil {
		return fmt.Errorf(errTplExecuteFailed, err)
	}
	w := writeFuncFromBuffer(&buf)
	m.AddAlternativeWriter(TypeTextHTML, w)
	return nil
}

// WriteTo writes the formatted Msg into a give io.Writer and satisfies the io.WriteTo interface
func (m *Msg) WriteTo(w io.Writer) (int64, error) {
	mw := &msgWriter{w: w, c: m.charset, en: m.encoder}
	mw.writeMsg(m)
	return mw.n, mw.err
}

// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path
func (m *Msg) WriteToSendmail() error {
	return m.WriteToSendmailWithCommand(SendmailPath)
}

// WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout
// of 5 seconds and a given sendmail path
func (m *Msg) WriteToSendmailWithCommand(sp string) error {
	tctx, tcfn := context.WithTimeout(context.Background(), time.Second*5)
	defer tcfn()
	return m.WriteToSendmailWithContext(tctx, sp)
}

// WriteToSendmailWithContext opens a pipe to the local sendmail binary and tries to send the
// mail though that. It takes a context.Context, the path to the sendmail binary and additional
// arguments for the sendmail binary as parameters
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...string) error {
	ec := exec.CommandContext(ctx, sp)
	ec.Args = append(ec.Args, "-oi", "-t")
	ec.Args = append(ec.Args, a...)

	se, err := ec.StderrPipe()
	if err != nil {
		return fmt.Errorf("failed to set STDERR pipe: %w", err)
	}

	si, err := ec.StdinPipe()
	if err != nil {
		return fmt.Errorf("failed to set STDIN pipe: %w", err)
	}
	if se == nil || si == nil {
		return fmt.Errorf("received nil for STDERR or STDIN pipe")
	}

	// Start the execution and write to STDIN
	if err = ec.Start(); err != nil {
		return fmt.Errorf("could not start sendmail execution: %w", err)
	}
	_, err = m.WriteTo(si)
	if err != nil {
		if !errors.Is(err, syscall.EPIPE) {
			return fmt.Errorf("failed to write mail to buffer: %w", err)
		}
	}

	// Close STDIN and wait for completion or cancellation of the sendmail executable
	if err = si.Close(); err != nil {
		return fmt.Errorf("failed to close STDIN pipe: %w", err)
	}

	// Read the stderr pipe for possible errors
	serr, err := io.ReadAll(se)
	if err != nil {
		return fmt.Errorf("failed to read STDERR pipe: %w", err)
	}
	if len(serr) > 0 {
		return fmt.Errorf("sendmail command failed: %s", string(serr))
	}

	if err = ec.Wait(); err != nil {
		return fmt.Errorf("sendmail command execution failed: %w", err)
	}

	return nil
}

// SendError returns the sendError field of the Msg
func (m *Msg) SendError() error {
	return m.sendError
}

// encodeString encodes a string based on the configured message encoder and the corresponding
// charset for the Msg
func (m *Msg) encodeString(s string) string {
	return m.encoder.Encode(string(m.charset), s)
}

// hasAlt returns true if the Msg has more than one part
func (m *Msg) hasAlt() bool {
	c := 0
	for _, p := range m.parts {
		if !p.del {
			c++
		}
	}
	return c > 1
}

// newPart returns a new Part for the Msg
func (m *Msg) newPart(ct ContentType) *Part {
	return &Part{
		ctype: ct,
		cset:  m.charset,
		enc:   m.encoding,
	}
}

// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func (m *Msg) setEncoder() {
	m.encoder = getEncoder(m.encoding)
}

// addDefaultHeader sets some default headers, if they haven't been set before
func (m *Msg) addDefaultHeader() {
	if _, ok := m.genHeader[HeaderDate]; !ok {
		m.SetDate()
	}
	if _, ok := m.genHeader[HeaderMessageID]; !ok {
		m.SetMessageID()
	}
	m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
}

// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(e Encoding) mime.WordEncoder {
	switch e {
	case EncodingQP:
		return mime.QEncoding
	case EncodingB64:
		return mime.BEncoding
	default:
		return mime.QEncoding
	}
}

// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
// often required by this library
func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
	w := func(w io.Writer) (int64, error) {
		nb, err := w.Write(buf.Bytes())
		return int64(nb), err
	}
	return w
}
