package mailruntime

import (
	"context"
	"encoding/json"
	"errors"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/token"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type draftAttachmentInput struct {
	AttachmentID string `json:"attachmentId"`
}

type createDraftInput struct {
	ClientRequestID string                 `json:"clientRequestId"`
	To              []string               `json:"to"`
	CC              []string               `json:"cc,omitempty"`
	BCC             []string               `json:"bcc,omitempty"`
	Subject         string                 `json:"subject"`
	Body            Body                   `json:"body"`
	Attachments     []draftAttachmentInput `json:"attachments,omitempty"`
}

type updateDraftInput struct {
	createDraftInput
	Draft            string `json:"draft"`
	ExpectedRevision uint64 `json:"expectedRevision"`
}

type replyDraftInput struct {
	ClientRequestID string                 `json:"clientRequestId"`
	Message         string                 `json:"message"`
	ReplyAll        bool                   `json:"replyAll,omitempty"`
	Body            Body                   `json:"body"`
	Attachments     []draftAttachmentInput `json:"attachments,omitempty"`
}

type forwardDraftInput struct {
	ClientRequestID string                 `json:"clientRequestId"`
	Message         string                 `json:"message"`
	To              []string               `json:"to"`
	CC              []string               `json:"cc,omitempty"`
	BCC             []string               `json:"bcc,omitempty"`
	Body            Body                   `json:"body"`
	Attachments     []draftAttachmentInput `json:"attachments,omitempty"`
}

type sendDraftInput struct {
	ClientRequestID  string `json:"clientRequestId"`
	Draft            string `json:"draft"`
	ExpectedRevision uint64 `json:"expectedRevision"`
}

type draftMutationOutput struct {
	Status    string `json:"status"`
	Draft     string `json:"draft,omitempty"`
	SendDraft string `json:"sendDraft,omitempty"`
	Revision  uint64 `json:"revision,omitempty"`
	UpdatedAt string `json:"updatedAt"`
	Truncated bool   `json:"truncated"`
}

type sendDraftOutput struct {
	Status         string `json:"status"`
	RecipientCount int    `json:"recipientCount"`
	SubjectExcerpt string `json:"subjectExcerpt,omitempty"`
	Truncated      bool   `json:"truncated"`
}

func (service *Service) createDraft(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input createDraftInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		!validClientRequestID(input.ClientRequestID) {
		return nil, invalidDraftInput(err)
	}
	content := createInputContent(input)
	if err := validateDraftContent(content); err != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return nil, err
	}
	attachments, err := service.loadOutboundAttachments(ctx, invocation, input.Attachments)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	// The fresh authorization fence runs last, immediately before the
	// provider mutation, after every computation and recheck.
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	ref, appendErr := service.imap.AppendDraft(
		ctx,
		runtime.material.IMAP,
		toProtocolDraft(runtime.material.SenderAddress, content, attachments),
	)
	return service.finishDraftMutation(
		ctx,
		runtime,
		ref,
		appendErr,
	)
}

func (service *Service) updateDraft(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input updateDraftInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		!validClientRequestID(input.ClientRequestID) ||
		input.Draft == "" ||
		input.ExpectedRevision == 0 {
		return nil, invalidDraftInput(err)
	}
	content := createInputContent(input.createDraftInput)
	if err := validateDraftContent(content); err != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return nil, err
	}
	messageRef, claims, err := service.openReference(
		input.Draft,
		runtime,
		token.KindDraft,
		actionUpdateDraft,
	)
	if err != nil {
		return nil, err
	}
	if claims.Revision == 0 ||
		claims.Revision != input.ExpectedRevision {
		return nil, mailError(
			ErrorConflict,
			"mail draft revision changed",
			nil,
		)
	}
	oldRef := protocol.DraftRef{
		Mailbox:     messageRef.Mailbox,
		UIDValidity: messageRef.UIDValidity,
		UID:         messageRef.UID,
		ModSeq:      claims.Revision,
	}
	attachments, err := service.loadOutboundAttachments(ctx, invocation, input.Attachments)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	// The fresh authorization fence runs last, immediately before the
	// provider mutation, after every computation and recheck.
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	ref, replaceErr := service.imap.ReplaceDraft(
		ctx,
		runtime.material.IMAP,
		oldRef,
		toProtocolDraft(runtime.material.SenderAddress, content, attachments),
	)
	return service.finishDraftMutation(
		ctx,
		runtime,
		ref,
		replaceErr,
	)
}

