package automation

import (
	"context"
	"strings"

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

// StandardActionCreatePreviewSupported reports whether the active flow shape can
// complete through PreviewStandardActionRecordCreateFields without hitting a
// known unsupported no-DML boundary. It is a discovery guard only; execution
// still validates the live path and can fail for data-dependent reasons.
func (e *FlowExecutor) StandardActionCreatePreviewSupported(ctx context.Context, flowApiName string, user *models.UserSession) (bool, error) {
	if e == nil || strings.TrimSpace(flowApiName) == "" {
		return false, nil
	}
	if !hasPreparedFlowClosure(ctx) && e.flowVersionFn == nil && e.query == nil {
		return false, nil
	}
	version, err := e.getActiveFlowVersion(ctx, flowApiName, user)
	if err != nil || version == nil {
		return false, err
	}
	hasLookupAfterCreate, err := e.flowVersionHasLookupAfterCreate(ctx, flowApiName, version, user, false, map[string]bool{}, e.previewSyntheticLookupOverlayAvailable())
	if err != nil {
		return false, err
	}
	if hasLookupAfterCreate {
		return false, nil
	}
	createdObjects := map[string]struct{}{}
	ok, err := e.flowVersionCreatePreviewSupported(ctx, flowApiName, version, user, map[string]bool{}, createdObjects)
	if err != nil || !ok {
		return ok, err
	}
	return e.recordBeforeCreatePreviewSupported(ctx, createdObjects, user)
}

// PreparedStandardActionCreatePreviewSupported evaluates preview eligibility
// against the same exact closure that will execute, rather than the currently
// active repository version.
func (e *FlowExecutor) PreparedStandardActionCreatePreviewSupported(
	ctx context.Context,
	flowAPIName string,
	versions map[string]*models.FlowVersion,
	user *models.UserSession,
) (bool, error) {
	prepared, err := withPreparedFlowClosure(ctx, versions)
	if err != nil {
		return false, err
	}
	return e.StandardActionCreatePreviewSupported(prepared, flowAPIName, user)
}

func (e *FlowExecutor) flowVersionCreatePreviewSupported(ctx context.Context, flowApiName string, version *models.FlowVersion, user *models.UserSession, seen map[string]bool, createdObjects map[string]struct{}) (bool, error) {
	if version == nil {
		return false, nil
	}
	key := strings.ToLower(strings.TrimSpace(flowApiName))
	if key != "" {
		if seen[key] {
			return true, nil
		}
		seen[key] = true
	}

	meta := version.Metadata
	if len(meta.RecordDeletes) > 0 || len(meta.Waits) > 0 || len(meta.Screens) > 0 {
		return false, nil
	}
	if len(meta.RecordUpdates) > 0 {
		return false, nil
	}
	// A no-DML create returns request-local synthetic IDs. Until the generic
	// records preparation/read paths are provenance-aware for those rows, any
	// flow that feeds one synthetic ID into later metadata execution is not a
	// server-authoritative preview candidate.
	syntheticRecordIDRefs, err := e.flowVersionSyntheticRecordIDReferences(ctx, flowApiName, version, user, map[string]bool{})
	if err != nil {
		return false, err
	}
	if len(syntheticRecordIDRefs) > 0 && e.flowVersionReferencesUnsupportedSyntheticRecordID(ctx, meta, syntheticRecordIDRefs) {
		return false, nil
	}
	for _, create := range meta.RecordCreates {
		objectAPIName := strings.TrimSpace(create.Object)
		if objectAPIName == "" {
			return false, nil
		}
		// Atomic external-ID upsert may adopt and update an existing row. The
		// preview persistence wrapper only captures insert-only RecordCreate
		// semantics, so advertising this flow as previewable would defer the
		// incompatibility until execution. Fail capability discovery closed
		// until preview has explicit create-or-adopt semantics.
		if strings.TrimSpace(create.UpsertByExternalIDField) != "" {
			return false, nil
		}
		createdObjects[objectAPIName] = struct{}{}
	}
	for _, action := range meta.ActionCalls {
		switch strings.ToLower(strings.TrimSpace(action.ActionType)) {
		case "flow":
			childName := strings.TrimSpace(action.ActionName)
			if childName == "" {
				return false, nil
			}
			ok, err := e.childFlowCreatePreviewSupported(ctx, childName, user, seen, createdObjects)
			if err != nil || !ok {
				return ok, err
			}
		case "previewrecordupdate":
			return false, nil
		default:
			return false, nil
		}
	}
	for _, subflow := range meta.Subflows {
		childName := strings.TrimSpace(subflow.FlowName)
		if childName == "" {
			return false, nil
		}
		ok, err := e.childFlowCreatePreviewSupported(ctx, childName, user, seen, createdObjects)
		if err != nil || !ok {
			return ok, err
		}
	}
	return true, nil
}

func (e *FlowExecutor) flowVersionHasLookupAfterCreate(ctx context.Context, flowApiName string, version *models.FlowVersion, user *models.UserSession, startsAfterCreate bool, seenFlows map[string]bool, syntheticOverlayAvailable bool) (bool, error) {
	if version == nil {
		return false, nil
	}
	key := strings.ToLower(strings.TrimSpace(flowApiName))
	if key != "" {
		if startsAfterCreate {
			key += ":after-create"
		} else {
			key += ":before-create"
		}
		if seenFlows[key] {
			return false, nil
		}
		seenFlows[key] = true
		defer delete(seenFlows, key)
	}

	summary, err := e.flowMetadataPreviewPathSummary(ctx, &version.Metadata, user, startsAfterCreate, seenFlows, syntheticOverlayAvailable)
	if err != nil {
		return false, err
	}
	return summary.lookupAfterCreate, nil
}

type previewPathSummary struct {
	lookupAfterCreate bool
	exitsAfterCreate  bool
}

func (e *FlowExecutor) flowMetadataPreviewPathSummary(ctx context.Context, meta *models.FlowMetadata, user *models.UserSession, startsAfterCreate bool, seenFlows map[string]bool, syntheticOverlayAvailable bool) (previewPathSummary, error) {
	if meta == nil {
		return previewPathSummary{exitsAfterCreate: startsAfterCreate}, nil
	}

	starts := flowStartTargets(meta)
	if len(starts) == 0 {
		return previewPathSummary{exitsAfterCreate: startsAfterCreate}, nil
	}

	seenElements := map[string]bool{}
	out := previewPathSummary{}
	for _, start := range starts {
		summary, err := e.flowElementPreviewPathSummary(ctx, meta, user, start, startsAfterCreate, seenElements, seenFlows, syntheticOverlayAvailable)
		if err != nil || summary.lookupAfterCreate {
			return summary, err
		}
		out.exitsAfterCreate = out.exitsAfterCreate || summary.exitsAfterCreate
	}
	return out, nil
}

func flowStartTargets(meta *models.FlowMetadata) []string {
	if meta == nil {
		return nil
	}
	if meta.Start != nil && meta.Start.Connector != nil && strings.TrimSpace(meta.Start.Connector.TargetReference) != "" {
		return []string{strings.TrimSpace(meta.Start.Connector.TargetReference)}
	}
	if strings.TrimSpace(meta.StartElementReference) != "" {
		return []string{strings.TrimSpace(meta.StartElementReference)}
	}
	if meta.Start == nil {
		return nil
	}
	out := make([]string, 0, len(meta.Start.ScheduledPaths))
	for _, path := range meta.Start.ScheduledPaths {
		if path.Connector == nil || strings.TrimSpace(path.Connector.TargetReference) == "" {
			continue
		}
		out = append(out, strings.TrimSpace(path.Connector.TargetReference))
	}
	return out
}

func (e *FlowExecutor) flowElementPreviewPathSummary(ctx context.Context, meta *models.FlowMetadata, user *models.UserSession, elementName string, afterCreate bool, seenElements map[string]bool, seenFlows map[string]bool, syntheticOverlayAvailable bool) (previewPathSummary, error) {
	elementName = strings.TrimSpace(elementName)
	if meta == nil || elementName == "" {
		return previewPathSummary{exitsAfterCreate: afterCreate}, nil
	}
	key := elementName
	if afterCreate {
		key += ":after-create"
	} else {
		key += ":before-create"
	}
	if seenElements[key] {
		return previewPathSummary{exitsAfterCreate: afterCreate}, nil
	}
	seenElements[key] = true

	if create := flowRecordCreateByName(meta, elementName); create != nil {
		afterCreate = true
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, create.Connector, create.FaultConnector)
	}
	if lookup := flowRecordLookupByName(meta, elementName); lookup != nil {
		if afterCreate && (!syntheticOverlayAvailable || !flowRecordLookupSyntheticOverlaySupported(*lookup)) {
			return previewPathSummary{lookupAfterCreate: true, exitsAfterCreate: true}, nil
		}
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, lookup.Connector, lookup.FaultConnector)
	}
	if action := flowActionCallByName(meta, elementName); action != nil {
		childAfterCreate := afterCreate
		if strings.EqualFold(strings.TrimSpace(action.ActionType), "flow") {
			childName := strings.TrimSpace(action.ActionName)
			if childName == "" {
				return previewPathSummary{exitsAfterCreate: afterCreate}, nil
			}
			childSummary, err := e.childFlowPreviewPathSummary(ctx, childName, user, afterCreate, seenFlows, syntheticOverlayAvailable)
			if err != nil || childSummary.lookupAfterCreate {
				return childSummary, err
			}
			childAfterCreate = childSummary.exitsAfterCreate
		}
		return e.followPreviewPathTargets(ctx, meta, user, childAfterCreate, seenElements, seenFlows, syntheticOverlayAvailable, action.Connector, action.FaultConnector)
	}
	if subflow := flowSubflowByName(meta, elementName); subflow != nil {
		childName := strings.TrimSpace(subflow.FlowName)
		if childName == "" {
			return previewPathSummary{exitsAfterCreate: afterCreate}, nil
		}
		childSummary, err := e.childFlowPreviewPathSummary(ctx, childName, user, afterCreate, seenFlows, syntheticOverlayAvailable)
		if err != nil || childSummary.lookupAfterCreate {
			return childSummary, err
		}
		return e.followPreviewPathTargets(ctx, meta, user, childSummary.exitsAfterCreate, seenElements, seenFlows, syntheticOverlayAvailable, subflow.Connector, subflow.FaultConnector)
	}
	if decision := flowDecisionByName(meta, elementName); decision != nil {
		targets := make([]*models.FlowConnector, 0, len(decision.Rules)+1)
		for i := range decision.Rules {
			targets = append(targets, decision.Rules[i].Connector)
		}
		targets = append(targets, decision.DefaultConnector)
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, targets...)
	}
	if assignment := flowAssignmentByName(meta, elementName); assignment != nil {
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, assignment.Connector)
	}
	if loop := flowLoopByName(meta, elementName); loop != nil {
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, loop.NextValueConnector, loop.NoMoreValuesConnector)
	}
	if customErr := flowCustomErrorByName(meta, elementName); customErr != nil {
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, customErr.Connector)
	}
	if screen := flowScreenByName(meta, elementName); screen != nil {
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, screen.Connector)
	}
	if wait := flowWaitByName(meta, elementName); wait != nil {
		targets := make([]*models.FlowConnector, 0, len(wait.WaitEvents)+2)
		for i := range wait.WaitEvents {
			targets = append(targets, wait.WaitEvents[i].Connector)
		}
		targets = append(targets, wait.DefaultConnector, wait.FaultConnector)
		return e.followPreviewPathTargets(ctx, meta, user, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable, targets...)
	}
	return previewPathSummary{exitsAfterCreate: afterCreate}, nil
}

