package formula

import (
	"github.com/expr-lang/expr/ast"
	"github.com/expr-lang/expr/parser"
)

// CollectExpressionIdentifiers returns the distinct identifier roots read by
// an authored formula expression. Function names and member-property names are
// excluded: FOO(Bar.Baz) reads the resource root Bar, not resources FOO or Baz.
//
// Flow's program checker uses this to build the Formula Resource dependency
// graph from the same normalized expression syntax that the runtime evaluates.
func CollectExpressionIdentifiers(expression string) ([]string, error) {
	tree, err := parser.Parse(normalizeFormulaExpression(expression))
	if err != nil {
		return nil, err
	}
	collector := expressionIdentifierCollector{
		excluded: make(map[*ast.IdentifierNode]struct{}),
	}
	ast.Walk(&tree.Node, &collector)

	seen := map[string]struct{}{}
	out := make([]string, 0, len(collector.identifiers))
	for _, identifier := range collector.identifiers {
		if _, excluded := collector.excluded[identifier]; excluded || identifier.Value == "" {
			continue
		}
		if _, duplicate := seen[identifier.Value]; duplicate {
			continue
		}
		seen[identifier.Value] = struct{}{}
		out = append(out, identifier.Value)
	}
	return out, nil
}

// expressionIdentifierCollector relies on expr's exhaustive AST walker so a
// newly introduced node kind cannot silently hide a Formula dependency. A
// CallNode's identifier callee is syntax (the function name), not a resource.
type expressionIdentifierCollector struct {
	identifiers []*ast.IdentifierNode
	excluded    map[*ast.IdentifierNode]struct{}
}

func (c *expressionIdentifierCollector) Visit(node *ast.Node) {
	switch n := (*node).(type) {
	case *ast.IdentifierNode:
		c.identifiers = append(c.identifiers, n)
	case *ast.CallNode:
		if callee, ok := n.Callee.(*ast.IdentifierNode); ok {
			c.excluded[callee] = struct{}{}
		}
	}
}
