package automation

import (
	"context"
	"fmt"
	"log"
	"reflect"
	"strings"
	"time"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/stagetiming"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func (e *FlowExecutor) applyRecordCreateFieldMapping(ctx context.Context, interview *models.FlowInterview, create *models.FlowRecordCreate, record models.SObject, user *models.UserSession) error {
	ref := create.FieldMapping
	if ref == nil {
		return nil
	}
	if e.fieldMappings == nil {
		return fmt.Errorf("RecordCreate %q declares a field-mapping ref but no FieldMappingApplier is wired", create.Name)
	}
	if ref.SourceObject == "" || ref.ActionType == "" || ref.SourceReference == "" {
		return fmt.Errorf("RecordCreate %q field-mapping ref requires sourceObject, sourceReference, and actionType", create.Name)
	}

	source, err := e.flowFieldMappingSource(ctx, interview, user, ref, "RecordCreate", create.Name)
	if err != nil {
		return err
	}

	targetObject := ref.TargetObject
	if targetObject == "" {
		targetObject = create.Object
	}
	mappingStarted := time.Now()
	err = e.fieldMappings.ApplyMappings(ctx, ref.SourceObject, targetObject, ref.ActionType, source, record)
	stagetiming.Add(ctx, stagetiming.StageMapping, time.Since(mappingStarted))
	return err
}

func (e *FlowExecutor) applyRecordCreateConnectorFieldMapping(ctx context.Context, interview *models.FlowInterview, create *models.FlowRecordCreate, record models.SObject, user *models.UserSession) error {
	ref := create.ConnectorFieldMapping
	if ref == nil {
		return nil
	}

	flowIdentity, ok := flowRuntimeContextFromCtx(ctx).CurrentFlow()
	source := strings.TrimSpace(flowIdentity.OwnerPack)
	if !ok || source == "" {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: executing Flow owner pack is required", create.Name)
	}
	if !flowIdentityIsPackOwnedStandard(flowIdentity) {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: Flow %q must be an uncustomized standard FlowVersion to resolve pack-owned connector mappings", create.Name, flowIdentity.APIName)
	}

	sourceAssertion, err := e.resolveConnectorFieldMappingString(ctx, interview, user, ref.Source)
	if err != nil {
		return err
	}
	if sourceAssertion != "" && sourceAssertion != source {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: source assertion %q does not match executing Flow owner pack %q", create.Name, sourceAssertion, source)
	}
	sourceObject, err := e.resolveConnectorFieldMappingString(ctx, interview, user, ref.SourceObject)
	if err != nil {
		return err
	}
	targetObject, err := e.resolveConnectorFieldMappingString(ctx, interview, user, ref.TargetObject)
	if err != nil {
		return err
	}
	if targetObject == "" {
		targetObject = create.Object
	}
	actionType, err := e.resolveConnectorFieldMappingString(ctx, interview, user, ref.ActionType)
	if err != nil {
		return err
	}
	direction, err := e.resolveConnectorFieldMappingString(ctx, interview, user, ref.Direction)
	if err != nil {
		return err
	}
	if sourceObject == "" {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: sourceObject must not be blank", create.Name)
	}
	if targetObject == "" {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: targetObject must not be blank", create.Name)
	}
	if actionType == "" {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: actionType must not be blank", create.Name)
	}
	if direction == "" {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: direction must not be blank", create.Name)
	}

	resolverFn := e.connectorPackMappingResolverFn
	if resolverFn == nil {
		if e.connectorMappingResolver == nil {
			return fmt.Errorf("RecordCreate %q connectorFieldMapping: no pack connector mapping resolver wired (set connectorMappingResolver or connectorPackMappingResolverFn)", create.Name)
		}
		resolverFn = e.connectorMappingResolver.ResolvePackConnectorMappings
	}
	mappings, err := resolverFn(ctx, connector.ResolveQuery{
		Source:       source,
		SourceObject: sourceObject,
		TargetObject: targetObject,
		ActionType:   actionType,
		Direction:    direction,
	})
	if err != nil {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: resolve mappings: %w", create.Name, err)
	}
	if err := validateConnectorFieldMappingRuntimeTargetWrites(mappings); err != nil {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: %w", create.Name, err)
	}
	if err := e.validateConnectorFieldMappingCreateTargetSchema(ctx, targetObject, mappings); err != nil {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: %w", create.Name, err)
	}

	targetFields := e.connectorFieldMappingTargetFields(ctx, targetObject)
	var extraUpdates models.SObject
	applyExtraUpdates, err := e.deepResolveFlowResource(ctx, interview, user, ref.ApplyExtraUpdates)
	if err != nil {
		return err
	}
	if connectorFieldMappingApplyExtraUpdates(applyExtraUpdates) {
		resolvedExtraUpdates, err := e.deepResolveFlowResource(ctx, interview, user, ref.ExtraUpdates)
		if err != nil {
			return err
		}
		extraUpdates, err = e.connectorFieldMappingCreateExtraUpdates(resolvedExtraUpdates, targetObject, targetFields)
		if err != nil {
			return fmt.Errorf("RecordCreate %q connectorFieldMapping: %w", create.Name, err)
		}
	}

	resolvedSourceRecord, err := e.deepResolveFlowResource(ctx, interview, user, ref.SourceRecord)
	if err != nil {
		return err
	}
	sourceRecord := coerceFlowRecord(resolvedSourceRecord)
	if sourceRecord == nil {
		sourceRecord = models.SObject{}
	}
	resolvedSkipBlank, err := e.deepResolveFlowResource(ctx, interview, user, ref.SkipBlankTargetFields)
	if err != nil {
		return err
	}
	skipBlankTargetFields := connectorFieldMappingSkipBlankTargetFields(resolvedSkipBlank)
	mappingStarted := time.Now()
	updates, err := e.buildConnectorFieldMappingUpdates(sourceRecord, record, mappings, targetFields, true, skipBlankTargetFields)
	stagetiming.Add(ctx, stagetiming.StageMapping, time.Since(mappingStarted))
	if err != nil {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: %w", create.Name, err)
	}
	if err := mergeConnectorFieldMappingExtraUpdates(updates, extraUpdates); err != nil {
		return fmt.Errorf("RecordCreate %q connectorFieldMapping: %w", create.Name, err)
	}
	for field, value := range updates {
		record[field] = value
	}
	return nil
}

