package automation

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"sync"

	"github.com/nexuscrm/kernel/pkg/flowactions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

var defaultAppFlowActionCallRegistrations = struct {
	sync.RWMutex
	registrations []FlowActionCallRegistration
}{}

var defaultAppFlowActionCallHandlerImplementations = struct {
	sync.RWMutex
	implementations []FlowActionCallHandlerImplementation
}{}

// FlowActionCallHandler executes a registered Flow actionCall.
type FlowActionCallHandler func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error)

// FlowActionCallAtomicPolicy classifies whether a registered actionCall may
// execute while the Flow runtime holds an atomic database boundary.
type FlowActionCallAtomicPolicy int

const (
	// FlowActionCallAtomicPolicyUnspecified is rejected during registration.
	FlowActionCallAtomicPolicyUnspecified FlowActionCallAtomicPolicy = iota
	// FlowActionCallAtomicPolicySafe marks the actionCall as safe for atomic execution.
	FlowActionCallAtomicPolicySafe
	// FlowActionCallAtomicPolicyReject rejects the actionCall from atomic execution.
	FlowActionCallAtomicPolicyReject
)

// FlowActionCallPreviewPolicy classifies whether a registered actionCall may
// execute during no-DML preview runs.
type FlowActionCallPreviewPolicy int

const (
	// FlowActionCallPreviewPolicyReject rejects the actionCall during previews.
	FlowActionCallPreviewPolicyReject FlowActionCallPreviewPolicy = iota
	// FlowActionCallPreviewPolicyAllow executes the handler during previews.
	FlowActionCallPreviewPolicyAllow
	// FlowActionCallPreviewPolicyAllowWithPreviewPersistence executes the handler
	// only on the top-level preview executor whose persistence is the no-DML wrapper.
	FlowActionCallPreviewPolicyAllowWithPreviewPersistence
	// FlowActionCallPreviewPolicyRejectEmailIO rejects with the email-specific
	// preview error that explains provider I/O is an external side effect.
	FlowActionCallPreviewPolicyRejectEmailIO
	// FlowActionCallPreviewPolicyInvokeHandler lets disabled/no-op handlers
	// return their own domain-specific error during previews.
	FlowActionCallPreviewPolicyInvokeHandler
)

// FlowActionCallContextRequirement declares context markers a registered
// actionCall must see before its handler may execute.
type FlowActionCallContextRequirement int

const (
	FlowActionCallContextNone FlowActionCallContextRequirement = iota
	FlowActionCallContextStandardActionProtected
)

// FlowActionCallRegistration describes one additive actionCall handler.
type FlowActionCallRegistration struct {
	ActionType               string
	HandlerRef               string
	Handler                  FlowActionCallHandler
	SendsEmail               bool
	ApprovalNotificationSafe bool
	AtomicPolicy             FlowActionCallAtomicPolicy
	AtomicRejectText         func(*models.FlowActionCall) string
	PreviewPolicy            FlowActionCallPreviewPolicy
	RequiresContext          FlowActionCallContextRequirement
}

// FlowActionCallRegistrationDescriptor is the metadata-facing projection of a
// runtime actionCall registration. Source-backed handler declarations compare
// against this shape before they can publish runtime metadata.
type FlowActionCallRegistrationDescriptor struct {
	ActionType      string
	HandlerRef      string
	SendsEmail      bool
	AtomicPolicy    string
	PreviewPolicy   string
	RequiresContext string
}

// FlowActionCallHandlerImplementation binds an opaque metadata handlerRef to
// executable host code. App packs own actionType/policy declarations; host
// distributions only provide implementations for handler refs they can execute.
type FlowActionCallHandlerImplementation struct {
	HandlerRef string
	Build      func(FlowActionCallRegistrationDescriptor) (FlowActionCallRegistration, error)
}

type flowActionCallRegistry struct {
	byType map[string]FlowActionCallRegistration
}

func newFlowActionCallRegistry(registrations ...FlowActionCallRegistration) *flowActionCallRegistry {
	r := &flowActionCallRegistry{byType: map[string]FlowActionCallRegistration{}}
	for _, registration := range registrations {
		r.Register(registration)
	}
	return r
}

func (r *flowActionCallRegistry) Register(registration FlowActionCallRegistration) {
	if r == nil {
		return
	}
	actionType := normalizeFlowActionType(registration.ActionType)
	if actionType == "" {
		panic("flow action-call registration requires actionType")
	}
	if registration.Handler == nil {
		panic(fmt.Sprintf("flow action-call registration %q requires handler", registration.ActionType))
	}
	if registration.AtomicPolicy == FlowActionCallAtomicPolicyUnspecified {
		panic(fmt.Sprintf("flow action-call registration %q requires atomicPolicy", registration.ActionType))
	}
	if _, exists := r.byType[actionType]; exists {
		panic(fmt.Sprintf("duplicate flow action-call registration for %q", registration.ActionType))
	}
	registration.ActionType = actionType
	registration.HandlerRef = strings.TrimSpace(registration.HandlerRef)
	r.byType[actionType] = registration
}

func (r *flowActionCallRegistry) Lookup(actionType string) (FlowActionCallRegistration, bool) {
	if r == nil {
		return FlowActionCallRegistration{}, false
	}
	registration, ok := r.byType[normalizeFlowActionType(actionType)]
	return registration, ok
}

func normalizeFlowActionType(actionType string) string {
	return strings.ToLower(strings.TrimSpace(actionType))
}

// Descriptor returns the metadata-facing registration projection for conflict
// checks between source-backed pack declarations and live runtime dispatch.
func (registration FlowActionCallRegistration) Descriptor() FlowActionCallRegistrationDescriptor {
	return FlowActionCallRegistrationDescriptor{
		ActionType:      normalizeFlowActionType(registration.ActionType),
		HandlerRef:      strings.TrimSpace(registration.HandlerRef),
		SendsEmail:      registration.SendsEmail,
		AtomicPolicy:    registration.AtomicPolicy.MetadataValue(),
		PreviewPolicy:   registration.PreviewPolicy.MetadataValue(),
		RequiresContext: registration.RequiresContext.MetadataValue(),
	}
}

