package automation

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	stdhtml "html"
	"net/mail"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/nexuscrm/kernel/pkg/flowactions"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

const (
	runtimeOutboundMailHandlerRef   = "platform:outbound-mail/enqueue"
	runtimeOutboundMailV3HandlerRef = "platform:outbound-mail/enqueue/v3"
)

// RuntimeOutboundMailFlowActionCallRegistrationDescriptor publishes the
// metadata-facing contract without installing a generic-process handler.
// Only standalone Runtime composition owns the transaction-bound intent sink.
func RuntimeOutboundMailFlowActionCallRegistrationDescriptor() FlowActionCallRegistrationDescriptor {
	return FlowActionCallRegistrationDescriptor{
		ActionType:      flowactions.ActionTypeEnqueueOutboundMail,
		HandlerRef:      runtimeOutboundMailHandlerRef,
		SendsEmail:      true,
		AtomicPolicy:    FlowActionCallAtomicPolicySafe.MetadataValue(),
		PreviewPolicy:   FlowActionCallPreviewPolicyReject.MetadataValue(),
		RequiresContext: FlowActionCallContextStandardActionProtected.MetadataValue(),
	}
}

// RuntimeOutboundMailV3FlowActionCallRegistrationDescriptor is a distinct
// metadata/runtime handshake for attachment-aware mail. Versioning both the
// action type and handler ref makes a newer pack fail closed on an older
// Runtime instead of silently sending a message without its attachments.
func RuntimeOutboundMailV3FlowActionCallRegistrationDescriptor() FlowActionCallRegistrationDescriptor {
	return FlowActionCallRegistrationDescriptor{
		ActionType:      flowactions.ActionTypeEnqueueOutboundMailV3,
		HandlerRef:      runtimeOutboundMailV3HandlerRef,
		SendsEmail:      true,
		AtomicPolicy:    FlowActionCallAtomicPolicySafe.MetadataValue(),
		PreviewPolicy:   FlowActionCallPreviewPolicyReject.MetadataValue(),
		RequiresContext: FlowActionCallContextStandardActionProtected.MetadataValue(),
	}
}

// RuntimeOutboundMailFlowActionCallRegistration records an immutable,
// provider-neutral SEND_MAIL intent inside the caller-owned transaction. It
// performs no provider I/O and is installed only with the Runtime intent sink.
func RuntimeOutboundMailFlowActionCallRegistration() FlowActionCallRegistration {
	return runtimeOutboundMailFlowActionCallRegistration(
		RuntimeOutboundMailFlowActionCallRegistrationDescriptor(), false,
	)
}

// RuntimeOutboundMailV3FlowActionCallRegistration installs the explicitly
// attachment-aware action contract while retaining the V1/V2 registration for
// already installed AppReleases.
func RuntimeOutboundMailV3FlowActionCallRegistration() FlowActionCallRegistration {
	return runtimeOutboundMailFlowActionCallRegistration(
		RuntimeOutboundMailV3FlowActionCallRegistrationDescriptor(), true,
	)
}

func runtimeOutboundMailFlowActionCallRegistration(
	descriptor FlowActionCallRegistrationDescriptor,
	allowAttachments bool,
) FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:      descriptor.ActionType,
		HandlerRef:      descriptor.HandlerRef,
		SendsEmail:      descriptor.SendsEmail,
		AtomicPolicy:    FlowActionCallAtomicPolicySafe,
		PreviewPolicy:   FlowActionCallPreviewPolicyReject,
		RequiresContext: FlowActionCallContextStandardActionProtected,
		Handler: protectedStandardActionHandler(
			descriptor.ActionType,
			func(ctx context.Context, executor *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return executor.executeRuntimeOutboundMail(ctx, interview, action, user, allowAttachments)
			},
		),
	}
}

