package automation

import (
	"context"
	"strings"

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

// buildFlowDeclaredNames returns the set of symbol names declared in the flow
// definition that can later appear as keys in ContextData. Used by
// resolveAssignmentRHSValueWithPresence to guard the implicit-$Record fallback:
// a root segment that matches a declared symbol must NOT be redirected to
// $Record, even if that symbol is absent from ContextData (declared-but-
// uninitialized symbols are sparse - RecordLookup results are absent until
// the element executes, and loop variables are absent before the first
// iteration).
//
// Covered symbol classes:
//   - FlowVariable.Name (all variable declarations)
//   - FlowRecordLookup.Name and .OutputReference (both keys executor stores under)
//   - FlowRecordLookup.OutputAssignments[*].AssignToReference (field-binding targets)
//   - FlowActionCall output-writing action types' OutputParameters targets (via extractFlowOutputTarget)
//   - FlowActionCall (actionType="previewRecordUpdate") InputParameters["outputReference"] alias
//   - FlowRecordCreate.Name and .AssignRecordIdToReference
//   - FlowLoop.AssignNextValueToReference (or .Name when that is empty)
//   - FlowSubflow.OutputAssignments[*].AssignToReference
//   - FlowAssignment.Items[*].AssignToReference root segments
//   - FlowFormula.Name (resolved on demand; never an implicit relationship root)
//   - FlowDecision.Rules[*].Name (written by executeDecision; absent before decision runs)
func buildFlowDeclaredNames(flowMeta *models.FlowMetadata) map[string]struct{} {
	names := make(map[string]struct{})
	if flowMeta == nil {
		return names
	}
	for _, v := range flowMeta.Variables {
		names[v.Name] = struct{}{}
	}
	for _, r := range flowMeta.RecordLookups {
		names[r.Name] = struct{}{}
		// OutputReference is the key under which the executor stores the SObject
		// result when it differs from Name (executor_record_ops.go storeKeys).
		// A flow can declare OutputReference: "Account" on a lookup named
		// "Get_Account"; before the lookup executes, "Account" is absent from
		// ContextData but IS a declared symbol - it must not be rebound to
		// $Record.Account.
		if r.OutputReference != "" {
			names[r.OutputReference] = struct{}{}
		}
		// OutputAssignments store individual field values under their
		// AssignToReference key (executor_record_ops.go:ContextData[out.AssignToReference]).
		// These targets are sparse until the element executes.
		for _, out := range r.OutputAssignments {
			if ref := declaredRootSegment(out.AssignToReference); ref != "" {
				names[ref] = struct{}{}
			}
		}
	}
	for _, a := range flowMeta.ActionCalls {
		// ActionCall.Name is NOT written to ContextData by the runtime - only its
		// output parameter targets and inputParameter "outputReference" aliases are.
		// Do not add a.Name here; adding it would cause the guard to block implicit
		// $Record fallback for flows where an unrelated action happens to share a
		// name with a relationship (e.g., an action named "Account" on a Contact flow).

		// OutputParameters are mapped back to the parent interview ContextData only
		// for action types that explicitly write them (executeFlowAction,
		// executeSendEmailAction, and resolveEmailRecipients). Other action types
		// (emailAlert, sendNotification, apex, quickAction, etc.) do not write
		// OutputParameters into ContextData, so their targets must not be declared.
		if actionCallWritesOutputParameters(a.ActionType) {
			for _, param := range a.OutputParameters {
				if target := extractFlowOutputTarget(param.Value); target != "" {
					if ref := declaredRootSegment(target); ref != "" {
						names[ref] = struct{}{}
					}
				}
			}
		}
		// InputParameters["outputReference"] is the key under which
		// executePreviewRecordUpdate stores its result in ContextData
		// (executor_actions.go:790). This is specific to actionType="previewRecordUpdate";
		// other action types may legitimately have an input named "outputReference"
		// as a normal data-passing parameter that is never written to ContextData.
		if strings.EqualFold(a.ActionType, "previewRecordUpdate") {
			for _, param := range a.InputParameters {
				if param.Name != "outputReference" {
					continue
				}
				if target := extractFlowOutputTarget(param.Value); target != "" {
					if ref := declaredRootSegment(target); ref != "" {
						names[ref] = struct{}{}
					}
				}
				break
			}
		}
	}
	for _, c := range flowMeta.RecordCreates {
		// RecordCreate.Name is written to ContextData only when StoreOutputAutomatically
		// is true (executor_record_ops.go:153-154). Guard on that flag to avoid
		// blocking the implicit $Record fallback for non-auto-store creates.
		if c.StoreOutputAutomatically {
			names[c.Name] = struct{}{}
		}
		// AssignRecordIdToReference is written by executeRecordCreate
		// (executor_record_ops.go:151) when set - a separate key from Name.
		if ref := declaredRootSegment(c.AssignRecordIdToReference); ref != "" {
			names[ref] = struct{}{}
		}
	}
	// Loop iteration variable: stored in ContextData at loop-entry time.
	// AssignNextValueToReference is the declared alias; Name is the fallback
	// (Process Builder convention - executor_elements.go mirrors this logic).
	for _, l := range flowMeta.Loops {
		iterVar := l.AssignNextValueToReference
		if iterVar == "" {
			iterVar = l.Name
		}
		if iterVar != "" {
			names[iterVar] = struct{}{}
		}
	}
	// Subflow output bindings: AssignToReference is written into ContextData
	// after the subflow executes and can hold SObject values.
	for _, s := range flowMeta.Subflows {
		for _, out := range s.OutputAssignments {
			if out.AssignToReference != "" {
				names[out.AssignToReference] = struct{}{}
			}
		}
	}
	// Assignment targets: setNestedContextValue writes the root segment of a
	// non-dotted AssignToReference as a new ContextData key (executor_elements.go).
	// These are sparse until the Assignment element executes.
	for _, assign := range flowMeta.Assignments {
		for _, item := range assign.Items {
			if ref := declaredRootSegment(item.AssignToReference); ref != "" {
				names[ref] = struct{}{}
			}
		}
	}
	// Formulas: immutable on-demand resources resolved from Flow metadata. Their
	// computed values are never stored in ContextData, but the declaration still
	// reserves the name before the first evaluation.
	for _, f := range flowMeta.Formulas {
		if f.Name != "" {
			names[f.Name] = struct{}{}
		}
	}
	// Decision rule outcomes: executeDecision (executor_elements.go:96) stores
	// the bool result of each rule as ContextData[rule.Name]. Absent before the
	// decision element executes. The same $ prefix guard as the executor applies.
	for _, d := range flowMeta.Decisions {
		for _, r := range d.Rules {
			if ref := declaredRootSegment(r.Name); ref != "" {
				names[ref] = struct{}{}
			}
		}
	}
	// Constants, TextTemplates, Choices, and Stages are authored flow resources
	// whose names can appear in elementReference syntax. They are not written to
	// ContextData by the executor, so they would not be resolvable and would
	// fall through to the implicit-$Record guard. A name collision (e.g. a
	// FlowConstant named "Account" on a Contact flow) must block the fallback -
	// the reference is authoring-time declared and must not silently rebind to
	// $Record.Account.* with possibly wrong parent data.
	for _, c := range flowMeta.Constants {
		if c.Name != "" {
			names[c.Name] = struct{}{}
		}
	}
	for _, t := range flowMeta.TextTemplates {
		if t.Name != "" {
			names[t.Name] = struct{}{}
		}
	}
	for _, c := range flowMeta.Choices {
		if c.Name != "" {
			names[c.Name] = struct{}{}
		}
	}
	for _, s := range flowMeta.Stages {
		if s.Name != "" {
			names[s.Name] = struct{}{}
		}
	}
	return names
}

func actionCallWritesOutputParameters(actionType string) bool {
	// emailSimple is intentionally NOT listed: like emailAlert it is legacy
	// migration-input (#1819), fire-and-forget with no native output contract.
	// Although it now routes through executeSendEmailAction, real emailSimple nodes
	// declare no OutputParameters (the frontend re-emits emailSimple without an
	// outputParameters block, deserializers/steps.ts), so assignActionCallOutputs is
	// a no-op for them. Declaring emailSimple output-writing here without the frontend
	// preserving its outputParameters would create a silent metadata-loss path. The
	// native sendEmail output contract activates when a flow is migrated to canonical
	// sendEmail, not for the legacy alias.
	return strings.EqualFold(actionType, "flow") ||
		strings.EqualFold(actionType, flowSendEmailActionType) ||
		strings.EqualFold(actionType, flowResolveEmailRecipientsActionType) ||
		strings.EqualFold(actionType, flowactions.ActionTypeResolveConnectorRemoteIdentity)
}

// declaredRootSegment returns the root name segment of a flow symbol reference
// that is suitable for use as a ContextData guard key. It strips dotted suffixes
// (e.g. "Account.Industry" -> "Account") and rejects dollar-prefixed globals
// (e.g. "$Record", "$Flow") since those are never flow-authored symbols.
// Returns "" for empty or dollar-prefixed inputs.
func declaredRootSegment(ref string) string {
	if ref == "" || strings.HasPrefix(ref, "$") {
		return ""
	}
	if idx := strings.Index(ref, "."); idx > 0 {
		return ref[:idx]
	}
	return ref
}

// resolveAssignmentRHSValueWithPresence resolves a DML assignment RHS value.
// It extends ResolveFlowResource with one additional behavior:
// when the reference is an unqualified cross-object path (e.g. "Account.Industry"
// without a "$Record." prefix) and the root segment names a proven relationship
// on the trigger object, it re-resolves as "$Record.Account.Industry".
//
// This implicit-$Record fallback is scoped to DML input-assignment evaluation
// (recordCreate/recordUpdate input.Value paths only). It is intentionally
// excluded from the general resolver because ContextData is sparse: declared
// but unrun RecordLookup variables are absent from ContextData until they
// execute, and a general-resolver fallback would silently redirect those
// references to $Record data instead of returning (nil, false).
//
// Guard: the root segment must not be a declared flow symbol (variable,
// RecordLookup output, ActionCall output, or RecordCreate output). If the
// segment matches a declared name, it is treated as a flow variable and the
// fallback does not fire - even if the symbol is absent from ContextData
// (declared-but-uninitialized symbols are sparse in ContextData).
func (e *FlowExecutor) resolveAssignmentRHSValueWithPresenceE(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, flowMeta *models.FlowMetadata, value interface{}) (interface{}, bool, error) {
	val, ok, err := e.ResolveFlowResource(ctx, interview, user, value)
	if err != nil {
		return nil, false, err
	}
	if ok {
		return val, ok, nil
	}
	// Attempt implicit $Record fallback for unqualified cross-object paths.
	// Extract the raw reference from two supported encodings:
	//   (a) {"elementReference": "Account.Industry"} - standard Flow metadata
	//   (b) bare string "Account.Industry" - Workflow / Process Builder encoding
	//       (see executor_helpers_test.go:1339-1341 for existing precedent).
	rawRef := ""
	if m, mok := value.(map[string]interface{}); mok {
		if r, exists := m["elementReference"]; exists {
			if s, isStr := r.(string); isStr {
				rawRef = s
			}
		}
	}
	// Also accept bare identifier-shaped dotted reference strings (Workflow/PB).
	// Guard: same criteria as ResolveFlowResource uses to route into
	// resolveNestedReferenceCtxWithPresence. Literal-typed wrappers would have
	// returned (val, true) above and never reach this point.
	if rawRef == "" {
		if s, isStr := value.(string); isStr && strings.Contains(s, ".") && looksLikeVariableReference(s) {
			rawRef = s
		}
	}
	if rawRef == "" || strings.HasPrefix(rawRef, "$") || !strings.Contains(rawRef, ".") {
		return nil, false, nil
	}
	dotIdx := strings.Index(rawRef, ".")
	rootName := rawRef[:dotIdx]
	// Guard: if the root segment is a declared flow symbol (variable, lookup
	// output, action output, or record-create output), do not redirect it to
	// $Record. ContextData is sparse - declared-but-uninitialized symbols are
	// absent until they execute, so absence from ContextData is NOT proof that
	// a name is a relationship and not a variable.
	declaredNames := buildFlowDeclaredNames(flowMeta)
	if _, isDeclared := declaredNames[rootName]; isDeclared {
		return nil, false, nil
	}
	if interview == nil || interview.ContextData == nil {
		return nil, false, nil
	}
	if triggerRecord := interview.ContextData["$Record"]; triggerRecord != nil {
		triggerObjType := flowRuntimeContextFromCtx(ctx).ObjectType()
		triggerSchema := e.lookupObjectSchema(ctx, triggerObjType)
		if e.implicitRelationshipProven(ctx, triggerSchema, triggerObjType, rootName) {
			return e.ResolveFlowResource(ctx, interview, user, map[string]interface{}{"elementReference": "$Record." + rawRef})
		}
	}
	return nil, false, nil
}

// currentAssignmentValueCtx is the __r-aware counterpart of currentAssignmentValue.
// Used by executeAssignment where both the read (of the current value) and the
// subsequent write need to agree on whether cross-object lookups happen.
func (e *FlowExecutor) currentAssignmentValueCtx(ctx context.Context, interview *models.FlowInterview, user *models.UserSession, ref string) interface{} {
	if !strings.Contains(ref, ".") {
		if interview != nil && interview.ContextData != nil {
			return interview.ContextData[ref]
		}
		return nil
	}
	return e.resolveNestedReferenceCtx(ctx, interview, user, ref)
}
