// Package spec is the single source of truth for the runtime formula function
// surface. FunctionSpec is a superset of the legacy formula.ArityRule it
// replaces: it carries the function Name alongside the inclusive
// [MinArgs, MaxArgs] argument-count bounds and an optional Predicate escape
// hatch for irregular arities (e.g. CASE's even-argument requirement) that
// plain bounds cannot express.
//
// VS2 (issue #1466) widens the spec from arity-only to the full call-shape
// contract: it adds the per-argument type contract (Args / VarArg) and the
// return-type derivation (Return), which previously lived in the
// inferCallType name-switch in schema_validation.go. See returntype.go for the
// ValueType / ArgSpec / ReturnSpec vocabulary.
//
// MaxArgs == -1 marks an unbounded variadic upper bound.
package spec

import "fmt"

// FunctionSpec declares the call-shape contract for one runtime function:
// argument-count bounds (VS1), per-argument types (VS2), and return type (VS2).
//
// Arity validation order (preserved from the legacy ArityRule check in
// pkg/formula/runtime_functions.go):
//  1. reject when argc < MinArgs
//  2. reject when MaxArgs != -1 and argc > MaxArgs
//  3. when Predicate != nil, reject when Predicate(argc) returns an error
type FunctionSpec struct {
	Name      string
	MinArgs   int             // inclusive
	MaxArgs   int             // inclusive; -1 means unbounded
	Predicate func(int) error // optional post-bounds validator; nil means accept

	// Return describes how the function's schema-aware result type is derived.
	// The zero value (ReturnFixed with an empty Fixed type) is not meaningful;
	// every catalogued function sets it explicitly. See returntype.go.
	Return ReturnSpec

	// Args carries the positional argument type contracts. Argument i is
	// validated against Args[i] when i < len(Args); arguments at or beyond
	// len(Args) fall back to VarArg. An empty Args + nil VarArg means the
	// function performs no per-argument type validation (e.g. TEXT, the
	// boolean predicates), matching the legacy switch cases that returned a
	// fixed type without inspecting their arguments.
	Args []ArgSpec

	// VarArg, when non-nil, is the contract applied to every argument at or
	// beyond len(Args). Currently unused by the catalog (every variadic
	// function — the boolean predicates, CASE — validates via Args or not at
	// all), but kept so a future uniform-variadic function (e.g. a CONCATENATE
	// that type-checks every argument) needs no struct change.
	VarArg *ArgSpec
}

// ArgSpecAt returns the argument contract that applies to position i and
// whether one exists. Positions within Args use the positional contract;
// positions beyond fall back to VarArg. When neither applies (a variadic tail
// of a function with no VarArg, e.g. SUBSTITUTE's optional 4th argument when
// absent), ok is false and the caller leaves the argument unvalidated —
// matching the legacy requireSchema* primitives, which returned nil for an
// index past the supplied argument count.
func (s FunctionSpec) ArgSpecAt(i int) (ArgSpec, bool) {
	if i < len(s.Args) {
		return s.Args[i], true
	}
	if s.VarArg != nil {
		return *s.VarArg, true
	}
	return ArgSpec{}, false
}

// Unbounded marks an unbounded variadic upper bound for MaxArgs.
const Unbounded = -1

// Validate reports whether argc satisfies the spec's arity contract, applying
// the bounds first and the Predicate (if any) last. It returns nil when argc
// is acceptable.
func (s FunctionSpec) Validate(argc int) error {
	if argc < s.MinArgs {
		return fmt.Errorf("%s expects %s, got %d", s.Name, s.BoundsDescription(), argc)
	}
	if s.MaxArgs != Unbounded && argc > s.MaxArgs {
		return fmt.Errorf("%s expects %s, got %d", s.Name, s.BoundsDescription(), argc)
	}
	if s.Predicate != nil {
		return s.Predicate(argc)
	}
	return nil
}

// BoundsDescription renders the [MinArgs, MaxArgs] bounds in the same form the
// legacy formatArityRule helper produced (">=N args" / "N args" / "N-M args").
func (s FunctionSpec) BoundsDescription() string {
	if s.MaxArgs == Unbounded {
		return fmt.Sprintf(">=%d args", s.MinArgs)
	}
	if s.MinArgs == s.MaxArgs {
		return fmt.Sprintf("%d args", s.MinArgs)
	}
	return fmt.Sprintf("%d-%d args", s.MinArgs, s.MaxArgs)
}