type runtimeOutboundMailParams struct {
	RequestID     string
	RecordID      string
	ObjectAPIName string
	Subject       string
	Body          string
	CcAddresses   []string
	BccAddresses  []string
	HTMLBody      string
	AttachmentIDs []string
	HasV2Input    bool
	HasV3Input    bool
}

func (e *FlowExecutor) executeRuntimeOutboundMail(
	ctx context.Context,
	interview *models.FlowInterview,
	action *models.FlowActionCall,
	user *models.UserSession,
	allowAttachments bool,
) (string, error) {
	fault := func(err error) (string, error) {
		if e != nil {
			return e.RouteActionCallFault(ctx, interview, action, err)
		}
		return "", err
	}
	if e == nil || e.runtimeOutboundIntentSink == nil || interview == nil || action == nil ||
		user == nil || !runtimeOutboundMailIdentifier(user.ID) {
		return fault(fmt.Errorf("outbound mail: Runtime intent sink, action, interview, and invoking user are required"))
	}
	identity, ok := flowRuntimeContextFromCtx(ctx).CurrentFlow()
	if !ok || !flowIdentityIsPackOwnedStandard(identity) {
		return fault(fmt.Errorf("outbound mail: exact uncustomized pack-owned Flow identity is required"))
	}
	view, ok := metadata.RuntimeMetadataViewFromContext(ctx)
	if !ok || view == nil || view.Profile() != catalogsnapshot.ProjectionRecordMutationV1 {
		return fault(fmt.Errorf("outbound mail: exact request-bound record-mutation Catalog view is required"))
	}
	params, err := e.resolveRuntimeOutboundMailParams(ctx, interview, action, user)
	if err != nil {
		return fault(err)
	}
	if params.HasV3Input && !allowAttachments {
		return fault(fmt.Errorf("outbound mail: action contract does not support attachments"))
	}
	route, err := resolveRuntimeOutboundMailRoute(view, identity, action, params.ObjectAPIName)
	if err != nil {
		return fault(err)
	}
	if e.fieldReadAuthorizer == nil {
		return fault(fmt.Errorf("outbound mail: invoking-user field read authorization must be wired"))
	}
	completion := route.MailCompletion
	readCtx := runtimeOutboundMailInvokingUserReadContext(ctx)
	for _, field := range []string{completion.RecipientAddressField, completion.RecipientOptOutField} {
		visible, visibilityErr := e.fieldReadAuthorizer.CheckFieldVisibilityWithUser(
			readCtx, params.ObjectAPIName, field, user,
		)
		if visibilityErr != nil {
			return fault(visibilityErr)
		}
		if !visible {
			return fault(fmt.Errorf("outbound mail: required source field is not readable by the invoking user"))
		}
	}
	current, err := e.loadRuntimeOutboundMailRecord(
		readCtx,
		params.ObjectAPIName,
		params.RecordID,
		[]string{completion.RecipientAddressField, completion.RecipientOptOutField},
		user,
	)
	if err != nil {
		return fault(fmt.Errorf("outbound mail: %w", err))
	}
	optedOut, present := strictRecordBoolean(current[completion.RecipientOptOutField])
	if !present {
		return fault(fmt.Errorf("outbound mail: recipient opt-out value is unavailable"))
	}
	if optedOut {
		return fault(fmt.Errorf("outbound mail: recipient has opted out of email"))
	}
	recipient := trimmedString(current[completion.RecipientAddressField])
	if recipient == "" {
		return fault(fmt.Errorf("outbound mail: recipient address is unavailable"))
	}

	mailV2 := completion.Message.CcAddressField != "" && completion.Message.BccAddressField != "" &&
		completion.Message.HTMLBodyField != ""
	mailV3 := mailV2 && completion.Message.AttachmentIDsField != "" && len(params.AttachmentIDs) != 0
	var payload []byte
	if mailV3 {
		if err := validateRuntimeOutboundMailCompletionValues(view, completion, params); err != nil {
			return fault(err)
		}
		attachments, attachmentErr := e.authorizeRuntimeOutboundMailAttachments(ctx, params.AttachmentIDs, user)
		if attachmentErr != nil {
			return fault(attachmentErr)
		}
		payload, err = json.Marshal(outboundintentv1.MailDeliveryV3{
			Version: outboundintentv1.MailPayloadFormatV3, AuthorizedActorID: user.ID,
			ToAddresses: []string{recipient}, CcAddresses: params.CcAddresses,
			BccAddresses: params.BccAddresses, Subject: params.Subject,
			TextBody: params.Body, HTMLBody: params.HTMLBody, Attachments: attachments,
		})
	} else if mailV2 {
		if params.HasV3Input {
			return fault(fmt.Errorf("outbound mail: Catalog route does not support attachments"))
		}
		if err := validateRuntimeOutboundMailCompletionValues(view, completion, params); err != nil {
			return fault(err)
		}
		payload, err = json.Marshal(outboundintentv1.MailDeliveryV2{
			Version: outboundintentv1.MailPayloadFormatV2, AuthorizedActorID: user.ID,
			ToAddresses: []string{recipient}, CcAddresses: params.CcAddresses,
			BccAddresses: params.BccAddresses, Subject: params.Subject,
			TextBody: params.Body, HTMLBody: params.HTMLBody,
		})
	} else {
		if params.HasV2Input || params.HasV3Input {
			return fault(fmt.Errorf("outbound mail: Catalog route does not support recipient classes, HTML, or attachments"))
		}
		payload, err = json.Marshal(outboundintentv1.MailDeliveryV1{
			Version: outboundintentv1.MailPayloadFormatV1, AuthorizedActorID: user.ID,
			ToAddresses: []string{recipient}, Subject: params.Subject, TextBody: params.Body,
		})
	}
	if err != nil {
		return fault(fmt.Errorf("outbound mail: encode neutral payload: %w", err))
	}
	payloadSum := sha256.Sum256(payload)
	payloadDigest := "sha256:" + hex.EncodeToString(payloadSum[:])
	lineage := view.Lineage()
	identitySum := sha256.Sum256([]byte(strings.Join([]string{
		"nexuscrm", "runtime-outbound-mail", view.OrgID(), view.AppInstallationID(),
		strconv.FormatUint(uint64(lineage.CatalogRevision), 10), params.RequestID,
	}, "\x00")))
	identityDigest := hex.EncodeToString(identitySum[:])
	bindings := make([]outboundintentv1.BindingReference, len(route.Bindings))
	for index, binding := range route.Bindings {
		bindings[index] = outboundintentv1.BindingReference{
			Kind: binding.Kind, OwnerPack: binding.OwnerPack,
			PackInstallationID: binding.PackInstallationID,
			PackPayloadDigest:  binding.PackPayloadDigest,
			BusinessKey:        binding.BusinessKey, DefinitionVersion: binding.DefinitionVersion,
		}
	}
	intent := outboundintentv1.Intent{
		IntentID: "outbound-mail-" + identityDigest, IdempotencyKey: "outbound-mail-state-" + identityDigest,
		PayloadFormatVersion: outboundintentv1.MailPayloadFormatV1, PayloadDigest: payloadDigest,
		CatalogRevision: uint64(lineage.CatalogRevision), Operation: outboundintentv1.OperationSendMail,
		SourceObject: route.SourceObject, SourceRecordID: params.RecordID,
		ConnectorOwnerKey: route.ConnectorOwnerKey,
		FlowDeveloperName: route.FlowDeveloperName, FlowActionName: route.FlowActionName,
		ActionSlotID: route.ActionSlotID, DestinationSlotID: route.DestinationSlotID,
		Bindings: bindings, CanonicalPayload: payload,
	}
	if mailV3 {
		intent.PayloadFormatVersion = outboundintentv1.MailPayloadFormatV3
	} else if mailV2 {
		intent.PayloadFormatVersion = outboundintentv1.MailPayloadFormatV2
	}
	if err := validateRuntimeOutboundMailIntent(intent); err != nil {
		return fault(fmt.Errorf("outbound mail: invalid provider-neutral delivery request"))
	}
	scope := persistence.RuntimeOutboundIntentScope{
		OrgID: view.OrgID(), AppInstallationID: view.AppInstallationID(),
		AuthorizedWorkloadID: route.AuthorizedWorkloadID,
	}
	row, _, err := e.runtimeOutboundIntentSink.Enqueue(ctx, scope, intent)
	if err != nil {
		return fault(fmt.Errorf("outbound mail: enqueue neutral intent: %w", err))
	}
	if row.Scope != scope || row.Intent.IntentID != intent.IntentID ||
		row.Intent.IdempotencyKey != intent.IdempotencyKey || row.Intent.PayloadDigest != intent.PayloadDigest {
		return fault(fmt.Errorf("outbound mail: durable intent receipt is invalid"))
	}
	deliveryState, failureCode, err := runtimeOutboundMailActionReceipt(row)
	if err != nil {
		return fault(err)
	}
	assignActionCallOutputs(interview, action, map[string]interface{}{
		"intentId": row.Intent.IntentID, "deliveryState": deliveryState, "failureCode": failureCode,
	})
	return connectorPushNext(action), nil
}

