package openapi3

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"math"
	"math/big"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"unicode/utf16"

	"github.com/go-openapi/jsonpointer"
	"github.com/mohae/deepcopy"
	"github.com/woodsbury/decimal128"
)

const (
	TypeArray   = "array"
	TypeBoolean = "boolean"
	TypeInteger = "integer"
	TypeNumber  = "number"
	TypeObject  = "object"
	TypeString  = "string"
	TypeNull    = "null"
)

var (
	// SchemaErrorDetailsDisabled disables printing of details about schema errors.
	SchemaErrorDetailsDisabled = false

	errSchema = errors.New("input does not match the schema")

	// ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema
	ErrOneOfConflict = errors.New("input matches more than one oneOf schemas")

	// ErrSchemaInputNaN may be returned when validating a number
	ErrSchemaInputNaN = errors.New("floating point NaN is not allowed")
	// ErrSchemaInputInf may be returned when validating a number
	ErrSchemaInputInf = errors.New("floating point Inf is not allowed")

	compiledPatterns sync.Map
)

// NewSchemaRef simply builds a SchemaRef
func NewSchemaRef(ref string, value *Schema) *SchemaRef {
	return &SchemaRef{
		Ref:   ref,
		Value: value,
	}
}

type SchemaRefs []*SchemaRef

var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil)

// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (s SchemaRefs) JSONLookup(token string) (any, error) {
	i, err := strconv.ParseUint(token, 10, 64)
	if err != nil {
		return nil, err
	}

	if i >= uint64(len(s)) {
		return nil, fmt.Errorf("index out of range: %d", i)
	}

	ref := s[i]

	if ref == nil || ref.Ref != "" {
		return &Ref{Ref: ref.Ref}, nil
	}
	return ref.Value, nil
}

// Schema is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
// and https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
type Schema struct {
	Extensions map[string]any `json:"-" yaml:"-"`
	Origin     *Origin        `json:"-" yaml:"-"`

	OneOf        SchemaRefs    `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
	AnyOf        SchemaRefs    `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
	AllOf        SchemaRefs    `json:"allOf,omitempty" yaml:"allOf,omitempty"`
	Not          *SchemaRef    `json:"not,omitempty" yaml:"not,omitempty"`
	Type         *Types        `json:"type,omitempty" yaml:"type,omitempty"`
	Title        string        `json:"title,omitempty" yaml:"title,omitempty"`
	Format       string        `json:"format,omitempty" yaml:"format,omitempty"`
	Description  string        `json:"description,omitempty" yaml:"description,omitempty"`
	Enum         []any         `json:"enum,omitempty" yaml:"enum,omitempty"`
	Default      any           `json:"default,omitempty" yaml:"default,omitempty"`
	Example      any           `json:"example,omitempty" yaml:"example,omitempty"`
	ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`

	// Array-related, here for struct compactness
	UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
	// Number-related, here for struct compactness
	ExclusiveMin ExclusiveBound `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` // Number for v3.1+ otherwise boolean
	ExclusiveMax ExclusiveBound `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // Number for v3.1+ otherwise boolean
	// Properties
	Nullable        bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
	ReadOnly        bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
	WriteOnly       bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
	AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
	Deprecated      bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
	XML             *XML `json:"xml,omitempty" yaml:"xml,omitempty"`

	// Number
	Min        *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
	Max        *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
	MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`

	// String
	MinLength uint64  `json:"minLength,omitempty" yaml:"minLength,omitempty"`
	MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
	Pattern   string  `json:"pattern,omitempty" yaml:"pattern,omitempty"`

	// Array
	MinItems uint64     `json:"minItems,omitempty" yaml:"minItems,omitempty"`
	MaxItems *uint64    `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
	Items    *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`

	// Object
	Required             []string             `json:"required,omitempty" yaml:"required,omitempty"`
	Properties           Schemas              `json:"properties,omitempty" yaml:"properties,omitempty"`
	MinProps             uint64               `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
	MaxProps             *uint64              `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
	AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
	Discriminator        *Discriminator       `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`

	Const                 any        `json:"const,omitempty" yaml:"const,omitempty"`                                 // OpenAPI >=3.1
	Examples              []any      `json:"examples,omitempty" yaml:"examples,omitempty"`                           // OpenAPI >=3.1
	PrefixItems           SchemaRefs `json:"prefixItems,omitempty" yaml:"prefixItems,omitempty"`                     // OpenAPI >=3.1
	Contains              *SchemaRef `json:"contains,omitempty" yaml:"contains,omitempty"`                           // OpenAPI >=3.1
	MinContains           *uint64    `json:"minContains,omitempty" yaml:"minContains,omitempty"`                     // OpenAPI >=3.1
	MaxContains           *uint64    `json:"maxContains,omitempty" yaml:"maxContains,omitempty"`                     // OpenAPI >=3.1
	PatternProperties     Schemas    `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"`         // OpenAPI >=3.1
	DependentSchemas      Schemas    `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"`           // OpenAPI >=3.1
	PropertyNames         *SchemaRef `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"`                 // OpenAPI >=3.1
	UnevaluatedItems      BoolSchema `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"`           // OpenAPI >=3.1
	UnevaluatedProperties BoolSchema `json:"unevaluatedProperties,omitempty" yaml:"unevaluatedProperties,omitempty"` // OpenAPI >=3.1

	If   *SchemaRef `json:"if,omitempty" yaml:"if,omitempty"`     // OpenAPI >=3.1
	Then *SchemaRef `json:"then,omitempty" yaml:"then,omitempty"` // OpenAPI >=3.1
	Else *SchemaRef `json:"else,omitempty" yaml:"else,omitempty"` // OpenAPI >=3.1

	DependentRequired map[string][]string `json:"dependentRequired,omitempty" yaml:"dependentRequired,omitempty"` // OpenAPI >=3.1

	Defs          Schemas `json:"$defs,omitempty" yaml:"$defs,omitempty"`       // OpenAPI >=3.1
	SchemaDialect string  `json:"$schema,omitempty" yaml:"$schema,omitempty"`   // OpenAPI >=3.1
	Comment       string  `json:"$comment,omitempty" yaml:"$comment,omitempty"` // OpenAPI >=3.1

	SchemaID      string `json:"$id,omitempty" yaml:"$id,omitempty"`                       // OpenAPI >=3.1
	Anchor        string `json:"$anchor,omitempty" yaml:"$anchor,omitempty"`               // OpenAPI >=3.1
	DynamicRef    string `json:"$dynamicRef,omitempty" yaml:"$dynamicRef,omitempty"`       // OpenAPI >=3.1
	DynamicAnchor string `json:"$dynamicAnchor,omitempty" yaml:"$dynamicAnchor,omitempty"` // OpenAPI >=3.1

	ContentMediaType string     `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` // OpenAPI >=3.1
	ContentEncoding  string     `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"`   // OpenAPI >=3.1
	ContentSchema    *SchemaRef `json:"contentSchema,omitempty" yaml:"contentSchema,omitempty"`       // OpenAPI >=3.1
}

// Types represents the type(s) of a schema.
//
// In OpenAPI 3.0, this is typically a single type (e.g., "string").
// In OpenAPI 3.1, it can be an array of types (e.g., ["string", "null"]).
//
// Serialization behavior:
//   - Single type: serializes as a string (e.g., "string")
//   - Multiple types: serializes as an array (e.g., ["string", "null"])
//   - Accepts both string and array formats when unmarshaling
//
// Example OpenAPI 3.0 (single type):
//
//	schema := &Schema{Type: &Types{"string"}}
//	// JSON: {"type": "string"}
//
// Example OpenAPI 3.1 (type array):
//
//	schema := &Schema{Type: &Types{"string", "null"}}
//	// JSON: {"type": ["string", "null"]}
type Types []string

// Is returns true if the schema has exactly one type and it matches the given type.
// This is useful for OpenAPI 3.0 style single-type checks.
//
// Example:
//
//	types := &Types{"string"}
//	types.Is("string")  // true
//	types.Is("number")  // false
//
//	types = &Types{"string", "null"}
//	types.Is("string")  // false (multiple types)
func (types *Types) Is(typ string) bool {
	return types != nil && len(*types) == 1 && (*types)[0] == typ
}

// Slice returns the types as a string slice.
// Returns nil if types is nil.
//
// Example:
//
//	types := &Types{"string", "null"}
//	slice := types.Slice()  // []string{"string", "null"}
func (types *Types) Slice() []string {
	if types == nil {
		return nil
	}
	return *types
}

// Includes returns true if the given type is included in the type array.
// Returns false if types is nil.
//
// Example:
//
//	types := &Types{"string", "null"}
//	types.Includes("string")  // true
//	types.Includes("null")    // true
//	types.Includes("number")  // false
func (pTypes *Types) Includes(typ string) bool {
	if pTypes == nil {
		return false
	}
	return slices.Contains(*pTypes, typ)
}

// Permits returns true if the given type is permitted.
// Returns true if types is nil (any type allowed), otherwise checks if the type is included.
//
// Example:
//
//	var nilTypes *Types
//	nilTypes.Permits("anything")  // true (nil permits everything)
//
//	types := &Types{"string"}
//	types.Permits("string")  // true
//	types.Permits("number")  // false
func (types *Types) Permits(typ string) bool {
	if types == nil {
		return true
	}
	return types.Includes(typ)
}

// IncludesNull returns true if the type array includes "null".
// This is useful for OpenAPI 3.1 where null is a first-class type.
//
// Example:
//
//	types := &Types{"string", "null"}
//	types.IncludesNull()  // true
//
//	types = &Types{"string"}
//	types.IncludesNull()  // false
func (types *Types) IncludesNull() bool {
	return types.Includes(TypeNull)
}

