package expression

import "github.com/expr-lang/expr/ast"

// innerMemberMarker is a first-pass AST visitor that records which MemberNode instances
// are "inner" nodes in a chained access (i.e., some outer MemberNode has them as its Node).
// This is used by sfFormulaPatcher to distinguish outermost chains from intermediate hops.
type innerMemberMarker struct {
	inner map[*ast.MemberNode]bool
}

func newInnerMemberMarker() *innerMemberMarker {
	return &innerMemberMarker{inner: make(map[*ast.MemberNode]bool)}
}

func (m *innerMemberMarker) Visit(node *ast.Node) {
	n, ok := (*node).(*ast.MemberNode)
	if !ok {
		return
	}
	if inner, ok := n.Node.(*ast.MemberNode); ok {
		m.inner[inner] = true
	}
}

// sfFormulaPatcher applies two Salesforce-semantic fixes to the parsed AST before compilation:
//
// Fix A — lazy IF: rewrites IF(cond, a, b) → (cond ? a : b).
// expr-lang evaluates function arguments eagerly; the native ternary is lazy.
// This matches SF semantics where only the chosen branch is executed.
//
// Fix B — null propagation: for any MemberNode chain of depth ≥ 2, marks non-root
// hops as Optional and wraps the outermost in a ChainNode so that a nil intermediate
// value short-circuits the chain and returns nil instead of "cannot fetch X from <nil>".
// This matches SF semantics where field traversal through a null relationship returns null.
type sfFormulaPatcher struct {
	marker    *innerMemberMarker
	processed map[*ast.MemberNode]bool
}

func newSFFormulaPatcher(marker *innerMemberMarker) *sfFormulaPatcher {
	return &sfFormulaPatcher{
		marker:    marker,
		processed: make(map[*ast.MemberNode]bool),
	}
}

func (p *sfFormulaPatcher) Visit(node *ast.Node) {
	switch n := (*node).(type) {
	case *ast.CallNode:
		p.visitCall(node, n)
	case *ast.MemberNode:
		p.visitMember(node, n)
	}
}

// visitCall applies Fix A: IF(cond, a, b) → ternary (cond ? a : b).
func (p *sfFormulaPatcher) visitCall(node *ast.Node, n *ast.CallNode) {
	ident, ok := n.Callee.(*ast.IdentifierNode)
	if !ok || ident.Value != "IF" || len(n.Arguments) != 3 {
		return
	}
	ast.Patch(node, &ast.ConditionalNode{
		Cond: n.Arguments[0],
		Exp1: n.Arguments[1],
		Exp2: n.Arguments[2],
	})
}

// visitMember applies Fix B: null-propagating optional chaining for all
// MemberNode depths.
//
// Depth ≥ 2 (e.g. Record.RecordType.DeveloperName): inner hops get Optional;
// the outermost is wrapped in ChainNode for JumpIfNil guards.
//
// Depth 1 (e.g. Account.Name, RecordType.DeveloperName as SF bare shorthand):
// same treatment — a nil root (definitive-null relation installed by
// augmentRecordWithRelations) propagates to nil rather than erroring with
// "type nil has no field X". Issue #2181 r2.
func (p *sfFormulaPatcher) visitMember(node *ast.Node, n *ast.MemberNode) {
	if p.processed[n] {
		return
	}
	p.processed[n] = true

	n.Optional = true

	if !p.marker.inner[n] {
		// Outermost in the chain: wrap in ChainNode so the compiler emits JumpIfNil guards.
		ast.Patch(node, &ast.ChainNode{Node: n})
	}
}