func (e *FlowExecutor) followPreviewPathTargets(ctx context.Context, meta *models.FlowMetadata, user *models.UserSession, afterCreate bool, seenElements map[string]bool, seenFlows map[string]bool, syntheticOverlayAvailable bool, connectors ...*models.FlowConnector) (previewPathSummary, error) {
	out := previewPathSummary{exitsAfterCreate: afterCreate}
	followed := false
	for _, connector := range connectors {
		if connector == nil || strings.TrimSpace(connector.TargetReference) == "" {
			continue
		}
		followed = true
		summary, err := e.flowElementPreviewPathSummary(ctx, meta, user, connector.TargetReference, afterCreate, seenElements, seenFlows, syntheticOverlayAvailable)
		if err != nil || summary.lookupAfterCreate {
			return summary, err
		}
		out.exitsAfterCreate = out.exitsAfterCreate || summary.exitsAfterCreate
	}
	if !followed {
		return previewPathSummary{exitsAfterCreate: afterCreate}, nil
	}
	return out, nil
}

func (e *FlowExecutor) childFlowPreviewPathSummary(ctx context.Context, flowApiName string, user *models.UserSession, startsAfterCreate bool, seenFlows map[string]bool, syntheticOverlayAvailable bool) (previewPathSummary, error) {
	if !hasPreparedFlowClosure(ctx) && e.flowVersionFn == nil && e.query == nil {
		return previewPathSummary{exitsAfterCreate: startsAfterCreate}, nil
	}
	version, err := e.getActiveFlowVersion(ctx, flowApiName, user)
	if err != nil || version == nil {
		return previewPathSummary{exitsAfterCreate: startsAfterCreate}, err
	}
	key := strings.ToLower(strings.TrimSpace(flowApiName))
	if key != "" {
		if startsAfterCreate {
			key += ":after-create"
		} else {
			key += ":before-create"
		}
		if seenFlows[key] {
			return previewPathSummary{exitsAfterCreate: startsAfterCreate}, nil
		}
		seenFlows[key] = true
		defer delete(seenFlows, key)
	}
	return e.flowMetadataPreviewPathSummary(ctx, &version.Metadata, user, startsAfterCreate, seenFlows, syntheticOverlayAvailable)
}