// MetadataValue returns the source-backed metadata spelling for an atomic
// actionCall registration policy.
func (policy FlowActionCallAtomicPolicy) MetadataValue() string {
	switch policy {
	case FlowActionCallAtomicPolicySafe:
		return "safe"
	case FlowActionCallAtomicPolicyReject:
		return "reject"
	default:
		return ""
	}
}

// MetadataValue returns the source-backed metadata spelling for a preview
// actionCall registration policy.
func (policy FlowActionCallPreviewPolicy) MetadataValue() string {
	switch policy {
	case FlowActionCallPreviewPolicyReject:
		return "reject"
	case FlowActionCallPreviewPolicyAllow:
		return "allow"
	case FlowActionCallPreviewPolicyAllowWithPreviewPersistence:
		return "allow_with_preview_persistence"
	case FlowActionCallPreviewPolicyRejectEmailIO:
		return "reject_email_io"
	case FlowActionCallPreviewPolicyInvokeHandler:
		return "invoke_handler"
	default:
		return ""
	}
}

// MetadataValue returns the source-backed metadata spelling for the required
// execution context of a registered actionCall.
func (requirement FlowActionCallContextRequirement) MetadataValue() string {
	switch requirement {
	case FlowActionCallContextNone:
		return "none"
	case FlowActionCallContextStandardActionProtected:
		return "standard_action_protected"
	default:
		return ""
	}
}

func flowActionCallEmailAtomicRejectText(action *models.FlowActionCall) string {
	return fmt.Sprintf("flows executing atomically cannot contain %s action calls; email provider I/O would hold the database transaction open across external I/O", action.ActionType)
}

func (registration FlowActionCallRegistration) atomicValidationError(action *models.FlowActionCall) string {
	if registration.AtomicRejectText != nil {
		return registration.AtomicRejectText(action)
	}
	actionType := action.ActionType
	if strings.TrimSpace(actionType) == "" {
		actionType = registration.ActionType
	}
	if registration.AtomicPolicy == FlowActionCallAtomicPolicyReject {
		return fmt.Sprintf("flows executing atomically cannot contain %s action calls; registered action type is not atomic-safe", actionType)
	}
	return fmt.Sprintf("flows executing atomically cannot contain %s action calls; registered action type is missing explicit atomic safety classification", actionType)
}

func (registration FlowActionCallRegistration) contextValidationError(ctx context.Context, action *models.FlowActionCall) error {
	switch registration.RequiresContext {
	case FlowActionCallContextNone:
		return nil
	case FlowActionCallContextStandardActionProtected:
		if constants.HasStandardActionExecutionContext(ctx) && constants.HasStandardActionProtectedFlowExecution(ctx) {
			return nil
		}
	}
	actionType := action.ActionType
	if strings.TrimSpace(actionType) == "" {
		actionType = registration.ActionType
	}
	return fmt.Errorf("%s requires protected standard action flow execution context", actionType)
}

// RequireStandardActionProtectedContext wraps app-owned handlers that must only
// run from protected standard-action Flow execution.
func RequireStandardActionProtectedContext(actionType string, next FlowActionCallHandler) FlowActionCallHandler {
	return protectedStandardActionHandler(actionType, next)
}

func defaultFlowActionCallRegistry() *flowActionCallRegistry {
	return newFlowActionCallRegistry(genericFlowActionCallRegistrations()...)
}

func genericFlowActionCallRegistrations() []FlowActionCallRegistration {
	return []FlowActionCallRegistration{
		{
			ActionType:   flowactions.ActionTypeFlow,
			HandlerRef:   "platform:automation/subflow",
			AtomicPolicy: FlowActionCallAtomicPolicySafe,
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return e.executeFlowAction(ctx, interview, action, user)
			},
		},
	}
}

// PlatformFlowActionCallRegistrations returns non-app-specific platform
// capability registrations installed by the application composition root.
func PlatformFlowActionCallRegistrations() []FlowActionCallRegistration {
	out := []FlowActionCallRegistration{}
	out = append(out, EmailFlowActionCallRegistrations()...)
	out = append(out, NotificationFlowActionCallRegistrations()...)
	out = append(out, ConnectorPlatformFlowActionCallRegistrations()...)
	out = append(out, StandardActionFlowActionCallRegistrations()...)
	out = append(out, ApprovalFlowActionCallRegistrations()...)
	return out
}

// PlatformFlowActionCallContractDescriptors returns metadata-facing platform
// contracts that validation/setup must recognize without implying that the
// current process owns the executable handler.
func PlatformFlowActionCallContractDescriptors() []FlowActionCallRegistrationDescriptor {
	return []FlowActionCallRegistrationDescriptor{
		ConnectorPushFlowActionCallRegistrationDescriptor(),
		RuntimeOutboundMailFlowActionCallRegistrationDescriptor(),
		RuntimeOutboundMailV3FlowActionCallRegistrationDescriptor(),
	}
}

// ReserveRuntimeFlowActionCallTypes installs the durable in-binary runtime
// actionType set into the public registry before host/app registrations are
// finalized. Temporary compatibility aliases are supplied by app-owned
// registration wiring, not the public platform-reserved contract.
func ReserveRuntimeFlowActionCallTypes() error {
	return flowactions.ReserveActionTypes(flowactions.PlatformReservedActionTypes()...)
}