func (e *FlowExecutor) resolveConnectorFieldMappingString(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, raw interface{}) (string, error) {
	if raw == nil {
		return "", nil
	}
	resolved, err := e.deepResolveFlowResource(ctx, interview, user, raw)
	if err != nil {
		return "", err
	}
	if resolved == nil {
		return "", nil
	}
	return strings.TrimSpace(fmt.Sprintf("%v", resolved)), nil
}

// toStringKeyedMap accepts either models.SObject or map[string]interface{}
// (flows unmarshal $Record into both shapes depending on the ingress path)
// and returns a mutable map[string]interface{} suitable for field mapping.
func toStringKeyedMap(v interface{}) (map[string]interface{}, bool) {
	switch m := v.(type) {
	case models.SObject:
		return m, true
	case map[string]interface{}:
		return m, true
	default:
		return nil, false
	}
}

// deriveObjectFromIdFilter inspects the filter list for a pattern of the form
// "Id EqualTo $Record.<lookupField>" and, when found, resolves the target
// object type from field metadata on the trigger record's object type.
//
// This supports Process Builder's implicit cross-object recordUpdate pattern
// where objectName is null and the target is identified solely by a lookup
// field reference (e.g. $Record.ContractId → Contract, $Record.Account__c →
// Account). Returns "" when the pattern is not present, metadata is
// unavailable, or the lookup field has multiple ReferenceTo targets (ambiguous).
func (e *FlowExecutor) deriveObjectFromIdFilter(ctx context.Context, interview *models.FlowInterview, filters []models.FlowRecordFilter) string {
	triggerObjectType := ""
	if trigRec, ok := interview.ContextData["$Record"].(models.SObject); ok {
		triggerObjectType = models.RecordObjectType(trigRec)
	}
	if triggerObjectType == "" {
		return ""
	}
	for _, f := range filters {
		if !strings.EqualFold(f.Field, "Id") || !strings.EqualFold(f.Operator, "EqualTo") {
			continue
		}
		ref := ""
		if m, ok := f.Value.(map[string]interface{}); ok {
			if er, ok := m["elementReference"].(string); ok {
				ref = er
			}
		}
		if !strings.HasPrefix(ref, "$Record.") {
			continue
		}
		fieldName := strings.TrimPrefix(ref, "$Record.")
		if strings.Contains(fieldName, ".") {
			// Cross-object traversal — cannot derive a single target type.
			continue
		}
		fm := e.lookupFieldMeta(ctx, triggerObjectType, fieldName)
		if fm == nil || len(fm.ReferenceTo) != 1 {
			continue
		}
		log.Printf("flow recordUpdate: derived target object %q from $Record.%s ReferenceTo", fm.ReferenceTo[0], fieldName)
		return fm.ReferenceTo[0]
	}
	return ""
}

