// Copyright (c) 2026 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.

package message

import (
	"bytes"
	"fmt"
	"mime"
	"net/mail"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/ProtonMail/gluon/rfc5322"
	"github.com/ProtonMail/gluon/rfc822"
	"github.com/ProtonMail/go-proton-api"
	"github.com/ProtonMail/gopenpgp/v2/crypto"
	"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
	"github.com/bradenaw/juniper/xslices"
	"github.com/emersion/go-message"
	"github.com/emersion/go-message/textproto"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/exp/maps"
)

var (
	ErrDecryptionFailed = errors.New("message could not be decrypted")
	ErrNoSuchKeyRing    = errors.New("the keyring to decrypt this message could not be found")
)

// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients.
const InternalIDDomain = `protonmail.internalid`

func BuildRFC822Into(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
	if opts.SanitizeMBOXHeaderLine {
		if err := sanitizeMBOXHeaderLine(decrypted); err != nil {
			return fmt.Errorf("failed to sanitize MBOX header: %w", err)
		}
	}

	switch {
	case len(decrypted.Msg.Attachments) > 0:
		return buildMultipartRFC822(decrypted, opts, buf)

	case decrypted.Msg.MIMEType == "multipart/mixed":
		return buildPGPRFC822(kr, decrypted, opts, buf)

	default:
		return buildSimpleRFC822(decrypted, opts, buf)
	}
}

func buildSimpleRFC822(decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
	if decrypted.BodyErr != nil {
		if !opts.IgnoreDecryptionErrors {
			return decrypted.BodyErr
		}

		return buildMultipartRFC822(decrypted, opts, buf)
	}

	hdr := getTextPartHeader(getMessageHeader(decrypted.Msg, opts), decrypted.Body.Bytes(), decrypted.Msg.MIMEType)

	w, err := message.CreateWriter(buf, hdr)
	if err != nil {
		return err
	}

	if _, err := w.Write(decrypted.Body.Bytes()); err != nil {
		return err
	}

	return w.Close()
}

func buildMultipartRFC822(
	decrypted *DecryptedMessage,
	opts JobOptions,
	buf *bytes.Buffer,
) error {
	boundary := newBoundary(decrypted.Msg.ID)

	hdr := getMessageHeader(decrypted.Msg, opts)

	hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})

	w, err := message.CreateWriter(buf, hdr)
	if err != nil {
		return err
	}

	var (
		inlineAtts []proton.Attachment
		inlineData []DecryptedAttachment
		attachAtts []proton.Attachment
		attachData []DecryptedAttachment
	)

	for index, att := range decrypted.Msg.Attachments {
		if att.Disposition == proton.InlineDisposition {
			inlineAtts = append(inlineAtts, att)
			inlineData = append(inlineData, decrypted.Attachments[index])
		} else {
			attachAtts = append(attachAtts, att)
			attachData = append(attachData, decrypted.Attachments[index])
		}
	}

	if len(inlineAtts) > 0 {
		if err := writeRelatedParts(w, boundary, decrypted, inlineAtts, inlineData, opts); err != nil {
			return err
		}
	} else if err := writeTextPart(w, decrypted, opts); err != nil {
		return err
	}

	for i, att := range attachAtts {
		if err := writeAttachmentPart(w, att, attachData[i], opts); err != nil {
			return err
		}
	}

	return w.Close()
}

func writeTextPart(
	w *message.Writer,
	decrypted *DecryptedMessage,
	opts JobOptions,
) error {
	if decrypted.BodyErr != nil {
		if !opts.IgnoreDecryptionErrors {
			return decrypted.BodyErr
		}

		return writeCustomTextPart(w, decrypted, decrypted.BodyErr)
	}

	return writePart(w, getTextPartHeader(message.Header{}, decrypted.Body.Bytes(), decrypted.Msg.MIMEType), decrypted.Body.Bytes())
}

func writeAttachmentPart(
	w *message.Writer,
	att proton.Attachment,
	decryptedAttachment DecryptedAttachment,
	opts JobOptions,
) error {
	if decryptedAttachment.Err != nil {
		if !opts.IgnoreDecryptionErrors {
			return decryptedAttachment.Err
		}

		log.
			WithField("attID", att.ID).
			WithError(decryptedAttachment.Err).
			Warn("Attachment decryption failed")

		var pgpMessageBuffer bytes.Buffer
		pgpMessageBuffer.Grow(len(decryptedAttachment.Packet) + len(decryptedAttachment.Encrypted))
		pgpMessageBuffer.Write(decryptedAttachment.Packet)
		pgpMessageBuffer.Write(decryptedAttachment.Encrypted)

		return writeCustomAttachmentPart(w, att, &crypto.PGPMessage{Data: pgpMessageBuffer.Bytes()}, decryptedAttachment.Err)
	}

	return writePart(w, getAttachmentPartHeader(att), decryptedAttachment.Data.Bytes())
}