func validateRuntimeOutboundMailCompletionValues(
	view *catalogsnapshot.RuntimeMetadataView,
	completion *catalogsnapshot.OutboundMailCompletionProjection,
	params runtimeOutboundMailParams,
) error {
	messageObject, ok := view.Object(completion.Message.ObjectAPIName)
	if !ok || messageObject == nil {
		return fmt.Errorf("outbound mail: completion message object is unavailable")
	}
	values := []struct {
		name  string
		field string
		value string
	}{
		{name: "cc", field: completion.Message.CcAddressField, value: strings.Join(params.CcAddresses, "; ")},
		{name: "bcc", field: completion.Message.BccAddressField, value: strings.Join(params.BccAddresses, "; ")},
		{name: "htmlBody", field: completion.Message.HTMLBodyField, value: params.HTMLBody},
	}
	if len(params.AttachmentIDs) != 0 {
		values = append(values, struct {
			name  string
			field string
			value string
		}{
			name: "attachmentIds", field: completion.Message.AttachmentIDsField,
			value: strings.Join(params.AttachmentIDs, ";"),
		})
	}
	validator := metadata.NewValidationService(nil)
	for _, candidate := range values {
		field := models.FindField(messageObject, candidate.field)
		if field == nil || !runtimeOutboundMailCompletionValueFits(validator, field, candidate.value) {
			return fmt.Errorf("outbound mail: %s does not satisfy the Catalog completion field constraints", candidate.name)
		}
	}
	return nil
}