// executeRecordUpdate updates existing records
func recordUpdateCollectionPayload(ctx context.Context, objectName, recordID string, record models.SObject, updates models.SObject) models.SObject {
	baseline, hasBaseline := flowRuntimeContextFromCtx(ctx).RecordLookupBaseline(objectName, recordID)
	payload := make(models.SObject, len(record)+len(updates))
	for k, v := range record {
		if models.IsRecordMetadataKey(k) || k == constants.FieldID {
			continue
		}
		if hasBaseline {
			if original, present := baseline[k]; present && reflect.DeepEqual(original, v) {
				continue
			}
		}
		payload[k] = v
	}
	for k, v := range updates {
		if models.IsRecordMetadataKey(k) || k == constants.FieldID {
			continue
		}
		payload[k] = v
	}
	return payload
}

// applyRecordUpdateFieldMapping applies _System_ObjectFieldMapping rows to a
// RecordUpdate element. Default mode writes mapped values into the update set;
// mergeMode=fillBlankOnly compares against the target SObject referenced by
// TargetReference (or InputReference when it already points at an SObject) and
// only writes fields that are currently blank. InputAssignments run afterwards
// and can intentionally override mapped values.
func (e *FlowExecutor) applyRecordUpdateFieldMapping(ctx context.Context, interview *models.FlowInterview, update *models.FlowRecordUpdate, updates models.SObject, user *models.UserSession) error {
	ref := update.FieldMapping
	if ref == nil {
		return nil
	}
	if e.fieldMappings == nil {
		return fmt.Errorf("RecordUpdate %q declares a field-mapping ref but no FieldMappingApplier is wired", update.Name)
	}
	if ref.SourceObject == "" || ref.ActionType == "" || ref.SourceReference == "" {
		return fmt.Errorf("RecordUpdate %q field-mapping ref requires sourceObject, sourceReference, and actionType", update.Name)
	}
	targetObject := ref.TargetObject
	if targetObject == "" {
		targetObject = update.Object
	}
	if targetObject == "" {
		return fmt.Errorf("RecordUpdate %q field-mapping ref requires targetObject or object", update.Name)
	}

	source, err := e.flowFieldMappingSource(ctx, interview, user, ref, "RecordUpdate", update.Name)
	if err != nil {
		return err
	}
	mapped := models.SObject{}
	mappingStarted := time.Now()
	if err := e.fieldMappings.ApplyMappings(ctx, ref.SourceObject, targetObject, ref.ActionType, source, mapped); err != nil {
		stagetiming.Add(ctx, stagetiming.StageMapping, time.Since(mappingStarted))
		return err
	}
	stagetiming.Add(ctx, stagetiming.StageMapping, time.Since(mappingStarted))

	switch strings.TrimSpace(ref.MergeMode) {
	case "", "overwrite":
		for k, v := range mapped {
			updates[k] = v
		}
	case flowFieldMappingMergeFillBlankOnly:
		// All Flow resources, flat or dotted, use the canonical resolver. Dotted
		// references retain the null-hop no-op behavior required by field mapping.
		target, skipReason, resolveErr := e.flowFieldMappingTargetWithExecutor(ctx, interview, update, ref, user)
		if resolveErr != nil {
			return resolveErr
		}
		if skipReason != "" {
			return nil
		}
		for k, v := range mapped {
			if isBlankFlowFieldMappingValue(v) {
				continue
			}
			if isBlankFlowFieldMappingValue(target[k]) {
				updates[k] = v
			}
		}
	default:
		return fmt.Errorf("RecordUpdate %q field-mapping mergeMode %q is not supported", update.Name, ref.MergeMode)
	}
	return nil
}