func flowDecisionByName(meta *models.FlowMetadata, name string) *models.FlowDecision {
	for i := range meta.Decisions {
		if meta.Decisions[i].Name == name {
			return &meta.Decisions[i]
		}
	}
	return nil
}

func flowAssignmentByName(meta *models.FlowMetadata, name string) *models.FlowAssignment {
	for i := range meta.Assignments {
		if meta.Assignments[i].Name == name {
			return &meta.Assignments[i]
		}
	}
	return nil
}

func flowRecordCreateByName(meta *models.FlowMetadata, name string) *models.FlowRecordCreate {
	for i := range meta.RecordCreates {
		if meta.RecordCreates[i].Name == name {
			return &meta.RecordCreates[i]
		}
	}
	return nil
}

func flowRecordLookupByName(meta *models.FlowMetadata, name string) *models.FlowRecordLookup {
	for i := range meta.RecordLookups {
		if meta.RecordLookups[i].Name == name {
			return &meta.RecordLookups[i]
		}
	}
	return nil
}

func flowActionCallByName(meta *models.FlowMetadata, name string) *models.FlowActionCall {
	for i := range meta.ActionCalls {
		if meta.ActionCalls[i].Name == name {
			return &meta.ActionCalls[i]
		}
	}
	return nil
}

func flowSubflowByName(meta *models.FlowMetadata, name string) *models.FlowSubflow {
	for i := range meta.Subflows {
		if meta.Subflows[i].Name == name {
			return &meta.Subflows[i]
		}
	}
	return nil
}

