package automation

import (
	"context"
	"strings"

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

// deepResolveFlowResource is the map/slice-aware counterpart of
// ResolveFlowResource.
// It recurses into literal map/slice values declared directly in flow
// metadata so an inline structure like
//
//	{"Parent": {elementReference: "parentId"}, "Child": ...}
//
// resolves every nested elementReference wrapper. Critically, it stops
// recursing as soon as it has unwrapped an elementReference: whatever
// lives inside a resolved variable is returned verbatim, so strings in a
// resolved SObject are never re-run through the variable resolver (which
// would echo unknown names back and risk accidental rebinding).
func (e *FlowExecutor) deepResolveFlowResource(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, value interface{}) (interface{}, error) {
	if m, ok := value.(map[string]interface{}); ok {
		if _, isRef := m["elementReference"]; isRef {
			resolved, _, err := e.ResolveFlowResource(ctx, interview, user, value)
			return resolved, err
		}
		if literal, wasLiteral := extractTypedValue(value); wasLiteral {
			return literal, nil
		}
		out := make(map[string]interface{}, len(m))
		for k, v := range m {
			resolved, err := e.deepResolveFlowResource(ctx, interview, user, v)
			if err != nil {
				return nil, err
			}
			out[k] = resolved
		}
		return out, nil
	}
	if s, ok := value.([]interface{}); ok {
		out := make([]interface{}, len(s))
		for i, v := range s {
			resolved, err := e.deepResolveFlowResource(ctx, interview, user, v)
			if err != nil {
				return nil, err
			}
			out[i] = resolved
		}
		return out, nil
	}
	resolved, _, err := e.ResolveFlowResource(ctx, interview, user, value)
	return resolved, err
}

// resolveNonFormulaValueCtxWithPresence distinguishes:
//
//   - "elementReference wrapper whose target EXISTS in ContextData with
//     value nil" — the caller/flow explicitly set null; the create MUST
//     write nil (e.g. to clear a nullable field, suppress a schema
//     DefaultValue via layer 1/2 semantics). exists=true.
//
//   - "elementReference wrapper whose target is ABSENT" — nothing was
//     provided; stage 3 (ActionFieldDefault) / stage 5 (schema
//     DefaultValue) should fill. exists=false → caller skips the
//     assignment.
//
// Literals always report exists=true (a
// literal nil is a deliberate write, not an unresolved reference).
// Raw strings preserve the legacy echo-back passthrough — only
// elementReference wrappers whose reference doesn't resolve report
// exists=false. Nested references (`$lead.Company`) delegate to
// resolveNestedReferenceCtxWithPresence, which does `(val, ok)` tracking
// at every hop so a leaf key that exists with value nil returns
// (nil, true) — RecordCreate stage 2 honors the author's explicit-null
// intent instead of letting stage 3/5 defaults overwrite it.
func (e *FlowExecutor) resolveNonFormulaValueCtxWithPresence(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, value interface{}) (interface{}, bool) {
	if value == nil {
		// Literal nil: the flow metadata explicitly authored null (or a
		// REST caller passed a JSON null). Either way it's "present".
		return nil, true
	}

	// Detect an elementReference typed-wrapper BEFORE unwrapping so we can
	// tell "unresolvable element reference" apart from "literal string".
	// Flow metadata encodes element/formula references as wrappers
	// (e.g. {"elementReference": "formula_2_..."}), never as bare strings.
	// When the referenced name has no entry in ContextData — the common case
	// being an unresolved Flow resource — the legacy code path echoed the reference
	// string back, so downstream executeRecordUpdate / executeRecordCreate
	// wrote the element's NAME into the DB column (see issue #1123 where
	// an Order picklist column was persisted as the literal string
	// "formula_2_myRule_1_A1_6505267583"). Returning nil instead surfaces
	// "no value" to the persistence layer, which matches the WARN-and-
	// continue contract.
	wasElementReference := false
	if m, ok := value.(map[string]interface{}); ok {
		if ref, exists := m["elementReference"]; exists {
			if _, isStr := ref.(string); isStr {
				wasElementReference = true
			}
		}
	}

	var wasLiteral bool
	value, wasLiteral = extractTypedValue(value)

	ref, ok := value.(string)
	if !ok {
		return value, true
	}
	if wasLiteral {
		return value, true
	}

	if strings.HasPrefix(ref, "$Flow.") {
		return e.resolveFlowSystemVariable(ref), true
	}
	if isFlowSystemContextReference(ref) {
		resolved := resolveFlowSystemContextVariable(ctx, ref)
		if _, isErr := resolved.(globalRefError); isErr {
			return nil, false
		}
		return resolved, true
	}
	if strings.HasPrefix(ref, "$User.") || strings.HasPrefix(ref, "$Profile.") || strings.HasPrefix(ref, "$UserRole.") {
		resolved := resolveFlowUserVariable(user, ref)
		if _, isErr := resolved.(globalRefError); isErr {
			// Unresolved global variable reference: fail-closed, treat as absent.
			return nil, false
		}
		return resolved, true
	}
	if strings.HasPrefix(ref, "$GlobalConstant.") {
		return e.resolveGlobalConstant(ref), true
	}
	// Route through nested-reference resolution when the string looks like a
	// variable path. Three signals qualify:
	//   1. wasElementReference: the original value was an elementReference
	//      wrapper (e.g. {"elementReference": "$Record.Id"}) — always a reference.
	//   2. looksLikeVariableReference: a bare string whose characters are all
	//      identifier-safe (letters, digits, underscores, $, dots) — e.g. a bare
	//      "$Record.Order__r.Status__c" passed directly as filter.Value. Salesforce
	//      Flow variable names cannot contain spaces, parens, or hyphens.
	//
	// wasLiteral=true (value came from a typed-value wrapper like stringValue)
	// overrides both: the flow author explicitly authored a concrete value, so it
	// is never a variable reference regardless of its character content. This
	// fixes issue #2046 where dotted identifier-shape literals like "v1.2.3" or
	// "name.tld" were misrouted through resolveNestedReferenceCtxWithPresence and
	// silently dropped (first segment lookup miss → (nil, false) → Assign skips).
	//
	// A literal stringValue that happens to contain dots but also contains spaces
	// or other non-identifier chars (e.g. "Acme Corp. Ltd.", "v1.2.3 (beta)")
	// fails looksLikeVariableReference and passes through unchanged — treating it
	// as a variable path would split it at the first dot, return nil from ContextData
	// lookup, and silently drop the write (issue #1817 Bug A: literals containing
	// spaces+dots silently lost their value because they were misrouted through
	// the nested-reference resolver).
	if strings.Contains(ref, ".") && (wasElementReference || looksLikeVariableReference(ref)) {
		// Nested ref: delegate to the presence-tracking sibling of
		// resolveNestedReferenceCtx. Leaf-level map lookup uses (val, ok)
		// form at every hop so a deep explicit-null like
		// `$lead.SomeNullableField` where the leaf KEY exists with value
		// nil returns (nil, true) — allowing RecordCreate stage 2 to
		// honor the author's null-claim instead of letting stage 3 / 5
		// defaults overwrite it. Same semantic class as the simple-ref
		// fix above; no "future work" asterisk.
		return e.resolveNestedReferenceCtxWithPresence(ctx, interview, user, ref, nil)
	}

	if interview != nil && interview.ContextData != nil {
		if resolved, exists := interview.ContextData[ref]; exists {
			// Key IS in ContextData. Value may be nil (caller set explicit
			// null) or any other value. Either way the semantic is "the
			// author/caller chose this; write it through".
			return resolved, true
		}
	}
	// Unresolved. For elementReference wrappers signal presence=false so
	// RecordCreate stage 2 can skip the assignment and let stages 3/5
	// fill. Raw literal strings keep their legacy passthrough — callers
	// such as evaluateCondition compare via fmt.Sprintf and tolerate the
	// echo; changing that would broaden the blast radius of this fix.
	if wasElementReference {
		return nil, false
	}
	return value, true
}