func (e *FlowExecutor) flowFieldMappingSource(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, ref *models.FlowFieldMappingRef, elementType, elementName string) (map[string]interface{}, error) {
	sourceRaw, present, err := e.ResolveFlowResource(ctx, interview, user, map[string]interface{}{"elementReference": ref.SourceReference})
	if err != nil {
		return nil, err
	}
	if !present {
		return nil, fmt.Errorf("%s %q field-mapping source %q not found in flow context", elementType, elementName, ref.SourceReference)
	}
	source, ok := toStringKeyedMap(sourceRaw)
	if !ok {
		return nil, fmt.Errorf("%s %q field-mapping source %q is not an SObject", elementType, elementName, ref.SourceReference)
	}
	return source, nil
}

func isBlankFlowFieldMappingValue(v interface{}) bool {
	if v == nil {
		return true
	}
	switch val := v.(type) {
	case string:
		return strings.TrimSpace(val) == ""
	case []byte:
		return strings.TrimSpace(string(val)) == ""
	default:
		return false
	}
}

// recordMatchesNodeFilters evaluates a recordUpdates node's own per-node
// filters against the in-flight record state. Used by executeRecordUpdate to
// implement Salesforce's per-node filter semantics: when a recordUpdates node
// carries its own Filters targeting the implicit $Record, the filters must be
// checked against the current $Record state before the DML fires.
//
// Returns true (DML proceeds) when all conditions pass (ANDed unless
// filterLogic is set); false (skip DML, connector still followed) otherwise.
//
// Filter RHS values are resolved via ResolveFlowResource so both literal
// typed-value wrappers ({"stringValue": "Banking"}) and element references
// ({"elementReference": "myVar"}) work correctly.
//
// Mirrors the entry-condition evaluator (checkEntryConditionsCtx): reads
// $Record__Old for IsChanged semantics, resolves field metadata for
// type-aware comparison (e.g. multi-picklist), and passes ctx for SF
// key-prefix matching on StartsWith/DoesNotStartWith operators.
func (e *FlowExecutor) recordMatchesNodeFilters(
	ctx context.Context,
	interview *models.FlowInterview,
	user *models.UserSession,
	record models.SObject,
	filters []models.FlowRecordFilter,
	filterLogic string,
) (bool, error) {
	if len(filters) == 0 {
		return true, nil
	}
	// Read the pre-trigger snapshot for IsChanged comparisons. Absent when the
	// flow is triggered by a CREATE (no prior record), in which case
	// evaluateRecordFilterForFieldCtx treats every field as changed — matching
	// Salesforce's IsChanged=true-on-create semantics.
	oldRecord, _ := interview.ContextData["$Record__Old"].(models.SObject)

	// Resolve the trigger object once so per-filter evaluators can apply
	// field-type-specific semantics (e.g. multi-picklist IsChanged).
	objectAPIName := flowRuntimeContextFromCtx(ctx).ObjectType()

	results := make([]bool, len(filters))
	for i, f := range filters {
		// ResolveFlowResource unwraps typed-value literals and resolves element
		// references against the interview context so the comparison always
		// uses the runtime value, not the raw metadata wrapper.
		resolvedValue, _, err := e.ResolveFlowResource(ctx, interview, user, f.Value)
		if err != nil {
			return false, err
		}
		resolved := f
		resolved.Value = resolvedValue
		var fieldMeta *models.FieldMetadata
		if objectAPIName != "" && f.Field != "" {
			fieldMeta = e.lookupFieldMeta(ctx, objectAPIName, f.Field)
		}
		results[i] = e.evaluateRecordFilterForFieldCtx(ctx, record, oldRecord, &resolved, fieldMeta)
	}
	return evaluateFilterLogic(strings.ToLower(filterLogic), results), nil
}