func flowLoopByName(meta *models.FlowMetadata, name string) *models.FlowLoop {
	for i := range meta.Loops {
		if meta.Loops[i].Name == name {
			return &meta.Loops[i]
		}
	}
	return nil
}

func flowCustomErrorByName(meta *models.FlowMetadata, name string) *models.FlowCustomError {
	for i := range meta.CustomErrors {
		if meta.CustomErrors[i].Name == name {
			return &meta.CustomErrors[i]
		}
	}
	return nil
}

func flowScreenByName(meta *models.FlowMetadata, name string) *models.FlowScreen {
	for i := range meta.Screens {
		if meta.Screens[i].Name == name {
			return &meta.Screens[i]
		}
	}
	return nil
}

func flowWaitByName(meta *models.FlowMetadata, name string) *models.FlowWait {
	for i := range meta.Waits {
		if meta.Waits[i].Name == name {
			return &meta.Waits[i]
		}
	}
	return nil
}

type syntheticRecordIDReferences map[string]map[string]struct{}

func newSyntheticRecordIDReferences() syntheticRecordIDReferences {
	return syntheticRecordIDReferences{}
}

func (refs syntheticRecordIDReferences) add(ref string, objectName string) bool {
	ref = strings.TrimSpace(ref)
	objectName = strings.TrimSpace(objectName)
	if ref == "" || objectName == "" {
		return false
	}
	objects := refs[ref]
	if objects == nil {
		objects = map[string]struct{}{}
		refs[ref] = objects
	}
	key := strings.ToLower(objectName)
	if _, ok := objects[key]; ok {
		return false
	}
	objects[key] = struct{}{}
	return true
}