// RegisterDefaultAppFlowActionCallRegistrations adds app/distribution-owned
// actionCall handlers supplied by the active product build. The automation
// substrate owns the generic registry; concrete app verbs must be registered by
// a distribution composition root.
func RegisterDefaultAppFlowActionCallRegistrations(registrations ...FlowActionCallRegistration) {
	defaultAppFlowActionCallRegistrations.Lock()
	defer defaultAppFlowActionCallRegistrations.Unlock()

	next := append([]FlowActionCallRegistration(nil), defaultAppFlowActionCallRegistrations.registrations...)
	next = append(next, registrations...)
	if err := validateDefaultAppFlowActionCallRegistrations(next); err != nil {
		panic(fmt.Sprintf("automation: default app flow action-call registrations: %v", err))
	}
	defaultAppFlowActionCallRegistrations.registrations = next
}

// RegisterDefaultAppFlowActionCallHandlerImplementations adds executable
// handlerRef bindings supplied by the active product build. The actionType set
// still comes from source-backed pack/runtime metadata, not this registry.
func RegisterDefaultAppFlowActionCallHandlerImplementations(implementations ...FlowActionCallHandlerImplementation) {
	defaultAppFlowActionCallHandlerImplementations.Lock()
	defer defaultAppFlowActionCallHandlerImplementations.Unlock()

	next := append([]FlowActionCallHandlerImplementation(nil), defaultAppFlowActionCallHandlerImplementations.implementations...)
	next = append(next, implementations...)
	if err := validateFlowActionCallHandlerImplementations(next); err != nil {
		panic(fmt.Sprintf("automation: default app flow action-call handler implementations: %v", err))
	}
	defaultAppFlowActionCallHandlerImplementations.implementations = next
}

// DefaultAppFlowActionCallRegistrations returns the active distribution's
// registered app-owned actionCall handlers.
func DefaultAppFlowActionCallRegistrations() []FlowActionCallRegistration {
	defaultAppFlowActionCallRegistrations.RLock()
	defer defaultAppFlowActionCallRegistrations.RUnlock()
	return append([]FlowActionCallRegistration(nil), defaultAppFlowActionCallRegistrations.registrations...)
}

// DefaultAppFlowActionCallHandlerImplementations returns executable handlerRef
// bindings supplied by the active distribution.
func DefaultAppFlowActionCallHandlerImplementations() []FlowActionCallHandlerImplementation {
	defaultAppFlowActionCallHandlerImplementations.RLock()
	defer defaultAppFlowActionCallHandlerImplementations.RUnlock()
	return append([]FlowActionCallHandlerImplementation(nil), defaultAppFlowActionCallHandlerImplementations.implementations...)
}

// ResetDefaultAppFlowActionCallRegistrationsForTest clears app-owned default
// actionCall registrations for tests and returns a restore function.
func ResetDefaultAppFlowActionCallRegistrationsForTest() func() {
	defaultAppFlowActionCallRegistrations.Lock()
	previous := append([]FlowActionCallRegistration(nil), defaultAppFlowActionCallRegistrations.registrations...)
	defaultAppFlowActionCallRegistrations.registrations = nil
	defaultAppFlowActionCallRegistrations.Unlock()

	return func() {
		defaultAppFlowActionCallRegistrations.Lock()
		defaultAppFlowActionCallRegistrations.registrations = previous
		defaultAppFlowActionCallRegistrations.Unlock()
	}
}

// ResetDefaultAppFlowActionCallHandlerImplementationsForTest clears handler
// implementation bindings for tests and returns a restore function.
func ResetDefaultAppFlowActionCallHandlerImplementationsForTest() func() {
	defaultAppFlowActionCallHandlerImplementations.Lock()
	previous := append([]FlowActionCallHandlerImplementation(nil), defaultAppFlowActionCallHandlerImplementations.implementations...)
	defaultAppFlowActionCallHandlerImplementations.implementations = nil
	defaultAppFlowActionCallHandlerImplementations.Unlock()

	return func() {
		defaultAppFlowActionCallHandlerImplementations.Lock()
		defaultAppFlowActionCallHandlerImplementations.implementations = previous
		defaultAppFlowActionCallHandlerImplementations.Unlock()
	}
}

// FlowActionCallRegistrationsFromDescriptors resolves metadata-owned
// flow_action_call_handler declarations into executable registrations for
// handler refs supplied by this host. Descriptors with no matching
// implementation are ignored so platform/external registrations can continue to
// validate through their existing registries.
func FlowActionCallRegistrationsFromDescriptors(descriptors []FlowActionCallRegistrationDescriptor, implementations ...FlowActionCallHandlerImplementation) ([]FlowActionCallRegistration, error) {
	if len(descriptors) == 0 || len(implementations) == 0 {
		return nil, nil
	}
	if err := validateFlowActionCallHandlerImplementations(implementations); err != nil {
		return nil, err
	}
	byRef := make(map[string]FlowActionCallHandlerImplementation, len(implementations))
	for _, implementation := range implementations {
		byRef[normalizeHandlerRef(implementation.HandlerRef)] = implementation
	}

	out := make([]FlowActionCallRegistration, 0, len(descriptors))
	for _, descriptor := range descriptors {
		descriptor = normalizeFlowActionCallRegistrationDescriptor(descriptor)
		if descriptor.ActionType == "" || descriptor.HandlerRef == "" {
			continue
		}
		implementation, ok := byRef[normalizeHandlerRef(descriptor.HandlerRef)]
		if !ok {
			continue
		}
		registration, err := implementation.Build(descriptor)
		if err != nil {
			return nil, fmt.Errorf("flow action-call handlerRef %s for actionType %s: %w", descriptor.HandlerRef, descriptor.ActionType, err)
		}
		registration.ActionType = normalizeFlowActionType(registration.ActionType)
		registration.HandlerRef = strings.TrimSpace(registration.HandlerRef)
		if err := validateFlowActionCallDescriptorMatchesRegistration(descriptor, registration); err != nil {
			return nil, err
		}
		out = append(out, registration)
	}
	sort.Slice(out, func(i, j int) bool {
		return normalizeFlowActionType(out[i].ActionType) < normalizeFlowActionType(out[j].ActionType)
	})
	if err := validateFlowActionCallRegistrationSet(out); err != nil {
		return nil, err
	}
	return out, nil
}