type resolvedCurrentRecordFilter struct {
	filter    models.FlowRecordFilter
	fieldMeta *models.FieldMetadata
}

// resolveCurrentRecordFilters freezes every RHS value and field-metadata fact
// before persistence acquires the child-row lock. The guard callback below is
// therefore a pure evaluation of the locked row and performs no nested reads
// while parent/child locks are held.
func (e *FlowExecutor) resolveCurrentRecordFilters(
	ctx context.Context,
	interview *models.FlowInterview,
	user *models.UserSession,
	objectAPIName string,
	filters []models.FlowRecordFilter,
	preResolved map[int]interface{},
) ([]resolvedCurrentRecordFilter, error) {
	resolvedValues := make(map[int]interface{}, len(filters))
	for index, filter := range filters {
		if resolved, ok := preResolved[index]; ok {
			resolvedValues[index] = resolved
			continue
		}
		resolved, _, err := e.ResolveFlowResource(ctx, interview, user, filter.Value)
		if err != nil {
			return nil, err
		}
		resolvedValues[index] = resolved
	}
	return e.resolveCurrentRecordFiltersWithResolverCtx(ctx, objectAPIName, filters, func(index int, value interface{}) interface{} {
		return resolvedValues[index]
	})
}

func (e *FlowExecutor) resolveCurrentRecordFiltersWithResolver(
	objectAPIName string,
	filters []models.FlowRecordFilter,
	resolve func(int, interface{}) interface{},
) ([]resolvedCurrentRecordFilter, error) {
	return e.resolveCurrentRecordFiltersWithResolverCtx(context.Background(), objectAPIName, filters, resolve)
}

func (e *FlowExecutor) resolveCurrentRecordFiltersWithResolverCtx(
	ctx context.Context,
	objectAPIName string,
	filters []models.FlowRecordFilter,
	resolve func(int, interface{}) interface{},
) ([]resolvedCurrentRecordFilter, error) {
	if resolve == nil {
		return nil, fmt.Errorf("guarded RecordUpdate filter resolver is not configured")
	}
	resolved := make([]resolvedCurrentRecordFilter, len(filters))
	for i, filter := range filters {
		if err := validateIdentifier(strings.TrimSpace(filter.Field)); err != nil {
			return nil, fmt.Errorf("guarded RecordUpdate filter %d has invalid field %q: %w", i+1, filter.Field, err)
		}
		if _, ok := models.CanonicalGuardedFlowFilterOperator(filter.Operator); !ok {
			return nil, fmt.Errorf("guarded RecordUpdate filter %d has unsupported operator %q", i+1, strings.TrimSpace(filter.Operator))
		}
		filter.Value = resolve(i, filter.Value)
		canonical, err := models.ValidateGuardedFlowRecordFilter(filter)
		if err != nil {
			return nil, fmt.Errorf("guarded RecordUpdate filter %d: %w", i+1, err)
		}
		resolved[i] = resolvedCurrentRecordFilter{filter: canonical}
		if objectAPIName != "" {
			resolved[i].fieldMeta = e.lookupFieldMeta(ctx, objectAPIName, canonical.Field)
		}
	}
	return resolved, nil
}