func (refs syntheticRecordIDReferences) addAll(ref string, objects map[string]struct{}) bool {
	changed := false
	for objectName := range objects {
		if refs.add(ref, objectName) {
			changed = true
		}
	}
	return changed
}

func cloneSyntheticRecordIDReferences(refs syntheticRecordIDReferences) syntheticRecordIDReferences {
	out := newSyntheticRecordIDReferences()
	for ref, objects := range refs {
		out.addAll(ref, objects)
	}
	return out
}

func singleSyntheticObject(objects map[string]struct{}) (string, bool) {
	if len(objects) != 1 {
		return "", false
	}
	for objectName := range objects {
		return objectName, true
	}
	return "", false
}

func (e *FlowExecutor) flowVersionSyntheticRecordIDReferences(ctx context.Context, flowApiName string, version *models.FlowVersion, user *models.UserSession, seen map[string]bool) (syntheticRecordIDReferences, error) {
	if version == nil {
		return newSyntheticRecordIDReferences(), nil
	}
	key := strings.ToLower(strings.TrimSpace(flowApiName))
	if key != "" {
		if seen[key] {
			return newSyntheticRecordIDReferences(), nil
		}
		seen[key] = true
		defer delete(seen, key)
	}

	meta := version.Metadata
	refs := previewSyntheticRecordIDReferences(meta.RecordCreates)
	childRefs, err := e.syntheticRecordIDReferencesFromChildOutputs(ctx, meta, user, seen)
	if err != nil {
		return nil, err
	}
	for ref, objects := range childRefs {
		refs.addAll(ref, objects)
	}
	return expandSyntheticRecordIDReferencesThroughAssignments(meta.Assignments, refs), nil
}

func (e *FlowExecutor) syntheticRecordIDReferencesFromChildOutputs(ctx context.Context, meta models.FlowMetadata, user *models.UserSession, seen map[string]bool) (syntheticRecordIDReferences, error) {
	refs := newSyntheticRecordIDReferences()
	for _, action := range meta.ActionCalls {
		if strings.ToLower(strings.TrimSpace(action.ActionType)) != "flow" {
			continue
		}
		childName := strings.TrimSpace(action.ActionName)
		if childName == "" {
			continue
		}
		childOutputs, err := e.childFlowSyntheticRecordIDOutputNames(ctx, childName, user, seen)
		if err != nil {
			return nil, err
		}
		for _, param := range action.OutputParameters {
			objects, ok := childOutputs[strings.TrimSpace(param.Name)]
			if !ok {
				continue
			}
			refs.addAll(extractFlowOutputTarget(param.Value), objects)
		}
	}
	for _, subflow := range meta.Subflows {
		childName := strings.TrimSpace(subflow.FlowName)
		if childName == "" {
			continue
		}
		childOutputs, err := e.childFlowSyntheticRecordIDOutputNames(ctx, childName, user, seen)
		if err != nil {
			return nil, err
		}
		for _, output := range subflow.OutputAssignments {
			objects, ok := childOutputs[strings.TrimSpace(output.Name)]
			if !ok {
				continue
			}
			refs.addAll(output.AssignToReference, objects)
		}
	}
	return refs, nil
}

