package automation

import (
	"context"
	"strings"
	"time"

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

func (e *FlowExecutor) resolveValue(context map[string]interface{}, value interface{}) interface{} {
	if value == nil {
		return nil
	}

	// Unwrap Salesforce-style typed value objects (e.g. {"elementReference": "..."})
	// before checking for string references. Flow metadata stores values as typed
	// wrappers when loaded from JSON with interface{} fields.
	var wasLiteral bool
	value, wasLiteral = extractTypedValue(value)

	// If it's a string reference, resolve it
	ref, ok := value.(string)
	if !ok {
		return value
	}

	// Handle $Flow system variables
	if strings.HasPrefix(ref, "$Flow.") {
		return e.resolveFlowSystemVariable(ref)
	}

	if isFlowSystemContextReference(ref) {
		return resolveFlowSystemContextVariable(nil, ref)
	}

	// Handle $GlobalConstant
	if strings.HasPrefix(ref, "$GlobalConstant.") {
		return e.resolveGlobalConstant(ref)
	}

	// Handle nested references like $Record.Account.Name or varName.FieldName.
	// Only route through resolveNestedReference when the string looks like an
	// identifier path: all characters must be letters, digits, underscores, $,
	// or dots. A literal string like "Acme Corp. Ltd." contains a space and
	// therefore cannot be a variable reference path — skip nested-reference
	// resolution and let it fall through to the literal passthrough below.
	// See issue #1817 Bug A and the same guard in ResolveFlowResource.
	//
	// Additionally, skip the heuristic entirely when wasLiteral=true: the value
	// arrived via a typed-value wrapper (stringValue/numberValue/…) which means
	// the flow author explicitly authored a literal — 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 resolveNestedReference and silently dropped.
	if !wasLiteral && strings.Contains(ref, ".") && looksLikeVariableReference(ref) {
		return e.resolveNestedReference(context, ref)
	}

	// Simple variable lookup
	if resolved, exists := context[ref]; exists {
		return resolved
	}

	return value
}

// looksLikeVariableReference returns true when s could be a Salesforce Flow
// variable reference path (e.g. "$Record.FieldName", "myVar.SubField").
//
// Salesforce Flow variable and field names are identifiers: they consist only
// of letters, digits, and underscores — no spaces, no hyphens, no parens.
// A reference path like "$Record.Region__c" satisfies this invariant.
// A literal string value like "Acme Corp. Ltd." or "Acme Co. (US), Inc."
// contains spaces and/or parens, so it cannot be a variable reference path
// even though it contains a dot. This check lets resolveValue skip the
// nested-reference lookup for such literals and return them unchanged.
//
// Fallback for bare-string callers: wrapper-aware callers (resolveValue,
// ResolveFlowResource) use the wasLiteral flag from extractTypedValue
// and skip this heuristic entirely when the value arrived via a typed-value
// wrapper — so dotted identifier-shape literals like "v1.2.3" or "name.tld"
// are never misrouted regardless of their character content (issue #2046).
func looksLikeVariableReference(s string) bool {
	for _, r := range s {
		if r == '.' {
			continue
		}
		if r == '$' || r == '_' {
			continue
		}
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			continue
		}
		return false
	}
	return true
}

// resolveFlowSystemVariable handles $Flow.* system variables
func (e *FlowExecutor) resolveFlowSystemVariable(ref string) interface{} {
	now := time.Now()

	switch ref {
	case "$Flow.CurrentDate":
		return now.Format("2006-01-02")
	case "$Flow.CurrentDateTime":
		return now.Format(time.RFC3339)
	case "$Flow.CurrentStage":
		return "" // Stage flows not yet supported
	case "$Flow.InterviewStartTime":
		return now.Format(time.RFC3339)
	case "$Flow.FaultMessage":
		return "" // Set during fault handling
	case "$Flow.ActiveStages":
		return []interface{}{}
	default:
		return nil
	}
}

func isFlowSystemContextReference(ref string) bool {
	return ref == "$SystemContext" ||
		ref == constants.FormulaContextSystemContext ||
		strings.HasPrefix(ref, "$SystemContext.") ||
		strings.HasPrefix(ref, constants.FormulaContextSystemContext+".")
}