func validateGuardedRecordUpdateFilterShape(filters []models.FlowRecordFilter, filterLogic string) (string, error) {
	if err := models.ValidateFlowFilterLogic(filterLogic, len(filters)); err != nil {
		return "", fmt.Errorf("guarded RecordUpdate has invalid filterLogic: %w", err)
	}
	for i := range filters {
		if err := validateIdentifier(strings.TrimSpace(filters[i].Field)); err != nil {
			return "", fmt.Errorf("guarded RecordUpdate filter %d has invalid field %q: %w", i+1, filters[i].Field, err)
		}
		if _, err := models.ValidateGuardedFlowRecordFilter(filters[i]); err != nil {
			return "", fmt.Errorf("guarded RecordUpdate filter %d: %w", i+1, err)
		}
	}
	return strings.Join(strings.Fields(filterLogic), " "), nil
}

func (e *FlowExecutor) buildGuardedFilterFromResolvedConditions(
	filters []resolvedCurrentRecordFilter,
	filterLogic string,
	pbNullSemantics bool,
) (string, error) {
	if err := models.ValidateFlowFilterLogic(filterLogic, len(filters)); err != nil {
		return "", fmt.Errorf("guarded RecordUpdate has invalid filterLogic: %w", err)
	}
	resolved := make([]models.FlowRecordFilter, len(filters))
	for i := range filters {
		if err := validateIdentifier(strings.TrimSpace(filters[i].filter.Field)); err != nil {
			return "", fmt.Errorf("guarded RecordUpdate filter %d has invalid field %q: %w", i+1, filters[i].filter.Field, err)
		}
		canonical, err := models.ValidateGuardedFlowRecordFilter(filters[i].filter)
		if err != nil {
			return "", fmt.Errorf("guarded RecordUpdate filter %d: %w", i+1, err)
		}
		resolved[i] = canonical
	}
	filter, conditionCount := e.buildFilterFromConditionsImplWithCount(resolved, filterLogic, pbNullSemantics, true, func(value interface{}) interface{} {
		return value
	})
	if conditionCount != len(resolved) || filter == "" || filter == "1=1" {
		return "", fmt.Errorf("guarded RecordUpdate selector emitted %d conditions for %d filters", conditionCount, len(resolved))
	}
	return filter, nil
}

func (e *FlowExecutor) lockedRecordMatchesResolvedFilters(
	ctx context.Context,
	record models.SObject,
	filters []resolvedCurrentRecordFilter,
	filterLogic string,
) (bool, error) {
	if err := models.ValidateFlowFilterLogic(filterLogic, len(filters)); err != nil {
		return false, fmt.Errorf("guarded RecordUpdate has invalid filterLogic: %w", err)
	}
	results := make([]bool, len(filters))
	for i := range filters {
		canonical, err := models.ValidateGuardedFlowRecordFilter(filters[i].filter)
		if err != nil {
			return false, fmt.Errorf("guarded RecordUpdate filter %d: %w", i+1, err)
		}
		filterValue, _ := extractTypedValue(canonical.Value)
		matched, handled := e.evaluateFlowBinaryOperator(canonical.Operator, record[canonical.Field], filterValue, flowBinaryOperatorOptions{
			ctx:               ctx,
			fieldMeta:         filters[i].fieldMeta,
			useStringEquality: true,
		})
		if !handled {
			return false, fmt.Errorf("guarded RecordUpdate filter %d has unsupported operator %q", i+1, canonical.Operator)
		}
		results[i] = matched
	}
	return evaluateFilterLogic(strings.ToLower(filterLogic), results), nil
}

// object inferred from $Record object-type metadata. Returns true (treat as
// cross-object) when the trigger object cannot be determined, so we default
// to the safe DetachTx path rather than risking snapshot staleness.
func isCrossObjectLookup(interview *models.FlowInterview, lookupObject string) bool {
	triggerRecord, ok := interview.ContextData["$Record"].(models.SObject)
	if !ok || triggerRecord == nil {
		return true // no $Record → cannot determine trigger object → default to DetachTx
	}
	triggerObject := models.RecordObjectType(triggerRecord)
	if triggerObject == "" {
		return true
	}
	return !strings.EqualFold(lookupObject, triggerObject)
}