func (service *Service) createReplyDraft(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input replyDraftInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		!validClientRequestID(input.ClientRequestID) ||
		input.Message == "" ||
		validateBody(input.Body) != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return nil, err
	}
	ref, _, err := service.openReference(
		input.Message,
		runtime,
		token.KindMessage,
		actionCreateReplyDraft,
	)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	source, err := service.imap.GetMessage(
		ctx,
		runtime.material.IMAP,
		ref,
	)
	if err != nil {
		return nil, classifyProtocolError(err)
	}
	if source.Ref != ref {
		return nil, mailError(
			ErrorUpstream,
			"mailbox returned the wrong source message",
			nil,
		)
	}
	if err := validateQuotedMessage(source); err != nil {
		return nil, mailError(
			ErrorConflict,
			"mailbox source message is incomplete",
			err,
		)
	}
	attachments, err := service.loadOutboundAttachments(ctx, invocation, input.Attachments)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	content, err := replyDraftContent(
		input.Body,
		runtime.material.SenderAddress,
		source,
		input.ReplyAll,
	)
	if err != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	// The fresh authorization fence runs last, immediately before the
	// provider mutation, after every computation and recheck.
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	draftRef, appendErr := service.imap.AppendDraft(
		ctx,
		runtime.material.IMAP,
		toProtocolDraft(runtime.material.SenderAddress, content, attachments),
	)
	return service.finishDraftMutation(
		ctx,
		runtime,
		draftRef,
		appendErr,
	)
}

