package outboundintentv1

import (
	"bytes"
	"encoding/json"
)

const (
	MailPayloadFormatV3         uint32 = 3
	MaxMailAttachments                 = 32
	MaxMailAttachmentBytes             = 25 * 1024 * 1024
	MaxMailAttachmentChunkBytes        = 64 * 1024
	MaxMailRawMessageBytes             = 35 * 1024 * 1024
	MaxMailAttachmentNameBytes         = 1024
	MaxMailContentTypeBytes            = 255
)

// MailAttachmentV1 freezes one Runtime Files content version without copying
// its bytes into the durable outbound intent. FileID identifies the immutable
// version; the remaining fields are integrity and MIME assertions rechecked by
// Runtime when Integration opens the intent-bound content stream.
type MailAttachmentV1 struct {
	FileID        string `json:"fileId"`
	FileName      string `json:"fileName"`
	ContentType   string `json:"contentType"`
	ContentLength uint64 `json:"contentLength"`
	ContentDigest string `json:"contentDigest"`
}

// MailDeliveryV3 adds immutable Runtime Files references to V2. Raw attachment
// content, credentials and provider syntax remain outside the durable intent.
type MailDeliveryV3 struct {
	Version           uint32             `json:"version"`
	AuthorizedActorID string             `json:"authorizedActorId"`
	ToAddresses       []string           `json:"toAddresses"`
	CcAddresses       []string           `json:"ccAddresses,omitempty"`
	BccAddresses      []string           `json:"bccAddresses,omitempty"`
	Subject           string             `json:"subject"`
	TextBody          string             `json:"textBody"`
	HTMLBody          string             `json:"htmlBody,omitempty"`
	Attachments       []MailAttachmentV1 `json:"attachments"`
}

func DecodeMailDeliveryV3(intent Intent) (MailDeliveryV3, error) {
	if err := validateIntent(intent); err != nil || intent.Operation != OperationSendMail ||
		intent.PayloadFormatVersion != MailPayloadFormatV3 {
		return MailDeliveryV3{}, invalid()
	}
	decoder := json.NewDecoder(bytes.NewReader(intent.CanonicalPayload))
	decoder.DisallowUnknownFields()
	var payload MailDeliveryV3
	if decoder.Decode(&payload) != nil || requireJSONEOF(decoder) != nil ||
		validateMailDeliveryV3(payload) != nil {
		return MailDeliveryV3{}, invalid()
	}
	canonical, err := json.Marshal(payload)
	if err != nil || !bytes.Equal(canonical, intent.CanonicalPayload) {
		return MailDeliveryV3{}, invalid()
	}
	return payload, nil
}

func validateMailDeliveryV3(payload MailDeliveryV3) error {
	v2 := MailDeliveryV2{
		Version: MailPayloadFormatV2, AuthorizedActorID: payload.AuthorizedActorID,
		ToAddresses: payload.ToAddresses, CcAddresses: payload.CcAddresses,
		BccAddresses: payload.BccAddresses, Subject: payload.Subject,
		TextBody: payload.TextBody, HTMLBody: payload.HTMLBody,
	}
	if payload.Version != MailPayloadFormatV3 || validateMailDeliveryV2(v2) != nil ||
		len(payload.Attachments) == 0 || len(payload.Attachments) > MaxMailAttachments {
		return invalid()
	}
	seen := make(map[string]struct{}, len(payload.Attachments))
	var projected uint64
	for _, attachment := range payload.Attachments {
		if validateMailAttachment(attachment) != nil {
			return invalid()
		}
		if _, duplicate := seen[attachment.FileID]; duplicate {
			return invalid()
		}
		seen[attachment.FileID] = struct{}{}
		projected += base64MIMEBytes(attachment.ContentLength) + uint64(len(attachment.FileName)+len(attachment.ContentType)+512)
	}
	projected += base64MIMEBytes(uint64(len(payload.TextBody))) +
		base64MIMEBytes(uint64(len(payload.HTMLBody))) + uint64(len(payload.Subject)+4096)
	if projected > MaxMailRawMessageBytes {
		return invalid()
	}
	return nil
}

func validateMailAttachment(attachment MailAttachmentV1) error {
	if identifier("attachment file ID", attachment.FileID) != nil ||
		!boundedText(attachment.FileName, MaxMailAttachmentNameBytes, false) || attachment.FileName == "" ||
		!boundedText(attachment.ContentType, MaxMailContentTypeBytes, false) || attachment.ContentType == "" ||
		attachment.ContentLength > MaxMailAttachmentBytes || attachment.ContentLength == 0 ||
		!digest(attachment.ContentDigest) {
		return invalid()
	}
	return nil
}

func base64MIMEBytes(size uint64) uint64 {
	encoded := (size + 2) / 3 * 4
	return encoded + (encoded+75)/76*2
}