func runtimeOutboundMailCompletionValueFits(
	validator *metadata.ValidationService,
	field *models.FieldMetadata,
	value string,
) bool {
	if validator == nil || field == nil {
		return false
	}
	if err := validator.ValidateRecordStaticConstraints(
		models.Record{field.APIName: value},
		&models.ObjectMetadata{Fields: []models.FieldMetadata{*field}},
	); err != nil {
		return false
	}
	maxBytes, ok := persistence.LogicalTextStorageMaxBytes(constants.NormalizeFieldType(string(field.Type)))
	if !ok {
		return false
	}
	return int64(len(value)) <= maxBytes
}

func (e *FlowExecutor) loadRuntimeOutboundMailRecord(
	ctx context.Context,
	objectName string,
	recordID string,
	fields []string,
	user *models.UserSession,
) (models.Record, error) {
	if e.connectorSourceRecordLoaderFn != nil {
		record, err := e.loadRuntimeOutboundIntentRecord(ctx, objectName, recordID)
		if err != nil {
			return nil, err
		}
		if e.recordReadAuthorizer == nil ||
			!e.recordReadAuthorizer.CheckRecordReadAccessWithUser(ctx, objectName, record, user) {
			return nil, fmt.Errorf("source record is not readable by the invoking user")
		}
		return runtimeOutboundMailFieldProjection(record, recordID, fields), nil
	}
	if e.query == nil {
		return nil, fmt.Errorf("invoking-user field-bounded source query is not wired")
	}
	selectFields := append([]string{constants.FieldID}, fields...)
	result, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: objectName,
		SelectFields:  selectFields,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", recordID),
		Limit:         2,
		SkipTotalSize: true,
	}, user)
	if err != nil {
		return nil, fmt.Errorf("read invoking-user source projection: %w", err)
	}
	if result == nil || len(result.Records) != 1 {
		return nil, fmt.Errorf("source record is not readable by the invoking user")
	}
	record := result.Records[0]
	if trimmedString(record[constants.FieldID]) != recordID {
		return nil, fmt.Errorf("source record identity drift")
	}
	return runtimeOutboundMailFieldProjection(record, recordID, fields), nil
}