func validateFlowActionCallHandlerImplementations(implementations []FlowActionCallHandlerImplementation) error {
	seen := map[string]struct{}{}
	for _, implementation := range implementations {
		handlerRef := normalizeHandlerRef(implementation.HandlerRef)
		switch {
		case handlerRef == "":
			return fmt.Errorf("flow action-call handler implementation requires handlerRef")
		case implementation.Build == nil:
			return fmt.Errorf("flow action-call handler implementation %q requires builder", implementation.HandlerRef)
		}
		if _, exists := seen[handlerRef]; exists {
			return fmt.Errorf("duplicate flow action-call handler implementation for handlerRef %q", implementation.HandlerRef)
		}
		seen[handlerRef] = struct{}{}
	}
	return nil
}

func validateFlowActionCallDescriptorMatchesRegistration(descriptor FlowActionCallRegistrationDescriptor, registration FlowActionCallRegistration) error {
	got := registration.Descriptor()
	want := normalizeFlowActionCallRegistrationDescriptor(descriptor)
	var mismatches []string
	if got.ActionType != want.ActionType {
		mismatches = append(mismatches, fmt.Sprintf("actionType declared %q runtime %q", want.ActionType, got.ActionType))
	}
	if got.HandlerRef != want.HandlerRef {
		mismatches = append(mismatches, fmt.Sprintf("handlerRef declared %q runtime %q", want.HandlerRef, got.HandlerRef))
	}
	if got.AtomicPolicy != want.AtomicPolicy {
		mismatches = append(mismatches, fmt.Sprintf("atomicPolicy declared %q runtime %q", want.AtomicPolicy, got.AtomicPolicy))
	}
	if got.PreviewPolicy != want.PreviewPolicy {
		mismatches = append(mismatches, fmt.Sprintf("previewPolicy declared %q runtime %q", want.PreviewPolicy, got.PreviewPolicy))
	}
	if got.RequiresContext != want.RequiresContext {
		mismatches = append(mismatches, fmt.Sprintf("requiresContext declared %q runtime %q", want.RequiresContext, got.RequiresContext))
	}
	if got.SendsEmail != want.SendsEmail {
		mismatches = append(mismatches, fmt.Sprintf("sendsEmail declared %t runtime %t", want.SendsEmail, got.SendsEmail))
	}
	if len(mismatches) > 0 {
		return fmt.Errorf("flow action-call handler %s conflicts with metadata declaration: %s", want.ActionType, strings.Join(mismatches, "; "))
	}
	return nil
}

func normalizeFlowActionCallRegistrationDescriptor(descriptor FlowActionCallRegistrationDescriptor) FlowActionCallRegistrationDescriptor {
	descriptor.ActionType = normalizeFlowActionType(descriptor.ActionType)
	descriptor.HandlerRef = strings.TrimSpace(descriptor.HandlerRef)
	descriptor.AtomicPolicy = strings.ToLower(strings.TrimSpace(descriptor.AtomicPolicy))
	descriptor.PreviewPolicy = strings.ToLower(strings.TrimSpace(descriptor.PreviewPolicy))
	if descriptor.PreviewPolicy == "" {
		descriptor.PreviewPolicy = FlowActionCallPreviewPolicyReject.MetadataValue()
	}
	descriptor.RequiresContext = strings.ToLower(strings.TrimSpace(descriptor.RequiresContext))
	if descriptor.RequiresContext == "" {
		descriptor.RequiresContext = FlowActionCallContextNone.MetadataValue()
	}
	return descriptor
}

func normalizeHandlerRef(handlerRef string) string {
	return strings.TrimSpace(handlerRef)
}

// EffectiveFlowActionCallRegistrations returns the full process-local runtime
// actionCall set: generic platform-owned capabilities plus external
// app/distribution registrations installed through backend/pkg/flowactions.
func EffectiveFlowActionCallRegistrations(appRegistrations ...FlowActionCallRegistration) ([]FlowActionCallRegistration, error) {
	out := PlatformFlowActionCallRegistrations()
	out = append(out, appRegistrations...)
	external, err := externalFlowActionCallRegistrations()
	if err != nil {
		return nil, err
	}
	out = append(out, external...)
	if err := validateFlowActionCallRegistrationSet(out); err != nil {
		return nil, err
	}
	return out, nil
}

// EffectiveFlowActionCallRegistrationDescriptors returns the process-local
// actionCall declarations the default platform executor can actually dispatch.
func EffectiveFlowActionCallRegistrationDescriptors(appRegistrations ...FlowActionCallRegistration) ([]FlowActionCallRegistrationDescriptor, error) {
	registry, err := platformFlowActionCallRegistry(appRegistrations...)
	if err != nil {
		return nil, err
	}
	actionTypes := make([]string, 0, len(registry.byType))
	for actionType := range registry.byType {
		actionTypes = append(actionTypes, actionType)
	}
	sort.Strings(actionTypes)
	descriptors := make([]FlowActionCallRegistrationDescriptor, 0, len(actionTypes))
	for _, actionType := range actionTypes {
		descriptors = append(descriptors, registry.byType[actionType].Descriptor())
	}
	return mergeFlowActionCallRegistrationDescriptors(descriptors, PlatformFlowActionCallContractDescriptors())
}

func mergeFlowActionCallRegistrationDescriptors(descriptors []FlowActionCallRegistrationDescriptor, additions []FlowActionCallRegistrationDescriptor) ([]FlowActionCallRegistrationDescriptor, error) {
	out := make([]FlowActionCallRegistrationDescriptor, 0, len(descriptors)+len(additions))
	indexByActionType := map[string]int{}
	for _, descriptor := range append(append([]FlowActionCallRegistrationDescriptor(nil), descriptors...), additions...) {
		descriptor = normalizeFlowActionCallRegistrationDescriptor(descriptor)
		if err := validateFlowActionCallRegistrationDescriptor(descriptor); err != nil {
			return nil, err
		}
		if existingIndex, ok := indexByActionType[descriptor.ActionType]; ok {
			existing := out[existingIndex]
			if !flowActionCallRegistrationDescriptorsEqual(existing, descriptor) {
				return nil, fmt.Errorf("conflicting flow action-call descriptor for actionType %q", descriptor.ActionType)
			}
			continue
		}
		indexByActionType[descriptor.ActionType] = len(out)
		out = append(out, descriptor)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].ActionType < out[j].ActionType
	})
	return out, nil
}