func writeRelatedParts(
	w *message.Writer,
	boundary *boundary,
	decrypted *DecryptedMessage,
	atts []proton.Attachment,
	attData []DecryptedAttachment,
	opts JobOptions,
) error {
	hdr := message.Header{}

	hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()})

	return createPart(w, hdr, func(rel *message.Writer) error {
		if err := writeTextPart(rel, decrypted, opts); err != nil {
			return err
		}

		for i, att := range atts {
			if err := writeAttachmentPart(rel, att, attData[i], opts); err != nil {
				return err
			}
		}

		return nil
	})
}

func buildPGPRFC822(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
	if decrypted.BodyErr != nil {
		if !opts.IgnoreDecryptionErrors {
			return decrypted.BodyErr
		}

		return buildPGPMIMEFallbackRFC822(decrypted, opts, buf)
	}

	hdr := getMessageHeader(decrypted.Msg, opts)

	sigs, err := proton.ExtractSignatures(kr, decrypted.Msg.Body)
	if err != nil {
		log.WithError(err).WithField("id", decrypted.Msg.ID).Warn("Extract signature failed")
	}

	if len(sigs) > 0 {
		return writeMultipartSignedRFC822(hdr, decrypted.Body.Bytes(), sigs[0], buf)
	}

	return writeMultipartEncryptedRFC822(hdr, decrypted.Body.Bytes(), buf)
}

func buildPGPMIMEFallbackRFC822(decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
	hdr := getMessageHeader(decrypted.Msg, opts)

	hdr.SetContentType("multipart/encrypted", map[string]string{
		"boundary": newBoundary(decrypted.Msg.ID).gen(),
		"protocol": "application/pgp-encrypted",
	})

	w, err := message.CreateWriter(buf, hdr)
	if err != nil {
		return err
	}

	var encHdr message.Header

	encHdr.SetContentType("application/pgp-encrypted", nil)
	encHdr.Set("Content-Description", "PGP/MIME version identification")

	if err := writePart(w, encHdr, []byte("Version: 1")); err != nil {
		return err
	}

	var dataHdr message.Header

	dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"})
	dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"})
	dataHdr.Set("Content-Description", "OpenPGP encrypted message")

	if err := writePart(w, dataHdr, []byte(decrypted.Msg.Body)); err != nil {
		return err
	}

	return w.Close()
}

func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.Signature, buf *bytes.Buffer) error {
	boundary := newBoundary("").gen()

	header.SetContentType("multipart/signed", map[string]string{
		"micalg":   sig.Hash,
		"protocol": "application/pgp-signature",
		"boundary": boundary,
	})

	if err := textproto.WriteHeader(buf, header.Header); err != nil {
		return err
	}

	mw := textproto.NewMultipartWriter(buf)

	if err := mw.SetBoundary(boundary); err != nil {
		return err
	}

	bodyHeader, bodyData, err := readHeaderBody(body)
	if err != nil {
		return err
	}

	bodyPart, err := mw.CreatePart(*bodyHeader)
	if err != nil {
		return err
	}

	if _, err := bodyPart.Write(bodyData); err != nil {
		return err
	}

	var sigHeader message.Header

	sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"})
	sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"})
	sigHeader.Set("Content-Description", "OpenPGP digital signature")

	sigPart, err := mw.CreatePart(sigHeader.Header)
	if err != nil {
		return err
	}

	sigData, err := sig.Data.GetArmored()
	if err != nil {
		return err
	}

	if _, err := sigPart.Write([]byte(sigData)); err != nil {
		return err
	}

	return mw.Close()
}

func writeMultipartEncryptedRFC822(header message.Header, body []byte, buf *bytes.Buffer) error {
	bodyHeader, bodyData, err := readHeaderBody(body)
	if err != nil {
		return err
	}

	// Remove old content type header as it is non-standard. Ensure that messages
	// without content type header entries don't become invalid.
	header.Del("Content-Type")

	entFields := bodyHeader.Fields()

	for entFields.Next() {
		// Only set the header field if it is present. Header sanitation will be overridden otherwise.
		if !header.Has(entFields.Key()) {
			header.Set(entFields.Key(), entFields.Value())
		}
	}

	if err := textproto.WriteHeader(buf, header.Header); err != nil {
		return err
	}

	if _, err := buf.Write(bodyData); err != nil {
		return err
	}

	return nil
}

func addressEmpty(address *mail.Address) bool {
	if address == nil {
		return true
	}

	if address.Name == "" && address.Address == "" {
		return true
	}

	return false
}