func runtimeOutboundMailFieldProjection(record models.Record, recordID string, fields []string) models.Record {
	projected := models.Record{constants.FieldID: recordID}
	for _, field := range fields {
		if value, ok := record[field]; ok {
			projected[field] = value
		}
	}
	return projected
}

func (e *FlowExecutor) resolveRuntimeOutboundMailParams(
	ctx context.Context,
	interview *models.FlowInterview,
	action *models.FlowActionCall,
	user *models.UserSession,
) (runtimeOutboundMailParams, error) {
	values, err := e.ResolveActionCallInputParameters(ctx, interview, action, user)
	if err != nil {
		return runtimeOutboundMailParams{}, fmt.Errorf("outbound mail: resolve action inputs: %w", err)
	}
	stringValue := func(name string) (string, bool) {
		value, ok := values[name].(string)
		return value, ok
	}
	requestID, requestOK := stringValue("requestId")
	recordID, recordOK := stringValue("recordId")
	objectAPIName, objectOK := stringValue("objectApiName")
	subject, subjectOK := stringValue("subject")
	body, bodyOK := stringValue("body")
	ccRaw, ccOK := optionalRuntimeOutboundMailString(values, "cc")
	bccRaw, bccOK := optionalRuntimeOutboundMailString(values, "bcc")
	htmlRaw, htmlOK := optionalRuntimeOutboundMailString(values, "htmlBody")
	attachmentIDs, attachmentsOK := optionalRuntimeOutboundMailStringCollection(values, "attachments")
	if !requestOK || !recordOK || !objectOK || !subjectOK || !bodyOK ||
		!ccOK || !bccOK || !htmlOK || !attachmentsOK ||
		!runtimeOutboundMailIdentifier(requestID) || !runtimeOutboundMailIdentifier(recordID) ||
		!runtimeOutboundMailIdentifier(objectAPIName) || strings.TrimSpace(subject) == "" ||
		strings.TrimSpace(body) == "" || len(subject) > outboundintentv1.MaxMailSubjectBytes ||
		len(body) > outboundintentv1.MaxMailTextBodyBytes {
		return runtimeOutboundMailParams{}, fmt.Errorf("outbound mail: requestId, recordId, objectApiName, subject, or body is invalid")
	}
	ccAddresses, err := parseRuntimeOutboundMailAddresses(ccRaw)
	if err != nil {
		return runtimeOutboundMailParams{}, fmt.Errorf("outbound mail: cc is invalid")
	}
	bccAddresses, err := parseRuntimeOutboundMailAddresses(bccRaw)
	if err != nil {
		return runtimeOutboundMailParams{}, fmt.Errorf("outbound mail: bcc is invalid")
	}
	htmlBody, err := sanitizeRuntimeOutboundMailHTML(htmlRaw)
	if err != nil {
		return runtimeOutboundMailParams{}, fmt.Errorf("outbound mail: htmlBody is invalid")
	}
	return runtimeOutboundMailParams{
		RequestID: requestID, RecordID: recordID, ObjectAPIName: objectAPIName,
		Subject: subject, Body: body, CcAddresses: ccAddresses,
		BccAddresses: bccAddresses, HTMLBody: htmlBody, AttachmentIDs: attachmentIDs,
		HasV2Input: ccRaw != "" || bccRaw != "" || htmlRaw != "",
		HasV3Input: len(attachmentIDs) != 0,
	}, nil
}