func validateFlowActionCallRegistrationDescriptor(descriptor FlowActionCallRegistrationDescriptor) error {
	switch {
	case descriptor.ActionType == "":
		return fmt.Errorf("flow action-call descriptor requires actionType")
	case descriptor.HandlerRef == "":
		return fmt.Errorf("flow action-call descriptor %q requires handlerRef", descriptor.ActionType)
	case descriptor.AtomicPolicy != FlowActionCallAtomicPolicySafe.MetadataValue() && descriptor.AtomicPolicy != FlowActionCallAtomicPolicyReject.MetadataValue():
		return fmt.Errorf("flow action-call descriptor %q has unsupported atomicPolicy %q", descriptor.ActionType, descriptor.AtomicPolicy)
	case descriptor.PreviewPolicy != FlowActionCallPreviewPolicyReject.MetadataValue() &&
		descriptor.PreviewPolicy != FlowActionCallPreviewPolicyAllow.MetadataValue() &&
		descriptor.PreviewPolicy != FlowActionCallPreviewPolicyAllowWithPreviewPersistence.MetadataValue() &&
		descriptor.PreviewPolicy != FlowActionCallPreviewPolicyRejectEmailIO.MetadataValue() &&
		descriptor.PreviewPolicy != FlowActionCallPreviewPolicyInvokeHandler.MetadataValue():
		return fmt.Errorf("flow action-call descriptor %q has unsupported previewPolicy %q", descriptor.ActionType, descriptor.PreviewPolicy)
	case descriptor.RequiresContext != FlowActionCallContextNone.MetadataValue() &&
		descriptor.RequiresContext != FlowActionCallContextStandardActionProtected.MetadataValue():
		return fmt.Errorf("flow action-call descriptor %q has unsupported requiresContext %q", descriptor.ActionType, descriptor.RequiresContext)
	}
	return nil
}

func flowActionCallRegistrationDescriptorsEqual(a FlowActionCallRegistrationDescriptor, b FlowActionCallRegistrationDescriptor) bool {
	a = normalizeFlowActionCallRegistrationDescriptor(a)
	b = normalizeFlowActionCallRegistrationDescriptor(b)
	return a.ActionType == b.ActionType &&
		a.HandlerRef == b.HandlerRef &&
		a.SendsEmail == b.SendsEmail &&
		a.AtomicPolicy == b.AtomicPolicy &&
		a.PreviewPolicy == b.PreviewPolicy &&
		a.RequiresContext == b.RequiresContext
}

func platformFlowActionCallRegistry(appRegistrations ...FlowActionCallRegistration) (*flowActionCallRegistry, error) {
	registrations := genericFlowActionCallRegistrations()
	effective, err := EffectiveFlowActionCallRegistrations(appRegistrations...)
	if err != nil {
		return nil, err
	}
	registrations = append(registrations, effective...)
	if err := validateFlowActionCallRegistrationSet(registrations); err != nil {
		return nil, err
	}
	return newFlowActionCallRegistry(registrations...), nil
}

func validateFlowActionCallRegistrationSet(registrations []FlowActionCallRegistration) error {
	seen := map[string]string{}
	for _, registration := range registrations {
		actionType := normalizeFlowActionType(registration.ActionType)
		switch {
		case actionType == "":
			return fmt.Errorf("flow action-call registration requires actionType")
		case registration.Handler == nil:
			return fmt.Errorf("flow action-call registration %q requires handler", registration.ActionType)
		case registration.AtomicPolicy == FlowActionCallAtomicPolicyUnspecified:
			return fmt.Errorf("flow action-call registration %q requires atomicPolicy", registration.ActionType)
		}
		if previous, exists := seen[actionType]; exists {
			return fmt.Errorf("duplicate flow action-call registration for %q (normalized actionType %q already registered by %q)", registration.ActionType, actionType, previous)
		}
		seen[actionType] = registration.ActionType
	}
	return nil
}

func validateDefaultAppFlowActionCallRegistrations(registrations []FlowActionCallRegistration) error {
	if err := validateFlowActionCallRegistrationSet(registrations); err != nil {
		return err
	}
	inBinaryActionTypes := inBinaryRuntimeFlowActionCallTypes()
	for _, registration := range registrations {
		actionType := normalizeFlowActionType(registration.ActionType)
		if _, exists := inBinaryActionTypes[actionType]; exists {
			return fmt.Errorf("app-owned flow action-call registration %q conflicts with internal runtime actionType %q", registration.ActionType, actionType)
		}
	}
	return nil
}

func externalFlowActionCallRegistrations() ([]FlowActionCallRegistration, error) {
	external := flowactions.Registrations()
	inBinaryActionTypes := inBinaryRuntimeFlowActionCallTypes()
	out := make([]FlowActionCallRegistration, 0, len(external))
	for _, registration := range external {
		registration := registration
		actionType := normalizeFlowActionType(registration.ActionType)
		if _, exists := inBinaryActionTypes[actionType]; exists {
			return nil, fmt.Errorf("external flow action-call registration %q conflicts with internal runtime actionType %q", registration.ActionType, actionType)
		}
		out = append(out, FlowActionCallRegistration{
			ActionType:               registration.ActionType,
			HandlerRef:               registration.HandlerRef,
			SendsEmail:               registration.SendsEmail,
			ApprovalNotificationSafe: registration.SendsEmail,
			AtomicPolicy:             flowActionCallAtomicPolicyFromExternal(registration.AtomicPolicy),
			AtomicRejectText:         registration.AtomicRejectText,
			PreviewPolicy:            flowActionCallPreviewPolicyFromExternal(registration.PreviewPolicy),
			RequiresContext:          flowActionCallContextRequirementFromExternal(registration.RequiresContext),
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				next, err := registration.Handler(ctx, externalFlowActionInvocation{
					interview: interview,
					metadata:  flowMeta,
					action:    action,
					user:      user,
				})
				if err != nil && action.FaultConnector != nil {
					return e.routeFault(ctx, interview, action.FaultConnector.TargetReference, err)
				}
				return next, err
			},
		})
	}
	return out, nil
}