func (service *Service) createForwardDraft(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input forwardDraftInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		!validClientRequestID(input.ClientRequestID) ||
		input.Message == "" {
		return nil, invalidDraftInput(err)
	}
	intro := DraftContent{
		To: input.To, CC: input.CC, BCC: input.BCC,
		Body: input.Body,
	}
	if err := validateDraftContent(intro); err != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return nil, err
	}
	ref, _, err := service.openReference(
		input.Message,
		runtime,
		token.KindMessage,
		actionCreateForwardDraft,
	)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	source, err := service.imap.GetMessage(
		ctx,
		runtime.material.IMAP,
		ref,
	)
	if err != nil {
		return nil, classifyProtocolError(err)
	}
	if source.Ref != ref {
		return nil, mailError(
			ErrorUpstream,
			"mailbox returned the wrong source message",
			nil,
		)
	}
	if err := validateQuotedMessage(source); err != nil {
		return nil, mailError(
			ErrorConflict,
			"mailbox source message is incomplete",
			err,
		)
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	sourceAttachments, err := service.loadSourceAttachments(ctx, runtime, ref)
	if err != nil {
		return nil, err
	}
	extraAttachments, err := service.loadOutboundAttachments(ctx, invocation, input.Attachments)
	if err != nil {
		return nil, err
	}
	attachments, err := combineForwardAttachments(sourceAttachments, extraAttachments)
	if err != nil {
		return nil, err
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	content, err := forwardDraftContent(intro, source)
	if err != nil {
		return nil, invalidDraftInput(err)
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	// The fresh authorization fence runs last, immediately before the
	// provider mutation, after every computation and recheck.
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	draftRef, appendErr := service.imap.AppendDraft(
		ctx,
		runtime.material.IMAP,
		toProtocolDraft(runtime.material.SenderAddress, content, attachments),
	)
	return service.finishDraftMutation(
		ctx,
		runtime,
		draftRef,
		appendErr,
	)
}

func (service *Service) finishDraftMutation(
	ctx context.Context,
	runtime runtimeContext,
	ref protocol.DraftRef,
	mutationErr error,
) (json.RawMessage, error) {
	if mutationErr != nil {
		switch protocol.ErrorClass(mutationErr) {
		case protocol.ErrorUnknown:
			return service.unknownDraftMutation()
		case protocol.ErrorFailed:
			return marshalBounded(draftMutationOutput{
				Status:    protocol.ResultFailed,
				UpdatedAt: service.now().UTC().Format(timeFormat),
				Truncated: false,
			})
		default:
			return nil, classifyProtocolError(mutationErr)
		}
	}
	requiresRevision := runtime.invocation.Action == actionUpdateDraft
	if err := validateDraftRef(ref); err != nil ||
		ref.Mailbox != runtime.material.Capabilities.DraftMailbox ||
		(requiresRevision && ref.ModSeq == 0) {
		return service.unknownDraftMutation()
	}
	if _, err := service.recheckRuntime(ctx, runtime); err != nil {
		return service.unknownDraftMutation()
	}
	// The provider mutation already happened, so a revoked authorization can
	// no longer surface as a typed error: the revision exists but its
	// acceptance cannot be confirmed.
	if err := service.authorizeMutation(ctx, runtime.invocation); err != nil {
		return service.unknownDraftMutation()
	}
	messageRef := protocol.MessageRef{
		Mailbox: ref.Mailbox, UIDValidity: ref.UIDValidity, UID: ref.UID,
	}
	draftToken, err := service.sealReference(
		runtime,
		token.KindDraft,
		actionUpdateDraft,
		messageRef,
		ref.ModSeq,
		"",
	)
	if err != nil {
		return service.unknownDraftMutation()
	}
	sendToken := ""
	if ref.ModSeq != 0 {
		sendToken, err = service.sealReference(
			runtime,
			token.KindDraft,
			actionSendDraft,
			messageRef,
			ref.ModSeq,
			"",
		)
		if err != nil {
			return service.unknownDraftMutation()
		}
	}
	return marshalBounded(draftMutationOutput{
		Status:    protocol.ResultAccepted,
		Draft:     draftToken,
		SendDraft: sendToken,
		Revision:  ref.ModSeq,
		UpdatedAt: service.now().UTC().Format(timeFormat),
		Truncated: false,
	})
}

func (service *Service) unknownDraftMutation() (
	json.RawMessage,
	error,
) {
	return marshalBounded(draftMutationOutput{
		Status:    protocol.ResultUnknown,
		UpdatedAt: service.now().UTC().Format(timeFormat),
		Truncated: false,
	})
}

// sendDraft performs at most one SMTP provider attempt. clientRequestId is
// audit correlation only; after an unknown result the caller must reread the
// mailbox before issuing another command and must never retry automatically.
func (service *Service) sendDraft(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input sendDraftInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		!validClientRequestID(input.ClientRequestID) ||
		input.Draft == "" ||
		input.ExpectedRevision == 0 {
		return nil, invalidDraftInput(err)
	}
	if strings.TrimSpace(invocation.AuthorizedByMemberID) == "" {
		return nil, mailError(
			ErrorPermissionDenied,
			"mail send requires an authorizing member",
			nil,
		)
	}
	prepared, err := service.loadDraftForSend(ctx, invocation, input)
	if err != nil {
		return nil, err
	}
	if _, err := service.recheckRuntime(ctx, prepared.runtime); err != nil {
		return nil, err
	}
	draft := prepared.draft
	frozenAttachments, err := service.loadDraftAttachments(ctx, prepared.runtime, draft)
	if err != nil {
		return nil, err
	}
	if _, err := service.recheckRuntime(ctx, prepared.runtime); err != nil {
		return nil, err
	}
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	result, sendErr := service.smtp.Send(
		ctx,
		prepared.runtime.material.SMTP,
		protocol.FrozenMessage{
			SenderAddress: draft.Content.From,
			To:            append([]string(nil), draft.Content.To...),
			CC:            append([]string(nil), draft.Content.CC...),
			BCC:           append([]string(nil), draft.Content.BCC...),
			Subject:       draft.Content.Subject,
			BodyFormat:    draft.Content.BodyFormat,
			Body:          draft.Content.Body,
			Attachments:   frozenAttachments,
		},
	)
	status := strings.TrimSpace(result.Status)
	switch status {
	case protocol.ResultAccepted:
		if sendErr != nil {
			status = protocol.ResultUnknown
		}
	case protocol.ResultFailed, protocol.ResultUnknown:
	default:
		if sendErr != nil {
			status = protocol.ResultFailed
		} else {
			status = protocol.ResultUnknown
		}
	}
	// Close the disconnect/reconnect and revocation races that can span the
	// provider attempt. Once SMTP may have happened, a failed recheck or a
	// failed fresh authorization can neither confirm nor refute delivery, so
	// the invocation may only report unknown.
	if _, err := service.recheckRuntime(ctx, prepared.runtime); err != nil {
		status = protocol.ResultUnknown
	}
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		status = protocol.ResultUnknown
	}
	return marshalBounded(sendDraftOutput{
		Status: status,
		RecipientCount: len(draft.Content.To) + len(draft.Content.CC) +
			len(draft.Content.BCC),
		SubjectExcerpt: boundedUTF8(draft.Content.Subject, 160),
		Truncated:      false,
	})
}

type sendDraftLoad struct {
	runtime runtimeContext
	draft   protocol.Draft
}

func (service *Service) loadDraftForSend(
	ctx context.Context,
	invocation Invocation,
	input sendDraftInput,
) (sendDraftLoad, error) {
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return sendDraftLoad{}, err
	}
	messageRef, claims, err := service.openReference(
		input.Draft,
		runtime,
		token.KindDraft,
		actionSendDraft,
	)
	if err != nil {
		return sendDraftLoad{}, err
	}
	if claims.Revision == 0 ||
		claims.Revision != input.ExpectedRevision {
		return sendDraftLoad{}, mailError(
			ErrorConflict,
			"mail draft revision changed",
			nil,
		)
	}
	ref := protocol.DraftRef{
		Mailbox: messageRef.Mailbox, UIDValidity: messageRef.UIDValidity,
		UID: messageRef.UID, ModSeq: claims.Revision,
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return sendDraftLoad{}, err
	}
	draft, err := service.imap.GetDraft(
		ctx,
		runtime.material.IMAP,
		ref,
	)
	if err != nil {
		return sendDraftLoad{}, classifyProtocolError(err)
	}
	if draft.Ref != ref ||
		draft.Content.From != runtime.material.SenderAddress {
		return sendDraftLoad{}, mailError(
			ErrorConflict,
			"mail draft is incomplete or changed",
			nil,
		)
	}
	if draft.Truncated {
		return sendDraftLoad{}, mailError(
			ErrorConflict,
			"mail draft is incomplete or changed",
			nil,
		)
	}
	if _, err := service.recheckRuntime(ctx, runtime); err != nil {
		return sendDraftLoad{}, err
	}
	return sendDraftLoad{
		runtime: runtime,
		draft:   draft,
	}, nil
}

