package formula

import (
	"fmt"
	"sort"
	"strings"

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

// priorStateMagicRoots are the env identifiers normalizeFormulaExpression emits
// when it rewrites prior-state functions:
//
//	ISCHANGED(f)  → (_has_prior_ && f != _prior_f)
//	PRIORVALUE(f) → _prior_f
//	ISNEW()       → _is_new_
//	ISCLONE()     → _is_clone_
//
// Detecting these on the NORMALIZED form is comment-, whitespace-, and
// alias-proof, unlike scanning the raw expression for "ISCHANGED(" (which
// IF(ISCHANGED/*x*/(Email),…) trivially evades).
var priorStateMagicRoots = map[string]struct{}{
	"_has_prior_": {},
	"_is_new_":    {},
	"_is_clone_":  {},
}

// ValidateConnectorTransformExpression validates a connector field-mapping
// TransformExpression against the connector executor's runtime contract. That
// executor evaluates with formula.Context{Record: flatSourceRecord} and nothing
// else — no Prior map, no session/system globals, no Fetcher for relationship
// traversal. Every construct that needs context the flat record cannot supply
// fails OPEN, not closed: ISNEW() is always true, ISCHANGED() always false,
// PRIORVALUE() and $Record-qualified relationship paths resolve to nil, and
// bare relationship/namespace paths fault with "unknown name". A pack carrying
// any of these installs and verifies clean, then misbehaves on first sync — so
// they must be rejected at install time.
//
// The check operates on the NORMALIZED expression (byte-identical to what the
// runtime evaluates) and walks the parsed AST, rejecting as the complement of a
// closed allowed-set:
//
//   - prior-state, via the normalized magic roots (_has_prior_, _prior_*,
//     _is_new_, _is_clone_)
//   - session/system globals: any "__"-prefixed root except __Record
//     ($User → __User, $Profile → __Profile, $Permission → __Permission, …)
//   - relationship traversal and foreign namespaces: any dotted member access
//     whose root is not the record (Account.Name, Owner.Email, user.Name,
//     prior.Status), plus __Record/record dotted deeper than one level
//     ($Record.Account.Name)
//
// Allowed: bare field references (email, FirstName), single-level record access
// (record.Email, $Record.email), builtins, literals, operators. Parse errors
// and unknown/over-arity functions surface through ValidateSyntax.
//
// Walking normalizeFormulaExpression + the same AST the runtime parses is what
// keeps this guard from diverging from the evaluator: there is no raw-string
// form (comment, whitespace, $-prefix, alias, bare-vs-qualified) that the
// validator sees differently from what the executor runs. This replaces the
// earlier per-form regexes, each of which the next syntactic variant evaded.
func (e *Engine) ValidateConnectorTransformExpression(expression string) error {
	expr := strings.TrimSpace(expression)
	if expr == "" {
		return nil
	}
	// Surface parse-level and unknown/over-arity function errors first, with the
	// same gate the runtime verifier uses.
	if err := e.ValidateSyntax(expr); err != nil {
		return err
	}
	normalized := normalizeFormulaExpression(expr)
	tree, err := parser.Parse(normalized)
	if err != nil {
		return err
	}
	// Reject whole-record references anywhere they would be evaluated as a value.
	// ast.Walk visits the record base of a valid $Record.email access on its own,
	// so this parent-aware pass distinguishes direct single-level field access
	// from UPPER($Record), 1 + $Record, IF(..., $Record, ...), and record[expr].
	if err := rejectConnectorWholeRecordReferences(tree.Node); err != nil {
		return err
	}
	v := &connectorTransformVisitor{}
	ast.Walk(&tree.Node, v)
	return v.err
}

// ConnectorTransformReferencedSourceFields returns the flat source fields read by
// a connector transform expression after validating that the expression is safe
// for the connector executor's Record-only formula context. The result is stable
// and de-duplicated so callers can run one FLS/readability check per field.
func (e *Engine) ConnectorTransformReferencedSourceFields(expression string) ([]string, error) {
	expr := strings.TrimSpace(expression)
	if expr == "" {
		return nil, nil
	}
	if err := e.ValidateConnectorTransformExpression(expr); err != nil {
		return nil, err
	}
	normalized := normalizeFormulaExpression(expr)
	tree, err := parser.Parse(normalized)
	if err != nil {
		return nil, err
	}
	fieldSet := make(map[string]struct{})
	collector := connectorTransformSourceFieldCollector{locals: make(map[string]int)}
	if err := collector.collect(tree.Node, fieldSet); err != nil {
		return nil, err
	}
	fields := make([]string, 0, len(fieldSet))
	for field := range fieldSet {
		fields = append(fields, field)
	}
	sort.Strings(fields)
	return fields, nil
}

// connectorTransformVisitor checks every dotted-path / identifier root against
// checkConnectorTransformRoot. It is driven by ast.Walk, which is exhaustive —
// it descends into every node kind (including computed/bracket member access
// such as record[Account.Name], slices, map literals, and predicate closures)
// and panics on any unknown node type. Using it instead of a hand-rolled
// type-switch is what closes the "another node kind hides a forbidden root"
// gap: there is no construct the walk can silently skip.
type connectorTransformVisitor struct{ err error }

func (v *connectorTransformVisitor) Visit(node *ast.Node) {
	if v.err != nil {
		return
	}
	if path := memberPath(*node); len(path) > 0 {
		if err := checkConnectorTransformRoot(path); err != nil {
			v.err = err
		}
	}
}

func isConnectorTransformNonFieldIdentifier(name string) bool {
	name = strings.TrimSpace(name)
	if name == "" || isConnectorRecordRoot(name) {
		return true
	}
	if _, ok := priorStateMagicRoots[name]; ok || strings.HasPrefix(name, "_prior_") {
		return true
	}
	return strings.HasPrefix(name, "__")
}

type connectorTransformSourceFieldCollector struct {
	locals map[string]int
}

func (c *connectorTransformSourceFieldCollector) isLocal(name string) bool {
	if c == nil {
		return false
	}
	return c.locals[strings.TrimSpace(name)] > 0
}

func (c *connectorTransformSourceFieldCollector) pushLocal(name string) func() {
	name = strings.TrimSpace(name)
	if name == "" {
		return func() {}
	}
	c.locals[name]++
	return func() {
		c.locals[name]--
		if c.locals[name] <= 0 {
			delete(c.locals, name)
		}
	}
}

func (c *connectorTransformSourceFieldCollector) collect(node ast.Node, fields map[string]struct{}) error {
	switch n := node.(type) {
	case nil:
		return nil
	case *ast.NilNode, *ast.IntegerNode, *ast.FloatNode, *ast.BoolNode, *ast.StringNode, *ast.ConstantNode, *ast.PointerNode:
		return nil
	case *ast.IdentifierNode:
		if !isConnectorTransformNonFieldIdentifier(n.Value) && !c.isLocal(n.Value) {
			fields[n.Value] = struct{}{}
		}
	case *ast.UnaryNode:
		return c.collect(n.Node, fields)
	case *ast.BinaryNode:
		if err := c.collect(n.Left, fields); err != nil {
			return err
		}
		return c.collect(n.Right, fields)
	case *ast.ChainNode:
		return c.collect(n.Node, fields)
	case *ast.MemberNode:
		if path := memberPath(n); len(path) == 2 && isConnectorRecordRoot(path[0]) {
			fields[path[1]] = struct{}{}
			return nil
		}
		if err := c.collect(n.Node, fields); err != nil {
			return err
		}
		return c.collect(n.Property, fields)
	case *ast.SliceNode:
		if err := c.collect(n.Node, fields); err != nil {
			return err
		}
		if err := c.collect(n.From, fields); err != nil {
			return err
		}
		return c.collect(n.To, fields)
	case *ast.CallNode:
		if _, ok := n.Callee.(*ast.IdentifierNode); !ok {
			if err := c.collect(n.Callee, fields); err != nil {
				return err
			}
		}
		for _, arg := range n.Arguments {
			if err := c.collect(arg, fields); err != nil {
				return err
			}
		}
	case *ast.BuiltinNode:
		for _, arg := range n.Arguments {
			if err := c.collect(arg, fields); err != nil {
				return err
			}
		}
		if err := c.collect(n.Map, fields); err != nil {
			return err
		}
	case *ast.PredicateNode:
		return c.collect(n.Node, fields)
	case *ast.VariableDeclaratorNode:
		if err := c.collect(n.Value, fields); err != nil {
			return err
		}
		pop := c.pushLocal(n.Name)
		defer pop()
		return c.collect(n.Expr, fields)
	case *ast.SequenceNode:
		for _, child := range n.Nodes {
			if err := c.collect(child, fields); err != nil {
				return err
			}
		}
	case *ast.ConditionalNode:
		if err := c.collect(n.Cond, fields); err != nil {
			return err
		}
		if err := c.collect(n.Exp1, fields); err != nil {
			return err
		}
		return c.collect(n.Exp2, fields)
	case *ast.ArrayNode:
		for _, child := range n.Nodes {
			if err := c.collect(child, fields); err != nil {
				return err
			}
		}
	case *ast.MapNode:
		for _, pair := range n.Pairs {
			if err := c.collect(pair, fields); err != nil {
				return err
			}
		}
	case *ast.PairNode:
		if err := c.collect(n.Key, fields); err != nil {
			return err
		}
		return c.collect(n.Value, fields)
	default:
		panic(fmt.Sprintf("undefined node type (%T)", node))
	}
	return nil
}

// checkConnectorTransformRoot enforces the connector transform contract on a
// single dotted path (path[0] is the root identifier).
func checkConnectorTransformRoot(path []string) error {
	root := path[0]

	// 1. Prior-state magic roots (normalized ISCHANGED/PRIORVALUE/ISNEW/ISCLONE).
	if _, ok := priorStateMagicRoots[root]; ok || strings.HasPrefix(root, "_prior_") {
		return fmt.Errorf("references prior-state (%s) which is not available in connector transforms — the executor has no Prior record; ISCHANGED/PRIORVALUE/ISNEW/ISCLONE cannot be used", priorStateLabel(root))
	}

	// 2. Session/system globals: any "__"-prefixed root other than the record.
	if strings.HasPrefix(root, "__") && root != "__Record" {
		return fmt.Errorf("references $%s which is not available in connector transforms — only the source record and its direct fields are accessible", strings.TrimPrefix(root, "__"))
	}

	// 3. The record itself. Single-level field access ($Record.email,
	//    record.email) is fine; a deeper path is relationship traversal the
	//    Fetcher-less executor cannot perform. A bare record/$Record reference
	//    (the whole source map) is rejected by rejectConnectorWholeRecordReferences
	//    before this generic ast.Walk pass, because ast.Walk also visits the record
	//    base of a legitimate $Record.email access as a standalone length-1 path.
	if root == "__Record" || root == "record" {
		if len(path) >= 3 {
			return fmt.Errorf("traverses relationship %q which is not available in connector transforms — the executor has no Fetcher; reference direct source fields only", strings.Join(path, "."))
		}
		return nil
	}

	// 4. Any other dotted root is a relationship or foreign namespace
	//    (Account.Name, Owner.Email, user.Name, prior.Status) that the flat
	//    source-record context does not inject. A bare identifier (no dot) is a
	//    plain field reference and is allowed.
	if len(path) >= 2 {
		return fmt.Errorf("references %q which is not available in connector transforms — %q is not a field on the flat source record (relationship traversal and runtime namespaces require context the executor does not inject); reference direct source fields only", strings.Join(path, "."), root)
	}
	return nil
}

func rejectConnectorWholeRecordReferences(node ast.Node) error {
	switch n := node.(type) {
	case nil:
		return nil
	case *ast.NilNode, *ast.IntegerNode, *ast.FloatNode, *ast.BoolNode, *ast.StringNode, *ast.ConstantNode, *ast.PointerNode:
		return nil
	case *ast.IdentifierNode:
		if isConnectorRecordRoot(n.Value) {
			return connectorWholeRecordReferenceError(n.Value)
		}
	case *ast.UnaryNode:
		return rejectConnectorWholeRecordReferences(n.Node)
	case *ast.BinaryNode:
		if err := rejectConnectorWholeRecordReferences(n.Left); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.Right)
	case *ast.ChainNode:
		return rejectConnectorWholeRecordReferences(n.Node)
	case *ast.MemberNode:
		if path := memberPath(n); len(path) == 2 && isConnectorRecordRoot(path[0]) {
			return nil
		}
		if err := rejectConnectorWholeRecordReferences(n.Node); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.Property)
	case *ast.SliceNode:
		if err := rejectConnectorWholeRecordReferences(n.Node); err != nil {
			return err
		}
		if err := rejectConnectorWholeRecordReferences(n.From); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.To)
	case *ast.CallNode:
		if err := rejectConnectorWholeRecordReferences(n.Callee); err != nil {
			return err
		}
		for _, arg := range n.Arguments {
			if err := rejectConnectorWholeRecordReferences(arg); err != nil {
				return err
			}
		}
	case *ast.BuiltinNode:
		for _, arg := range n.Arguments {
			if err := rejectConnectorWholeRecordReferences(arg); err != nil {
				return err
			}
		}
		if err := rejectConnectorWholeRecordReferences(n.Map); err != nil {
			return err
		}
	case *ast.PredicateNode:
		return rejectConnectorWholeRecordReferences(n.Node)
	case *ast.VariableDeclaratorNode:
		if err := rejectConnectorWholeRecordReferences(n.Value); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.Expr)
	case *ast.SequenceNode:
		for _, child := range n.Nodes {
			if err := rejectConnectorWholeRecordReferences(child); err != nil {
				return err
			}
		}
	case *ast.ConditionalNode:
		if err := rejectConnectorWholeRecordReferences(n.Cond); err != nil {
			return err
		}
		if err := rejectConnectorWholeRecordReferences(n.Exp1); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.Exp2)
	case *ast.ArrayNode:
		for _, child := range n.Nodes {
			if err := rejectConnectorWholeRecordReferences(child); err != nil {
				return err
			}
		}
	case *ast.MapNode:
		for _, pair := range n.Pairs {
			if err := rejectConnectorWholeRecordReferences(pair); err != nil {
				return err
			}
		}
	case *ast.PairNode:
		if err := rejectConnectorWholeRecordReferences(n.Key); err != nil {
			return err
		}
		return rejectConnectorWholeRecordReferences(n.Value)
	default:
		panic(fmt.Sprintf("undefined node type (%T)", node))
	}
	return nil
}

func isConnectorRecordRoot(root string) bool {
	return root == "__Record" || root == "record"
}

func connectorWholeRecordReferenceError(root string) error {
	return fmt.Errorf("connector transform is a bare reference to the whole source record (%s); a transform must produce a single field value — reference a specific field such as $Record.fieldName", root)
}

// priorStateLabel maps a normalized prior-state magic root back to a friendly
// function name for the validation error.
func priorStateLabel(root string) string {
	switch {
	case root == "_has_prior_":
		return "ISCHANGED"
	case root == "_is_new_":
		return "ISNEW"
	case root == "_is_clone_":
		return "ISCLONE"
	case strings.HasPrefix(root, "_prior_"):
		return "PRIORVALUE/ISCHANGED on " + strings.TrimPrefix(root, "_prior_")
	default:
		return root
	}
}