func inBinaryRuntimeFlowActionCallTypes() map[string]struct{} {
	actionTypes := inBinaryRuntimeFlowActionCallTypeNames()
	out := make(map[string]struct{}, len(actionTypes))
	for _, actionType := range actionTypes {
		normalized := normalizeFlowActionType(actionType)
		if normalized == "" {
			continue
		}
		out[normalized] = struct{}{}
	}
	return out
}

func inBinaryRuntimeFlowActionCallTypeNames() []string {
	return flowactions.PlatformReservedActionTypes()
}

func flowActionCallAtomicPolicyFromExternal(policy flowactions.AtomicPolicy) FlowActionCallAtomicPolicy {
	switch policy {
	case flowactions.AtomicPolicySafe:
		return FlowActionCallAtomicPolicySafe
	case flowactions.AtomicPolicyReject:
		return FlowActionCallAtomicPolicyReject
	default:
		return FlowActionCallAtomicPolicyUnspecified
	}
}

func flowActionCallPreviewPolicyFromExternal(policy flowactions.PreviewPolicy) FlowActionCallPreviewPolicy {
	switch policy {
	case flowactions.PreviewPolicyReject:
		return FlowActionCallPreviewPolicyReject
	case flowactions.PreviewPolicyAllow:
		return FlowActionCallPreviewPolicyAllow
	case flowactions.PreviewPolicyAllowWithPreviewPersistence:
		return FlowActionCallPreviewPolicyAllowWithPreviewPersistence
	case flowactions.PreviewPolicyRejectEmailIO:
		return FlowActionCallPreviewPolicyRejectEmailIO
	case flowactions.PreviewPolicyInvokeHandler:
		return FlowActionCallPreviewPolicyInvokeHandler
	default:
		panic(fmt.Sprintf("unsupported external flow action-call previewPolicy %q", policy))
	}
}

func flowActionCallContextRequirementFromExternal(requirement flowactions.ContextRequirement) FlowActionCallContextRequirement {
	switch requirement {
	case flowactions.ContextRequirementNone:
		return FlowActionCallContextNone
	case flowactions.ContextRequirementStandardActionProtected:
		return FlowActionCallContextStandardActionProtected
	default:
		panic(fmt.Sprintf("unsupported external flow action-call requiresContext %q", requirement))
	}
}

type externalFlowActionInvocation struct {
	interview *models.FlowInterview
	metadata  *models.FlowMetadata
	action    *models.FlowActionCall
	user      *models.UserSession
}

func (i externalFlowActionInvocation) Interview() *models.FlowInterview {
	return i.interview
}

func (i externalFlowActionInvocation) Metadata() *models.FlowMetadata {
	return i.metadata
}

func (i externalFlowActionInvocation) Action() *models.FlowActionCall {
	return i.action
}

func (i externalFlowActionInvocation) User() *models.UserSession {
	return i.user
}

// EmailFlowActionCallRegistrations returns notification/email actionCall
// handlers owned by the email capability wiring.
func EmailFlowActionCallRegistrations() []FlowActionCallRegistration {
	return []FlowActionCallRegistration{
		FlowActionCallRegistration{
			ActionType:               flowactions.ActionTypeSendEmail,
			HandlerRef:               "platform:automation/send-email",
			SendsEmail:               true,
			ApprovalNotificationSafe: true,
			AtomicPolicy:             FlowActionCallAtomicPolicyReject,
			AtomicRejectText:         flowActionCallEmailAtomicRejectText,
			PreviewPolicy:            FlowActionCallPreviewPolicyRejectEmailIO,
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return e.executeSendEmailAction(ctx, interview, flowMeta, action, user)
			},
		},
		FlowActionCallRegistration{
			ActionType:               flowactions.ActionTypeResolveEmailRecipients,
			HandlerRef:               "platform:automation/resolve-email-recipients",
			ApprovalNotificationSafe: true,
			AtomicPolicy:             FlowActionCallAtomicPolicySafe,
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return e.executeResolveEmailRecipientsAction(ctx, interview, action, user)
			},
		},
	}
}

// NotificationFlowActionCallRegistrations returns in-app notification
// actionCall handlers owned by the platform notification capability.
// ApprovalNotificationSafe is true because the forcing use case is notifying
// a stamped third-party record owner from the approval-notification sandbox,
// which otherwise bans record mutation elements; the write here is
// encapsulated in Go handler code, not a RecordCreate element.
func NotificationFlowActionCallRegistrations() []FlowActionCallRegistration {
	return []FlowActionCallRegistration{
		FlowActionCallRegistration{
			ActionType:               flowactions.ActionTypeSendNotification,
			HandlerRef:               "platform:automation/send-notification",
			SendsEmail:               false,
			ApprovalNotificationSafe: true,
			AtomicPolicy:             FlowActionCallAtomicPolicySafe,
			PreviewPolicy:            FlowActionCallPreviewPolicyReject,
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return e.executeSendNotificationAction(ctx, interview, flowMeta, action, user)
			},
		},
	}
}