func (e *FlowExecutor) childFlowSyntheticRecordIDOutputNames(ctx context.Context, flowApiName string, user *models.UserSession, seen map[string]bool) (syntheticRecordIDReferences, error) {
	if !hasPreparedFlowClosure(ctx) && e.flowVersionFn == nil && e.query == nil {
		return newSyntheticRecordIDReferences(), nil
	}
	version, err := e.getActiveFlowVersion(ctx, flowApiName, user)
	if err != nil || version == nil {
		return nil, err
	}
	refs, err := e.flowVersionSyntheticRecordIDReferences(ctx, flowApiName, version, user, seen)
	if err != nil {
		return nil, err
	}
	return flowVersionSyntheticRecordIDOutputNames(version.Metadata, refs), nil
}

func flowVersionSyntheticRecordIDOutputNames(meta models.FlowMetadata, syntheticRecordIDRefs syntheticRecordIDReferences) syntheticRecordIDReferences {
	outputs := newSyntheticRecordIDReferences()
	if len(syntheticRecordIDRefs) == 0 {
		return outputs
	}
	for _, variable := range meta.Variables {
		name := strings.TrimSpace(variable.Name)
		if !variable.IsOutput || name == "" {
			continue
		}
		if objects, ok := flowReferenceObjects(name, syntheticRecordIDRefs); ok {
			outputs.addAll(name, objects)
		}
	}
	return outputs
}

func previewSyntheticRecordIDReferences(creates []models.FlowRecordCreate) syntheticRecordIDReferences {
	refs := newSyntheticRecordIDReferences()
	for _, create := range creates {
		objectName := strings.TrimSpace(create.Object)
		if objectName == "" {
			continue
		}
		if ref := strings.TrimSpace(create.AssignRecordIdToReference); ref != "" {
			refs.add(ref, objectName)
		}
		if create.StoreOutputAutomatically {
			if ref := strings.TrimSpace(create.Name); ref != "" {
				refs.add(ref, objectName)
			}
		}
	}
	return refs
}

func expandSyntheticRecordIDReferencesThroughAssignments(assignments []models.FlowAssignment, refs syntheticRecordIDReferences) syntheticRecordIDReferences {
	if len(assignments) == 0 || len(refs) == 0 {
		return refs
	}
	expanded := cloneSyntheticRecordIDReferences(refs)
	for changed := true; changed; {
		changed = false
		for _, assignment := range assignments {
			for _, item := range assignment.Items {
				objects, ok := flowValueSyntheticReferenceObjectsExact(item.Value, expanded)
				if !ok {
					continue
				}
				if expanded.addAll(item.AssignToReference, objects) {
					changed = true
				}
			}
		}
	}
	return expanded
}