func resolveFlowSystemContextVariable(ctx context.Context, ref string) interface{} {
	systemCtx := constants.FormulaSystemContextFromContext(ctx)
	if ref == "$SystemContext" || ref == constants.FormulaContextSystemContext {
		return systemCtx
	}
	fieldName := strings.TrimPrefix(ref, "$SystemContext.")
	fieldName = strings.TrimPrefix(fieldName, constants.FormulaContextSystemContext+".")
	if val, ok := systemCtx[fieldName]; ok {
		return val
	}
	return globalRefError{ref: ref}
}

// globalRefError is returned by resolveFlowUserVariable when a $User.<f>,
// $Profile.<f>, or $UserRole.<f> reference names a field that does not exist
// on the session-bound user context. Returning a non-nil error (rather than
// silent nil) keeps the resolver fail-closed per registry/declaration
// discipline (CLAUDE.md §Rules).
type globalRefError struct{ ref string }

func (e globalRefError) Error() string {
	return "flow: unresolved global variable reference: " + e.ref
}

// resolveFlowUserVariable resolves $User.<f> / $Profile.<f> / $UserRole.<f>
// formula references through the shared formula.BuildFormulaUserContext —
// the SINGLE read path for Salesforce-modeled formula globals shared with
// the validation-rule path (records/persistence_service.go:buildUserContext).
// Both consumers see identical values for identical sessions, which guarantees
// validation rules and flow formulas agree on $Profile.Name etc. within a
// single save transaction. See pkg/formula/user_context.go and CLAUDE.md
// §"Salesforce Boundary".
//
// Unknown field references return globalRefError (fail-closed, not silent nil).
// A nil user returns nil without error.
func resolveFlowUserVariable(user *models.UserSession, ref string) interface{} {
	if user == nil {
		return nil
	}
	userCtx := formula.BuildFormulaUserContext(user)

	resolve := func(globalKey string) interface{} {
		fieldName := strings.TrimPrefix(ref, "$"+strings.TrimPrefix(globalKey, "__")+".")
		if m, ok := userCtx[globalKey].(map[string]interface{}); ok {
			if val, ok := m[fieldName]; ok {
				return val
			}
		}
		return globalRefError{ref: ref}
	}

	switch {
	case strings.HasPrefix(ref, "$User."):
		return resolve("__User")
	case strings.HasPrefix(ref, "$Profile."):
		return resolve("__Profile")
	case strings.HasPrefix(ref, "$UserRole."):
		return resolve("__UserRole")
	}
	return globalRefError{ref: ref}
}

// resolveGlobalConstant handles $GlobalConstant.* references
func (e *FlowExecutor) resolveGlobalConstant(ref string) interface{} {
	// Extract constant name
	parts := strings.SplitN(ref, ".", 2)
	if len(parts) < 2 {
		return nil
	}

	// Common global constants
	switch parts[1] {
	case "True":
		return true
	case "False":
		return false
	case "EmptyString":
		return ""
	default:
		return nil
	}
}

// resolveNestedReference handles dot-notation references like $Record.Account.Name.
//
// This is the pure in-memory variant: it walks map-like structures only. Segments
// ending in `__r` are NOT lazy-loaded here — this variant is used by scheduled
// filter builders and tests that have no ctx/user/interview. Runtime callers that
// need cross-object `__r` traversal must use resolveNestedReferenceCtx instead.
func (e *FlowExecutor) resolveNestedReference(context map[string]interface{}, ref string) interface{} {
	parts := strings.Split(ref, ".")
	if len(parts) == 0 {
		return nil
	}

	// Get the root object
	current := context[parts[0]]
	if current == nil {
		return nil
	}

	// Traverse the path
	for i := 1; i < len(parts); i++ {
		fieldName := parts[i]

		switch obj := current.(type) {
		case models.SObject:
			current = obj[fieldName]
		case map[string]interface{}:
			current = obj[fieldName]
		default:
			return nil
		}

		if current == nil {
			return nil
		}
	}

	return current
}