// LegacySendEmailAliasFlowActionCallRegistration adapts an app-owned legacy
// send-email alias onto the durable sendEmail implementation while generated
// metadata is normalized.
func LegacySendEmailAliasFlowActionCallRegistration(actionType string, handlerRef string) FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:               strings.TrimSpace(actionType),
		HandlerRef:               strings.TrimSpace(handlerRef),
		SendsEmail:               true,
		ApprovalNotificationSafe: true,
		AtomicPolicy:             FlowActionCallAtomicPolicyReject,
		AtomicRejectText:         flowActionCallEmailAtomicRejectText,
		PreviewPolicy:            FlowActionCallPreviewPolicyRejectEmailIO,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeSendEmailAction(ctx, interview, flowMeta, normalizeEmailSimpleAction(action), user)
		},
	}
}

func LegacySendEmailAliasFlowActionCallHandlerImplementation(handlerRef string) FlowActionCallHandlerImplementation {
	return FlowActionCallHandlerImplementation{
		HandlerRef: strings.TrimSpace(handlerRef),
		Build: func(descriptor FlowActionCallRegistrationDescriptor) (FlowActionCallRegistration, error) {
			return LegacySendEmailAliasFlowActionCallRegistration(descriptor.ActionType, descriptor.HandlerRef), nil
		},
	}
}

// DisabledWorkflowAlertFlowActionCallRegistration keeps migration-only alert
// actionCalls fail-closed through an app-owned registration until zero
// dependencies allow removing the compatibility input.
func DisabledWorkflowAlertFlowActionCallRegistration(actionType string, handlerRef string) FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:    strings.TrimSpace(actionType),
		HandlerRef:    strings.TrimSpace(handlerRef),
		AtomicPolicy:  FlowActionCallAtomicPolicyReject,
		PreviewPolicy: FlowActionCallPreviewPolicyInvokeHandler,
		AtomicRejectText: func(action *models.FlowActionCall) string {
			return fmt.Sprintf("flows executing atomically cannot contain %s action calls; %s", action.ActionType, workflowAlertRuntimeDisabledReason)
		},
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, _ *models.UserSession) (string, error) {
			err := legacyWorkflowAlertRuntimeDisabledError("Flow " + strings.TrimSpace(actionType))
			if action.FaultConnector != nil {
				return e.routeFault(ctx, interview, action.FaultConnector.TargetReference, err)
			}
			return "", err
		},
	}
}

func DisabledWorkflowAlertFlowActionCallHandlerImplementation(handlerRef string) FlowActionCallHandlerImplementation {
	return FlowActionCallHandlerImplementation{
		HandlerRef: strings.TrimSpace(handlerRef),
		Build: func(descriptor FlowActionCallRegistrationDescriptor) (FlowActionCallRegistration, error) {
			return DisabledWorkflowAlertFlowActionCallRegistration(descriptor.ActionType, descriptor.HandlerRef), nil
		},
	}
}

// ConnectorPlatformFlowActionCallRegistrations returns connector actionCall
// handlers that remain platform-owned. PushToConnector is intentionally absent:
// the host installs RuntimeOutboundIntentFlowActionCallRegistration to enqueue
// intents for the independent Integration process.
func ConnectorPlatformFlowActionCallRegistrations() []FlowActionCallRegistration {
	return []FlowActionCallRegistration{
		ConnectorFieldMappingFlowActionCallRegistration(),
		FlowActionCallRegistration{
			ActionType:   flowactions.ActionTypeRegisterConnectorInboundAuthorship,
			HandlerRef:   "platform:connector/register-inbound-remote-authorship",
			AtomicPolicy: FlowActionCallAtomicPolicySafe,
			Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, _ *models.UserSession) (string, error) {
				return e.executeRegisterConnectorInboundRemoteAuthorship(ctx, interview, action)
			},
		},
		ConnectorInboundRemoteIdentityFinalizationFlowActionCallRegistration(),
		ConnectorRemoteIdentityResolutionFlowActionCallRegistration(),
		ConnectorRemoteIdentityTransferFlowActionCallRegistration(),
		ConnectorRetriableFlowActionCallRegistration(),
		IdentityConvergenceDependencyFlowActionCallRegistration(),
		ConnectorInboundRemoteIdentityRecoveryRequestFlowActionCallRegistration(),
	}
}

func ConnectorRemoteIdentityResolutionFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeResolveConnectorRemoteIdentity,
		HandlerRef:   "platform:connector/resolve-remote-identity",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeResolveConnectorRemoteIdentity(ctx, interview, action, user)
		},
	}
}

// ConnectorRemoteIdentityTransferFlowActionCallRegistration returns the
// atomic identity-transfer mechanism. The Runtime worker installs this narrow
// registration only alongside the immutable installed-contract resolver.
func ConnectorRemoteIdentityTransferFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeTransferConnectorRemoteIdentity,
		HandlerRef:   "platform:connector/transfer-remote-identity",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeTransferConnectorRemoteIdentity(ctx, interview, action, user)
		},
	}
}

// IdentityConvergenceDependencyFlowActionCallRegistration returns only the
// atomic suspension marker. It performs no provider I/O and is safe to install
// in the Platform-command Worker without installing the wider connector action
// set; the command profile separately decides whether a Flow may use it.
func IdentityConvergenceDependencyFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeSetIdentityConvergenceDependency,
		HandlerRef:   "platform:connector/set-identity-convergence-dependency",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeSetIdentityConvergenceDependency(ctx, interview, flowMeta, action, user)
		},
	}
}

// ConnectorInboundRemoteIdentityFinalizationFlowActionCallRegistration
// returns only the atomic, no-HTTP finalization mechanism. Runtime Worker uses
// this narrow registration instead of installing the wider connector lifecycle
// action set or PushToConnector provider effects.
func ConnectorInboundRemoteIdentityFinalizationFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeFinalizeConnectorInboundRemoteIdentity,
		HandlerRef:   "platform:connector/finalize-inbound-remote-identity",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeFinalizeConnectorInboundRemoteIdentity(ctx, interview, action, user)
		},
	}
}

