package msgraph

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

// Attachment is a fully-resolved file the worker has already fetched from object
// storage, ready to be MIME-encoded.
type Attachment struct {
	Filename string
	MimeType string
	Data     []byte
}

type hdr struct{ name, value string }

// buildMIME assembles a complete RFC 5322 message from the given top-level
// headers and bodies. This is the only reliable way to control Message-ID,
// In-Reply-To/References, and custom warmup/unsubscribe headers on Graph, which
// the JSON sendMail shape cannot set. Body HTML (already carrying the tracking
// pixel + rewritten links) is preserved verbatim. Shapes emitted:
//   - text/plain only          -> single text/plain part (warmup sends)
//   - text/plain + text/html   -> multipart/alternative
//   - + attachments            -> multipart/mixed(alternative, files...)
func buildMIME(hdrs []hdr, bodyPlain, bodyHTML string, attachments []Attachment) ([]byte, error) {
	var buf bytes.Buffer
	writeHeaders := func(extra ...hdr) {
		for _, h := range hdrs {
			fmt.Fprintf(&buf, "%s: %s\r\n", h.name, h.value)
		}
		for _, h := range extra {
			fmt.Fprintf(&buf, "%s: %s\r\n", h.name, h.value)
		}
	}

	// Plain-only: no multipart wrapper needed.
	if len(attachments) == 0 && bodyHTML == "" {
		writeHeaders(hdr{"Content-Type", "text/plain; charset=UTF-8"}, hdr{"Content-Transfer-Encoding", "quoted-printable"})
		buf.WriteString("\r\n")
		if err := writeQuotedPrintable(&buf, bodyPlain); err != nil {
			return nil, err
		}
		return buf.Bytes(), nil
	}

	// Text + HTML with no attachments: a single multipart/alternative.
	if len(attachments) == 0 {
		alt := multipart.NewWriter(&buf)
		writeHeaders(hdr{"Content-Type", fmt.Sprintf("multipart/alternative; boundary=%s", alt.Boundary())})
		buf.WriteString("\r\n")
		if err := writeAltBodies(alt, bodyPlain, bodyHTML); err != nil {
			return nil, err
		}
		return buf.Bytes(), alt.Close()
	}

	// Attachments present: multipart/mixed wrapping the alternative + one part
	// per file.
	mixed := multipart.NewWriter(&buf)
	writeHeaders(hdr{"Content-Type", fmt.Sprintf("multipart/mixed; boundary=%s", mixed.Boundary())})
	buf.WriteString("\r\n")

	var altBuf bytes.Buffer
	alt := multipart.NewWriter(&altBuf)
	if err := writeAltBodies(alt, bodyPlain, bodyHTML); err != nil {
		return nil, err
	}
	if err := alt.Close(); err != nil {
		return nil, err
	}
	altPart, err := mixed.CreatePart(textproto.MIMEHeader{
		"Content-Type": {fmt.Sprintf("multipart/alternative; boundary=%s", alt.Boundary())},
	})
	if err != nil {
		return nil, err
	}
	if _, err := altPart.Write(altBuf.Bytes()); err != nil {
		return nil, err
	}

	for _, a := range attachments {
		mimeType := a.MimeType
		if mimeType == "" {
			mimeType = "application/octet-stream"
		}
		fn := mime.QEncoding.Encode("utf-8", a.Filename)
		part, perr := mixed.CreatePart(textproto.MIMEHeader{
			"Content-Type":              {fmt.Sprintf("%s; name=%q", mimeType, fn)},
			"Content-Transfer-Encoding": {"base64"},
			"Content-Disposition":       {fmt.Sprintf("attachment; filename=%q", fn)},
		})
		if perr != nil {
			return nil, perr
		}
		if werr := writeBase64Wrapped(part, a.Data); werr != nil {
			return nil, werr
		}
	}

	return buf.Bytes(), mixed.Close()
}

// writeAltBodies writes the text/plain (and optional text/html) parts of a
// multipart/alternative body, quoted-printable encoded.
func writeAltBodies(w *multipart.Writer, bodyPlain, bodyHTML string) error {
	if err := writeTextPart(w, "text/plain; charset=UTF-8", bodyPlain); err != nil {
		return err
	}
	if bodyHTML != "" {
		if err := writeTextPart(w, "text/html; charset=UTF-8", bodyHTML); err != nil {
			return err
		}
	}
	return nil
}

func writeTextPart(w *multipart.Writer, contentType, body string) error {
	part, err := w.CreatePart(textproto.MIMEHeader{
		"Content-Type":              {contentType},
		"Content-Transfer-Encoding": {"quoted-printable"},
	})
	if err != nil {
		return err
	}
	return writeQuotedPrintable(part, body)
}

func writeQuotedPrintable(w io.Writer, body string) error {
	qp := quotedprintable.NewWriter(w)
	if _, err := qp.Write([]byte(body)); err != nil {
		return err
	}
	return qp.Close()
}

// writeBase64Wrapped writes data as base64 hard-wrapped at 76 columns per RFC
// 2045 so strict MTAs accept the message.
func writeBase64Wrapped(w io.Writer, data []byte) error {
	encoded := base64.StdEncoding.EncodeToString(data)
	const lineLen = 76
	for i := 0; i < len(encoded); i += lineLen {
		end := i + lineLen
		if end > len(encoded) {
			end = len(encoded)
		}
		if _, err := w.Write([]byte(encoded[i:end] + "\r\n")); err != nil {
			return err
		}
	}
	return nil
}