func (e *FlowExecutor) authorizeRuntimeOutboundMailAttachments(
	ctx context.Context,
	fileIDs []string,
	user *models.UserSession,
) ([]outboundintentv1.MailAttachmentV1, error) {
	if e == nil || e.standardActionDeps.RecordAttachmentMetadata == nil || user == nil ||
		len(fileIDs) == 0 || len(fileIDs) > outboundintentv1.MaxMailAttachments {
		return nil, fmt.Errorf("outbound mail: attachment authorization is unavailable")
	}
	result := make([]outboundintentv1.MailAttachmentV1, 0, len(fileIDs))
	seen := make(map[string]struct{}, len(fileIDs))
	readCtx := runtimeOutboundMailInvokingUserReadContext(ctx)
	for _, rawID := range fileIDs {
		fileID := strings.TrimSpace(rawID)
		if !runtimeOutboundMailIdentifier(fileID) {
			return nil, fmt.Errorf("outbound mail: attachment ID is invalid")
		}
		if _, duplicate := seen[fileID]; duplicate {
			return nil, fmt.Errorf("outbound mail: attachment IDs must be unique")
		}
		seen[fileID] = struct{}{}
		metadata, found, err := e.standardActionDeps.RecordAttachmentMetadata.AuthorizeRecordAttachmentMetadata(readCtx, fileID, user)
		if err != nil || !found || metadata == nil {
			return nil, fmt.Errorf("outbound mail: attachment is not readable by the invoking user")
		}
		result = append(result, outboundintentv1.MailAttachmentV1{
			FileID: metadata.FileID, FileName: metadata.FileName,
			ContentType: metadata.ContentType, ContentLength: metadata.ContentLength,
			ContentDigest: metadata.ContentDigest,
		})
	}
	return result, nil
}

func optionalRuntimeOutboundMailStringCollection(values map[string]interface{}, name string) ([]string, bool) {
	value, exists := values[name]
	if !exists || value == nil {
		return nil, true
	}
	var result []string
	switch typed := value.(type) {
	case []string:
		result = append(result, typed...)
	case []interface{}:
		result = make([]string, 0, len(typed))
		for _, item := range typed {
			text, ok := item.(string)
			if !ok {
				return nil, false
			}
			result = append(result, text)
		}
	default:
		return nil, false
	}
	return result, len(result) <= outboundintentv1.MaxMailAttachments
}

func optionalRuntimeOutboundMailString(values map[string]interface{}, name string) (string, bool) {
	value, exists := values[name]
	if !exists || value == nil {
		return "", true
	}
	result, ok := value.(string)
	return result, ok
}

func parseRuntimeOutboundMailAddresses(value string) ([]string, error) {
	if strings.TrimSpace(value) == "" {
		return nil, nil
	}
	parts := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' })
	addresses := make([]string, 0, len(parts))
	for _, part := range parts {
		candidate := strings.TrimSpace(part)
		parsed, err := mail.ParseAddress(candidate)
		if err != nil || parsed.Address != candidate || strings.IndexFunc(candidate, unicode.IsControl) >= 0 {
			return nil, fmt.Errorf("invalid bare email address")
		}
		addresses = append(addresses, candidate)
	}
	return addresses, nil
}