// ConnectorRetriableFlowActionCallRegistration returns the connector retry
// marker whose complete execution closure is the current Flow runtime context.
// It owns no remote-identity store, provider effect, profile resolver, or
// metadata fallback, so a durable command role can install it without
// advertising the wider connector lifecycle.
func ConnectorRetriableFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeSetRetriable,
		HandlerRef:   "platform:connector/set-retriable",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, _ *models.UserSession) (string, error) {
			return e.executeSetRetriable(ctx, interview, action)
		},
	}
}

// ConnectorFieldMappingFlowActionCallRegistration returns the one connector
// action whose complete execution closure is the revision-bound mutation view
// plus the Records persistence graph. Runtime roles that do not own the
// connector remote-identity lifecycle can install this action without
// advertising the durable-store/profile/contract-dependent actions returned by
// ConnectorPlatformFlowActionCallRegistrations.
func ConnectorFieldMappingFlowActionCallRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   flowactions.ActionTypeApplyConnectorFieldMappings,
		HandlerRef:   "platform:connector/apply-field-mappings",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
			return e.executeApplyConnectorFieldMappings(ctx, interview, action, user)
		},
	}
}

// ConnectorPushFlowActionCallRegistrationDescriptor returns the
// metadata-facing PushToConnector contract. It is intentionally descriptor-only
// for setup/pack validation; only a server host with connector runtime wired may
// install the executable registration below.
func ConnectorPushFlowActionCallRegistrationDescriptor() FlowActionCallRegistrationDescriptor {
	return FlowActionCallRegistrationDescriptor{
		ActionType:      flowactions.ActionTypePushToConnector,
		HandlerRef:      "platform:connector/push-to-connector",
		AtomicPolicy:    FlowActionCallAtomicPolicyReject.MetadataValue(),
		PreviewPolicy:   FlowActionCallPreviewPolicyReject.MetadataValue(),
		RequiresContext: FlowActionCallContextNone.MetadataValue(),
	}
}

// StandardActionFlowActionCallRegistrations returns actionCall handlers owned
// by the standard-action capability wiring.
func StandardActionFlowActionCallRegistrations() []FlowActionCallRegistration {
	return []FlowActionCallRegistration{
		FlowActionCallRegistration{
			ActionType:      flowactions.ActionTypePreviewRecordUpdate,
			HandlerRef:      "platform:standard-action/preview-record-update",
			AtomicPolicy:    FlowActionCallAtomicPolicySafe,
			PreviewPolicy:   FlowActionCallPreviewPolicyAllowWithPreviewPersistence,
			RequiresContext: FlowActionCallContextStandardActionProtected,
			Handler: protectedStandardActionHandler(flowactions.ActionTypePreviewRecordUpdate, func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
				return e.executePreviewRecordUpdate(ctx, interview, action, user)
			}),
		},
		FlowActionCallRegistration{
			ActionType:      flowactions.ActionTypeCommitPreparedRecordUpdate,
			HandlerRef:      "platform:standard-action/commit-prepared-record-update",
			AtomicPolicy:    FlowActionCallAtomicPolicySafe,
			PreviewPolicy:   FlowActionCallPreviewPolicyReject,
			RequiresContext: FlowActionCallContextStandardActionProtected,
			Handler: protectedStandardActionHandler(
				flowactions.ActionTypeCommitPreparedRecordUpdate,
				executeCommitPreparedRecordUpdate,
			),
		},
		FlowActionCallRegistration{
			ActionType:      flowactions.ActionTypeTransferRecordOwner,
			HandlerRef:      "platform:standard-action/transfer-record-owner",
			AtomicPolicy:    FlowActionCallAtomicPolicySafe,
			PreviewPolicy:   FlowActionCallPreviewPolicyReject,
			RequiresContext: FlowActionCallContextStandardActionProtected,
			Handler: protectedStandardActionHandler(
				flowactions.ActionTypeTransferRecordOwner,
				executeTransferRecordOwner,
			),
		},
	}
}

func protectedStandardActionHandler(actionType string, next FlowActionCallHandler) FlowActionCallHandler {
	return func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, flowMeta *models.FlowMetadata, action *models.FlowActionCall, user *models.UserSession) (string, error) {
		if !constants.HasStandardActionExecutionContext(ctx) || !constants.HasStandardActionProtectedFlowExecution(ctx) {
			err := fmt.Errorf("%s requires protected standard action flow execution context", actionType)
			if action.FaultConnector != nil {
				return e.routeFault(ctx, interview, action.FaultConnector.TargetReference, err)
			}
			return "", err
		}
		return next(ctx, e, interview, flowMeta, action, user)
	}
}

func (e *FlowExecutor) flowActionCallRegistry() *flowActionCallRegistry {
	if e != nil && e.actionCallRegistry != nil {
		return e.actionCallRegistry
	}
	return defaultFlowActionCallRegistry()
}

func (e *FlowExecutor) ensureFlowActionCallRegistry() *flowActionCallRegistry {
	if e == nil {
		return defaultFlowActionCallRegistry()
	}
	if e.actionCallRegistry == nil {
		e.actionCallRegistry = defaultFlowActionCallRegistry()
	}
	return e.actionCallRegistry
}

// RegisterFlowActionCalls adds registrations to this executor's registry.
// Callers should use this from composition roots after constructing the
// executor when a capability must install action handlers after dependency
// wiring. Duplicate action types still fail closed.
func (e *FlowExecutor) RegisterFlowActionCalls(registrations ...FlowActionCallRegistration) {
	if len(registrations) == 0 {
		return
	}
	registry := e.ensureFlowActionCallRegistry()
	for _, registration := range registrations {
		registry.Register(registration)
	}
}

func (r *flowActionCallRegistry) actionCallAllowedInApprovalNotification(actionType string) bool {
	registration, ok := r.Lookup(actionType)
	return ok && registration.ApprovalNotificationSafe
}

func (r *flowActionCallRegistry) actionCallUsesMetadataBackedAction(actionType string) bool {
	_, ok := r.Lookup(actionType)
	return !ok
}