func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
	hdr := toMessageHeader(msg.ParsedHeaders)

	// SetText will RFC2047-encode.
	if msg.Subject != "" {
		setUTF8EncodedHeaderIfNeeded(&hdr, "Subject", msg.Subject)
	}

	// mail.Address.String() will RFC2047-encode if necessary.
	if !addressEmpty(msg.Sender) {
		setHeaderIfNeeded(&hdr, "From", msg.Sender.String())
	}

	if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
		if len(msg.ReplyTos) != 1 || !addressEmpty(msg.ReplyTos[0]) {
			setHeaderIfNeeded(&hdr, "Reply-To", toAddressList(msg.ReplyTos))
		}
	}

	if len(msg.ToList) > 0 {
		setHeaderIfNeeded(&hdr, "To", toAddressList(msg.ToList))
	}

	if len(msg.CCList) > 0 {
		setHeaderIfNeeded(&hdr, "Cc", toAddressList(msg.CCList))
	}

	if len(msg.BCCList) > 0 {
		setHeaderIfNeeded(&hdr, "Bcc", toAddressList(msg.BCCList))
	}

	setMessageIDIfNeeded(msg, &hdr)

	// Sanitize the date; it needs to have a valid unix timestamp.
	if opts.SanitizeDate {
		if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
			msgDate := SanitizeMessageDate(msg.Time)
			setHeaderIfNeeded(&hdr, "Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
			// We clobbered the date so we save it under X-Original-Date only if no such value exists.
			if !hdr.Has("X-Original-Date") {
				hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
			}
		}
	}

	// Set our internal ID if requested.
	// This is important for us to detect whether APPENDed things are actually "move like outlook".
	if opts.AddInternalID {
		setHeaderIfNeeded(&hdr, "X-Pm-Internal-Id", msg.ID)
	}

	// Set our external ID if requested.
	// This was useful during debugging of applemail recovered messages; doesn't help with any behaviour.
	if opts.AddExternalID {
		if msg.ExternalID != "" {
			hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">")
		}
	}

	// Set our server date if requested.
	// Can be useful to see how long it took for a message to arrive.
	if opts.AddMessageDate {
		setHeaderIfNeeded(&hdr, "X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
	}

	// Include the message ID in the references (supposedly this somehow improves outlook support...).
	if opts.AddMessageIDReference {
		if refs := hdr.Values("References"); xslices.IndexFunc(refs, func(ref string) bool {
			return strings.Contains(ref, msg.ID)
		}) < 0 {
			hdr.Set("References", strings.Join(append(refs, "<"+msg.ID+"@"+InternalIDDomain+">"), " "))
		}
	}

	return hdr
}

// SanitizeMessageDate will return time from msgTime timestamp. If timestamp is
// not after epoch the RFC822 publish day will be used. No message should
// realistically be older than RFC822 itself.
func SanitizeMessageDate(msgTime int64) time.Time {
	if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) {
		return msgTime
	}
	return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC)
}

// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not
// already set.
func setMessageIDIfNeeded(msg proton.Message, hdr *message.Header) {
	if hdr.Get("Message-Id") == "" {
		if msg.ExternalID != "" {
			hdr.Set("Message-Id", "<"+msg.ExternalID+">")
		} else {
			hdr.Set("Message-Id", "<"+msg.ID+"@"+InternalIDDomain+">")
		}
	}
}

// setTextHeaderIfNeeded sets a text (UTF-encoded) header entry if its does not exists or if value is changed.
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
func setUTF8EncodedHeaderIfNeeded(header *message.Header, k, v string) {
	encoded := mime.QEncoding.Encode("utf-8", v)
	if header.Has(k) && (header.Get(k) == encoded) {
		return
	}
	header.Set(k, encoded)
}

// setHeaderIfNeeded sets a header entry if its does not exists or if value is changed.
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
func setHeaderIfNeeded(header *message.Header, key, value string) {
	if header.Has(key) && (header.Get(key) == value) {
		return
	}
	header.Set(key, value)
}

func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header {
	params := make(map[string]string)

	if utf8.Valid(body) {
		params["charset"] = "utf-8"
	}

	hdr.SetContentType(string(mimeType), params)

	// Use quoted-printable for all text/... parts
	hdr.Set("Content-Transfer-Encoding", "quoted-printable")

	return hdr
}