// loadDraftAttachments re-reads the exact MIME parts of every draft
// attachment, bounded by the same five-layer limits as outbound input.
func (service *Service) loadDraftAttachments(
	ctx context.Context,
	runtime runtimeContext,
	draft protocol.Draft,
) ([]protocol.FrozenAttachment, error) {
	if len(draft.Attachments) > protocol.MaxMessageAttachments {
		return nil, mailError(
			ErrorValidation,
			"mail draft attachments exceeded limit",
			nil,
		)
	}
	total := 0
	attachments := make([]protocol.FrozenAttachment, 0, len(draft.Attachments))
	for _, metadata := range draft.Attachments {
		if len(metadata.Part) == 0 {
			return nil, mailError(
				ErrorConflict,
				"mail draft is incomplete or changed",
				nil,
			)
		}
		if _, err := service.recheckRuntime(ctx, runtime); err != nil {
			return nil, err
		}
		if err := service.authorizeMutation(ctx, runtime.invocation); err != nil {
			return nil, err
		}
		content, err := service.imap.GetAttachment(
			ctx,
			runtime.material.IMAP,
			protocol.MessageRef{
				Mailbox:     draft.Ref.Mailbox,
				UIDValidity: draft.Ref.UIDValidity,
				UID:         draft.Ref.UID,
			},
			metadata.Part,
		)
		if err != nil {
			return nil, classifyProtocolError(err)
		}
		if content.Filename != metadata.Filename ||
			content.MediaType != metadata.MediaType {
			return nil, mailError(
				ErrorConflict,
				"mail draft is incomplete or changed",
				nil,
			)
		}
		total += len(content.Blob)
		if total > protocol.MaxTotalAttachmentBytes {
			return nil, mailError(
				ErrorValidation,
				"mail draft attachments exceeded limit",
				nil,
			)
		}
		attachments = append(attachments, protocol.FrozenAttachment{
			Filename:  content.Filename,
			MediaType: content.MediaType,
			Blob:      content.Blob,
		})
	}
	return attachments, nil
}

// loadOutboundAttachments resolves AgentTeam attachment IDs through the trusted
// member-scoped read authority and enforces the attachment limits.
func (service *Service) loadOutboundAttachments(
	ctx context.Context,
	invocation Invocation,
	inputs []draftAttachmentInput,
) ([]protocol.DraftAttachment, error) {
	if len(inputs) == 0 {
		return nil, nil
	}
	if service.attachments == nil {
		return nil, mailError(
			ErrorPermissionDenied,
			"mail attachments are unavailable",
			nil,
		)
	}
	if len(inputs) > protocol.MaxMessageAttachments {
		return nil, mailError(
			ErrorValidation,
			"mail attachments exceeded limit",
			nil,
		)
	}
	seen := make(map[string]struct{}, len(inputs))
	resolved := make([]protocol.DraftAttachment, 0, len(inputs))
	total := 0
	for _, input := range inputs {
		attachmentID := strings.TrimSpace(input.AttachmentID)
		if attachmentID == "" || len(attachmentID) > 128 ||
			strings.ContainsAny(attachmentID, "\r\n\x00") {
			return nil, invalidDraftInput(nil)
		}
		if _, duplicate := seen[attachmentID]; duplicate {
			return nil, invalidDraftInput(nil)
		}
		seen[attachmentID] = struct{}{}
		loaded, err := service.attachments.LoadAgentAttachment(
			ctx,
			invocation.WorkspaceID,
			invocation.AgentMemberID,
			attachmentID,
		)
		if err != nil {
			return nil, classifyAttachmentLoaderError(err)
		}
		total += len(loaded.Blob)
		if len(loaded.Blob) == 0 ||
			len(loaded.Blob) > protocol.MaxAttachmentPartBytes ||
			total > protocol.MaxTotalAttachmentBytes {
			return nil, mailError(
				ErrorValidation,
				"mail attachments exceeded limit",
				nil,
			)
		}
		resolved = append(resolved, protocol.DraftAttachment{
			Filename:  loaded.Filename,
			MediaType: loaded.MediaType,
			Blob:      loaded.Blob,
		})
	}
	return resolved, nil
}