func (e *FlowExecutor) flowVersionReferencesUnsupportedSyntheticRecordID(ctx context.Context, meta models.FlowMetadata, syntheticRecordIDRefs syntheticRecordIDReferences) bool {
	for _, create := range meta.RecordCreates {
		for _, assignment := range create.InputAssignments {
			objects, ok := flowValueSyntheticReferenceObjectsExact(assignment.Value, syntheticRecordIDRefs)
			if !ok {
				if flowValueReferencesAny(assignment.Value, syntheticRecordIDRefs) {
					return true
				}
				continue
			}
			objectName, ok := singleSyntheticObject(objects)
			if !ok {
				return true
			}
			if e.recordCreateReferenceAssignmentSupportsSyntheticID(ctx, create.Object, assignment.Field, objectName) {
				continue
			}
			return true
		}
		if create.FieldMapping != nil {
			if flowReferenceMatchesAny(create.FieldMapping.SourceReference, syntheticRecordIDRefs) ||
				flowReferenceMatchesAny(create.FieldMapping.TargetReference, syntheticRecordIDRefs) {
				return true
			}
		}
	}
	for _, lookup := range meta.RecordLookups {
		for _, filter := range lookup.Filters {
			if flowValueReferencesAny(filter.Value, syntheticRecordIDRefs) {
				return true
			}
		}
	}
	for _, action := range meta.ActionCalls {
		for _, param := range action.InputParameters {
			if flowValueReferencesAny(param.Value, syntheticRecordIDRefs) {
				return true
			}
		}
	}
	for _, subflow := range meta.Subflows {
		for _, param := range subflow.InputAssignments {
			if flowValueReferencesAny(param.Value, syntheticRecordIDRefs) {
				return true
			}
		}
	}
	return false
}

func (e *FlowExecutor) recordCreateReferenceAssignmentSupportsSyntheticID(ctx context.Context, targetObject string, fieldAPIName string, syntheticObject string) bool {
	targetObject = strings.TrimSpace(targetObject)
	fieldAPIName = strings.TrimSpace(fieldAPIName)
	syntheticObject = strings.TrimSpace(syntheticObject)
	if targetObject == "" || fieldAPIName == "" || syntheticObject == "" {
		return false
	}
	field := e.recordCreateAssignmentField(ctx, targetObject, fieldAPIName)
	if field == nil {
		return false
	}
	if !constants.IsReferenceType(constants.NormalizeFieldType(string(field.Type))) {
		return false
	}
	return recordCreateReferenceFieldSupportsSyntheticObject(ctx, field, syntheticObject)
}

func recordCreateReferenceFieldSupportsSyntheticObject(ctx context.Context, field *models.FieldMetadata, syntheticObject string) bool {
	if field == nil {
		return false
	}
	if !fieldReferencesObject(field.ReferenceTo, syntheticObject) {
		return false
	}
	if len(field.WritableReferenceTo) == 0 || constants.GetSecurityContext(ctx).BypassesReferenceTargetWriteRestrictions() {
		return true
	}
	return fieldReferencesObject(field.WritableReferenceTo, syntheticObject)
}

func fieldReferencesObject(objects []string, target string) bool {
	for _, refObject := range objects {
		if strings.EqualFold(strings.TrimSpace(refObject), strings.TrimSpace(target)) {
			return true
		}
	}
	return false
}

func (e *FlowExecutor) recordCreateAssignmentField(ctx context.Context, objectAPIName string, fieldAPIName string) *models.FieldMetadata {
	if e == nil {
		return nil
	}
	if e.fieldMetaLookup != nil {
		return e.fieldMetaLookup(objectAPIName, fieldAPIName)
	}
	if e.metadata == nil {
		return nil
	}
	schema := e.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return nil
	}
	return models.FindField(schema, fieldAPIName)
}

func flowValueReferencesAny(value interface{}, refs syntheticRecordIDReferences) bool {
	_, ok := flowValueSyntheticReferenceObjects(value, refs)
	return ok
}

func flowValueSyntheticReferenceObjects(value interface{}, refs syntheticRecordIDReferences) (map[string]struct{}, bool) {
	m, ok := value.(map[string]interface{})
	if !ok {
		return nil, false
	}
	if ref, ok := m["elementReference"].(string); ok {
		return flowReferenceObjects(ref, refs)
	}
	return nil, false
}

func flowValueSyntheticReferenceObjectsExact(value interface{}, refs syntheticRecordIDReferences) (map[string]struct{}, bool) {
	m, ok := value.(map[string]interface{})
	if !ok {
		return nil, false
	}
	if ref, ok := m["elementReference"].(string); ok {
		return flowReferenceObjectsExact(ref, refs)
	}
	return nil, false
}