func getAttachmentPartHeader(att proton.Attachment) message.Header {
	hdr := toMessageHeader(att.Headers)

	// All attachments have a content type.
	mimeType, params, err := mime.ParseMediaType(string(att.MIMEType))
	if err != nil {
		logrus.WithError(err).Errorf("Failed to parse mime type: '%v'", att.MIMEType)
		hdr.Set("Content-Type", string(att.MIMEType))
	} else {
		// Merge the overridden name into the params
		encodedName := mime.QEncoding.Encode("utf-8", att.Name)
		params["name"] = encodedName
		params["filename"] = encodedName
		hdr.SetContentType(mimeType, params)
	}

	// All attachments have a content disposition.
	hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})

	// Use base64 for all attachments except embedded RFC822 messages.
	if att.MIMEType != rfc822.MessageRFC822 {
		hdr.Set("Content-Transfer-Encoding", "base64")
	} else {
		hdr.Del("Content-Transfer-Encoding")
	}

	return hdr
}

func toMessageHeader(hdr proton.Headers) message.Header {
	var res message.Header
	// go-message's message.Header are in reversed order (you should only add fields at the top, so storing in reverse order offer faster performances).
	for i := len(hdr.Order) - 1; i >= 0; i-- {
		key := hdr.Order[i]
		values := hdr.Values[key]
		for j := len(values) - 1; j >= 0; j-- {
			// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
			// This buffer is used latter on in message writer to construct message and avoid crash
			// when key length is more than 76 characters long.
			res.AddRaw([]byte(key + ": " + values[j] + "\r\n"))
		}
	}

	return res
}

func toAddressList(addrs []*mail.Address) string {
	res := make([]string, len(addrs))

	for i, addr := range addrs {
		res[i] = addr.String()
	}

	return strings.Join(res, ", ")
}

func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error {
	part, err := w.CreatePart(hdr)
	if err != nil {
		return err
	}

	if err := fn(part); err != nil {
		return err
	}

	return part.Close()
}

func writePart(w *message.Writer, hdr message.Header, body []byte) error {
	return createPart(w, hdr, func(part *message.Writer) error {
		if _, err := part.Write(body); err != nil {
			return errors.Wrap(err, "failed to write part body")
		}

		return nil
	})
}

type boundary struct {
	val string
}

func newBoundary(seed string) *boundary {
	return &boundary{val: seed}
}

func (bw *boundary) gen() string {
	bw.val = algo.HashHexSHA256(bw.val)
	return bw.val
}

func mboxFrom() []byte {
	return []byte("From ")
}

func mboxGtFrom() []byte {
	return []byte(">From ")
}

func sanitizeMBOXHeaderLine(decrypted *DecryptedMessage) error {
	if decrypted == nil {
		return nil
	}

	if decrypted.Body.Len() == 0 {
		return nil
	}

	i := indexMBOXHeaderLine(decrypted.Body.Bytes())
	for i >= 0 {
		var buf bytes.Buffer

		// copy until mbox line
		if i > 0 {
			if _, err := buf.Write(decrypted.Body.Next(i)); err != nil {
				return fmt.Errorf("cannot copy first lines: %w", err)
			}
		}

		// dump mbox line
		eol := bytes.IndexRune(decrypted.Body.Bytes(), '\n')
		if eol == 0 || eol == -1 {
			return errors.New("cannot find end of mbox line")
		}

		_ = decrypted.Body.Next(eol + 1)

		// copy rest
		if _, err := buf.Write(decrypted.Body.Bytes()); err != nil {
			return fmt.Errorf("cannot copy rest of message: %w", err)
		}

		decrypted.Body = buf
		i = indexMBOXHeaderLine(decrypted.Body.Bytes())
	}

	return nil
}

func extractHeaderPartEnd(body []byte) int {
	headerEnd := bytes.Index(body, []byte("\n\n"))
	if headerEnd < 0 {
		headerEnd = bytes.Index(body, []byte("\r\n\r\n"))
	}

	if headerEnd < 0 {
		return -1
	}

	requiredHeaders := map[string]bool{
		"from": false,
		"to":   false,
		"date": false,
	}

	headerSection := body[:headerEnd]
	lines := bytes.Split(headerSection, []byte{'\n'})
	for _, line := range lines {
		colonIdx := bytes.IndexByte(line, byte(':'))
		if colonIdx <= 0 {
			continue
		}

		fieldName := bytes.ToLower(line[:colonIdx])
		switch string(fieldName) {
		case "from", "to", "date":
			requiredHeaders[string(fieldName)] = true
		}
	}

	for _, val := range maps.Values(requiredHeaders) {
		if !val {
			return -1
		}
	}

	return headerEnd
}

func indexMBOXHeaderLine(body []byte) int {
	headerEnd := extractHeaderPartEnd(body)

	for i := 0; i < headerEnd; i++ {
		if i != 0 && body[i] != '\n' {
			continue
		}

		j := 0
		if i != 0 {
			j = i + 1
		}

		if bytes.HasPrefix(body[j:], mboxFrom()) || bytes.HasPrefix(body[j:], mboxGtFrom()) {
			return j
		}
	}

	return -1
}