// IsMultiple returns true if multiple types are specified.
// This is an OpenAPI 3.1 feature that enables type arrays.
//
// Example:
//
//	types := &Types{"string"}
//	types.IsMultiple()  // false
//
//	types = &Types{"string", "null"}
//	types.IsMultiple()  // true
func (types *Types) IsMultiple() bool {
	return types != nil && len(*types) > 1
}

// IsSingle returns true if exactly one type is specified.
//
// Example:
//
//	types := &Types{"string"}
//	types.IsSingle()  // true
//
//	types = &Types{"string", "null"}
//	types.IsSingle()  // false
func (types *Types) IsSingle() bool {
	return types != nil && len(*types) == 1
}

// IsEmpty returns true if no types are specified (nil or empty array).
// When a schema has no type specified, it permits any type.
//
// Example:
//
//	var nilTypes *Types
//	nilTypes.IsEmpty()  // true
//
//	types := &Types{}
//	types.IsEmpty()  // true
//
//	types = &Types{"string"}
//	types.IsEmpty()  // false
func (types *Types) IsEmpty() bool {
	return types == nil || len(*types) == 0
}

func (pTypes *Types) MarshalJSON() ([]byte, error) {
	x, err := pTypes.MarshalYAML()
	if err != nil {
		return nil, err
	}
	return json.Marshal(x)
}

func (pTypes *Types) MarshalYAML() (any, error) {
	if pTypes == nil {
		return nil, nil
	}
	types := *pTypes
	switch len(types) {
	case 0:
		return nil, nil
	case 1:
		return types[0], nil
	default:
		return []string(types), nil
	}
}

func (types *Types) UnmarshalJSON(data []byte) error {
	var strings []string
	if err := json.Unmarshal(data, &strings); err != nil {
		var s string
		if err := json.Unmarshal(data, &s); err != nil {
			return unmarshalError(err)
		}
		strings = []string{s}
	}
	*types = strings
	return nil
}

// BoolSchema represents a JSON Schema keyword that can be either a boolean or a schema object.
// Used for additionalProperties, unevaluatedProperties, and unevaluatedItems.
type BoolSchema struct {
	Has    *bool
	Schema *SchemaRef
}

// AdditionalProperties is a type alias for BoolSchema, kept for backward compatibility.
type AdditionalProperties = BoolSchema

// MarshalYAML returns the YAML encoding of BoolSchema.
func (bs BoolSchema) MarshalYAML() (any, error) {
	if x := bs.Has; x != nil {
		if *x {
			return true, nil
		}
		return false, nil
	}
	if x := bs.Schema; x != nil {
		return x.MarshalYAML()
	}
	return nil, nil
}

// MarshalJSON returns the JSON encoding of BoolSchema.
func (bs BoolSchema) MarshalJSON() ([]byte, error) {
	x, err := bs.MarshalYAML()
	if err != nil {
		return nil, err
	}
	return json.Marshal(x)
}

// UnmarshalJSON sets BoolSchema to a copy of data.
func (bs *BoolSchema) UnmarshalJSON(data []byte) error {
	var x any
	if err := json.Unmarshal(data, &x); err != nil {
		return unmarshalError(err)
	}
	switch y := x.(type) {
	case nil:
	case bool:
		bs.Has = &y
	case map[string]any:
		if len(y) == 0 {
			bs.Schema = &SchemaRef{Value: &Schema{}}
		} else {
			buf := new(bytes.Buffer)
			_ = json.NewEncoder(buf).Encode(y)
			if err := json.NewDecoder(buf).Decode(&bs.Schema); err != nil {
				return err
			}
		}
	default:
		return errors.New("cannot unmarshal: value must be either a schema object or a boolean")
	}
	return nil
}

// ExclusiveBound represents exclusiveMinimum/exclusiveMaximum which changed type between OpenAPI versions.
// In OpenAPI 3.0 (JSON Schema draft-04): boolean that modifies minimum/maximum
// In OpenAPI 3.1 (JSON Schema 2020-12): number representing the actual exclusive bound
type ExclusiveBound struct {
	Bool  *bool    // For OpenAPI 3.0 style (modifier for min/max)
	Value *float64 // For OpenAPI 3.1 style (actual bound value)
}

// IsSet returns true if either Bool or Value is set.
func (eb ExclusiveBound) IsSet() bool {
	return eb.Bool != nil || eb.Value != nil
}

// IsTrue returns true if the bound is set as a boolean true (OpenAPI 3.0 style).
func (eb ExclusiveBound) IsTrue() bool {
	return eb.Bool != nil && *eb.Bool
}

// MarshalYAML returns the YAML encoding of ExclusiveBound.
func (eb ExclusiveBound) MarshalYAML() (any, error) {
	if eb.Value != nil {
		return *eb.Value, nil
	}
	if eb.Bool != nil {
		return *eb.Bool, nil
	}
	return nil, nil
}

// MarshalJSON returns the JSON encoding of ExclusiveBound.
func (eb ExclusiveBound) MarshalJSON() ([]byte, error) {
	x, err := eb.MarshalYAML()
	if err != nil {
		return nil, err
	}
	if x == nil {
		return nil, nil
	}
	return json.Marshal(x)
}

// UnmarshalJSON sets ExclusiveBound to a copy of data.
func (eb *ExclusiveBound) UnmarshalJSON(data []byte) error {
	var x any
	if err := json.Unmarshal(data, &x); err != nil {
		return unmarshalError(err)
	}
	switch y := x.(type) {
	case nil:
		// nothing to do
	case bool:
		eb.Bool = &y
	case float64:
		eb.Value = &y
	default:
		return errors.New("cannot unmarshal exclusiveMinimum/exclusiveMaximum: value must be either a number or a boolean")
	}
	return nil
}

var _ jsonpointer.JSONPointable = (*Schema)(nil)

func NewSchema() *Schema {
	return &Schema{}
}

// MarshalJSON returns the JSON encoding of Schema.
func (schema Schema) MarshalJSON() ([]byte, error) {
	m, err := schema.MarshalYAML()
	if err != nil {
		return nil, err
	}

	return json.Marshal(m)
}

// MarshalYAML returns the YAML encoding of Schema.
func (schema Schema) MarshalYAML() (any, error) {
	m := make(map[string]any, 61+len(schema.Extensions))
	maps.Copy(m, schema.Extensions)

	if x := schema.OneOf; len(x) != 0 {
		m["oneOf"] = x
	}
	if x := schema.AnyOf; len(x) != 0 {
		m["anyOf"] = x
	}
	if x := schema.AllOf; len(x) != 0 {
		m["allOf"] = x
	}
	if x := schema.Not; x != nil {
		m["not"] = x
	}
	if x := schema.Type; x != nil {
		m["type"] = x
	}
	if x := schema.Title; len(x) != 0 {
		m["title"] = x
	}
	if x := schema.Format; len(x) != 0 {
		m["format"] = x
	}
	if x := schema.Description; len(x) != 0 {
		m["description"] = x
	}
	if x := schema.Enum; len(x) != 0 {
		m["enum"] = x
	}
	if x := schema.Default; x != nil {
		m["default"] = x
	}
	if x := schema.Example; x != nil {
		m["example"] = x
	}
	if x := schema.ExternalDocs; x != nil {
		m["externalDocs"] = x
	}

	// Array-related
	if x := schema.UniqueItems; x {
		m["uniqueItems"] = x
	}
	// Number-related
	if x := schema.ExclusiveMin; x.IsSet() {
		m["exclusiveMinimum"] = x
	}
	if x := schema.ExclusiveMax; x.IsSet() {
		m["exclusiveMaximum"] = x
	}
	// Properties
	if x := schema.Nullable; x {
		m["nullable"] = x
	}
	if x := schema.ReadOnly; x {
		m["readOnly"] = x
	}
	if x := schema.WriteOnly; x {
		m["writeOnly"] = x
	}
	if x := schema.AllowEmptyValue; x {
		m["allowEmptyValue"] = x
	}
	if x := schema.Deprecated; x {
		m["deprecated"] = x
	}
	if x := schema.XML; x != nil {
		m["xml"] = x
	}

	// Number
	if x := schema.Min; x != nil {
		m["minimum"] = x
	}
	if x := schema.Max; x != nil {
		m["maximum"] = x
	}
	if x := schema.MultipleOf; x != nil {
		m["multipleOf"] = x
	}

	// String
	if x := schema.MinLength; x != 0 {
		m["minLength"] = x
	}
	if x := schema.MaxLength; x != nil {
		m["maxLength"] = x
	}
	if x := schema.Pattern; x != "" {
		m["pattern"] = x
	}

	// Array
	if x := schema.MinItems; x != 0 {
		m["minItems"] = x
	}
	if x := schema.MaxItems; x != nil {
		m["maxItems"] = x
	}
	if x := schema.Items; x != nil {
		m["items"] = x
	}

	// Object
	if x := schema.Required; len(x) != 0 {
		m["required"] = x
	}
	if x := schema.Properties; len(x) != 0 {
		m["properties"] = x
	}
	if x := schema.MinProps; x != 0 {
		m["minProperties"] = x
	}
	if x := schema.MaxProps; x != nil {
		m["maxProperties"] = x
	}
	if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil {
		m["additionalProperties"] = &x
	}
	if x := schema.Discriminator; x != nil {
		m["discriminator"] = x
	}

	// OpenAPI 3.1 / JSON Schema 2020-12 fields
	if x := schema.Const; x != nil {
		m["const"] = x
	}
	if x := schema.Examples; len(x) != 0 {
		m["examples"] = x
	}
	if x := schema.PrefixItems; len(x) != 0 {
		m["prefixItems"] = x
	}
	if x := schema.Contains; x != nil {
		m["contains"] = x
	}
	if x := schema.MinContains; x != nil {
		m["minContains"] = x
	}
	if x := schema.MaxContains; x != nil {
		m["maxContains"] = x
	}
	if x := schema.PatternProperties; len(x) != 0 {
		m["patternProperties"] = x
	}
	if x := schema.DependentSchemas; len(x) != 0 {
		m["dependentSchemas"] = x
	}
	if x := schema.PropertyNames; x != nil {
		m["propertyNames"] = x
	}
	if x := schema.UnevaluatedItems; x.Has != nil || x.Schema != nil {
		m["unevaluatedItems"] = &x
	}
	if x := schema.UnevaluatedProperties; x.Has != nil || x.Schema != nil {
		m["unevaluatedProperties"] = &x
	}
	if x := schema.If; x != nil {
		m["if"] = x
	}
	if x := schema.Then; x != nil {
		m["then"] = x
	}
	if x := schema.Else; x != nil {
		m["else"] = x
	}
	if x := schema.DependentRequired; len(x) != 0 {
		m["dependentRequired"] = x
	}
	if x := schema.Defs; len(x) != 0 {
		m["$defs"] = x
	}
	if x := schema.SchemaDialect; x != "" {
		m["$schema"] = x
	}
	if x := schema.Comment; x != "" {
		m["$comment"] = x
	}
	if x := schema.SchemaID; x != "" {
		m["$id"] = x
	}
	if x := schema.Anchor; x != "" {
		m["$anchor"] = x
	}
	if x := schema.DynamicRef; x != "" {
		m["$dynamicRef"] = x
	}
	if x := schema.DynamicAnchor; x != "" {
		m["$dynamicAnchor"] = x
	}
	if x := schema.ContentMediaType; x != "" {
		m["contentMediaType"] = x
	}
	if x := schema.ContentEncoding; x != "" {
		m["contentEncoding"] = x
	}
	if x := schema.ContentSchema; x != nil {
		m["contentSchema"] = x
	}

	return m, nil
}