// implicitFilterReference renders the variable reference inside a
// FlowRecordFilter.Value typed-value wrapper for inclusion in error
// messages emitted by the executor. Salesforce typed-values are encoded
// as map[string]interface{} with either an "elementReference" key
// (variable lookup) or one of the scalar wrappers ("stringValue",
// "booleanValue", ...). For error-message use we only need a stable,
// readable rendering that names the unresolved variable; non-reference
// shapes fall back to %v so the caller still gets a useful breadcrumb.
func implicitFilterReference(v interface{}) string {
	if m, ok := v.(map[string]interface{}); ok {
		if ref, ok := m["elementReference"].(string); ok && ref != "" {
			return ref
		}
	}
	return fmt.Sprintf("%v", v)
}

// findImplicitIdFilter scans a slice of FlowRecordFilters for the Salesforce
// Process Builder "implicit-Id" pattern and returns the first matching filter,
// or nil if none is found.
//
// The implicit-Id pattern identifies a Process Builder-generated "Update Records"
// action where the target record is identified via a relationship lookup rather
// than a user-authored query. The filter always has:
//   - Field = "Id"
//   - Operator = "EqualTo"
//   - ProcessMetadataValues containing {Name:"implicit", Value.booleanValue:true}
//
// When this pattern is present, the surrounding filter set should be evaluated
// with Salesforce Process Builder null semantics (not SQL semantics): a NULL
// field value is treated as "not equal to any specific non-null value".
func findImplicitIdFilter(filters []models.FlowRecordFilter) (int, *models.FlowRecordFilter) {
	for i := range filters {
		f := &filters[i]
		if !strings.EqualFold(f.Field, "Id") || !strings.EqualFold(f.Operator, "EqualTo") {
			continue
		}
		for _, pmv := range f.ProcessMetadataValues {
			if pmv.Name != "implicit" {
				continue
			}
			// The PMV value is a Salesforce typed-value wrapper; extract booleanValue.
			var boolVal interface{} = pmv.Value
			if m, ok := pmv.Value.(map[string]interface{}); ok {
				if v, exists := m["booleanValue"]; exists {
					boolVal = v
				}
			}
			if b, ok := boolVal.(bool); ok && b {
				return i, f
			}
		}
	}
	return -1, nil
}