func flowReferenceMatchesAny(ref string, refs syntheticRecordIDReferences) bool {
	_, ok := flowReferenceObjects(ref, refs)
	return ok
}

func flowReferenceObjectsExact(ref string, refs syntheticRecordIDReferences) (map[string]struct{}, bool) {
	ref = strings.TrimSpace(ref)
	if ref == "" {
		return nil, false
	}
	objects, ok := refs[ref]
	return objects, ok
}

func flowReferenceObjects(ref string, refs syntheticRecordIDReferences) (map[string]struct{}, bool) {
	ref = strings.TrimSpace(ref)
	if ref == "" {
		return nil, false
	}
	if objects, ok := refs[ref]; ok {
		return objects, true
	}
	if idx := strings.Index(ref, "."); idx > 0 {
		if objects, ok := refs[strings.TrimSpace(ref[:idx])]; ok {
			return objects, true
		}
	}
	return nil, false
}

func (e *FlowExecutor) childFlowCreatePreviewSupported(ctx context.Context, flowApiName string, user *models.UserSession, seen map[string]bool, createdObjects map[string]struct{}) (bool, error) {
	if !hasPreparedFlowClosure(ctx) && e.flowVersionFn == nil && e.query == nil {
		return false, nil
	}
	version, err := e.getActiveFlowVersion(ctx, flowApiName, user)
	if err != nil || version == nil {
		return false, err
	}
	return e.flowVersionCreatePreviewSupported(ctx, flowApiName, version, user, seen, createdObjects)
}

func (e *FlowExecutor) recordBeforeCreatePreviewSupported(ctx context.Context, createdObjects map[string]struct{}, user *models.UserSession) (bool, error) {
	for objectAPIName := range createdObjects {
		ok, err := e.objectBeforeCreatePreviewSupported(ctx, objectAPIName, user)
		if err != nil || !ok {
			return ok, err
		}
	}
	return true, nil
}

func (e *FlowExecutor) objectBeforeCreatePreviewSupported(ctx context.Context, objectAPIName string, user *models.UserSession) (bool, error) {
	objectAPIName = strings.TrimSpace(objectAPIName)
	if objectAPIName == "" {
		return false, nil
	}
	if e.beforeCreatePreviewSafeFn != nil {
		return e.beforeCreatePreviewSafeFn(ctx, objectAPIName, user)
	}
	if e.metadata == nil {
		return false, nil
	}
	schema := e.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil || schema.KeyPrefix == nil || strings.TrimSpace(*schema.KeyPrefix) == "" {
		return false, nil
	}
	rows, err := e.metadata.ListActiveTriggerSubscriptions(ctx, strings.TrimSpace(*schema.KeyPrefix), "RecordBeforeSave", "Create")
	if err != nil {
		return false, nil
	}
	if len(rows) == 0 {
		return true, nil
	}

	ids := make([]string, 0, len(rows))
	seenIDs := map[string]struct{}{}
	for _, row := range rows {
		id := strings.TrimSpace(row.FlowVersionId)
		if id == "" {
			return false, nil
		}
		if _, ok := seenIDs[id]; ok {
			continue
		}
		seenIDs[id] = struct{}{}
		ids = append(ids, id)
	}
	versions, err := e.metadata.GetFlowVersionsByIDs(ctx, ids)
	if err != nil {
		return false, nil
	}
	for _, id := range ids {
		version := versions[id]
		if version == nil {
			return false, nil
		}
		version.HydrateMetadata()
		if !beforeCreateFlowPreviewSupported(&version.Metadata) {
			return false, nil
		}
	}
	return true, nil
}

func beforeCreateFlowPreviewSupported(meta *models.FlowMetadata) bool {
	if meta == nil {
		return false
	}
	if err := ValidateBeforeSaveFlowEffects(meta); err != nil {
		return false
	}
	// Side-effect safety is not enough for standard-action preview: before-
	// create durable reads cannot observe earlier no-DML creates until the
	// synthetic read overlay has production read-projection parity.
	return len(meta.RecordLookups) == 0
}