// UnmarshalJSON sets Schema to a copy of data.
func (schema *Schema) UnmarshalJSON(data []byte) error {
	type SchemaBis Schema
	var x SchemaBis
	if err := json.Unmarshal(data, &x); err != nil {
		return unmarshalError(err)
	}
	_ = json.Unmarshal(data, &x.Extensions)

	delete(x.Extensions, "oneOf")
	delete(x.Extensions, "anyOf")
	delete(x.Extensions, "allOf")
	delete(x.Extensions, "not")
	delete(x.Extensions, "type")
	delete(x.Extensions, "title")
	delete(x.Extensions, "format")
	delete(x.Extensions, "description")
	delete(x.Extensions, "enum")
	delete(x.Extensions, "default")
	delete(x.Extensions, "example")
	delete(x.Extensions, "externalDocs")

	// Array-related
	delete(x.Extensions, "uniqueItems")
	// Number-related
	delete(x.Extensions, "exclusiveMinimum")
	delete(x.Extensions, "exclusiveMaximum")
	// Properties
	delete(x.Extensions, "nullable")
	delete(x.Extensions, "readOnly")
	delete(x.Extensions, "writeOnly")
	delete(x.Extensions, "allowEmptyValue")
	delete(x.Extensions, "deprecated")
	delete(x.Extensions, "xml")

	// Number
	delete(x.Extensions, "minimum")
	delete(x.Extensions, "maximum")
	delete(x.Extensions, "multipleOf")

	// String
	delete(x.Extensions, "minLength")
	delete(x.Extensions, "maxLength")
	delete(x.Extensions, "pattern")

	// Array
	delete(x.Extensions, "minItems")
	delete(x.Extensions, "maxItems")
	delete(x.Extensions, "items")

	// Object
	delete(x.Extensions, "required")
	delete(x.Extensions, "properties")
	delete(x.Extensions, "minProperties")
	delete(x.Extensions, "maxProperties")
	delete(x.Extensions, "additionalProperties")
	delete(x.Extensions, "discriminator")

	// OpenAPI 3.1 / JSON Schema 2020-12 fields
	delete(x.Extensions, "const")
	delete(x.Extensions, "examples")
	delete(x.Extensions, "prefixItems")
	delete(x.Extensions, "contains")
	delete(x.Extensions, "minContains")
	delete(x.Extensions, "maxContains")
	delete(x.Extensions, "patternProperties")
	delete(x.Extensions, "dependentSchemas")
	delete(x.Extensions, "propertyNames")
	delete(x.Extensions, "unevaluatedItems")
	delete(x.Extensions, "unevaluatedProperties")
	delete(x.Extensions, "if")
	delete(x.Extensions, "then")
	delete(x.Extensions, "else")
	delete(x.Extensions, "dependentRequired")
	delete(x.Extensions, "$defs")
	delete(x.Extensions, "$schema")
	delete(x.Extensions, "$comment")
	delete(x.Extensions, "$id")
	delete(x.Extensions, "$anchor")
	delete(x.Extensions, "$dynamicRef")
	delete(x.Extensions, "$dynamicAnchor")
	delete(x.Extensions, "contentMediaType")
	delete(x.Extensions, "contentEncoding")
	delete(x.Extensions, "contentSchema")

	if len(x.Extensions) == 0 {
		x.Extensions = nil
	}

	*schema = Schema(x)
	return nil
}

// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (schema Schema) JSONLookup(token string) (any, error) {
	switch token {
	case "additionalProperties":
		if addProps := schema.AdditionalProperties.Has; addProps != nil {
			return *addProps, nil
		}
		if addProps := schema.AdditionalProperties.Schema; addProps != nil {
			if addProps.Ref != "" {
				return &Ref{Ref: addProps.Ref}, nil
			}
			return addProps.Value, nil
		}
	case "not":
		if schema.Not != nil {
			if schema.Not.Ref != "" {
				return &Ref{Ref: schema.Not.Ref}, nil
			}
			return schema.Not.Value, nil
		}
	case "items":
		if schema.Items != nil {
			if schema.Items.Ref != "" {
				return &Ref{Ref: schema.Items.Ref}, nil
			}
			return schema.Items.Value, nil
		}
	case "oneOf":
		return schema.OneOf, nil
	case "anyOf":
		return schema.AnyOf, nil
	case "allOf":
		return schema.AllOf, nil
	case "type":
		return schema.Type, nil
	case "title":
		return schema.Title, nil
	case "format":
		return schema.Format, nil
	case "description":
		return schema.Description, nil
	case "enum":
		return schema.Enum, nil
	case "default":
		return schema.Default, nil
	case "example":
		return schema.Example, nil
	case "externalDocs":
		return schema.ExternalDocs, nil
	case "uniqueItems":
		return schema.UniqueItems, nil
	case "exclusiveMin":
		return schema.ExclusiveMin, nil
	case "exclusiveMax":
		return schema.ExclusiveMax, nil
	case "nullable":
		return schema.Nullable, nil
	case "readOnly":
		return schema.ReadOnly, nil
	case "writeOnly":
		return schema.WriteOnly, nil
	case "allowEmptyValue":
		return schema.AllowEmptyValue, nil
	case "xml":
		return schema.XML, nil
	case "deprecated":
		return schema.Deprecated, nil
	case "min":
		return schema.Min, nil
	case "max":
		return schema.Max, nil
	case "multipleOf":
		return schema.MultipleOf, nil
	case "minLength":
		return schema.MinLength, nil
	case "maxLength":
		return schema.MaxLength, nil
	case "pattern":
		return schema.Pattern, nil
	case "minItems":
		return schema.MinItems, nil
	case "maxItems":
		return schema.MaxItems, nil
	case "required":
		return schema.Required, nil
	case "properties":
		return schema.Properties, nil
	case "minProps":
		return schema.MinProps, nil
	case "maxProps":
		return schema.MaxProps, nil
	case "discriminator":
		return schema.Discriminator, nil

	// OpenAPI 3.1 / JSON Schema 2020-12 fields
	case "const":
		return schema.Const, nil
	case "examples":
		return schema.Examples, nil
	case "prefixItems":
		return schema.PrefixItems, nil
	case "contains":
		if schema.Contains != nil {
			if schema.Contains.Ref != "" {
				return &Ref{Ref: schema.Contains.Ref}, nil
			}
			return schema.Contains.Value, nil
		}
	case "minContains":
		return schema.MinContains, nil
	case "maxContains":
		return schema.MaxContains, nil
	case "patternProperties":
		return schema.PatternProperties, nil
	case "dependentSchemas":
		return schema.DependentSchemas, nil
	case "propertyNames":
		if schema.PropertyNames != nil {
			if schema.PropertyNames.Ref != "" {
				return &Ref{Ref: schema.PropertyNames.Ref}, nil
			}
			return schema.PropertyNames.Value, nil
		}
	case "unevaluatedItems":
		if ui := schema.UnevaluatedItems.Has; ui != nil {
			return *ui, nil
		}
		if ui := schema.UnevaluatedItems.Schema; ui != nil {
			if ui.Ref != "" {
				return &Ref{Ref: ui.Ref}, nil
			}
			return ui.Value, nil
		}
	case "unevaluatedProperties":
		if up := schema.UnevaluatedProperties.Has; up != nil {
			return *up, nil
		}
		if up := schema.UnevaluatedProperties.Schema; up != nil {
			if up.Ref != "" {
				return &Ref{Ref: up.Ref}, nil
			}
			return up.Value, nil
		}
	case "if":
		if schema.If != nil {
			if schema.If.Ref != "" {
				return &Ref{Ref: schema.If.Ref}, nil
			}
			return schema.If.Value, nil
		}
	case "then":
		if schema.Then != nil {
			if schema.Then.Ref != "" {
				return &Ref{Ref: schema.Then.Ref}, nil
			}
			return schema.Then.Value, nil
		}
	case "else":
		if schema.Else != nil {
			if schema.Else.Ref != "" {
				return &Ref{Ref: schema.Else.Ref}, nil
			}
			return schema.Else.Value, nil
		}
	case "dependentRequired":
		return schema.DependentRequired, nil
	case "$defs":
		return schema.Defs, nil
	case "$schema":
		return schema.SchemaDialect, nil
	case "$comment":
		return schema.Comment, nil
	case "$id":
		return schema.SchemaID, nil
	case "$anchor":
		return schema.Anchor, nil
	case "$dynamicRef":
		return schema.DynamicRef, nil
	case "$dynamicAnchor":
		return schema.DynamicAnchor, nil
	case "contentMediaType":
		return schema.ContentMediaType, nil
	case "contentEncoding":
		return schema.ContentEncoding, nil
	case "contentSchema":
		if schema.ContentSchema != nil {
			if schema.ContentSchema.Ref != "" {
				return &Ref{Ref: schema.ContentSchema.Ref}, nil
			}
			return schema.ContentSchema.Value, nil
		}
	}

	v, _, err := jsonpointer.GetForToken(schema.Extensions, token)
	return v, err
}