// resolveDottedRecordReference resolves a dotted input reference like
// "$Record.Parent__r" to the related SObject it points to, returning the
// record, its object API name, its record ID, a skip reason (non-empty when
// resolution should be skipped without error), and an ok flag.
//
// Only dotted refs are handled here. Non-dotted refs (including "$Record",
// plain variable names) return (nil, "", "", "non-dotted-ref-routed-to-resolver", false)
// so the caller can route them through the existing flat-lookup path.
//
// Skip reasons and their semantics:
//
//	"non-dotted-ref-routed-to-resolver"  — caller must use the flat-lookup path
//	"root-fk-null"                       — hop 0 FK is null; no target record
//	"intermediate-fk-null"               — a mid-chain FK is null; no target record
//	"unresolved-reference"               — the dotted path does not exist in context
//	"resolved-record-missing-id-or-type" — resolved, but Id or object type absent
func (e *FlowExecutor) resolveDottedRecordReference(
	ctx context.Context,
	interview *models.FlowInterview,
	user *models.UserSession,
	ref string,
) (record models.SObject, objectName, recordId, skipReason string, ok bool, err error) {
	if ref == "" {
		return nil, "", "", "", false, nil
	}
	if !strings.Contains(ref, ".") {
		return nil, "", "", "non-dotted-ref-routed-to-resolver", false, nil
	}

	firstNullHopIndex := -1
	var value interface{}
	var present bool
	root := ref[:strings.IndexByte(ref, '.')]
	if scope := flowFormulaResourcesFromContext(ctx); scope != nil {
		if _, isFormula := scope.resources[root]; isFormula {
			value, present, err = e.ResolveFlowResource(ctx, interview, user, map[string]interface{}{"elementReference": ref})
			if err != nil {
				return nil, "", "", "", false, err
			}
		} else {
			value, present = e.resolveNestedReferenceCtxWithPresence(ctx, interview, user, ref, &firstNullHopIndex)
		}
	} else {
		value, present = e.resolveNestedReferenceCtxWithPresence(ctx, interview, user, ref, &firstNullHopIndex)
	}

	switch {
	case present && value != nil:
		// Successfully resolved to a non-nil record. Normalize to a map.
		sobj, castOk := toStringKeyedMap(value)
		if !castOk {
			log.Printf("flow resolveDottedRecordReference %q: resolved value is not an SObject (%T)", ref, value)
			return nil, "", "", "resolved-record-missing-id-or-type", false, nil
		}
		id, _ := sobj[constants.FieldID].(string)
		typeName := models.RecordObjectType(sobj)
		if id == "" || typeName == "" {
			log.Printf("flow resolveDottedRecordReference %q: resolved SObject missing Id or object-type metadata (id=%q, type=%q)", ref, id, typeName)
			return nil, "", "", "resolved-record-missing-id-or-type", false, nil
		}
		return models.SObject(sobj), typeName, id, "", true, nil

	case present && value == nil && firstNullHopIndex == 0:
		skipReason = "root-fk-null"

	case present && value == nil && firstNullHopIndex > 0:
		skipReason = "intermediate-fk-null"

	default:
		skipReason = "unresolved-reference"
	}

	log.Printf("flow resolveDottedRecordReference %q: skip (%s)", ref, skipReason)
	return nil, "", "", skipReason, false, nil
}

// flowFieldMappingTargetWithExecutor resolves the target record for a
// fill-blank-only field mapping that uses a dotted reference. This is the
// executor-method counterpart of the package-level flowFieldMappingTarget:
// it supports dotted ref resolution via resolveDottedRecordReference in
// addition to the existing flat-lookup path.
//
// THREE-WAY return:
//
//	(target≠nil, skipReason="",  err=nil) → success
//	(target=nil,  skipReason≠"", err=nil) → skip gracefully (no DML)
//	(target=nil,  skipReason="", err≠nil) → hard error, propagate
func (e *FlowExecutor) flowFieldMappingTargetWithExecutor(
	ctx context.Context,
	interview *models.FlowInterview,
	update *models.FlowRecordUpdate,
	ref *models.FlowFieldMappingRef,
	user *models.UserSession,
) (target map[string]interface{}, skipReason string, err error) {
	targetReference := ref.TargetReference
	if targetReference == "" {
		targetReference = update.InputReference
	}
	if targetReference == "" {
		return nil, "", fmt.Errorf("RecordUpdate %q field-mapping mergeMode %q requires targetReference or SObject inputReference", update.Name, ref.MergeMode)
	}

	if strings.Contains(targetReference, ".") {
		record, _, _, reason, ok, resolveErr := e.resolveDottedRecordReference(ctx, interview, user, targetReference)
		if resolveErr != nil {
			return nil, "", resolveErr
		}
		if !ok {
			return nil, reason, nil
		}
		// Return the resolved SObject directly — it is not stored as a flat key
		// in interview.ContextData, so a subsequent ContextData lookup would fail.
		return map[string]interface{}(record), "", nil
	}

	targetRaw, present, resolveErr := e.ResolveFlowResource(
		ctx,
		interview,
		user,
		map[string]interface{}{"elementReference": targetReference},
	)
	if resolveErr != nil {
		return nil, "", resolveErr
	}
	if !present {
		return nil, "", fmt.Errorf("RecordUpdate %q field-mapping target %q not found in flow context", update.Name, targetReference)
	}
	t, ok := toStringKeyedMap(targetRaw)
	if !ok {
		return nil, "", fmt.Errorf("RecordUpdate %q field-mapping target %q is not an SObject", update.Name, targetReference)
	}
	return t, "", nil
}