func sanitizeRuntimeOutboundMailHTML(value string) (string, error) {
	if value == "" {
		return "", nil
	}
	if !utf8.ValidString(value) || len(value) > outboundintentv1.MaxMailHTMLBodyBytes {
		return "", fmt.Errorf("invalid HTML body")
	}
	contextNode := &html.Node{Type: html.ElementNode, Data: "div", DataAtom: atom.Div}
	nodes, err := html.ParseFragment(strings.NewReader(value), contextNode)
	if err != nil {
		return "", err
	}
	var result bytes.Buffer
	for _, node := range nodes {
		renderRuntimeOutboundMailHTML(&result, node)
	}
	if result.Len() > outboundintentv1.MaxMailHTMLBodyBytes {
		return "", fmt.Errorf("sanitized HTML body is too large")
	}
	return strings.TrimSpace(result.String()), nil
}

var runtimeOutboundMailAllowedHTML = map[string]struct{}{
	"p": {}, "br": {}, "strong": {}, "em": {}, "b": {}, "i": {}, "u": {}, "s": {},
	"ul": {}, "ol": {}, "li": {}, "blockquote": {}, "pre": {}, "code": {},
	"h1": {}, "h2": {}, "h3": {}, "h4": {}, "h5": {}, "h6": {},
}

var runtimeOutboundMailUnsafeHTML = map[string]struct{}{
	"script": {}, "style": {}, "iframe": {}, "object": {}, "embed": {}, "svg": {}, "math": {},
}

func renderRuntimeOutboundMailHTML(result *bytes.Buffer, node *html.Node) {
	switch node.Type {
	case html.TextNode:
		result.WriteString(stdhtml.EscapeString(node.Data))
	case html.ElementNode:
		name := strings.ToLower(node.Data)
		if _, unsafe := runtimeOutboundMailUnsafeHTML[name]; unsafe {
			return
		}
		_, allowed := runtimeOutboundMailAllowedHTML[name]
		if allowed {
			result.WriteByte('<')
			result.WriteString(name)
			result.WriteByte('>')
		}
		for child := node.FirstChild; child != nil; child = child.NextSibling {
			renderRuntimeOutboundMailHTML(result, child)
		}
		if allowed && name != "br" {
			result.WriteString("</")
			result.WriteString(name)
			result.WriteByte('>')
		}
	}
}

func validateRuntimeOutboundMailIntent(intent outboundintentv1.Intent) error {
	switch intent.PayloadFormatVersion {
	case outboundintentv1.MailPayloadFormatV1:
		_, err := outboundintentv1.DecodeMailDeliveryV1(intent)
		return err
	case outboundintentv1.MailPayloadFormatV2:
		_, err := outboundintentv1.DecodeMailDeliveryV2(intent)
		return err
	case outboundintentv1.MailPayloadFormatV3:
		_, err := outboundintentv1.DecodeMailDeliveryV3(intent)
		return err
	default:
		return fmt.Errorf("unsupported mail payload version")
	}
}

func resolveRuntimeOutboundMailRoute(
	view *catalogsnapshot.RuntimeMetadataView,
	identity FlowExecutionIdentity,
	action *models.FlowActionCall,
	objectAPIName string,
) (catalogsnapshot.OutboundIntentRoute, error) {
	if view == nil || action == nil {
		return catalogsnapshot.OutboundIntentRoute{}, fmt.Errorf("outbound mail: route authority is incomplete")
	}
	var matched []catalogsnapshot.OutboundIntentRoute
	for _, route := range view.OutboundIntentRoutes() {
		if route.Operation == outboundintentv1.OperationSendMail && route.MailCompletion != nil &&
			route.ConnectorOwnerKey == strings.TrimSpace(identity.OwnerPack) &&
			route.FlowDeveloperName == strings.TrimSpace(identity.APIName) &&
			route.FlowActionName == strings.TrimSpace(action.Name) &&
			route.SourceObject == objectAPIName {
			matched = append(matched, route)
		}
	}
	if len(matched) != 1 {
		return catalogsnapshot.OutboundIntentRoute{}, fmt.Errorf(
			"outbound mail: exact Catalog route count is %d, want 1", len(matched),
		)
	}
	return matched[0], nil
}

