package formula

import (
	"errors"
	"fmt"
	"strings"

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

// ErrDynamicRecordFieldAccess reports a record-namespace member access whose
// property is not a static name (e.g. `record[SomeVar]`): the field read at
// runtime is the VALUE of the property expression, so no static reference
// set can cover it. Callers that derive read scopes from the collection must
// treat the expression as statically unanalyzable rather than under-read it.
var ErrDynamicRecordFieldAccess = errors.New("formula: dynamic record field access is not statically analyzable")

// CollectCurrentObjectFieldReferences returns the distinct, order-preserved set
// of direct current-object field API names referenced by the expression.
//
// It intentionally excludes relationship traversals such as `Parent__r.Name`
// and session/global roots such as `$User.Name`, because those are not direct
// physical columns on the current object's table. A dynamic record-namespace
// member access (`record[Key]`) fails the collection with
// ErrDynamicRecordFieldAccess — see there.
func CollectCurrentObjectFieldReferences(expression string) ([]string, error) {
	normalized := normalizeFormulaExpression(expression)
	tree, err := parser.Parse(normalized)
	if err != nil {
		return nil, err
	}

	seen := make(map[string]struct{})
	out := make([]string, 0)
	add := func(name string) {
		if name == "" {
			return
		}
		if _, ok := seen[name]; ok {
			return
		}
		seen[name] = struct{}{}
		out = append(out, name)
	}

	var visit func(ast.Node) error
	visit = func(n ast.Node) error {
		if n == nil {
			return nil
		}
		switch v := n.(type) {
		case *ast.NilNode, *ast.IntegerNode, *ast.FloatNode, *ast.BoolNode, *ast.StringNode, *ast.ConstantNode, *ast.PointerNode:
			return nil
		case *ast.IdentifierNode:
			if isCurrentRecordMemberRoot(v.Value) {
				// A bare whole-record reference (e.g. len(record)) reads an
				// unknowable field set — same unanalyzable class as dynamic
				// member access. Record-namespace roots only reach this case
				// when bare: member chains never recurse into their root.
				return fmt.Errorf("%w: bare record reference %q", ErrDynamicRecordFieldAccess, v.Value)
			}
			add(currentObjectFieldFromIdentifier(v.Value))
			return nil
		case *ast.UnaryNode:
			return visit(v.Node)
		case *ast.BinaryNode:
			if err := visit(v.Left); err != nil {
				return err
			}
			return visit(v.Right)
		case *ast.ChainNode:
			return visit(v.Node)
		case *ast.MemberNode:
			// Dynamic access wins over static classification: memberChain
			// DROPS computed segments, so a nested form like
			// record[Selector].Amount would otherwise collapse to a
			// plausible-looking static "Amount" chain. Any computed property
			// anywhere in a record-rooted chain makes the whole access
			// statically unanalyzable.
			if dynamicRecordMemberAccess(v) {
				// record[Key] resolves the field named by Key's runtime
				// VALUE — no static reference set can cover it, so a caller
				// deriving a read basis from this collection would silently
				// under-read. Surface it instead of guessing; frozen schema
				// validation already rejects this shape for authored formula
				// fields, this closes the raw-expression paths.
				return fmt.Errorf("%w: %s", ErrDynamicRecordFieldAccess, v.String())
			}
			if name, ok := directRecordFieldReference(v); ok {
				add(name)
				return nil
			}
			// Do not recurse into member chains generically: relationship
			// traversals such as Parent__r.Name should not surface as direct
			// current-object field references.
			return nil
		case *ast.SliceNode:
			if err := visit(v.Node); err != nil {
				return err
			}
			if err := visit(v.From); err != nil {
				return err
			}
			return visit(v.To)
		case *ast.CallNode:
			if _, ok := v.Callee.(*ast.IdentifierNode); !ok {
				if err := visit(v.Callee); err != nil {
					return err
				}
			}
			for i := range v.Arguments {
				if err := visit(v.Arguments[i]); err != nil {
					return err
				}
			}
			return nil
		case *ast.BuiltinNode:
			for i := range v.Arguments {
				if err := visit(v.Arguments[i]); err != nil {
					return err
				}
			}
			return nil
		case *ast.PredicateNode:
			return visit(v.Node)
		case *ast.ConditionalNode:
			if err := visit(v.Cond); err != nil {
				return err
			}
			if err := visit(v.Exp1); err != nil {
				return err
			}
			return visit(v.Exp2)
		case *ast.VariableDeclaratorNode:
			if err := visit(v.Value); err != nil {
				return err
			}
			return visit(v.Expr)
		case *ast.SequenceNode:
			for i := range v.Nodes {
				if err := visit(v.Nodes[i]); err != nil {
					return err
				}
			}
			return nil
		case *ast.ArrayNode:
			for i := range v.Nodes {
				if err := visit(v.Nodes[i]); err != nil {
					return err
				}
			}
			return nil
		case *ast.MapNode:
			for i := range v.Pairs {
				if err := visit(v.Pairs[i]); err != nil {
					return err
				}
			}
			return nil
		case *ast.PairNode:
			if err := visit(v.Key); err != nil {
				return err
			}
			return visit(v.Value)
		default:
			return fmt.Errorf("formula field references: unsupported ast node type %T", n)
		}
	}

	if err := visit(tree.Node); err != nil {
		return nil, err
	}
	return out, nil
}

// currentObjectFieldFromIdentifier resolves a bare identifier to the schema
// field it reads, or "" when it is not a field reference. Prior-state
// lowered identifiers (`_prior_X`, produced by prior-state function
// normalization) resolve to their underlying field X: the engine seeds
// `_prior_<field>` from the record's CURRENT values on no-prior paths, so
// the field itself is what any reference-driven basis (projection need, read
// caps, rollup relevance) must carry — emitting the lowered name verbatim
// would silently drop the field from a capped read. Engine magic sentinels
// and double-underscore reserved roots are not fields. Mirrors
// validationProjectionFieldFromIdentifier's classification.
func currentObjectFieldFromIdentifier(name string) string {
	if name == "" || hasReservedFormulaRootPrefix(name) {
		return ""
	}
	switch name {
	case "_has_prior_", "_is_new_", "_is_clone_":
		return ""
	}
	return strings.TrimPrefix(name, "_prior_")
}

func directRecordFieldReference(n *ast.MemberNode) (string, bool) {
	root, segments := memberChain(n)
	if !isCurrentRecordMemberRoot(root) || len(segments) != 1 {
		return "", false
	}
	if segments[0] == "" {
		return "", false
	}
	return segments[0], true
}

// isCurrentRecordMemberRoot reports whether a member-chain root resolves to
// the current record's field namespace. Besides the normalized `__Record`
// root ($Record.Field), the engine exposes the record under the runtime
// `record`/`prior` env names and the normalized prior-state roots — a field
// accessed through any of them is a direct current-object field reference
// (the prior-state value of field X is still field X). This mirrors the root
// classification in validationProjectionFieldFromMember; session/global
// namespaces (__User etc.) and relationship traversals stay excluded.
func isCurrentRecordMemberRoot(root string) bool {
	switch root {
	case "__Record", "__Record__Old", "__Record__Prior", "record", "prior":
		return true
	}
	return false
}

// memberChain resolves a member chain to its root identifier and STATIC
// property segments. Only string properties are static: `a.b` and `a["b"]`
// both lower the property to a StringNode, while `a[b]` keeps an expression
// node whose runtime value names the field — that is not a segment (same
// static-property rule as filterformula_validation's memberPath; the
// dynamic form is surfaced by dynamicRecordMemberAccess instead).
func memberChain(n ast.Node) (string, []string) {
	member, ok := n.(*ast.MemberNode)
	if !ok {
		switch v := n.(type) {
		case *ast.IdentifierNode:
			return v.Value, nil
		case *ast.ChainNode:
			return memberChain(v.Node)
		}
		return "", nil
	}
	root, segments := memberChain(member.Node)
	if prop, ok := member.Property.(*ast.StringNode); ok {
		segments = append(segments, prop.Value)
	}
	return root, segments
}

// dynamicRecordMemberAccess reports whether a member chain is rooted in the
// current-record namespace AND carries a non-static (computed) property
// anywhere along the chain, e.g. `record[Key]` or `__Record[Name]`.
func dynamicRecordMemberAccess(n *ast.MemberNode) bool {
	root, _ := memberChain(n)
	if !isCurrentRecordMemberRoot(root) {
		return false
	}
	var hasDynamicProperty func(ast.Node) bool
	hasDynamicProperty = func(node ast.Node) bool {
		switch v := node.(type) {
		case *ast.MemberNode:
			if _, ok := v.Property.(*ast.StringNode); !ok {
				return true
			}
			return hasDynamicProperty(v.Node)
		case *ast.ChainNode:
			return hasDynamicProperty(v.Node)
		}
		return false
	}
	return hasDynamicProperty(n)
}

func hasReservedFormulaRootPrefix(name string) bool {
	return len(name) >= 2 && name[:2] == "__"
}

// CollectValidationProjectionFieldRefs returns all current-object field API
// names that a validation rule expression could read at evaluation time,
// including both current-record and prior-state references.
//
// It uses the same normalization and parser as the runtime evaluator
// (normalizeFormulaExpression + parser.Parse) and walks the AST exhaustively
// via ast.Walk. Resolution follows a fail-closed model: any identifier or
// member path that cannot be positively classified as a non-record reference
// (session globals, built-in magic vars) is included. A false-include costs
// one extra formula eval; a false-exclude is a correctness regression.
//
// Covered syntax forms:
//   - Bare identifiers: FormulaA__c → field FormulaA__c
//   - $Record.Field / record.Field (normalized to __Record.Field) → field Field
//   - $Record__Prior.Field (normalized to __Record__Prior.Field) → field Field
//   - PRIORVALUE(Field) (normalized to _prior_Field) → field Field
//   - ISCHANGED(Field) (normalized to include _prior_Field) → field Field
func CollectValidationProjectionFieldRefs(expression string) []string {
	normalized := normalizeFormulaExpression(expression)
	tree, err := parser.Parse(normalized)
	if err != nil {
		return nil
	}

	seen := make(map[string]struct{})
	v := &validationFieldRefVisitor{seen: seen}
	ast.Walk(&tree.Node, v)

	out := make([]string, 0, len(seen))
	for field := range seen {
		out = append(out, field)
	}
	return out
}

// validationFieldRefVisitor is driven by ast.Walk which is exhaustive — there
// is no construct the walk can silently skip. For each identifier or member
// path, it determines whether the node resolves to a current-object field.
type validationFieldRefVisitor struct {
	seen map[string]struct{}
}

func (v *validationFieldRefVisitor) Visit(node *ast.Node) {
	if node == nil || *node == nil {
		return
	}
	n := *node
	switch t := n.(type) {
	case *ast.IdentifierNode:
		if field := validationProjectionFieldFromIdentifier(t.Value); field != "" {
			v.seen[field] = struct{}{}
		}
	case *ast.MemberNode:
		if field := validationProjectionFieldFromMember(t); field != "" {
			v.seen[field] = struct{}{}
		}
	}
}

// validationProjectionFieldFromIdentifier resolves a bare identifier to a
// schema field name. Fail-closed: if we can't prove it's NOT a field, include
// it. Only explicitly known non-field identifiers are excluded.
func validationProjectionFieldFromIdentifier(name string) string {
	if name == "" {
		return ""
	}
	// Prior-state lowered identifiers: _prior_FieldName → FieldName
	if strings.HasPrefix(name, "_prior_") {
		return strings.TrimPrefix(name, "_prior_")
	}
	// Magic/sentinel identifiers that are NOT schema fields
	if name == "_has_prior_" || name == "_is_new_" || name == "_is_clone_" {
		return ""
	}
	// Runtime built-in helper identifiers injected by the engine
	if strings.HasPrefix(name, "__ncrm_") || strings.HasPrefix(name, "__NCRM_") {
		return ""
	}
	// Reserved internal roots (double-underscore prefix: __Record, __User, etc.)
	// These are member-access roots, not bare field refs
	if hasReservedFormulaRootPrefix(name) {
		return ""
	}
	// Fail-closed: treat as a schema field reference
	return name
}

// validationProjectionFieldFromMember resolves a member access chain to a
// schema field name for current/prior record access patterns. For relationship
// traversals (Parent__r.Name, PrimaryContact.Name), returns the root segment
// which the caller resolves to a FK field via ResolveRelationFK. Fail-closed:
// any member root not positively identified as a session/global namespace is
// returned as a potential current-object field or relationship name.
func validationProjectionFieldFromMember(n *ast.MemberNode) string {
	path := memberPath(n)
	if len(path) < 2 {
		return ""
	}
	root := path[0]
	field := path[1]
	switch root {
	// Current/prior record namespace: return the accessed field directly
	case "__Record", "__Record__Old", "__Record__Prior", "record", "prior":
		return field
	// Session/global namespaces: NOT current-object fields
	case "__User", "__Profile", "__UserRole", "__Permission", "__Organization":
		return ""
	}
	// Fail-closed: unclassified root is either a relationship traversal
	// (Parent__r.Name) or an unknown namespace. Return the root so the caller
	// can attempt ResolveRelationFK. A false-include (extra eval) is safe;
	// a false-exclude (omitted FK) is the correctness regression.
	return root
}
