package outboundintentv1

import (
	"bytes"
	"encoding/json"
	"net/mail"
	"strings"
	"unicode"
	"unicode/utf8"
)

const (
	MailPayloadFormatV1 uint32 = 1
	MailResultFormatV1  uint32 = 1

	MaxMailRecipients    = 50
	MaxMailAddressBytes  = 320
	MaxMailSubjectBytes  = 998
	MaxMailTextBodyBytes = 1024 * 1024
	MaxMailResultBytes   = 16 * 1024
)

// MailDeliveryV1 is the provider-neutral SEND_MAIL payload. Runtime supplies
// the authorized actor and recipients, while Integration selects that actor's
// active connected-principal generation. Provider, sender, token, endpoint,
// MIME, retry, and completion-mutation vocabulary is intentionally absent.
type MailDeliveryV1 struct {
	Version           uint32   `json:"version"`
	AuthorizedActorID string   `json:"authorizedActorId"`
	ToAddresses       []string `json:"toAddresses"`
	Subject           string   `json:"subject"`
	TextBody          string   `json:"textBody"`
}

type MailDeliveryOutcome string

const (
	MailOutcomeDelivered             MailDeliveryOutcome = "DELIVERED"
	MailOutcomeRejected              MailDeliveryOutcome = "REJECTED"
	MailOutcomeUnknown               MailDeliveryOutcome = "OUTCOME_UNKNOWN"
	MailFailureNone                                      = ""
	MailFailureNotConnected                              = "MAILBOX_NOT_CONNECTED"
	MailFailureRetired                                   = "MAILBOX_RETIRED"
	MailFailureScope                                     = "MAILBOX_SCOPE_INCOMPATIBLE"
	MailFailureProviderRejected                          = "PROVIDER_REJECTED"
	MailFailureAttachmentUnavailable                     = "ATTACHMENT_UNAVAILABLE"
	MailFailureOutcomeUnknown                            = "PROVIDER_OUTCOME_UNKNOWN"
)

// MailDeliveryResultV1 is Integration's bounded terminal hand-back. A proven
// provider receipt exists only for DELIVERED. OUTCOME_UNKNOWN is terminal so
// an ambiguous post-dispatch observation cannot be blindly retried.
type MailDeliveryResultV1 struct {
	Version         uint32              `json:"version"`
	Outcome         MailDeliveryOutcome `json:"outcome"`
	RemoteMessageID string              `json:"remoteMessageId,omitempty"`
	SenderAddress   string              `json:"senderAddress,omitempty"`
	FailureCode     string              `json:"failureCode,omitempty"`
}

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

func validateMailDeliveryV1(payload MailDeliveryV1) error {
	if payload.Version != MailPayloadFormatV1 || identifier("authorized actor ID", payload.AuthorizedActorID) != nil ||
		len(payload.ToAddresses) == 0 || len(payload.ToAddresses) > MaxMailRecipients ||
		!boundedText(payload.Subject, MaxMailSubjectBytes, false) ||
		!boundedText(payload.TextBody, MaxMailTextBodyBytes, true) || payload.TextBody == "" {
		return invalid()
	}
	seen := make(map[string]struct{}, len(payload.ToAddresses))
	for _, address := range payload.ToAddresses {
		if !bareMailAddress(address) {
			return invalid()
		}
		key := strings.ToLower(address)
		if _, exists := seen[key]; exists {
			return invalid()
		}
		seen[key] = struct{}{}
	}
	return nil
}

func DecodeMailDeliveryResultV1(raw []byte) (MailDeliveryResultV1, error) {
	if len(raw) == 0 || len(raw) > MaxMailResultBytes {
		return MailDeliveryResultV1{}, invalid()
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var result MailDeliveryResultV1
	if decoder.Decode(&result) != nil || requireJSONEOF(decoder) != nil ||
		validateMailDeliveryResultV1(result) != nil {
		return MailDeliveryResultV1{}, invalid()
	}
	canonical, err := json.Marshal(result)
	if err != nil || !bytes.Equal(canonical, raw) {
		return MailDeliveryResultV1{}, invalid()
	}
	return result, nil
}

func validateMailDeliveryResultV1(result MailDeliveryResultV1) error {
	if result.Version != MailResultFormatV1 {
		return invalid()
	}
	switch result.Outcome {
	case MailOutcomeDelivered:
		if identifier("remote message ID", result.RemoteMessageID) != nil ||
			!bareMailAddress(result.SenderAddress) || result.FailureCode != MailFailureNone {
			return invalid()
		}
	case MailOutcomeRejected:
		if result.RemoteMessageID != "" || result.SenderAddress != "" ||
			!validRejectedMailFailure(result.FailureCode) {
			return invalid()
		}
	case MailOutcomeUnknown:
		if result.RemoteMessageID != "" || result.SenderAddress != "" ||
			result.FailureCode != MailFailureOutcomeUnknown {
			return invalid()
		}
	default:
		return invalid()
	}
	return nil
}

func validRejectedMailFailure(value string) bool {
	switch value {
	case MailFailureNotConnected, MailFailureRetired, MailFailureScope, MailFailureProviderRejected, MailFailureAttachmentUnavailable:
		return true
	default:
		return false
	}
}

func bareMailAddress(value string) bool {
	if !boundedText(value, MaxMailAddressBytes, false) {
		return false
	}
	parsed, err := mail.ParseAddress(value)
	return err == nil && parsed.Name == "" && parsed.Address == value && strings.Contains(value, "@")
}

func boundedText(value string, maximum int, allowLineBreaks bool) bool {
	if len(value) > maximum || !utf8.ValidString(value) {
		return false
	}
	for _, current := range value {
		if current == 0 || (!allowLineBreaks && unicode.IsControl(current)) ||
			(allowLineBreaks && unicode.IsControl(current) && current != '\n' && current != '\r' && current != '\t') {
			return false
		}
	}
	return true
}