func (schema *Schema) NewRef() *SchemaRef {
	return &SchemaRef{
		Value: schema,
	}
}

func NewOneOfSchema(schemas ...*Schema) *Schema {
	refs := make([]*SchemaRef, 0, len(schemas))
	for _, schema := range schemas {
		refs = append(refs, &SchemaRef{Value: schema})
	}
	return &Schema{
		OneOf: refs,
	}
}

func NewAnyOfSchema(schemas ...*Schema) *Schema {
	refs := make([]*SchemaRef, 0, len(schemas))
	for _, schema := range schemas {
		refs = append(refs, &SchemaRef{Value: schema})
	}
	return &Schema{
		AnyOf: refs,
	}
}

func NewAllOfSchema(schemas ...*Schema) *Schema {
	refs := make([]*SchemaRef, 0, len(schemas))
	for _, schema := range schemas {
		refs = append(refs, &SchemaRef{Value: schema})
	}
	return &Schema{
		AllOf: refs,
	}
}

func NewBoolSchema() *Schema {
	return &Schema{
		Type: &Types{TypeBoolean},
	}
}

func NewFloat64Schema() *Schema {
	return &Schema{
		Type: &Types{TypeNumber},
	}
}

func NewIntegerSchema() *Schema {
	return &Schema{
		Type: &Types{TypeInteger},
	}
}

func NewInt32Schema() *Schema {
	return &Schema{
		Type:   &Types{TypeInteger},
		Format: "int32",
	}
}

func NewInt64Schema() *Schema {
	return &Schema{
		Type:   &Types{TypeInteger},
		Format: "int64",
	}
}

func NewStringSchema() *Schema {
	return &Schema{
		Type: &Types{TypeString},
	}
}

func NewDateTimeSchema() *Schema {
	return &Schema{
		Type:   &Types{TypeString},
		Format: "date-time",
	}
}

func NewUUIDSchema() *Schema {
	return &Schema{
		Type:   &Types{TypeString},
		Format: "uuid",
	}
}

func NewBytesSchema() *Schema {
	return &Schema{
		Type:   &Types{TypeString},
		Format: "byte",
	}
}

func NewArraySchema() *Schema {
	return &Schema{
		Type: &Types{TypeArray},
	}
}

func NewObjectSchema() *Schema {
	return &Schema{
		Type:       &Types{TypeObject},
		Properties: make(Schemas),
	}
}

func (schema *Schema) WithNullable() *Schema {
	schema.Nullable = true
	return schema
}

func (schema *Schema) WithMin(value float64) *Schema {
	schema.Min = &value
	return schema
}

func (schema *Schema) WithMax(value float64) *Schema {
	schema.Max = &value
	return schema
}

// WithExclusiveMin sets exclusiveMinimum as a boolean (OpenAPI 3.0 style).
func (schema *Schema) WithExclusiveMin(value bool) *Schema {
	schema.ExclusiveMin = ExclusiveBound{Bool: &value}
	return schema
}

// WithExclusiveMax sets exclusiveMaximum as a boolean (OpenAPI 3.0 style).
func (schema *Schema) WithExclusiveMax(value bool) *Schema {
	schema.ExclusiveMax = ExclusiveBound{Bool: &value}
	return schema
}

// WithExclusiveMinValue sets exclusiveMinimum as a number (OpenAPI 3.1 style).
func (schema *Schema) WithExclusiveMinValue(value float64) *Schema {
	schema.ExclusiveMin = ExclusiveBound{Value: &value}
	return schema
}

// WithExclusiveMaxValue sets exclusiveMaximum as a number (OpenAPI 3.1 style).
func (schema *Schema) WithExclusiveMaxValue(value float64) *Schema {
	schema.ExclusiveMax = ExclusiveBound{Value: &value}
	return schema
}

func (schema *Schema) WithEnum(values ...any) *Schema {
	schema.Enum = values
	return schema
}

func (schema *Schema) WithDefault(defaultValue any) *Schema {
	schema.Default = defaultValue
	return schema
}

func (schema *Schema) WithFormat(value string) *Schema {
	schema.Format = value
	return schema
}

func (schema *Schema) WithLength(i int64) *Schema {
	n := uint64(i)
	schema.MinLength = n
	schema.MaxLength = &n
	return schema
}

func (schema *Schema) WithMinLength(i int64) *Schema {
	n := uint64(i)
	schema.MinLength = n
	return schema
}

func (schema *Schema) WithMaxLength(i int64) *Schema {
	n := uint64(i)
	schema.MaxLength = &n
	return schema
}

func (schema *Schema) WithLengthDecodedBase64(i int64) *Schema {
	n := uint64(i)
	v := (n*8 + 5) / 6
	schema.MinLength = v
	schema.MaxLength = &v
	return schema
}

func (schema *Schema) WithMinLengthDecodedBase64(i int64) *Schema {
	n := uint64(i)
	schema.MinLength = (n*8 + 5) / 6
	return schema
}

func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema {
	n := uint64(i)
	schema.MinLength = (n*8 + 5) / 6
	return schema
}

func (schema *Schema) WithPattern(pattern string) *Schema {
	schema.Pattern = pattern
	return schema
}

func (schema *Schema) WithItems(value *Schema) *Schema {
	schema.Items = &SchemaRef{
		Value: value,
	}
	return schema
}

func (schema *Schema) WithMinItems(i int64) *Schema {
	n := uint64(i)
	schema.MinItems = n
	return schema
}

func (schema *Schema) WithMaxItems(i int64) *Schema {
	n := uint64(i)
	schema.MaxItems = &n
	return schema
}

func (schema *Schema) WithUniqueItems(unique bool) *Schema {
	schema.UniqueItems = unique
	return schema
}

func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema {
	return schema.WithPropertyRef(name, &SchemaRef{
		Value: propertySchema,
	})
}

func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
	if schema.Properties == nil {
		schema.Properties = make(Schemas)
	}
	schema.Properties[name] = ref
	return schema
}

func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
	result := make(Schemas, len(properties))
	for k, v := range properties {
		result[k] = &SchemaRef{
			Value: v,
		}
	}
	schema.Properties = result
	return schema
}

func (schema *Schema) WithRequired(required []string) *Schema {
	schema.Required = required
	return schema
}

func (schema *Schema) WithMinProperties(i int64) *Schema {
	n := uint64(i)
	schema.MinProps = n
	return schema
}

func (schema *Schema) WithMaxProperties(i int64) *Schema {
	n := uint64(i)
	schema.MaxProps = &n
	return schema
}

func (schema *Schema) WithAnyAdditionalProperties() *Schema {
	schema.AdditionalProperties = AdditionalProperties{Has: Ptr(true)}
	return schema
}

func (schema *Schema) WithoutAdditionalProperties() *Schema {
	schema.AdditionalProperties = AdditionalProperties{Has: Ptr(false)}
	return schema
}

func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema {
	schema.AdditionalProperties = AdditionalProperties{}
	if v != nil {
		schema.AdditionalProperties.Schema = &SchemaRef{Value: v}
	}
	return schema
}

func (schema *Schema) PermitsNull() bool {
	return schema.Nullable || schema.Type.IncludesNull()
}