func runtimeOutboundMailIdentifier(value string) bool {
	if value == "" || strings.TrimSpace(value) != value ||
		len(value) > outboundintentv1.MaxIdentifierBytes || !utf8.ValidString(value) {
		return false
	}
	return strings.IndexFunc(value, unicode.IsControl) < 0
}

func runtimeOutboundMailInvokingUserReadContext(ctx context.Context) context.Context {
	ctx = constants.WithoutStandardActionInternalAccessScopes(ctx)
	return permissions.WithAccessInputCache(
		constants.WithSecurityContext(ctx, constants.UserContext, "outbound mail invoking-user read authorization"),
	)
}

func runtimeOutboundMailActionReceipt(row persistence.RuntimeOutboundIntentRow) (string, string, error) {
	switch row.AdmissionState {
	case persistence.RuntimeOutboundIntentPending:
		if row.IntegrationCommandID != "" || row.RejectionCode != "" || runtimeOutboundMailHasTerminalResult(row) {
			return "", "", fmt.Errorf("outbound mail: pending durable intent receipt is invalid")
		}
		return row.AdmissionState, "", nil
	case persistence.RuntimeOutboundIntentAccepted:
		if !runtimeOutboundMailIdentifier(row.IntegrationCommandID) || row.RejectionCode != "" ||
			runtimeOutboundMailHasTerminalResult(row) {
			return "", "", fmt.Errorf("outbound mail: accepted durable intent receipt is invalid")
		}
		return row.AdmissionState, "", nil
	case persistence.RuntimeOutboundIntentRejected:
		if row.IntegrationCommandID != "" || !runtimeOutboundMailIdentifier(row.RejectionCode) ||
			runtimeOutboundMailHasTerminalResult(row) {
			return "", "", fmt.Errorf("outbound mail: rejected durable intent receipt is invalid")
		}
		// Admission rejection codes belong to the generic Integration handoff,
		// not the closed typed mail-result vocabulary. The state is enough for
		// the caller to render failure without exposing a foreign code.
		return row.AdmissionState, "", nil
	case persistence.RuntimeOutboundIntentCompleted:
		if row.RejectionCode != "" {
			return "", "", fmt.Errorf("outbound mail: terminal durable intent receipt is invalid")
		}
		request := outboundintentv1.CommitResultRequest{
			IntentID: row.Intent.IntentID, PayloadDigest: row.Intent.PayloadDigest,
			CatalogRevision: row.Intent.CatalogRevision, IntegrationCommandID: row.IntegrationCommandID,
			RemoteID: row.ResultRemoteID, ResultFormatVersion: row.ResultFormatVersion,
			ResultDigest: row.ResultDigest, CanonicalResult: row.CanonicalResult,
		}
		if outboundintentv1.ValidateCommitResultRequestForIntent(row.Intent, request) != nil {
			return "", "", fmt.Errorf("outbound mail: terminal durable intent receipt is invalid")
		}
		result, err := outboundintentv1.DecodeMailDeliveryResultV1(row.CanonicalResult)
		if err != nil {
			return "", "", fmt.Errorf("outbound mail: terminal durable intent receipt is invalid")
		}
		return string(result.Outcome), result.FailureCode, nil
	default:
		return "", "", fmt.Errorf("outbound mail: durable intent receipt is invalid")
	}
}

func runtimeOutboundMailHasTerminalResult(row persistence.RuntimeOutboundIntentRow) bool {
	return row.ResultRemoteID != "" || row.ResultFormatVersion != 0 || row.ResultDigest != "" ||
		len(row.CanonicalResult) != 0
}