// loadSourceAttachments copies the exact bounded MIME parts of a source
// message so a forward draft preserves its attachments.
func (service *Service) loadSourceAttachments(
	ctx context.Context,
	runtime runtimeContext,
	ref protocol.MessageRef,
) ([]protocol.DraftAttachment, error) {
	metadata, err := service.imap.ListAttachments(
		ctx,
		runtime.material.IMAP,
		ref,
	)
	if err != nil {
		return nil, classifyProtocolError(err)
	}
	if len(metadata) > protocol.MaxMessageAttachments {
		return nil, mailError(
			ErrorValidation,
			"mail source attachments exceeded limit",
			nil,
		)
	}
	total := 0
	attachments := make([]protocol.DraftAttachment, 0, len(metadata))
	for _, item := range metadata {
		if _, err := service.recheckRuntime(ctx, runtime); err != nil {
			return nil, err
		}
		if err := service.authorizeMutation(ctx, runtime.invocation); err != nil {
			return nil, err
		}
		content, err := service.imap.GetAttachment(
			ctx,
			runtime.material.IMAP,
			ref,
			item.Part,
		)
		if err != nil {
			return nil, classifyProtocolError(err)
		}
		if content.Filename != item.Filename ||
			content.MediaType != item.MediaType {
			return nil, mailError(
				ErrorConflict,
				"mailbox source message changed",
				nil,
			)
		}
		total += len(content.Blob)
		if total > protocol.MaxTotalAttachmentBytes {
			return nil, mailError(
				ErrorValidation,
				"mail source attachments exceeded limit",
				nil,
			)
		}
		attachments = append(attachments, protocol.DraftAttachment{
			Filename:  content.Filename,
			MediaType: content.MediaType,
			Blob:      content.Blob,
		})
	}
	return attachments, nil
}

func combineForwardAttachments(
	source []protocol.DraftAttachment,
	extra []protocol.DraftAttachment,
) ([]protocol.DraftAttachment, error) {
	if len(source)+len(extra) > protocol.MaxMessageAttachments {
		return nil, mailError(
			ErrorValidation,
			"mail attachments exceeded limit",
			nil,
		)
	}
	total := 0
	combined := make([]protocol.DraftAttachment, 0, len(source)+len(extra))
	combined = append(combined, source...)
	combined = append(combined, extra...)
	for _, attachment := range combined {
		total += len(attachment.Blob)
	}
	if total > protocol.MaxTotalAttachmentBytes {
		return nil, mailError(
			ErrorValidation,
			"mail attachments exceeded limit",
			nil,
		)
	}
	return combined, nil
}

func classifyAttachmentLoaderError(err error) error {
	switch {
	case err == nil:
		return nil
	case errors.Is(err, storeerr.ErrNotFound),
		errors.Is(err, storeerr.ErrForbidden):
		return mailError(
			ErrorPermissionDenied,
			"mail attachment is not readable",
			nil,
		)
	case errors.Is(err, storeerr.ErrBadRequest),
		errors.Is(err, storeerr.ErrUnprocessable):
		return mailError(
			ErrorValidation,
			"mail attachment reference is invalid",
			nil,
		)
	default:
		return mailError(
			ErrorUpstream,
			"mail attachment could not be resolved",
			err,
		)
	}
}

func createInputContent(input createDraftInput) DraftContent {
	return DraftContent{
		To: input.To, CC: input.CC, BCC: input.BCC,
		Subject: input.Subject, Body: input.Body,
	}
}

func validClientRequestID(value string) bool {
	return value != "" &&
		len(value) <= 128 &&
		strings.TrimSpace(value) == value &&
		!strings.ContainsAny(value, "\r\n\x00")
}

func invalidDraftInput(cause error) error {
	return mailError(
		ErrorValidation,
		"mail draft input is invalid",
		cause,
	)
}