// IsEmpty tells whether schema is equivalent to the empty schema `{}`.
func (schema *Schema) IsEmpty() bool {
	if schema.Type != nil || schema.Format != "" || len(schema.Enum) != 0 ||
		schema.UniqueItems || schema.ExclusiveMin.IsSet() || schema.ExclusiveMax.IsSet() ||
		schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue ||
		schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil ||
		schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" ||
		schema.MinItems != 0 || schema.MaxItems != nil ||
		len(schema.Required) != 0 ||
		schema.MinProps != 0 || schema.MaxProps != nil ||
		schema.Const != nil {
		return false
	}
	if n := schema.Not; n != nil && n.Value != nil && !n.Value.IsEmpty() {
		return false
	}
	if ap := schema.AdditionalProperties.Schema; ap != nil && ap.Value != nil && !ap.Value.IsEmpty() {
		return false
	}
	if apa := schema.AdditionalProperties.Has; apa != nil && !*apa {
		return false
	}
	if items := schema.Items; items != nil && items.Value != nil && !items.Value.IsEmpty() {
		return false
	}
	for _, s := range schema.PrefixItems {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	if c := schema.Contains; c != nil && c.Value != nil && !c.Value.IsEmpty() {
		return false
	}
	if schema.MinContains != nil || schema.MaxContains != nil {
		return false
	}
	for _, s := range schema.Properties {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	for _, s := range schema.PatternProperties {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	for _, s := range schema.DependentSchemas {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	if pn := schema.PropertyNames; pn != nil && pn.Value != nil && !pn.Value.IsEmpty() {
		return false
	}
	if ui := schema.UnevaluatedItems.Schema; ui != nil && ui.Value != nil && !ui.Value.IsEmpty() {
		return false
	}
	if uih := schema.UnevaluatedItems.Has; uih != nil && !*uih {
		return false
	}
	if up := schema.UnevaluatedProperties.Schema; up != nil && up.Value != nil && !up.Value.IsEmpty() {
		return false
	}
	if uph := schema.UnevaluatedProperties.Has; uph != nil && !*uph {
		return false
	}
	if len(schema.Examples) != 0 {
		return false
	}
	for _, s := range schema.OneOf {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	for _, s := range schema.AnyOf {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	for _, s := range schema.AllOf {
		if ss := s.Value; ss != nil && !ss.IsEmpty() {
			return false
		}
	}
	if f := schema.If; f != nil && f.Value != nil && !f.Value.IsEmpty() {
		return false
	}
	if t := schema.Then; t != nil && t.Value != nil && !t.Value.IsEmpty() {
		return false
	}
	if e := schema.Else; e != nil && e.Value != nil && !e.Value.IsEmpty() {
		return false
	}
	if len(schema.DependentRequired) != 0 {
		return false
	}
	if len(schema.Defs) != 0 {
		return false
	}
	if schema.SchemaDialect != "" || schema.Comment != "" {
		return false
	}
	if schema.SchemaID != "" || schema.Anchor != "" || schema.DynamicRef != "" || schema.DynamicAnchor != "" {
		return false
	}
	if schema.ContentMediaType != "" || schema.ContentEncoding != "" {
		return false
	}
	if cs := schema.ContentSchema; cs != nil && cs.Value != nil && !cs.Value.IsEmpty() {
		return false
	}
	return true
}

// Validate returns an error if Schema does not comply with the OpenAPI spec.
func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) error {
	// Apply document-level validation options to the context
	ctx = WithValidationOptions(ctx, opts...)

	// Perform schema validation with the options in context
	_, err := schema.validate(ctx, []*Schema{})
	return err
}

// returns the updated stack and an error if Schema does not comply with the OpenAPI spec.
func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, error) {
	if slices.Contains(stack, schema) {
		return stack, nil
	}

	validationOpts := getValidationOptions(ctx)

	stack = append(stack, schema)

	if schema.ReadOnly && schema.WriteOnly {
		return stack, newSchemaReadOnlyWriteOnlyExclusive(schema.Origin)
	}

	// Reject fields that only exist in OAS 3.1 / JSON Schema 2020-12 when the
	// document is OAS 3.0. Fields explicitly allowed via AllowExtraSiblingFields
	// are skipped (opt-in escape hatch for 3.0 docs that reference external
	// JSON Schema documents). Once 3.0 moves to its own schema validator this
	// block becomes a no-op.
	if !validationOpts.isOpenAPI31OrLater {
		allowed := validationOpts.extraSiblingFieldsAllowed
		reject := func(field string) error {
			if _, ok := allowed[field]; ok {
				return nil
			}
			return errFieldFor31Plus(field, schema.Origin)
		}
		if schema.Const != nil {
			if err := reject("const"); err != nil {
				return stack, err
			}
		}
		if len(schema.Examples) != 0 {
			if err := reject("examples"); err != nil {
				return stack, err
			}
		}
		if len(schema.PrefixItems) != 0 {
			if err := reject("prefixItems"); err != nil {
				return stack, err
			}
		}
		if schema.Contains != nil {
			if err := reject("contains"); err != nil {
				return stack, err
			}
		}
		if schema.MinContains != nil {
			if err := reject("minContains"); err != nil {
				return stack, err
			}
		}
		if schema.MaxContains != nil {
			if err := reject("maxContains"); err != nil {
				return stack, err
			}
		}
		if len(schema.PatternProperties) != 0 {
			if err := reject("patternProperties"); err != nil {
				return stack, err
			}
		}
		if len(schema.DependentSchemas) != 0 {
			if err := reject("dependentSchemas"); err != nil {
				return stack, err
			}
		}
		if schema.PropertyNames != nil {
			if err := reject("propertyNames"); err != nil {
				return stack, err
			}
		}
		if schema.UnevaluatedItems.Has != nil || schema.UnevaluatedItems.Schema != nil {
			if err := reject("unevaluatedItems"); err != nil {
				return stack, err
			}
		}
		if schema.UnevaluatedProperties.Has != nil || schema.UnevaluatedProperties.Schema != nil {
			if err := reject("unevaluatedProperties"); err != nil {
				return stack, err
			}
		}
		if schema.If != nil {
			if err := reject("if"); err != nil {
				return stack, err
			}
		}
		if schema.Then != nil {
			if err := reject("then"); err != nil {
				return stack, err
			}
		}
		if schema.Else != nil {
			if err := reject("else"); err != nil {
				return stack, err
			}
		}
		if len(schema.DependentRequired) != 0 {
			if err := reject("dependentRequired"); err != nil {
				return stack, err
			}
		}
		if len(schema.Defs) != 0 {
			if err := reject("$defs"); err != nil {
				return stack, err
			}
		}
		if schema.SchemaDialect != "" {
			if err := reject("$schema"); err != nil {
				return stack, err
			}
		}
		if schema.Comment != "" {
			if err := reject("$comment"); err != nil {
				return stack, err
			}
		}
		if schema.SchemaID != "" {
			if err := reject("$id"); err != nil {
				return stack, err
			}
		}
		if schema.Anchor != "" {
			if err := reject("$anchor"); err != nil {
				return stack, err
			}
		}
		if schema.DynamicRef != "" {
			if err := reject("$dynamicRef"); err != nil {
				return stack, err
			}
		}
		if schema.DynamicAnchor != "" {
			if err := reject("$dynamicAnchor"); err != nil {
				return stack, err
			}
		}
		if schema.ContentMediaType != "" {
			if err := reject("contentMediaType"); err != nil {
				return stack, err
			}
		}
		if schema.ContentEncoding != "" {
			if err := reject("contentEncoding"); err != nil {
				return stack, err
			}
		}
		if schema.ContentSchema != nil {
			if err := reject("contentSchema"); err != nil {
				return stack, err
			}
		}
	}

	for _, item := range schema.OneOf {
		v := item.Value
		if v == nil {
			return stack, newUnresolvedRef(item.Ref, item.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	for _, item := range schema.AnyOf {
		v := item.Value
		if v == nil {
			return stack, newUnresolvedRef(item.Ref, item.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	for _, item := range schema.AllOf {
		v := item.Value
		if v == nil {
			return stack, newUnresolvedRef(item.Ref, item.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	if ref := schema.Not; ref != nil {
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	if ref := schema.If; ref != nil {
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}
		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if ref := schema.Then; ref != nil {
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}
		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if ref := schema.Else; ref != nil {
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}
		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	for _, schemaType := range schema.Type.Slice() {
		switch schemaType {
		case TypeBoolean:
		case TypeNumber:
			if format := schema.Format; len(format) > 0 {
				switch format {
				case "float", "double":
				default:
					if _, ok := SchemaNumberFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
						return stack, unsupportedFormat(format)
					}
				}
			}
		case TypeInteger:
			if format := schema.Format; len(format) > 0 {
				switch format {
				case "int32", "int64":
				default:
					if _, ok := SchemaIntegerFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
						return stack, unsupportedFormat(format)
					}
				}
			}
		case TypeString:
			if format := schema.Format; len(format) > 0 {
				switch format {
				// Supported by OpenAPIv3.0.3:
				// https://spec.openapis.org/oas/v3.0.3
				case "byte", "binary", "date", "date-time", "password":
				// In JSON Draft-07 (not validated yet though):
				// https://json-schema.org/draft-07/json-schema-release-notes.html#formats
				case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname":
				case "json-pointer", "relative-json-pointer", "regex", "time":
				// In JSON Draft 2019-09 (not validated yet though):
				// https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary
				case "duration", "uuid":
				// Defined in some other specification
				case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
				default:
					if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
						return stack, unsupportedFormat(format)
					}
				}
			}
			if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" {
				if _, err := schema.compilePattern(validationOpts.regexCompilerFunc); err != nil {
					return stack, err
				}
			}
		case TypeArray:
			if schema.Items == nil && !validationOpts.jsonSchema2020ValidationEnabled && len(schema.PrefixItems) == 0 {
				return stack, newSchemaItemsRequired(schema.Origin)
			}
		case TypeObject:
		case TypeNull:
			if !validationOpts.jsonSchema2020ValidationEnabled {
				return stack, newSchemaTypeError(schemaType, schema.Origin)
			}
		default:
			return stack, newSchemaTypeError(schemaType, schema.Origin)
		}
	}

	if ref := schema.Items; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	for _, name := range componentNames(schema.Properties) {
		ref := schema.Properties[name]
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil {
		return stack, newSchemaAdditionalPropertiesBothForms(schema.Origin)
	}
	if ref := schema.AdditionalProperties.Schema; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	// OpenAPI 3.1 / JSON Schema 2020-12 sub-schemas
	for _, ref := range schema.PrefixItems {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if ref := schema.Contains; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	for _, name := range componentNames(schema.PatternProperties) {
		ref := schema.PatternProperties[name]
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	for _, name := range componentNames(schema.DependentSchemas) {
		ref := schema.DependentSchemas[name]
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	for _, name := range componentNames(schema.Defs) {
		ref := schema.Defs[name]
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if ref := schema.PropertyNames; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if schema.UnevaluatedItems.Has != nil && schema.UnevaluatedItems.Schema != nil {
		return stack, newSchemaUnevaluatedItemsBothForms(schema.Origin)
	}
	if ref := schema.UnevaluatedItems.Schema; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if schema.UnevaluatedProperties.Has != nil && schema.UnevaluatedProperties.Schema != nil {
		return stack, newSchemaUnevaluatedPropertiesBothForms(schema.Origin)
	}
	if ref := schema.UnevaluatedProperties.Schema; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}
	if ref := schema.ContentSchema; ref != nil {
		if err := ref.validateExtras(ctx); err != nil {
			return stack, err
		}
		v := ref.Value
		if v == nil {
			return stack, newUnresolvedRef(ref.Ref, ref.Origin)
		}

		var err error
		if stack, err = v.validate(ctx, stack); err != nil {
			return stack, err
		}
	}

	if v := schema.ExternalDocs; v != nil {
		if err := v.Validate(ctx); err != nil {
			return stack, &SectionValidationError{Section: "external docs", Cause: err}
		}
	}

	if v := schema.Default; v != nil && !validationOpts.schemaDefaultsValidationDisabled {
		if err := validateExampleValue(ctx, v, schema); err != nil {
			return stack, newSchemaValueError("default", err, schema.Origin)
		}
	}

	if x := schema.Example; x != nil && !validationOpts.examplesValidationDisabled {
		if err := validateExampleValue(ctx, x, schema); err != nil {
			return stack, newSchemaValueError("example", err, schema.Origin)
		}
	}

	return stack, validateExtensions(ctx, schema.Extensions, schema.Origin)
}

func (schema *Schema) IsMatching(value any) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

func (schema *Schema) IsMatchingJSONBoolean(value bool) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

func (schema *Schema) IsMatchingJSONNumber(value float64) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

func (schema *Schema) IsMatchingJSONString(value string) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

func (schema *Schema) IsMatchingJSONArray(value []any) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

func (schema *Schema) IsMatchingJSONObject(value map[string]any) bool {
	settings := newSchemaValidationSettings(FailFast())
	return schema.visitJSON(settings, value) == nil
}

// VisitJSON applies a Schema to the given data, considering opts.
// To validate data against an OpenAPIv3.1+ schema, be sure to pass the EnableJSONSchema2020() option.
func (schema *Schema) VisitJSON(value any, opts ...SchemaValidationOption) error {
	settings := newSchemaValidationSettings(opts...)

	if settings.useJSONSchema2020 {
		return schema.useJSONSchema2020(settings, value)
	}
	return schema.visitJSON(settings, value)
}

func (schema *Schema) visitJSON(settings *schemaValidationSettings, value any) (err error) {
	switch value := value.(type) {
	case nil:
		// Don't use VisitJSONNull, as we still want to reach 'visitXOFOperations', since
		// those could allow for a nullable value even though this one doesn't
		if schema.PermitsNull() {
			return
		}
	case float64:
		if math.IsNaN(value) {
			return ErrSchemaInputNaN
		}
		if math.IsInf(value, 0) {
			return ErrSchemaInputInf
		}
	}

	if schema.IsEmpty() {
		switch value.(type) {
		case nil:
			return schema.visitJSONNull(settings)
		default:
			return
		}
	}

	if err = schema.visitNotOperation(settings, value); err != nil {
		return
	}
	var run bool
	if err, run = schema.visitXOFOperations(settings, value); err != nil || !run {
		return
	}
	if err = schema.visitEnumOperation(settings, value); err != nil {
		return
	}
	if err = schema.visitConstOperation(settings, value); err != nil {
		return
	}

	switch value := value.(type) {
	case nil:
		return schema.visitJSONNull(settings)
	case bool:
		return schema.visitJSONBoolean(settings, value)
	case json.Number:
		valueFloat64, err := value.Float64()
		if err != nil {
			return &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "type",
				Reason:                "cannot convert json.Number to float64",
				customizeMessageError: settings.customizeMessageError,
				Origin:                err,
			}
		}
		return schema.visitJSONNumber(settings, valueFloat64)
	case int:
		return schema.visitJSONNumber(settings, float64(value))
	case int32:
		return schema.visitJSONNumber(settings, float64(value))
	case int64:
		return schema.visitJSONNumber(settings, float64(value))
	case float64:
		return schema.visitJSONNumber(settings, value)
	case string:
		return schema.visitJSONString(settings, value)
	case []any:
		return schema.visitJSONArray(settings, value)
	case map[string]any:
		return schema.visitJSONObject(settings, value)
	case map[any]any: // for YAML cf. issue https://github.com/getkin/kin-openapi/issues/444
		values := make(map[string]any, len(value))
		for key, v := range value {
			if k, ok := key.(string); ok {
				values[k] = v
			}
		}
		if len(value) == len(values) {
			return schema.visitJSONObject(settings, values)
		}
	}

	// Catch slice of non-empty interface type
	if reflect.TypeOf(value).Kind() == reflect.Slice {
		valueR := reflect.ValueOf(value)
		newValue := make([]any, 0, valueR.Len())
		for i := 0; i < valueR.Len(); i++ {
			newValue = append(newValue, valueR.Index(i).Interface())
		}
		return schema.visitJSONArray(settings, newValue)
	}

	return &SchemaError{
		Value:                 value,
		Schema:                schema,
		SchemaField:           "type",
		Reason:                fmt.Sprintf("unhandled value of type %T", value),
		customizeMessageError: settings.customizeMessageError,
	}
}

func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, value any) (err error) {
	if enum := schema.Enum; len(enum) != 0 {
		for _, v := range enum {
			switch c := value.(type) {
			case json.Number:
				var f float64
				if f, err = strconv.ParseFloat(c.String(), 64); err != nil {
					return err
				}
				if v == f {
					return
				}
			case int64:
				if v == float64(c) {
					return
				}
			default:
				if reflect.DeepEqual(v, value) {
					return
				}
			}
		}
		if settings.failfast {
			return errSchema
		}
		allowedValues, _ := json.Marshal(enum)
		return &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "enum",
			Reason:                "value is not one of the allowed values " + string(allowedValues),
			customizeMessageError: settings.customizeMessageError,
		}
	}
	return
}

func (schema *Schema) visitConstOperation(settings *schemaValidationSettings, value any) (err error) {
	if schema.Const == nil {
		return
	}
	var match bool
	switch c := value.(type) {
	case json.Number:
		var f float64
		if f, err = strconv.ParseFloat(c.String(), 64); err != nil {
			return err
		}
		match = reflect.DeepEqual(schema.Const, f)
	case int64:
		match = reflect.DeepEqual(schema.Const, float64(c))
	default:
		match = reflect.DeepEqual(schema.Const, value)
	}
	if !match {
		if settings.failfast {
			return errSchema
		}
		constVal, _ := json.Marshal(schema.Const)
		return &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "const",
			Reason:                "value must be " + string(constVal),
			customizeMessageError: settings.customizeMessageError,
		}
	}
	return
}

func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, value any) (err error) {
	if ref := schema.Not; ref != nil {
		v := ref.Value
		if v == nil {
			return newUnresolvedRef(ref.Ref, ref.Origin)
		}
		if err := v.visitJSON(settings, value); err == nil {
			if settings.failfast {
				return errSchema
			}
			return &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "not",
				customizeMessageError: settings.customizeMessageError,
			}
		}
	}
	return
}

// If the XOF operations pass successfully, abort further run of validation, as they will already be satisfied (unless the schema
// itself is badly specified
// resolveDiscriminatorRef resolves the discriminator reference for oneOf/anyOf validation.
// Returns the discriminator ref string and any error encountered during resolution.
func (schema *Schema) resolveDiscriminatorRef(value any) (string, error) {
	if schema.Discriminator == nil {
		return "", nil
	}
	pn := schema.Discriminator.PropertyName
	valuemap, okcheck := value.(map[string]any)
	if !okcheck {
		return "", nil
	}
	discriminatorVal, okcheck := valuemap[pn]
	if !okcheck {
		return "", &SchemaError{
			Schema:      schema,
			SchemaField: "discriminator",
			Reason:      fmt.Sprintf("input does not contain the discriminator property %q", pn),
		}
	}

	discriminatorValString, okcheck := discriminatorVal.(string)
	if !okcheck {
		return "", &SchemaError{
			Value:       discriminatorVal,
			Schema:      schema,
			SchemaField: "discriminator",
			Reason:      fmt.Sprintf("value of discriminator property %q is not a string", pn),
		}
	}

	if discriminatorRef, okcheck := schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
		return "", &SchemaError{
			Value:       discriminatorVal,
			Schema:      schema,
			SchemaField: "discriminator",
			Reason:      fmt.Sprintf("discriminator property %q has invalid value", pn),
		}
	} else {
		return discriminatorRef.Ref, nil
	}
}

func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) {
	var visitedOneOf, visitedAnyOf, visitedAllOf bool
	if v := schema.OneOf; len(v) > 0 {
		discriminatorRef, err := schema.resolveDiscriminatorRef(value)
		if err != nil {
			return err, false
		}

		var (
			ok                  = 0
			validationErrors    = multiErrorForOneOf{}
			matchedOneOfIndices = make([]int, 0)
			tempValue           = value
		)
		for idx, item := range v {
			v := item.Value
			if v == nil {
				return newUnresolvedRef(item.Ref, item.Origin), false
			}

			if discriminatorRef != "" && discriminatorRef != item.Ref {
				continue
			}

			// make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema
			if settings.asreq || settings.asrep {
				tempValue = deepcopy.Copy(value)
			}

			if err := v.visitJSON(settings, tempValue); err != nil {
				validationErrors = append(validationErrors, err)
				continue
			}

			matchedOneOfIndices = append(matchedOneOfIndices, idx)
			ok++
		}

		if ok != 1 {
			if settings.failfast {
				return errSchema, false
			}
			e := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "oneOf",
				customizeMessageError: settings.customizeMessageError,
			}
			if ok > 1 {
				e.Origin = ErrOneOfConflict
				e.Reason = fmt.Sprintf(`value matches more than one schema from "oneOf" (matches schemas at indices %v)`, matchedOneOfIndices)
			} else {
				e.Origin = fmt.Errorf("doesn't match schema due to: %w", validationErrors)
				e.Reason = `value doesn't match any schema from "oneOf"`
			}

			return e, false
		}

		// run again to inject default value that defined in matched oneOf schema
		if settings.asreq || settings.asrep {
			_ = v[matchedOneOfIndices[0]].Value.visitJSON(settings, value)
		}
		visitedOneOf = true
	}

	if v := schema.AnyOf; len(v) > 0 {
		discriminatorRef, err := schema.resolveDiscriminatorRef(value)
		if err != nil {
			return err, false
		}

		var (
			ok              = false
			matchedAnyOfIdx = 0
			tempValue       = value
		)
		for idx, item := range v {
			v := item.Value
			if v == nil {
				return newUnresolvedRef(item.Ref, item.Origin), false
			}

			if discriminatorRef != "" && discriminatorRef != item.Ref {
				continue
			}

			// make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
			if settings.asreq || settings.asrep {
				tempValue = deepcopy.Copy(value)
			}
			if err := v.visitJSON(settings, tempValue); err == nil {
				ok = true
				matchedAnyOfIdx = idx
				break
			}
		}
		if !ok {
			if settings.failfast {
				return errSchema, false
			}
			return &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "anyOf",
				Reason:                `doesn't match any schema from "anyOf"`,
				customizeMessageError: settings.customizeMessageError,
			}, false
		}

		_ = v[matchedAnyOfIdx].Value.visitJSON(settings, value)
		visitedAnyOf = true
	}

	validationErrors := multiErrorForAllOf{}
	for _, item := range schema.AllOf {
		v := item.Value
		if v == nil {
			return newUnresolvedRef(item.Ref, item.Origin), false
		}
		if err := v.visitJSON(settings, value); err != nil {
			if settings.failfast {
				return errSchema, false
			}
			validationErrors = append(validationErrors, err)
		}
		visitedAllOf = true
	}
	if len(validationErrors) > 0 {
		return &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "allOf",
			Reason:                `doesn't match all schemas from "allOf"`,
			Origin:                fmt.Errorf("doesn't match schema due to: %w", validationErrors),
			customizeMessageError: settings.customizeMessageError,
		}, false
	}

	run = !((visitedOneOf || visitedAnyOf || visitedAllOf) && value == nil)
	return
}

// The value is not considered in visitJSONNull because according to the spec
// "null is not supported as a type" unless `nullable` is also set to true
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
	if schema.PermitsNull() {
		return
	}
	if settings.failfast {
		return errSchema
	}
	return &SchemaError{
		Value:                 nil,
		Schema:                schema,
		SchemaField:           "nullable",
		Reason:                "Value is not nullable",
		customizeMessageError: settings.customizeMessageError,
	}
}

func (schema *Schema) VisitJSONBoolean(value bool) error {
	settings := newSchemaValidationSettings()
	return schema.visitJSONBoolean(settings, value)
}

func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
	if !schema.Type.Permits(TypeBoolean) {
		return schema.expectedType(settings, value)
	}
	return
}

func (schema *Schema) VisitJSONNumber(value float64) error {
	settings := newSchemaValidationSettings()
	return schema.visitJSONNumber(settings, value)
}

func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error {
	var me MultiError
	schemaType := schema.Type
	requireInteger := false
	if schemaType.Permits(TypeInteger) && !schemaType.Permits(TypeNumber) {
		requireInteger = true
		if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "type",
				Reason:                "value must be an integer",
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	} else if !(schemaType.Permits(TypeInteger) || schemaType.Permits(TypeNumber)) {
		return schema.expectedType(settings, value)
	}

	// formats
	var formatStrErr string
	var formatErr error
	format := schema.Format
	if format != "" {
		if requireInteger {
			// Check per-validation validators first, then fall back to global
			f, ok := settings.integerFormats[format]
			if !ok {
				f, ok = SchemaIntegerFormats[format]
			}
			if ok {
				if err := f.Validate(int64(value)); err != nil {
					var reason string
					schemaErr := &SchemaError{}
					if errors.As(err, &schemaErr) {
						reason = schemaErr.Reason
					} else {
						reason = err.Error()
					}
					formatStrErr = fmt.Sprintf(`integer doesn't match the format %q (%v)`, format, reason)
					formatErr = fmt.Errorf("integer doesn't match the format %q: %w", format, err)
				}
			}
		} else {
			// Check per-validation validators first, then fall back to global
			f, ok := settings.numberFormats[format]
			if !ok {
				f, ok = SchemaNumberFormats[format]
			}
			if ok {
				if err := f.Validate(value); err != nil {
					var reason string
					schemaErr := &SchemaError{}
					if errors.As(err, &schemaErr) {
						reason = schemaErr.Reason
					} else {
						reason = err.Error()
					}
					formatStrErr = fmt.Sprintf(`number doesn't match the format %q (%v)`, format, reason)
					formatErr = fmt.Errorf("number doesn't match the format %q: %w", format, err)
				}
			}
		}
	}

	if formatStrErr != "" || formatErr != nil {
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "format",
			Reason:                formatStrErr,
			Origin:                formatErr,
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "exclusiveMinimum"
	// OpenAPI 3.0: boolean modifier for minimum
	// OpenAPI 3.1: number representing the actual exclusive bound
	if eb := schema.ExclusiveMin; eb.IsSet() {
		var exclusiveMinBound float64
		var valid bool
		if eb.Value != nil {
			// OpenAPI 3.1 style: exclusiveMinimum is the bound itself
			exclusiveMinBound = *eb.Value
			valid = value > exclusiveMinBound
		} else if eb.Bool != nil && *eb.Bool && schema.Min != nil {
			// OpenAPI 3.0 style: exclusiveMinimum modifies minimum
			exclusiveMinBound = *schema.Min
			valid = value > exclusiveMinBound
		} else {
			valid = true
		}
		if !valid {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "exclusiveMinimum",
				Reason:                fmt.Sprintf("number must be more than %g", exclusiveMinBound),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	// "exclusiveMaximum"
	// OpenAPI 3.0: boolean modifier for maximum
	// OpenAPI 3.1: number representing the actual exclusive bound
	if eb := schema.ExclusiveMax; eb.IsSet() {
		var exclusiveMaxBound float64
		var valid bool
		if eb.Value != nil {
			// OpenAPI 3.1 style: exclusiveMaximum is the bound itself
			exclusiveMaxBound = *eb.Value
			valid = value < exclusiveMaxBound
		} else if eb.Bool != nil && *eb.Bool && schema.Max != nil {
			// OpenAPI 3.0 style: exclusiveMaximum modifies maximum
			exclusiveMaxBound = *schema.Max
			valid = value < exclusiveMaxBound
		} else {
			valid = true
		}
		if !valid {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "exclusiveMaximum",
				Reason:                fmt.Sprintf("number must be less than %g", exclusiveMaxBound),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	// "minimum"
	if v := schema.Min; v != nil && !(*v <= value) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "minimum",
			Reason:                fmt.Sprintf("number must be at least %g", *v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "maximum"
	if v := schema.Max; v != nil && !(*v >= value) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "maximum",
			Reason:                fmt.Sprintf("number must be at most %g", *v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "multipleOf"
	if v := schema.MultipleOf; v != nil {
		// "A numeric instance is valid only if division by this keyword's
		//    value results in an integer."
		numParsed, _ := decimal128.Parse(fmt.Sprintf("%.10f", value))
		denParsed, _ := decimal128.Parse(fmt.Sprintf("%.10f", *v))
		_, remainder := numParsed.QuoRem(denParsed)
		if !remainder.IsZero() {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "multipleOf",
				Reason:                fmt.Sprintf("number must be a multiple of %g", *v),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	if len(me) > 0 {
		return me
	}

	return nil
}

func (schema *Schema) VisitJSONString(value string) error {
	settings := newSchemaValidationSettings()
	return schema.visitJSONString(settings, value)
}

func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
	if !schema.Type.Permits(TypeString) {
		return schema.expectedType(settings, value)
	}

	var me MultiError

	// "minLength" and "maxLength"
	minLength := schema.MinLength
	maxLength := schema.MaxLength
	if minLength != 0 || maxLength != nil {
		// JSON schema string lengths are UTF-16, not UTF-8!
		length := int64(0)
		for _, r := range value {
			if utf16.IsSurrogate(r) {
				length += 2
			} else {
				length++
			}
		}
		if minLength != 0 && length < int64(minLength) {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "minLength",
				Reason:                fmt.Sprintf("minimum string length is %d", minLength),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
		if maxLength != nil && length > int64(*maxLength) {
			if settings.failfast {
				return errSchema
			}
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "maxLength",
				Reason:                fmt.Sprintf("maximum string length is %d", *maxLength),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	// "pattern"
	if !settings.patternValidationDisabled && schema.Pattern != "" {
		cpiface, _ := compiledPatterns.Load(schema.Pattern)
		cp, _ := cpiface.(RegexMatcher)
		if cp == nil {
			var err error
			if cp, err = schema.compilePattern(settings.regexCompiler); err != nil {
				if !settings.multiError {
					return err
				}
				me = append(me, err)
			}
		}
		if !cp.MatchString(value) {
			err := &SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "pattern",
				Reason:                fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
				customizeMessageError: settings.customizeMessageError,
			}
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	// "format"
	var formatStrErr string
	var formatErr error
	if format := schema.Format; format != "" {
		// Check per-validation validators first, then fall back to global
		f, ok := settings.stringFormats[format]
		if !ok {
			f, ok = SchemaStringFormats[format]
		}
		if ok {
			if err := f.Validate(value); err != nil {
				var reason string
				schemaErr := &SchemaError{}
				if errors.As(err, &schemaErr) {
					reason = schemaErr.Reason
				} else {
					reason = err.Error()
				}
				formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, reason)
				formatErr = fmt.Errorf("string doesn't match the format %q: %w", format, err)
			}
		}
	}
	if formatStrErr != "" || formatErr != nil {
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "format",
			Reason:                formatStrErr,
			Origin:                formatErr,
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)

	}

	if len(me) > 0 {
		return me
	}

	return nil
}

func (schema *Schema) VisitJSONArray(value []any) error {
	settings := newSchemaValidationSettings()
	return schema.visitJSONArray(settings, value)
}

func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []any) error {
	if !schema.Type.Permits(TypeArray) {
		return schema.expectedType(settings, value)
	}

	var me MultiError

	lenValue := int64(len(value))

	// "minItems"
	if v := schema.MinItems; v != 0 && lenValue < int64(v) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "minItems",
			Reason:                fmt.Sprintf("minimum number of items is %d", v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "maxItems"
	if v := schema.MaxItems; v != nil && lenValue > int64(*v) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "maxItems",
			Reason:                fmt.Sprintf("maximum number of items is %d", *v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "uniqueItems"
	if sliceUniqueItemsChecker == nil {
		sliceUniqueItemsChecker = isSliceOfUniqueItems
	}
	if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "uniqueItems",
			Reason:                "duplicate items found",
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "items"
	if itemSchemaRef := schema.Items; itemSchemaRef != nil {
		itemSchema := itemSchemaRef.Value
		if itemSchema == nil {
			return newUnresolvedRef(itemSchemaRef.Ref, itemSchemaRef.Origin)
		}
		for i, item := range value {
			if err := itemSchema.visitJSON(settings, item); err != nil {
				err = markSchemaErrorIndex(err, i)
				if !settings.multiError {
					return err
				}
				if itemMe, ok := err.(MultiError); ok {
					me = append(me, itemMe...)
				} else {
					me = append(me, err)
				}
			}
		}
	}

	if len(me) > 0 {
		return me
	}

	return nil
}

func (schema *Schema) VisitJSONObject(value map[string]any) error {
	settings := newSchemaValidationSettings()
	return schema.visitJSONObject(settings, value)
}

func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]any) error {
	if !schema.Type.Permits(TypeObject) {
		return schema.expectedType(settings, value)
	}

	var me MultiError

	if settings.asreq || settings.asrep {
		for _, propName := range componentNames(schema.Properties) {
			propSchema := schema.Properties[propName]
			reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled
			repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled

			if f := settings.defaultsSet; f != nil && value[propName] == nil {
				if dflt := propSchema.Value.Default; dflt != nil && !reqRO && !repWO {
					value[propName] = dflt
					settings.onceSettingDefaults.Do(f)
				}
			}

			if value[propName] != nil {
				if reqRO {
					me = append(me, fmt.Errorf("readOnly property %q in request", propName))
				} else if repWO {
					me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
				}
			}
		}
	}

	// "properties"
	properties := schema.Properties
	lenValue := int64(len(value))

	// "minProperties"
	if v := schema.MinProps; v != 0 && lenValue < int64(v) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "minProperties",
			Reason:                fmt.Sprintf("there must be at least %d properties", v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "maxProperties"
	if v := schema.MaxProps; v != nil && lenValue > int64(*v) {
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "maxProperties",
			Reason:                fmt.Sprintf("there must be at most %d properties", *v),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "additionalProperties"
	var additionalProperties *Schema
	if ref := schema.AdditionalProperties.Schema; ref != nil {
		additionalProperties = ref.Value
	}
	for _, k := range componentNames(value) {
		v := value[k]
		if properties != nil {
			propertyRef := properties[k]
			if propertyRef != nil {
				p := propertyRef.Value
				if p == nil {
					return newUnresolvedRef(propertyRef.Ref, propertyRef.Origin)
				}
				if err := p.visitJSON(settings, v); err != nil {
					if settings.failfast {
						return errSchema
					}
					err = markSchemaErrorKey(err, k)
					if !settings.multiError {
						return err
					}
					if v, ok := err.(MultiError); ok {
						me = append(me, v...)
						continue
					}
					me = append(me, err)
				}
				continue
			}
		}
		if allowed := schema.AdditionalProperties.Has; allowed == nil || *allowed {
			if additionalProperties != nil {
				if err := additionalProperties.visitJSON(settings, v); err != nil {
					if settings.failfast {
						return errSchema
					}
					err = markSchemaErrorKey(err, k)
					if !settings.multiError {
						return err
					}
					if v, ok := err.(MultiError); ok {
						me = append(me, v...)
						continue
					}
					me = append(me, err)
				}
			}
			continue
		}
		if settings.failfast {
			return errSchema
		}
		err := &SchemaError{
			Value:                 value,
			Schema:                schema,
			SchemaField:           "properties",
			Reason:                fmt.Sprintf("property %q is unsupported", k),
			customizeMessageError: settings.customizeMessageError,
		}
		if !settings.multiError {
			return err
		}
		me = append(me, err)
	}

	// "required"
	for _, k := range schema.Required {
		if _, ok := value[k]; !ok {
			if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq {
				continue
			}
			if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep {
				continue
			}
			if settings.failfast {
				return errSchema
			}
			err := markSchemaErrorKey(&SchemaError{
				Value:                 value,
				Schema:                schema,
				SchemaField:           "required",
				Reason:                fmt.Sprintf("property %q is missing", k),
				customizeMessageError: settings.customizeMessageError,
			}, k)
			if !settings.multiError {
				return err
			}
			me = append(me, err)
		}
	}

	if len(me) > 0 {
		return me
	}

	return nil
}

func (schema *Schema) expectedType(settings *schemaValidationSettings, value any) error {
	if settings.failfast {
		return errSchema
	}

	a := "a"
	var x string
	schemaTypes := (*schema.Type)
	if len(schemaTypes) == 1 {
		x = schemaTypes[0]
		switch x {
		case TypeArray, TypeObject, TypeInteger:
			a = "an"
		}
	} else {
		a = "one of"
		x = strings.Join(schemaTypes, ", ")
	}
	return &SchemaError{
		Value:                 value,
		Schema:                schema,
		SchemaField:           "type",
		Reason:                fmt.Sprintf("value must be %s %s", a, x),
		customizeMessageError: settings.customizeMessageError,
	}
}

// SchemaError is an error that occurs during schema validation.
type SchemaError struct {
	// Value is the value that failed validation.
	Value any
	// reversePath is the path to the value that failed validation.
	reversePath []string
	// Schema is the schema that failed validation.
	Schema *Schema
	// SchemaField is the field of the schema that failed validation.
	SchemaField string
	// Reason is a human-readable message describing the error.
	// The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages.
	Reason string
	// Origin is the original error that caused this error.
	Origin error
	// customizeMessageError is a function that can be used to customize the error message.
	customizeMessageError func(err *SchemaError) string
}

var _ interface{ Unwrap() error } = SchemaError{}

func markSchemaErrorKey(err error, key string) error {

	if v, ok := err.(*SchemaError); ok {
		v.reversePath = append(v.reversePath, key)
		if v.Origin != nil {
			if unwrapped := errors.Unwrap(v.Origin); unwrapped != nil {
				if me, ok := unwrapped.(multiErrorForOneOf); ok {
					_ = markSchemaErrorKey(MultiError(me), key)
				}
				if me, ok := unwrapped.(multiErrorForAllOf); ok {
					_ = markSchemaErrorKey(MultiError(me), key)
				}
			}
		}
		return v
	}
	if v, ok := err.(MultiError); ok {
		for _, e := range v {
			_ = markSchemaErrorKey(e, key)
		}
		return v
	}
	return err
}

func markSchemaErrorIndex(err error, index int) error {
	return markSchemaErrorKey(err, strconv.FormatInt(int64(index), 10))
}

func (err *SchemaError) JSONPointer() []string {
	reversePath := err.reversePath
	path := append([]string(nil), reversePath...)
	for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 {
		path[left], path[right] = path[right], path[left]
	}
	return path
}

func (err *SchemaError) Error() string {
	if err.customizeMessageError != nil {
		if msg := err.customizeMessageError(err); msg != "" {
			return msg
		}
	}

	buf := bytes.NewBuffer(make([]byte, 0, 256))

	if len(err.reversePath) > 0 {
		buf.WriteString(`Error at "`)
		reversePath := err.reversePath
		for i := len(reversePath) - 1; i >= 0; i-- {
			buf.WriteByte('/')
			buf.WriteString(reversePath[i])
		}
		buf.WriteString(`": `)
	}

	if err.Origin != nil {
		buf.WriteString(err.Origin.Error())

		return buf.String()
	}

	reason := err.Reason
	if reason == "" {
		buf.WriteString(`Doesn't match schema "`)
		buf.WriteString(err.SchemaField)
		buf.WriteString(`"`)
	} else {
		buf.WriteString(reason)
	}

	if !SchemaErrorDetailsDisabled {
		buf.WriteString("\nSchema:\n  ")
		encoder := json.NewEncoder(buf)
		encoder.SetIndent("  ", "  ")
		if err := encoder.Encode(err.Schema); err != nil {
			panic(err)
		}
		buf.WriteString("\nValue:\n  ")
		if err := encoder.Encode(err.Value); err != nil {
			panic(err)
		}
	}

	return buf.String()
}

func (err SchemaError) Unwrap() error {
	return err.Origin
}

func isSliceOfUniqueItems(xs []any) bool {
	s := len(xs)
	m := make(map[string]struct{}, s)
	for _, x := range xs {
		// The input slice is converted from a JSON string, there shall
		// have no error when convert it back.
		key, _ := json.Marshal(&x)
		m[string(key)] = struct{}{}
	}
	return s == len(m)
}

// SliceUniqueItemsChecker is an function used to check if an given slice
// have unique items.
type SliceUniqueItemsChecker func(items []any) bool

// By default using predefined func isSliceOfUniqueItems which make use of
// json.Marshal to generate a key for map used to check if a given slice
// have unique items.
var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems

// RegisterArrayUniqueItemsChecker is used to register a customized function
// used to check if JSON array have unique items.
func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) {
	sliceUniqueItemsChecker = fn
}

func unsupportedFormat(format string) error {
	return fmt.Errorf("unsupported 'format' value %q", format)
}

// UnmarshalJSON sets Schemas to a copy of data.
func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) {
	*schemas, err = unmarshalStringMapP[SchemaRef](data)
	return
}
